一本通动态规划篇解题报告
一本通动态规划篇解题报告
\(\text{By DaiRuiChen007}\)
数位 dp
0. 过程模板
求解数位 dp 时,用 \(dp_{i,j}\) 表示长度为 \(i\),最高位为 \(j\) 时满足数字的个数,则统计答案的流程如下:
- 分解原数位 \(a_{len}\sim a_1\)(\(a_{len}\) 是最高位)
- 加上所有长度长度小于 \(len\) 的,\(\text{Answer}\gets\sum\limits_{i=1}^{len}\sum\limits_{j=1}^9 dp_{i,j}\)
- 加上所有长度等于 \(len\),且最高位小于 \(a_{len}\) 的,\(\text{Answer}\gets \sum\limits_{i=1}^{a_{len}-1}dp_{len,i}\)
- 加上所有前 \(i-1\) 位与原数相等,第 \(i\) 位小于原数的,\(\text{Answer}\gets\sum\limits_{i=len-1}^{1} \sum\limits_{j=0}^{a_i-1} dp_{i,j}\),如果到某一位的时候前面若干位组成的数已经不满足题目要求,则直接退出
这样做可以统计出区间 \([1,x)\) 之间满足条件的数的个数
I. Amount of Degrees
思路分析
考虑拆成两个前缀和,然后答案转化为统计 \([1,x]\) 之间满足条件的个数,将 \(x\) 变成 \(b\) 进制,其中恰好有 \(k\) 个数码 \(1\),所以答案与 \(x\) 中 \(\ge 2\) 的数码无关,如果遇到这样的数码,就将这一位及其后面的所有数码都设为 \(1\),不影响答案,然后二进制数位 dp 即可,只需要考虑第四种情况即可
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
int k,b;
inline int C(int n,int m) {
int res=1;
for(int x=n;x>n-m;--x) res*=x;
for(int x=1;x<=m;++x) res/=x;
return res;
}
inline int calc(int x) {
int len=0,a[35],res=0;
while(x) {
a[++len]=x%b;
x/=b;
}
for(int i=len;i>=1;--i) {
if(a[i]>1) {
for(int j=i;j>=1;--j) {
a[j]=1;
}
break;
}
}
int t=k;
for(int i=len;i>=1;--i) {
if(!a[i]) continue;
res+=C(i-1,t);
--t; if(t<0) break;
}
return res;
}
signed main() {
int x,y;
scanf("%lld%lld%lld%lld",&x,&y,&k,&b);
printf("%lld\n",calc(y+1)-calc(x));
return 0;
}
II. 数字游戏
思路分析
拆前缀和然后套模板,预处理 dp 的状态转移方程如下:
代码呈现
#include<bits/stdc++.h>
#define int long long
#define f puts("comehere");
using namespace std;
int dp[11][10];
inline int calc(int x) {
int len=0,a[11],res=0;
while(x) {
a[++len]=x%10;
x/=10;
}
for(int i=1;i<len;++i) {
for(int j=1;j<=9;++j) {
res+=dp[i][j];
}
}
for(int i=1;i<a[len];++i) res+=dp[len][i];
for(int i=len-1;i>=1;--i) {
for(int j=a[i+1];j<a[i];++j) {
res+=dp[i][j];
}
if(a[i]<a[i+1]) break;
}
return res;
}
signed main() {
for(int i=0;i<=9;++i) dp[1][i]=1;
for(int i=2;i<=10;++i) {
for(int j=0;j<=9;++j) {
for(int k=j;k<=9;++k) {
dp[i][j]+=dp[i-1][k];
}
}
}
int a,b;
while(scanf("%lld%lld",&a,&b)!=EOF) printf("%lld\n",calc(b+1)-calc(a));
return 0;
}
III.Windy 数
思路分析
同样模板,状态转移方程如下:
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
int dp[20][20],a[20];
inline int calc(int x) {
int len=0,res=0;
while(x) a[++len]=x%10,x/=10;
for(int i=1;i<a[len];++i) res+=dp[len][i];
for(int l=1;l<len;++l) {
for(int i=1;i<=9;++i) res+=dp[l][i];
}
for(int l=len-1;l>0;--l) {
for(int i=0;i<a[l];++i) {
if(abs(i-a[l+1])>=2) res+=dp[l][i];
}
if(abs(a[l+1]-a[l])<2) break;
}
return res;
}
signed main() {
for(int i=0;i<=9;++i) dp[1][i]=1;
for(int i=2;i<=10;++i) {
for(int j=0;j<=9;++j) {
for(int k=0;k<=9;++k) {
if(abs(j-k)>=2) dp[i][j]+=dp[i-1][k];
}
}
}
int start,end;
scanf("%lld%lld",&start,&end);
printf("%lld\n",calc(end+1)-calc(start));
return 0;
}
IV. 数字游戏
思路分析
略有不同,设 \(dp_{i,j,r}\) 表示长度为 \(i\),最高位为 \(j\),\(\bmod N\) 的余数为 \(r\) 的数的个数,套模板的时候统计后 \(i\) 位的余数不是 \(0\),而是与前 \(i-1\) 位的和是 \(N\) 的倍数的数,状态转移方程如下:
转移时用刷表法更为方便,注意对于多组数据要分别处理 \(dp\) 数组
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
int dp[11][10][100],mod;
inline int calc(int x) {
int len=0,a[11],res=0;
while(x) {
a[++len]=x%10;
x/=10;
}
for(int i=1;i<len;++i) {
for(int j=1;j<=9;++j) {
res+=dp[i][j][0];
}
}
for(int i=1;i<a[len];++i) res+=dp[len][i][0];
int sum=mod-a[len]%mod; sum=(sum+mod)%mod;
for(int i=len-1;i>=1;--i) {
for(int j=0;j<a[i];++j) {
res+=dp[i][j][sum];
}
sum+=mod-a[i]%mod;
sum=(sum+mod)%mod;
}
return res;
}
signed main() {
int a,b;
while(scanf("%lld%lld%lld",&a,&b,&mod)!=EOF) {
memset(dp,0,sizeof(dp));
for(int i=0;i<=9;++i) dp[1][i][i%mod]=1;
for(int i=2;i<=10;++i) {
for(int j=0;j<=9;++j) {
for(int k=0;k<=9;++k) {
for(int r=0;r<mod;++r) {
dp[i][j][(r+j)%mod]+=dp[i-1][k][r];
}
}
}
}
printf("%lld\n",calc(b+1)-calc(a));
}
return 0;
}
V. 不要 62
思路分析
模板 dp,考虑前后两位之间的关系就可以去除含 \(62\) 的情况,注意在统计第三种情况的时候不要忘记排除第 \(i-1\) 为是 \(6\),这一位又恰好考虑到 \(2\) 的情况,状态转移方程如下:
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
int dp[11][10];
inline int calc(int x) {
int len=0,a[11],res=0;
while(x) {
a[++len]=x%10;
x/=10;
}
for(int i=1;i<len;++i) {
for(int j=1;j<=9;++j) {
res+=dp[i][j];
}
}
for(int i=1;i<a[len];++i) res+=dp[len][i];
if(a[len]==4) return res;
for(int i=len-1;i>=1;--i) {
for(int j=0;j<a[i];++j) {
if(a[i+1]==6&&j==2) continue;
res+=dp[i][j];
}
if((a[i]==4)||(a[i+1]==6&&a[i]==2)) break;
}
return res;
}
signed main() {
for(int i=0;i<=9;++i) if(i!=4) dp[1][i]=1;
for(int i=2;i<=10;++i) {
for(int j=0;j<=9;++j) {
if(j==4) continue;
for(int k=0;k<=9;++k) {
if(j==6&&k==2) continue;
dp[i][j]+=dp[i-1][k];
}
}
}
int a,b;
while(true) {
scanf("%lld%lld",&a,&b);
if(a==0&&b==0) break;
printf("%lld\n",calc(b+1)-calc(a));
}
return 0;
}
VI. 恨 7 不成妻
思路分析
数位 dp?大模拟!(误)
考虑 \(dp_{i,j,d,s,0/1/2}\) 分别表示前 \(i\) 位,开头为 \(j\),数字和模 \(7\) 余 \(d\),原数字模 \(7\) 余 \(s\),的数的个数/数字和/平方和
转移平方和的时候记得用完全平方和展开,这里直接贴转移方程的 C++ 源码了(实在是太长了)
for(int i=0;i<=9;++i) {
if(i==7) continue;
dp[1][i][i%7][i%7][0]=1;
dp[1][i][i%7][i%7][1]=i;
dp[1][i][i%7][i%7][2]=i*i;
}
for(int i=2;i<=19;++i) {
for(int j=0;j<=9;++j) {
if(j==7) continue;
for(int k=0;k<=9;++k) {
for(int dig=0;dig<7;++dig) {
for(int sum=0;sum<7;++sum) {
int con=j*pow10(i-1),t=con%MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][0]+=dp[i-1][k][dig][sum][0];
dp[i][j][(dig+j)%7][(sum+con%7)%7][0]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][1]+=dp[i-1][k][dig][sum][0]*t%MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][1]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][1]+=dp[i-1][k][dig][sum][1];
dp[i][j][(dig+j)%7][(sum+con%7)%7][1]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]+=t*t%MOD*dp[i-1][k][dig][sum][0]%MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]+=t*dp[i-1][k][dig][sum][1]%MOD*2%MOD; dp[i][j][(dig+j)%7][(sum+con%7)%7][2]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]+=dp[i-1][k][dig][sum][2];
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]%=MOD;
}
}
}
}
}
注意:一定一定一定要注意是否会溢出
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD=1e9+7;
int dp[21][10][7][7][3];
//length start_digit digit_sum%7 number%7 answer_power
inline int pow10(int x) {
int res=1;
for(int i=1;i<=x;++i) res=res*10;
return res;
}
inline int calc(int x) {
int len=0,a[21],res=0;
while(x) {
a[++len]=x%10;
x/=10;
}
for(int i=1;i<len;++i) {
for(int j=1;j<=9;++j) {
for(int dig=1;dig<7;++dig) {
for(int sum=1;sum<7;++sum) {
res=(res+dp[i][j][dig][sum][2])%MOD;
}
}
}
}
for(int i=1;i<a[len];++i) {
for(int dig=1;dig<7;++dig) {
for(int sum=1;sum<7;++sum) {
res=(res+dp[len][i][dig][sum][2])%MOD;
}
}
}
int got_dig=a[len]%7,con=a[len]*pow10(len-1)%MOD,got_sum=a[len]*pow10(len-1)%7;
if(a[len]==7) return res;
for(int i=len-1;i>=1;--i) {
for(int j=0;j<a[i];++j) {
for(int dig=0;dig<7;++dig) {
if((dig+got_dig)%7==0) continue;
for(int sum=0;sum<7;++sum) {
if((sum+got_sum)%7==0) continue;
res+=dp[i][j][dig][sum][0]*con%MOD*con%MOD; res%=MOD;
res+=2*con%MOD*dp[i][j][dig][sum][1]%MOD; res%=MOD;
res+=dp[i][j][dig][sum][2]; res%=MOD;
}
}
}
if(a[i]==7) break;
got_dig=(got_dig+a[i])%7;
con=(con+a[i]*pow10(i-1))%MOD;
got_sum=(got_sum+a[i]*pow10(i-1))%7;
}
return res;
}
signed main() {
for(int i=0;i<=9;++i) {
if(i==7) continue;
dp[1][i][i%7][i%7][0]=1;
dp[1][i][i%7][i%7][1]=i;
dp[1][i][i%7][i%7][2]=i*i;
}
for(int i=2;i<=19;++i) {
for(int j=0;j<=9;++j) {
if(j==7) continue;
for(int k=0;k<=9;++k) {
for(int dig=0;dig<7;++dig) {
for(int sum=0;sum<7;++sum) {
int con=j*pow10(i-1),t=con%MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][0]+=dp[i-1][k][dig][sum][0];
dp[i][j][(dig+j)%7][(sum+con%7)%7][0]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][1]+=dp[i-1][k][dig][sum][0]*t%MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][1]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][1]+=dp[i-1][k][dig][sum][1];
dp[i][j][(dig+j)%7][(sum+con%7)%7][1]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]+=t*t%MOD*dp[i-1][k][dig][sum][0]%MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]+=t*dp[i-1][k][dig][sum][1]%MOD*2%MOD; dp[i][j][(dig+j)%7][(sum+con%7)%7][2]%=MOD;
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]+=dp[i-1][k][dig][sum][2];
dp[i][j][(dig+j)%7][(sum+con%7)%7][2]%=MOD;
}
}
}
}
}
int T;
scanf("%lld",&T);
while(T--) {
int a,b; scanf("%lld%lld",&a,&b);
printf("%lld\n",((calc(b+1)%MOD-calc(a)%MOD)%MOD+MOD)%MOD);
}
return 0;
}
VII. 数字计数
思路分析
经典数位 dp,设 \(dp_{i,j,d}\) 表示长度为 \(i\),最高位为 \(j\) 的数字中 \(d\) 出现的次数,状态转移方程如下:
注意计数的时候要考虑数字第 \(i\) 位对答案是否有贡献
代码呈现
#include<bits/stdc++.h>
#define int __int128
using namespace std;
inline int read() {
int x=0;char c=getchar();
while(!isdigit(c)) c=getchar();
while(isdigit(c)) x=(x<<3)+(x<<1)+c-'0',c=getchar();
return x;
}
inline void write(int x) {
if(x>=10) write(x/10);
putchar(x%10+'0');
return ;
}
int dp[15][10][10];
/*
dp[i][j][k]:
i-digit j-end k-count which digit
*/
int a[15],pw[15];
inline int calc(int x,int digit) {
int res=0,len=0;
while(x) {
a[++len]=x%10,x/=10;
}
for(int i=1;i<a[len];++i) res+=dp[len][i][digit];
for(int l=1;l<len;++l) {
for(int i=1;i<=9;++i) res+=dp[l][i][digit];
}
for(int l=len-1;l>0;--l) {
for(int i=0;i<a[l];++i) res+=dp[l][i][digit];
for(int i=len;i>l;--i) if(a[i]==digit) res+=pw[l-1]*a[l];
}
return res;
}
signed main() {
for(int i=0,r=1;i<=12;++i,r*=10) pw[i]=r;
for(int i=0;i<=9;++i) dp[1][i][i]=1;
for(int l=2;l<=12;++l) {
for(int i=0;i<=9;++i) {
dp[l][i][i]+=pw[l-1];
for(int d=0;d<=9;++d) {
for(int j=0;j<=9;++j) {
//[ij...]
dp[l][i][d]+=dp[l-1][j][d];
}
}
}
}
int a=read(),b=read();
for(int i=0;i<=9;++i) {
write(calc(b+1,i)-calc(a,i));
putchar(' ');
}
puts("");return 0;
}
状压 dp
I. 国王
思路分析
如果某行的某个位置有国王则其状态的二进制下对应位为 \(1\),否则为 \(0\)
设 \(dp_{i,j,s}\) 表示前 \(i\) 行共放了 \(j\) 个国王,第 \(i\) 行状态为 \(s\) 的方案数,预处理出可行单行方案和其对应的放置国王数,然后枚举上一行的放置情况,如果满足则转移
设单行的可能放置方案的集合为 \(\mathbf S\),时间复杂度为 \(\Theta(nk|\mathbf S|^2)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=11;
int king[1<<MAXN],dp[MAXN][1<<MAXN][MAXN*MAXN],ans,n,l;
vector <int> choice;
signed main() {
scanf("%lld%lld",&n,&l);
for(int i=0;i<(1<<n);++i) {
if((i<<1)&i||(i>>1)&i) continue;
choice.push_back(i);
int j=i;
king[i]=__builtin_popcount(i);
}
for(int i:choice) if(king[i]<=l) ++dp[1][i][king[i]];
for(int i=2;i<=n;++i) {
for(int j:choice) {
for(int k:choice) {
if((k&j)||((k<<1)&j)||((k>>1)&j)) continue;
for(int h=1;h<=l;++h) {
if(h+king[j]>l) continue;
dp[i][j][h+king[j]]+=dp[i-1][k][h];
}
}
}
}
for(int i=1;i<=n;++i) {
for(int j:choice) {
ans+=dp[i][j][l];
}
}
printf("%lld",ans);
return 0;
}
II. 牧场的安排
思路分析
如果某行的某个位置被选取了则其状态的二进制下对应位为 \(1\),否则为 \(0\),预处理出每行可能的状态集 \(\mathbf S\)
设 \(dp_{i,s}\) 表示第 \(i\) 行的状态为 \(s\) 时前 \(i\) 行的方案总数,如果 \(s\) 中选取的位置都不是贫瘠的,那么就枚举上一行的情况并转移,时间复杂度 \(\Theta(n|\mathbf S|^2)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MOD=1e8;
int a[15],state[151],dp[151][15];
signed main() {
int n,m,tot=0;
scanf("%d%d",&n,&m);
for(int s=0;s<(1<<m);++s) {
if((s<<1)&s||(s>>1)&s) continue;
state[++tot]=s;
}
for(int i=1;i<=n;++i) {
for(int j=0;j<m;++j) {
int x;
scanf("%d",&x);
a[i]=(a[i]<<1)+(!x);
}
}
for(int i=1;i<=tot;++i) {
if(state[i]&a[1]) continue;
dp[i][1]=1;
}
for(int r=2;r<=n;++r) {
for(int i=1;i<=tot;++i) {
if(a[r-1]&state[i]) continue;
for(int j=1;j<=tot;++j) {
if(a[r]&state[j]) continue;
if(state[i]&state[j]) continue;
dp[j][r]=(dp[j][r]+dp[i][r-1])%MOD;
}
}
}
int res=0;
for(int i=1;i<=tot;++i) res=(res+dp[i][n])%MOD;
printf("%d\n",res);
return 0;
}
III. 涂抹果酱
思路分析
三进制状态压缩,某行的某个位置的颜色(\(1\sim 3\))对应其状态中对应位的数码(\(0\sim 2\)),预处理出每行的合法状态集 \(\mathbf S\)
设 \(dp_{i,s}\) 表示第 \(i\) 行状态为 \(s\) 时,前 \(i\) 行的涂色方案数,每次枚举往前一行状态,合法则转移
计算答案的时候,将第 \(k\) 行的状态作为初始第 \(1\) 行的状态,因为上下两行独立,所以
时间复杂度 \(\Theta(n|\mathbf S|^2)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+1,MAXS=243,MOD=1e6;
int dp[MAXN][243],pw[6]={1,3,9,27,81,243};
inline int calc(int s,int pos) {
return (s/pw[pos])%3;
}
signed main() {
int n,m,k,st=0;
scanf("%lld%lld%lld",&n,&m,&k);
vector <int> state;
for(int i=0;i<pw[m];++i) {
bool flg=true;
for(int j=1;j<m;++j) {
if(calc(i,j-1)==calc(i,j)) {
flg=false;
break;
}
}
if(flg) state.push_back(i);
}
for(int i=0;i<m;++i) {
int col; scanf("%lld",&col);
st=st*3+col-1;
}
if((*lower_bound(state.begin(),state.end(),st))!=st) return 0&puts("0");
dp[1][st]=1;
for(int i=2;i<=max(n-k+1,k);++i) {
for(int s:state) {
for(int l:state) {
for(int p=0;p<m;++p) if(calc(s,p)==calc(l,p)) goto invalid;
dp[i][s]=(dp[i][s]+dp[i-1][l])%MOD;
invalid:;
}
}
}
int res1=0,res2=0;
for(int s:state) res1=(res1+dp[k][s])%MOD,res2=(res2+dp[n-k+1][s])%MOD;
printf("%lld\n",res1*res2%MOD);
return 0;
}
IV. 炮兵阵地
思路分析
某行的某个位置如果有炮兵阵地,则该行状态的对应位置为 \(1\) 否则为 \(0\),预处理出每行符合要求的状态集 \(\mathbf S\),同时计算出 \(\mathbf S\) 中的每个状态的价值
设 \(dp_{i,s,l}\) 表示第 \(i\) 行状态为 \(s\),第 \(i-1\) 行状态为 \(s-1\) 时,前 \(i\) 行最多摆放的阵地个数,转移时分别枚举第 \(i,i-1,i-2\) 三行的状态,满足则转移,时间复杂度 \(\Theta(n|\mathbf S|^3)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[105],state[63],cost[63],dp[63][63][105];
char str[105];
inline int calc(int x) {
return __builtin_popcount(x);
}
signed main() {
int n,m,tot=0;
scanf("%lld%lld",&n,&m);
for(int s=0;s<(1<<m);++s) {
if((s<<2)&s||(s<<1)&s||(s>>1)&s||(s>>2)&s) continue;
state[++tot]=s;
cost[tot]=calc(s);
}
for(int i=1;i<=n;++i) {
scanf("%s",str);
for(register int j=0;j<m;++j) {
a[i]=(a[i]<<1)+(str[j]=='H'?1:0);
}
}
for(int i=1;i<=tot;++i) {
if(state[i]&a[1]) continue;
dp[0][i][1]=cost[i];
}
for(int i=1;i<=tot;++i) {
if(state[i]&a[1]) continue;
for(int j=1;j<=tot;++j) {
if(state[j]&a[2]) continue;
if(state[i]&state[j]) continue;
dp[i][j][2]=max(dp[i][j][2],dp[0][i][1]+cost[j]);
}
}
for(int r=3;r<=n;++r) {
for(int i=1;i<=tot;++i) {
if(state[i]&a[r-2]) continue;
for(int j=1;j<=tot;++j) {
if(state[j]&a[r-1]) continue;
if(state[i]&state[j]) continue;
for(int k=1;k<=tot;++k) {
if(state[k]&a[r]) continue;
if(state[k]&state[i]||state[k]&state[j]) continue;
dp[j][k][r]=max(dp[j][k][r],dp[i][j][r-1]+cost[k]);
}
}
}
}
int res=0;
for(int i=0;i<=tot;++i) {
for(int j=0;j<=tot;++j) {
res=max(res,dp[i][j][n]);
}
}
printf("%lld\n",res);
return 0;
}
V. 动物园
题目大意
令 \(dp_{i,s}\) 表示区间 \([i,i+4]\) 内的动物状态为 \(s\)(存在为 \(1\),否则为 \(0\)),预处理出状态 \(s\) 可以使多少个 \(i\) 上的小朋友满意
转移时保证前四位不变的前提下枚举第 \(i-1\) 位的值,边界条件由枚举起点状态得来,为保证原图是环,统计答案的状态必须与初始状态一致
思路分析
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+1,INF=1e9;
int dp[MAXN][32],val[MAXN][32];
signed main() {
int n,m,res=0;
scanf("%d%d",&n,&m);
for(int t=1;t<=m;++t) {
int a,b,c,x=0,y=0;
scanf("%d%d%d",&a,&b,&c);
for(int i=1;i<=b;++i) {
int u;
scanf("%d",&u);
x+=1<<((u-a+n)%n);
}
for(int i=1;i<=c;++i) {
int u;
scanf("%d",&u);
y+=1<<((u-a+n)%n);
}
for(int s=0;s<32;++s) if(((~s)&x)||(s&y)) ++val[a][s];
}
for(int start=0;start<32;++start) {
for(int i=0;i<32;++i) dp[0][i]=i==start?0:-INF;
for(int i=1;i<=n;++i) {
for(int s=0;s<32;++s) {
dp[i][s]=max(dp[i-1][(s&15)<<1],dp[i-1][(s&15)<<1|1])+val[i][s];
}
}
res=max(res,dp[n][start]);
}
printf("%d\n",res);
return 0;
}
树形 dp
0. 约定
\(\mathbf C_{p}\) 表示 \(p\) 的子节点构成的集合
\(w_{u,v}\) 表示 \(u,v\) 之间边的边权
\(\mathbf{L}\) 表示树的叶节点构成的集合
\(\mathbf T\) 为整棵树所有节点构成的集合
\(r\) 为树的根节点
I. 二叉苹果树
思路分析
经典树上背包,枚举每个儿子保留多少条边,父亲保留多少条边
结合 01 背包的思想,设 \(dp_{p,w}\) 表示当前以 \(p\) 为根的子树保留最多 \(w\) 条边的答案,得到如下状态转移方程:
时间复杂度 \(\Theta(nq^2)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
int dp[101][101],val[101],n,q;
struct node {
int des,val;
};
vector <node> edge[101];
inline void dfs(int p,int f) {
if(edge[p].size()==1) return ;
for(node t:edge[p]) {
int v=t.des;
if(v==f) continue;
dfs(v,p);
for(int i=q;i>0;--i) {
for(int j=0;j<i;++j) {
dp[p][i]=max(dp[p][i],dp[v][j]+dp[p][i-j-1]+t.val);
}
}
}
}
signed main() {
scanf("%lld%lld",&n,&q);
for(int i=1;i<n;++i) {
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
edge[u].push_back((node){v,w});
edge[v].push_back((node){u,w});
}
dfs(1,0);
printf("%lld",dp[1][q]);
return 0;
}
II. 选课
思路分析
树形 dp,以 \(0\) 作为根节点,最终形态会变成一棵树
同样 01 背包,设 \(dp_{p,w}\) 表示以 \(p\) 为根的子树中至多选择 \(w\) 节课的答案,有如下状态转移方程:
注意初始值所有节点的 \(dp\) 值都是 \(a_p\),边界条件中的 \(p\) 可以不是叶节点
时间复杂度 \(\Theta(nm^2)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=105;
int w[MAXN],dp[MAXN][MAXN],m,n;
vector <int> edge[MAXN];
inline void dfs(int x) {
for(int i=1;i<=m;++i) dp[x][i]=w[x];
for(int i=0;i<edge[x].size();++i) {
int s=edge[x][i];
dfs(s);
for(int j=m;j>0;--j) {
for(int k=1;k<j;++k) {
dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[s][k]);
}
}
}
return ;
}
int main() {
scanf("%d%d",&n,&m);
++m;
for(int i=1;i<=n;++i) {
int input;
scanf("%d%d",&input,&w[i]);
edge[input].push_back(i);
}
dfs(0);
printf("%d\n",dp[0][m]);
return 0;
}
III. 数字转换
思路分析
按题目要求将节点 \(1\sim n\) 全部连接,发现此时这些节点构成的图是一个森林,所以原题目转化为求森林最长的直径,直接 dp
设 \(dp_{p}\) 表示以 \(p\) 为根的最长链,不难得到:
求答案时枚举每一个节点为根时的最长链长度与次长链长度相加即可
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e4+1;
vector <int> edge[MAXN];
int dp[MAXN][2],ans=0;
bool vis[MAXN];
inline int calc(int x) {
int res=0;
for(int i=1;i*i<=x;++i) {
if(x%i) continue;
res+=i+(x/i);
if(i*i==x) res-=i;
}
return res-x;
}
inline void dfs(int p,int f) {
vis[p]=true;
vector <int> res;
for(int v:edge[p]) {
if(v==f) continue;
dfs(v,p);
res.push_back(dp[v][0]+1);
}
sort(res.begin(),res.end(),greater<int>());
if(res.size()>0) dp[p][0]=res[0];
if(res.size()>1) dp[p][1]=res[1];
ans=max(ans,dp[p][0]+dp[p][1]);
return ;
}
signed main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i) {
int t=calc(i);
if(t>=i||t<=0) continue;
edge[t].push_back(i);
edge[i].push_back(t);
}
for(int i=1;i<=n;++i) if(!vis[i]) dfs(i,0);
printf("%d\n",ans);
return 0;
}
IV. 战略游戏
思路分析
经典的树上覆盖问题,考虑 dp,设 \(dp_{p,0/1}\) 表示不选或者选 \(p\) 节点后覆盖 \(p\) 的子树的最小代价
如果节点 \(p\) 不覆盖,那么 \(p\) 的每个儿子都必须覆盖,如果节点 \(p\) 被覆盖,那么 \(p\) 的每个儿子可以覆盖或不覆盖,得到如下状态转移方程:
最终答案为 \(\min(dp_{r,0},dp_{r,1})\),时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
int dp[1505][2];
vector <int> edge[1505];
void dfs(int x) {
dp[x][1]=1;
dp[x][0]=0;
for(register int i=0;i<edge[x].size();++i) {
int s=edge[x][i];
dfs(s);
dp[x][0]+=dp[s][1];
dp[x][1]+=min(dp[s][0],dp[s][1]);
}
}
int main() {
int n;
scanf("%d",&n);
for(register int i=0;i<n;++i) {
int now,tot;
scanf("%d%d",&now,&tot);
for(register int j=1;j<=tot;++j) {
int v;
scanf("%d",&v);
edge[now].push_back(v);
}
}
dfs(0);
printf("%d",min(dp[0][0],dp[0][1]));
}
V. 皇宫看守
思路分析
遇上一题类似,不过这一题是覆盖相邻点,所以每个点 \(p\) 被覆盖的来源有 \(3\) 种情况:
- \(p\) 的父亲覆盖了 \(p\),\(p\) 的儿子可能是自身覆盖或者被儿子覆盖
- \(p\) 的儿子覆盖了 \(p\),\(p\) 的儿子可能是被自身覆盖或者被儿子覆盖,注意转移时必须保证至少覆盖了一个儿子
- \(p\) 本身被覆盖了,\(p\) 的儿子三种覆盖情况都有可能
设 \(dp_{p,0/1/2}\) 分别表示节点 \(p\) 从以上第 \(1/2/3\) 中情况被覆盖的子树最小花费,得到如下状态转移方程:
注:转移式 \(dp_{p,1}\) 前面的那一堆复杂的东西是选择至少一个节点 \(v\) 是由本身覆盖的最少新增价值,是为了保证 \(p\) 一定可以被儿子覆盖
最终的答案是 \(\min(dp_{r,1},dp_{r,2})\),因为根节点不可能再有父亲了
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1501,INF=INT_MAX;
int dp[MAXN][3],w[MAXN];
vector <int> edge[MAXN];
void dfs(int p,int f) {
int val=INF;
for(int v:edge[p]) {
if(v==f) continue;
dfs(v,p);
dp[p][0]+=min(dp[v][1],dp[v][2]);
dp[p][1]+=min(dp[v][1],dp[v][2]);
val=min(val,dp[v][2]-min(dp[v][1],dp[v][2]));
dp[p][2]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
}
dp[p][1]+=val,dp[p][2]+=w[p];
return ;
}
int main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i) {
int u,tot;
scanf("%d",&u);scanf("%d%d",&w[u],&tot);
for(int j=1;j<=tot;++j) {
int v;
scanf("%d",&v);
edge[u].push_back(v);
edge[v].push_back(u);
}
}
dfs(1,0);
printf("%d",min(dp[1][1],dp[1][2]));
}
VI. 加分二叉树
思路分析
伪装成树形 dp 的区间 dp(误)
不考虑树形 dp,考虑区间 dp 设 \(dp_{l,r}\) 表示将区间 \([l,r]\) 整合成一棵树时的最大价值,状态转移方程如下:
记得记录转移方式然后 dfs 输出先序遍历,时间复杂度 \(\Theta(n^3)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
long long f[50][50],root[50][50],val[50];
void print(int start,int end) {
if(start<=end) {
printf("%d ",root[start][end]);
if(start<end) {
print(start,root[start][end]-1);
print(root[start][end]+1,end);
}
}
return ;
}
int main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) {
scanf("%d",&val[i]);
root[i][i]=i;
f[i][i]=val[i];
}
for(int l=1;l<=n;l++) {
for(int i=1;i<=n-l;i++) {
int j=l+i;
f[i][j]=f[i+1][j]+val[i];
root[i][j]=i;
for(int k=i+1;k<j;k++) {
if(f[i][j]<f[i][k-1]*f[k+1][j]+val[k]) {
root[i][j]=k;
f[i][j]=f[i][k-1]*f[k+1][j]+val[k];
}
}
if(f[i][j]<f[i][j-1]+val[j]) {
root[i][j]=j;
f[i][j]=f[i][j-1]+val[j];
}
}
}
printf("%d\n",f[1][n]);
print(1,n);
printf("\n");
return 0;
}
VII. 旅游规划
思路分析
求树的直径覆盖的点,首先 dp 求出每个点的最长链和树的直径,然后 dfs 输出所有可能的答案即可,时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int MAXN=2e5+1;
int dp[MAXN][2],tr[MAXN][2],ans;
vector <int> edge[MAXN],res;
vector <pii> tmp[MAXN];
inline void dfs(int p,int f) {
for(int v:edge[p]) {
if(v==f) continue;
dfs(v,p);
tmp[p].push_back(make_pair(dp[v][0]+1,v));
}
sort(tmp[p].begin(),tmp[p].end(),greater<pii>());
if(tmp[p].size()>0) dp[p][0]=tmp[p][0].first;
if(tmp[p].size()>1) dp[p][1]=tmp[p][1].first;
ans=max(ans,dp[p][0]+dp[p][1]);
return ;
}
inline void go(int p) {
res.push_back(p);
if(tmp[p].empty()) return ;
go(tmp[p][0].second);
for(int i=1;i<tmp[p].size();++i) {
if(tmp[p][i].first!=tmp[p][0].first) break;
go(tmp[p][i].second);
}
}
inline void print(int p,int f) {
if(dp[p][0]+dp[p][1]==ans) {
res.push_back(p);
if(tmp[p].size()>0) {
go(tmp[p][0].second);
for(int i=1;i<tmp[p].size();++i) {
if(tmp[p][i].first!=tmp[p][0].first) break;
go(tmp[p][i].second);
}
}
if(tmp[p].size()>1) {
go(tmp[p][1].second);
for(int i=2;i<tmp[p].size();++i) {
if(tmp[p][i].first!=tmp[p][1].first) break;
go(tmp[p][i].second);
}
}
}
for(int v:edge[p]) {
if(v==f) continue;
print(v,p);
}
}
signed main() {
memset(tr,-1,sizeof(tr));
int n;
scanf("%d",&n);
for(int i=1;i<n;++i) {
int u,v;
scanf("%d%d",&u,&v);
edge[u].push_back(v);
edge[v].push_back(u);
}
dfs(0,-1);
print(0,-1);
sort(res.begin(),res.end());
res.erase(unique(res.begin(),res.end()),res.end());
for(int v:res) printf("%d\n",v);
return 0&puts("");
}
VIII. 周年纪念晚会
思路分析
没有上司的舞会,经典树形 dp 题,设 \(dp_{p,0/1}\) 表示第 \(p\) 个人不来或来其子树可以获得的最大价值,得到如下状态转移方程:
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
vector <int> v[6005];
int f[6005][2];
bool vis[6005],isRoot[6005];
void dp(int root) {
vis[root]=true;
for(int i=0;i<v[root].size();i++) {
if(!vis[v[root][i]]) {
dp(v[root][i]);
}
f[root][1]+=f[v[root][i]][0];
f[root][0]+=max(f[v[root][i]][0],f[v[root][i]][1]);
}
}
int main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) {
scanf("%d",&f[i][1]);
}
for(int i=1;i<n;i++) {
int l,k;
scanf("%d%d",&l,&k);
isRoot[l]=true;
v[k].push_back(l);
}
for(int i=1;i<=n;i++) {
if(!isRoot[i]) {
dp(i);
printf("%d\n",max(f[i][0],f[i][1]));
break;
}
}
return 0;
}
XI. 叶子的染色
思路分析
根节点的位置并不影响答案,所以任选一个非叶节点进行 dp,设 \(dp_{p,0/1/2}\) 分别表示 \(p\) 被染成白色/染成黑色/未被染色时以 \(p\) 为根的子树都被覆盖的最小代价,可以得到如下状态转移方程:
注:
- \(dp_{p,0}\) 从 \(dp_{v,0}\) 转移而来时,可以不用在 \(v\) 染色,因为 \(p\) 同样染色了,可以直接贡献 \(v\) 的子树,所以转移时 \(-1\),\(dp_{p,1}\) 的转移同理
- \(dp_{p,2}\) 的边界条件是 \(1\),因为 \(p\) 如果不染色,其他节点至少要有一个染色
最终答案是 \(\min(dp_{r,0},dp_{r,1},dp_{r,2})\)
时间复杂度 \(\Theta(m)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+1,INF=1e9;
int c[MAXN],dp[MAXN][3],n,m;
vector <int> edge[MAXN];
inline void dfs(int p,int f) {
if(p<=n) {
dp[p][c[p]]=dp[p][2]=1;
dp[p][c[p]^1]=INF;
} else {
dp[p][0]=dp[p][1]=1;
}
for(int v:edge[p]) {
if(v==f) continue;
dfs(v,p);
dp[p][0]+=min(dp[v][0]-1,min(dp[v][1],dp[v][2]));
dp[p][1]+=min(dp[v][1]-1,min(dp[v][0],dp[v][2]));
dp[p][2]+=min(dp[v][2],min(dp[v][0],dp[v][1]));
}
return ;
}
signed main() {
scanf("%d%d",&m,&n);
for(int i=1;i<=n;++i) scanf("%d",&c[i]);
for(int i=1;i<m;++i) {
int u,v;
scanf("%d%d",&u,&v);
edge[u].push_back(v);
edge[v].push_back(u);
}
dfs(n+1,0);
printf("%d\n",min(dp[n+1][2],min(dp[n+1][0],dp[n+1][1])));
return 0;
}
X. 骑士
思路分析
基环树上 dp,如果将每个骑士与仇恨的人连一条边,则每条边上至多选一个人,转化成普通 dp 即可,套用周年纪念晚会的模板即可
注意:
- 本题的图是基环树森林
- 基环树上 dp 需要先找到环,然后将环上任意两个节点断开进行 dp
- 如果将每个骑士单向连接至他的仇人,则基环树是一颗内向基环树,在内向基环树上找环更方便,不用构造无向边
- 如果将每个骑士单向连接至仇恨他的人,则基环树是一颗外向基环树,在外向基环树上 dp 更加方便
- dp 的时候如果通过环上的边连回树根时,不能从树根状态转移
- 为了方便起见,我们在 dp 的时候不选树根,防止环上的其他节点与树根互相仇恨
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN=1e6+1;
vector <int> edge[MAXN];
int f[MAXN],w[MAXN],dp[MAXN][2],root;
bool vis[MAXN];
inline void dfs(int p) {
vis[p]=true;
dp[p][0]=0,dp[p][1]=w[p];
for(int v:edge[p]) {
if(v==root) continue;
dfs(v);
dp[p][0]+=max(dp[v][0],dp[v][1]);
dp[p][1]+=dp[v][0];
}
return ;
}
signed main() {
int n,res=0; scanf("%lld",&n);
for(int i=1;i<=n;++i) {
int v;
scanf("%lld%lld",&w[i],&v);
edge[v].push_back(i);
f[i]=v;
}
for(int i=1;i<=n;++i) {
if(vis[i]) continue;
int x=i;
while(!vis[f[x]]) vis[x]=true,x=f[x];
int lst=0;
dfs(root=x); lst=dp[x][0];
dfs(root=f[x]);
res+=max(lst,dp[f[x]][0]);
}
printf("%lld\n",res);
return 0;
}
区间 dp
I. 石子合并
思路分析
首先拆环为链,然后再在链上考虑区间 dp,令 \(dp_{l,r,0/1}\) 分别表示将区间 \([l,r]\) 合并为一个元素后的最大/最小权值
不难的出如下状态转移方程:
按区间长度从小到大枚举,对于每个区间枚举中间断点 \(k\),时间复杂度 \(\Theta(n^3)\)
注意:拆环为链之后,每个长度为 \(n\) 的区间的 dp 值未必相等,而且可能同时作为答案,需要依次统计
代码呈现
#include<bits/stdc++.h>
using namespace std;
int fMin[210][210],fMax[210][210],sum[210],a[210];
int main() {
int n;
cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i];
a[i+n]=a[i];
}
for(int i=1;i<=n*2;i++) {
sum[i]=sum[i-1]+a[i];
}
for(int l=1;l<n;l++) {
for(int i=1;i<2*n-l;i++) {
int j=i+l;
fMin[i][j]=fMin[i][i]+fMin[i+1][j]+sum[j]-sum[i-1];
for(int k=i;k<j;k++) {
fMax[i][j]=max(fMax[i][k]+fMax[k+1][j]+sum[j]-sum[i-1],fMax[i][j]);
fMin[i][j]=min(fMin[i][k]+fMin[k+1][j]+sum[j]-sum[i-1],fMin[i][j]);
}
}
}
int ansMax=0,ansMin=fMin[1][n];
for(int i=1;i<=n;i++) {
ansMax=max(ansMax,fMax[i][i+n-1]);
ansMin=min(ansMin,fMin[i][i+n-1]);
}
cout<<ansMin<<endl<<ansMax<<endl;
return 0;
}
II. 能量项链
思路分析
和上一题一样的区间 dp 模板题,直接拆环为链,令 \(dp_{l,r}\) 为合并区间 \([l,r]\) 获得的最大价值,状态转移方程式如下:
时间复杂度 \(\Theta(n^3)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e2+1;
int dp[MAXN][MAXN],s[MAXN],ans;
int main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i) {
scanf("%d",&s[i]);
s[i+n]=s[i];
}
int m=n<<1;
for(int l=1;l<m;++l) {
for(int i=1;i+l<=m;++i) {
int j=i+l;
for(int k=i;k<j;++k) {
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+s[i]*s[j+1]*s[k+1]);
}
}
}
for(int i=1;i<=n;++i) {
ans=max(ans,dp[i][i+n-1]);
}
printf("%d\n",ans);
return 0;
}
III. 凸多边形的划分
思路分析
同样环上区间 dp,但是不用拆环为链
注意到如果将凸多边形的第 \(l\) 个顶点到第 \(r\) 个顶点分割成若干个小三角形,则必然连接顶点 \(l,r\),等价于将顶点区间 \([l,r]\) 看成一个新的凸多边形
所以可以令 \(dp_{l,r}\) 为将第 \(l\) 个顶点到第 \(r\) 个顶点分割成若干个小三角形的最小代价,状态转移方程如下:
记得开 __int128
或者高精,时间复杂度 \(\Theta(n^3)\)
代码呈现
#include<bits/stdc++.h>
#define ll __int128
const ll INF=1e30;
inline ll read() {
ll x=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) {
x=x*10+(ch-'0');
ch=getchar();
}
return x;
}
inline void write(ll x) {
if(x>=10) write(x/10);
putchar((char)(x%10+'0'));
return ;
}
ll a[55],dp[55][55];
signed main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i) a[i]=read();
for(int len=2;len<n;++len) {
for(int l=1,r=l+len;r<=n;++l,++r) {
dp[l][r]=INF;
for(int i=l+1;i<=r-1;++i) {
dp[l][r]=std::min(dp[l][r],dp[l][i]+dp[i][r]+a[i]*a[l]*a[r]);
}
}
}
write(dp[1][n]);
return 0;
}
IV. 括号配对
思路分析
很像 CSP-S2021 T2 的题,同样考虑区间 dp,设 \(dp_{l,r}\) 表示使得 \([l,r]\) 变成 GBE 的最小操作次数,直接枚举断点得到如下状态转移:
类似 CSP-S2021 T2,每次转移的时候同样考虑需不需要将两边的括号通过添加来配对,得到如下方案:
综合两种转移,时间复杂度 \(\Theta(n^3)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
char s[105];
int dp[105][105];
signed main() {
int n;
scanf("%s",s+1);
n=strlen(s+1);
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;++i) dp[i][i]=1;
for(int i=1;i<n;++i) dp[i+1][i]=0;
for(int len=1;len<n;++len) {
for(int l=1,r=l+len;r<=n;++l,++r) {
if(s[l]=='('&&s[r]==')') dp[l][r]=min(dp[l][r],dp[l+1][r-1]);
else if(s[l]=='(') dp[l][r]=min(dp[l][r],dp[l+1][r]+1);
else if(s[r]==')') dp[l][r]=min(dp[l][r],dp[l][r-1]+1);
if(s[l]=='['&&s[r]==']') dp[l][r]=min(dp[l][r],dp[l+1][r-1]);
else if(s[l]=='[') dp[l][r]=min(dp[l][r],dp[l+1][r]+1);
else if(s[r]==']') dp[l][r]=min(dp[l][r],dp[l][r-1]+1);
for(int k=l;k<r;++k) dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]);
}
}
printf("%d\n",dp[1][n]);
return 0;
}
V. 分离与合体
思路分析
考验阅读理解的区间 dp 题(误)
同样设 \(dp_{l,r}\) 表示取得 \([l,r]\) 之间每个钥匙的最大价值,状态转移方程如下:
记得纪录转移方案,输出方案的时候请使用 BFS,时间复杂度 \(\Theta(n^3)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
int a[301],dp[301][301],f[301][301];
signed main() {
int n;
scanf("%lld",&n);
for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
memset(dp,-0x3f,sizeof(dp));
for(int i=1;i<=n;++i) dp[i][i]=0;
for(int len=1;len<n;++len) {
for(int l=1,r=l+len;r<=n;++l,++r) {
for(int k=l;k<r;++k) {
int w=dp[l][k]+dp[k+1][r]+(a[l]+a[r])*a[k];
if(w>dp[l][r]) dp[l][r]=w,f[l][r]=k;
}
}
}
printf("%lld\n",dp[1][n]);
queue <pii> q;
q.push(make_pair(1,n));
while(!q.empty()) {
pii p=q.front();q.pop();
if(p.first==p.second) continue;
int tr=f[p.first][p.second];
printf("%lld ",tr);
q.push(make_pair(p.first,tr));
q.push(make_pair(tr+1,p.second));
}
return 0&putchar('\n');
}
VI. 矩阵取数游戏
思路分析
经典区间 dp,对于每行分开考虑,设 \(dp_{i,l,r}\) 将 \([l,r]\) 这个区间看做一整行的答案,每次转移的时候,之前获得的答案要记得 \(\times2\)
本题对 \(2^i\) 的处理并没有直接体现,而是体现在从短区间转移到长区间时的 \(\times2\),时间复杂度 \(\Theta(n^2)\)
另注:本题需要 __int128
或高精
代码呈现
#include<bits/stdc++.h>
using namespace std;
__int128 ans=0,f[100][100],mp[100][100];
__int128 read() {
__int128 x=0;
int f=1;
char input=getchar();
while(!isdigit(input)) {
if(input=='-') {
f=-1;
}
input=getchar();
}
while(isdigit(input)) {
x=x*10+input-'0';
input=getchar();
}
return x*f;
}
void write(__int128 x) {
if(x>10) {
write(x/10);
}
putchar(x%10+'0');
return ;
}
int main() {
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
mp[i][j]=read();
}
}
for(int row=1;row<=n;row++) {
for(int i=1;i<=m;++i) f[i][i]=2*mp[row][i];
for(int l=1;l<=m;l++) {
for(int i=1;i+l<=m;i++) {
int j=i+l;
f[i][j]=max(2*f[i+1][j]+2*mp[row][i],2*f[i][j-1]+2*mp[row][j]);
}
}
ans+=f[1][m];
}
write(ans);
return 0;
}
单调队列优化 dp
I. 滑动窗口
思路分析
单调队列模板题,在维护的队列中元素满足单调性的前提下,记得从队首按时序要求删除不符合要求的答案
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+1;
struct node {
int data,rank;
};
deque <node> q[2];
int ans[MAXN][2];
int main()
{
int n,m,input,turn=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%d",&input);
while(!q[0].empty()&&q[0].back().data<=input) q[0].pop_back();
q[0].push_back((node){input,i});
while(!q[1].empty()&&q[1].back().data>=input) q[1].pop_back();
q[1].push_back((node){input,i});
if(i>m)
{
++turn;
while(q[0].front().rank<=turn) q[0].pop_front();
ans[turn][0]=q[0].front().data;
while(q[1].front().rank<=turn) q[1].pop_front();
ans[turn][1]=q[1].front().data;
}
else if(i==m) {
ans[0][0]=q[0].front().data;
ans[0][1]=q[1].front().data;
}
}
for(int i=0;i<=turn;i++) printf("%d ",ans[i][1]);
puts("");
for(int i=0;i<=turn;i++) printf("%d ",ans[i][0]);
puts("");
return 0;
}
II. 最大连续和
思路分析
设 \(dp_i\) 表示以第 \(i\) 个元素为结尾的序列中,长度不超过 \(m\) 的序列中的最大和, \(sum_i\) 表示数组 \(a_i\) 的前缀和,不难得到如下转移方程:
不难看出状态转移时只需要对于权值 \(sum_i\) 维护一个长度为 \(m\) 的递增单调队列即可,答案为 \(\max\limits_{i=1}^n\{dp_i\}\)
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+1,INF=1e18;
int s[MAXN],q[MAXN];
signed main() {
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;++i) scanf("%lld",&s[i]),s[i]+=s[i-1];
int head=1,tail=1,res=-INF;
for(int i=1;i<=n;++i) {
while(head<=tail&&i-q[head]>m) ++head;
if(head<=tail) res=max(res,s[i]-s[q[head]]);
while(head<=tail&&s[q[tail]]>s[i]) --tail;
q[++tail]=i;
}
printf("%lld\n",res);
return 0;
}
III. 修剪草坪
思路分析
设 \(dp_{i,0/1}\) 表示考虑到第 \(i\) 个位置不选/选时前 \(i\) 位的答案,\(sum_i\) 为 \(E_i\) 的前缀和,状态转移 \(dp_{i,1}\) 时,枚举上一个没有选择的位置即可,状态转移方程如下:
对于 \(dp_{i,1}\) 的转移,可以通过对于权值 \(dp_{i,0}-sum_i\) 维护一个长度为 \(k\) 的递减单调队列即可
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+1;
int s[MAXN],q[MAXN],dp[MAXN][2];
signed main() {
int n,k;
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;++i) scanf("%lld",&s[i]),s[i]+=s[i-1];
int head=0,tail=0;
for(int i=1;i<=n;++i) {
dp[i][0]=max(dp[i-1][1],dp[i-1][0]);
while(head<=tail&&q[head]<i-k) ++head;
dp[i][1]=dp[q[head]][0]+s[i]-s[q[head]];
while(head<=tail&&dp[q[tail]][0]+s[i]-s[q[tail]]<dp[i][0]) --tail;
q[++tail]=i;
}
printf("%lld",max(dp[n][0],dp[n][1]));
return 0;
}
IV. 旅行问题
思路分析
顺时针逆时针相似,这里以顺时针为例,环上问题,首先考虑拆环为链,然后预处理出从 \(1\) 出发到达第 \(i+1\) 个点时耗费的的油量,此时如果判断从点 \(k\) 出发是否可行,则需满足 \(\forall i\in[k,k+n],s_i\ge s_{k-1}\),不难发现只需要对于权值 \(s_i\) 维护一个长度为 \(n\) 的递增单调队列即可
注意解决逆时针时点 \(i\) 到下一个点的距离是 \(d_{i-1}\)
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e6+1;
int s[MAXN],q[MAXN],p[MAXN],d[MAXN],n;
bool ok[MAXN];
signed main() {
scanf("%lld",&n);
for(int i=1;i<=n;++i) scanf("%lld%lld",&p[i],&d[i]);
for(int i=1;i<=n;++i) s[i]=s[i-1]+p[i]-d[i];
for(int i=1;i<=n;++i) s[i+n]=s[i+n-1]+p[i]-d[i];
int head=1,tail=0;
for(int i=1;i<=n;++i) {
while(head<=tail&&s[q[tail]]>s[i]) --tail;
q[++tail]=i;
}
for(int i=1;i<=n;++i) {
while(head<=tail&&q[head]<i) ++head;
if(s[q[head]]>=s[i-1]) ok[i]=true;
while(head<=tail&&s[q[tail]]>s[i+n]) --tail;
q[++tail]=i+n;
}
int tmp=d[n];
for(int i=n;i>1;--i) d[i]=d[i-1];
d[1]=tmp;
reverse(p+1,p+n+1);reverse(d+1,d+n+1);
for(int i=1;i<=n;++i) s[i]=s[i-1]+p[i]-d[i];
for(int i=1;i<=n;++i) s[i+n]=s[i+n-1]+p[i]-d[i];
head=1,tail=0;
for(int i=1;i<=n;++i) {
while(head<=tail&&s[q[tail]]>s[i]) --tail;
q[++tail]=i;
}
for(int i=1;i<=n;++i) {
while(head<=tail&&q[head]<i) ++head;
if(s[q[head]]>=s[i-1]) ok[n-i+1]=true;
while(head<=tail&&s[q[tail]]>s[i+n]) --tail;
q[++tail]=i+n;
}
for(int i=1;i<=n;++i) {
if(ok[i]) puts("TAK");
else puts("NIE");
}
return 0;
}
V. Banknotes
思路分析
简单的多重背包模板,单调队列或二进制优化维护都可以,这里选择的是二进制优化
将一件价值为 \(b_i\),有 \(c_i\) 个的物品分别拆成价值 \(2^0\times b_i,2^1\times b_i,2^2\times b_i\cdots\) 的物品,得到新的若干件货币的价值 \(v_i\) 与其权重 \(w_i\)(即其个数),然后采用 01 背包的策略进行优化
设 \(dp_{i,j}\) 表示前 \(i\) 件物品凑出 \(j\) 的面值的最小花费,得到如下状态转移方程:
通过滚动数组优化掉第一维即可
时间复杂度 \(\Theta(kn\log n)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e3+1,MAXK=2e4+1;
int w[MAXN],v[MAXN],dp[MAXK];
int b[MAXN],c[MAXN];
signed main() {
memset(dp,0x3f,sizeof(dp));
int n,cnt=0,k;
scanf("%lld",&n);
for(int i=1;i<=n;++i) scanf("%lld",&b[i]);
for(int i=1;i<=n;++i) scanf("%lld",&c[i]);
scanf("%lld",&k);
for(int i=1;i<=n;++i) {
for(int t=1;t<=c[i];t=(t<<1)) {
w[++cnt]=b[i]*t;
v[cnt]=t;
c[i]-=t;
}
if(c[i]) {
w[++cnt]=b[i]*c[i];
v[cnt]=c[i];
}
}
dp[0]=0;
for(int i=1;i<=cnt;++i) {
for(int j=k;j>=w[i];--j) {
dp[j]=min(dp[j],dp[j-w[i]]+v[i]);
}
}
printf("%lld\n",dp[k]);
return 0;
}
VI. 烽火传递
思路分析
类似第三题,设 \(dp_{i,0/1}\) 表示第 \(i\) 个位置不设置或设置时,前 \(i\) 个烽火台的最小花费,得到如下状态转移方程
状态转移时以 \(dp_{i,1}\) 作为权值维护一个长度为 \(k\) 的递减单调队列即可
时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+1;
int a[MAXN],dp[MAXN][2],q[MAXN];
signed main() {
memset(dp,0x3f,sizeof(dp));
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
dp[0][0]=dp[0][1]=0;
int head=1,tail=1;
for(int i=1;i<=n;++i) {
dp[i][1]=min(dp[i-1][0],dp[i-1][1])+a[i];
while(head<=tail&&q[head]<=i-m) ++head;
dp[i][0]=dp[q[head]][1];
while(head<=tail&&dp[q[tail]][1]>dp[i][1]) --tail;
q[++tail]=i;
}
printf("%lld\n",min(dp[n][0],dp[n][1]));
return 0;
}
VII. 绿色通道
思路分析
简单题,再上一题的基础 dp 上用二分枚举最长空串长度即可,用耗时检验是否可行
时间复杂度 \(\Theta(n\log n)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e4+1;
int a[MAXN],dp[MAXN][2],q[MAXN],n,t;
inline bool check(int v) {
memset(dp,0x3f,sizeof(dp));
dp[0][0]=dp[0][1]=0;
int head=1,tail=1;
for(int i=1;i<=n;++i) {
dp[i][1]=min(dp[i-1][0],dp[i-1][1])+a[i];
while(head<=tail&&q[head]<i-v) ++head;
dp[i][0]=dp[q[head]][1];
while(head<=tail&&dp[q[tail]][1]>dp[i][1]) --tail;
q[++tail]=i;
}
return min(dp[n][0],dp[n][1])<=t;
}
signed main() {
scanf("%lld%lld",&n,&t);
for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
int l=0,r=n,res=-1;
while(l<=r) {
int mid=(l+r)>>1;
if(check(mid)) res=mid,r=mid-1;
else l=mid+1;
}
printf("%lld\n",res);
return 0;
}
VII. 理想的正方形
思路分析
对于每一行维护一个长度为 \(n\) 的单调队列,求出每个数所在行往前 \(n\) 位的区间中的最大最小值
然后枚举正方形单位右下角点,查询向上 \(k\) 行的最大最小值暴力即可
时间复杂度 \(\Theta(a\times b\times n)\)
代码呈现
#include<bits/stdc++.h>
const int MAXN=1001,INF=1e9;
int min[MAXN][MAXN],max[MAXN][MAXN];
int a[MAXN][MAXN],q[MAXN];
signed main() {
int n,m,k,head,tail;
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;++i) {
for(int j=1;j<=m;++j) {
scanf("%d",&a[i][j]);
}
}
for(int i=1;i<=n;++i) {
head=1,tail=0;
for(int j=1;j<=m;++j) {
while(head<=tail&&q[head]<=j-k) ++head;
min[i][j]=a[i][j];
if(head<=tail) min[i][j]=std::min(min[i][j],a[i][q[head]]);
while(head<=tail&&a[i][q[tail]]>a[i][j]) --tail;
q[++tail]=j;
}
}
for(int i=1;i<=n;++i) {
head=1,tail=0;
for(int j=1;j<=m;++j) {
while(head<=tail&&q[head]<=j-k) ++head;
max[i][j]=a[i][j];
if(head<=tail) max[i][j]=std::max(max[i][j],a[i][q[head]]);
while(head<=tail&&a[i][q[tail]]<a[i][j]) --tail;
q[++tail]=j;
}
}
int res=INT_MAX;
for(int i=k;i<=n;++i) {
for(int j=k;j<=m;++j) {
int maxv=-INF,minv=INF;
for(int t=i-k+1;t<=i;++t) {
maxv=std::max(maxv,max[t][j]);
minv=std::min(minv,min[t][j]);
}
res=std::min(res,maxv-minv);
}
}
printf("%d\n",res);
return 0;
}
IX. 股票交易
思路分析
类似背包(大概?)的一道 dp 题,思路较简单,细节处理和代码实现有一定难度
设 \(dp_{i,j}\) 表示在第 \(i\) 天持有 \(j\) 支股票所获得的最大利润
边界条件:
考虑按不同操作进行分类讨论状态转移
- 第 \(i\) 天什么都不做
此时的状态转移方程最简单,直接从前一天转移过来即可:
- 第 \(i\) 天首次买入股票
此时状态转移直接从 \(0\) 只股票买到 \(j\) 只股票,注意购买股票数量的限制
- 第 \(i\) 天买股票
由于两次买股票有时间限制,所以从第 \(i-w-1\) 天的状态转移,枚举之前的股票数
不难发现状态转移时只需要对于权值 \(dp_{i-w-1,k}+k\times AP_i\) 维护一个长度为 \(AS_i\) 的单调队列即可
- 第 \(i\) 天卖股票
同上,可得状态转移方程:
同样维护一个长度为 \(BS_i\) 的单调队列即可,注意枚举状态 \(j\) v的时候要倒序
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2001;
int dp[MAXN][MAXN],q[MAXN];
signed main() {
memset(dp,-0x3f,sizeof(dp));
int n,m,k,head,tail;
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;++i) {
int ap,bp,as,bs;
scanf("%d%d%d%d",&ap,&bp,&as,&bs);
for(int j=0;j<=as;++j) dp[i][j]=max(dp[i][j],-j*ap);
for(int j=0;j<=m;++j) dp[i][j]=max(dp[i][j],dp[i-1][j]);
if(i<=k) continue;
head=1,tail=0;
for(int j=0;j<=m;++j) {
while(head<=tail&&q[head]<j-as) ++head;
while(head<=tail&&dp[i-k-1][q[tail]]+q[tail]*ap<dp[i-k-1][j]+j*ap) --tail;
q[++tail]=j;
if(head<=tail) dp[i][j]=max(dp[i][j],dp[i-k-1][q[head]]+q[head]*ap-j*ap);
}
head=1,tail=0;
for(int j=m;j>=0;--j) {
while(head<=tail&&q[head]>j+bs) ++head;
while(head<=tail&&dp[i-k-1][q[tail]]+q[tail]*bp<dp[i-k-1][j]+j*bp) --tail;
q[++tail]=j;
if(head<=tail) dp[i][j]=max(dp[i][j],dp[i-k-1][q[head]]+q[head]*bp-j*bp);
}
}
int res=INT_MIN;
for(int j=0;j<=m;++j) res=max(res,dp[n][j]);
printf("%d\n",res);
return 0;
}