动态规划 学习笔记
动态规划
线性
Luogu P1541
直接枚举每种棋子使用的次数。转移时可能从四个地方转移上来,分别判断。
d[0][0][0][0]=a[1];
for(int i=0;i<=x[1];i++)for(int j=0;j<=x[2];j++)for(int k=0;k<=x[3];k++)for(int l=0;l<=x[4];l++){
int t=a[1+i+j*2+k*3+l*4];
if(i)d[i][j][k][l]=max(d[i][j][k][l],d[i-1][j][k][l]+t);
if(j)d[i][j][k][l]=max(d[i][j][k][l],d[i][j-1][k][l]+t);
if(k)d[i][j][k][l]=max(d[i][j][k][l],d[i][j][k-1][l]+t);
if(l)d[i][j][k][l]=max(d[i][j][k][l],d[i][j][k][l-1]+t);
}
Luogu P1868
将所有区间按照右端点排序。对于每个区间,二分查找在它之前最近的那个区间。进行转移,在前一个区间和相邻区间中取最大值。
for(int i=1;i<=n;i++){
int l=a[i].first;
int p=lower_bound(a+1,a+1+i,make_pair(0,l),cmp)-(a+1);
d[i]=max(d[i-1],d[p]+a[i].second-a[i].first+1);
}
YZOJ P6439
先考虑每行内的选择情况:定义 g[j][k][l]
为这一行内前 j
个数选了 k
个模为 l
的数时最大的和。
memset(g,-0x3f3f,sizeof(g)),g[0][0][0]=0;
for(int j=1;j<=m;j++){
for(int k=0;k<=j;k++){
for(int l=0;l<p;l++){
g[j][k][l]=g[j-1][k][l];
if(k)g[j][k][l]=max(g[j][k][l],g[j-1][k-1][(l-a[i][j]%p+p)%p]+a[i][j]);
}
}
}
接下来是行之间的转移,考虑 f[i][j]
为前 i
行中选数模为 j
的最大的和,在每行求完 g
数组后转移,需要枚举这一行内选数的数量。
for(int j=0;j<p;j++){
for(int k=0;k<=m/2;k++){
for(int l=0;l<p;l++){
f[i][j]=max(f[i][j],f[i-1][(j-l+p)%p]+g[m][k][l]);
}
}
}
Luogu P2679
定义 f[i][j][k][1/0]
为 A 在 i,B 在 j,目前有 k 个串时的方案数,最后一维表示是否匹配。
转移:对于不匹配的,从 i-1 的两种方案转移过来,j 和 k 都不变。
如果两个字符相等,那么可以从三种情况转移:上一位不匹配、这一位新开始匹配;上一位匹配,这一位继续;上一位匹配,这一位划分新的串。否则能匹配方案数为 0。
滚掉第一维。
f[0][0][0][0]=f[1][0][0][0]=1;
for(int i=1;i<=n;i++,v^=1){
for(int j=1;j<=m;j++){
for(int k=1;k<=K;k++){
f[v][j][k][0]=(f[v^1][j][k][0]+f[v^1][j][k][1])%mod;
if(a[i]==b[j])f[v][j][k][1]=(f[v^1][j-1][k-1][0]+f[v^1][j-1][k-1][1]+f[v^1][j-1][k][1])%mod;
else f[v][j][k][1]=0;
}
}
}
背包
Luogu P1156
把垃圾按照时间排序。每次可以选择增加高度或者增长生命,即 f[i][j]+=b
或 f[i][j+a]=f[i][j]
。实际上使用被动转移。
mxa
是生命值最大值,mxb
是高度最大值。若最大高度大于总高度则能爬出,否则最长存活时间就是生命最大值。
memset(f,-0x3f3f,sizeof(f)),f[0][10]=0,mxa=10;
sort(s+1,s+1+m,cmp);
for(int i=1;i<=m;i++){
int tmp=mxa;
for(int j=s[i].t;j<=tmp;j++){
f[i][j]=max(f[i][j],f[i-1][j]+s[i].b),mxb=max(mxb,f[i][j]);
f[i][j+s[i].a]=max(f[i][j+s[i].a],f[i-1][j]),mxa=max(mxa,j+s[i].a);
}
}
Luogu U224668
二进制优化的多重背包。需要提前选出一个,把额外价值加上,其余的按二进制分组。
for(int i=1;i<=m;i++){
int a=read(),b=read(),c=read(),d=read(),k=1;
v[++n]=c,w[n]=b+d,a--;
while(a>=k)v[++n]=c*k,w[n]=b*k,a-=k,k*=2;
if(a)v[++n]=c*a,w[n]=b*a;
}
for(int i=1;i<=n;i++)for(int j=t;j>=v[i];j--)f[j]=max(f[j],f[j-v[i]]+w[i]);
区间
Luogu P3146
考虑 f[l][r]
表示区间 [l,r]
全部合并得到的值,无法合并则为 0。进行区间 dp,对于两个区间的值相同且不为 0 就更新。
初始值 f[i][i]=a[i]
,统计答案初值应当设为单点最大值(防止完全不能合并),在所有区间中寻找最大值([1,n]
不一定能合并)。
for(int i=1;i<=n;i++)f[i][i]=read(),ans=max(ans,f[i][i]);
for(int len=2;len<=n;len++)for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
for(int k=i;k<j;k++)if(f[i][k]==f[k+1][j]&&f[i][k]){
f[i][j]=max(f[i][j],f[i][k]+1),ans=max(ans,f[i][j]);
}
}
Luogu P1063
拆环。合并的长度从 2 到 n+1(尾巴和头接)。d[i][j]
为 [i,j]
合并结果,从 d[i][k]+d[k][j]
合并,k
应当被合并两次。
细节,第三层循环时 k 取值 [i+1,j)
(?)
for(int len=2;len<=n+1;len++)for(int i=1;i+len-1<=2*n;i++){
int j=i+len-1;
for(int k=i+1;k<j;k++){
d[i][j]=max(d[i][j],d[i][k]+d[k][j]+a[i]*a[k]*a[j]);
ans=max(ans,d[i][j]);
}
}
Luogu P1005
每行操作互不影响。定义 f[i][j]
表示当前未选走的区间为 [i,j]
(因为选数是从两头进行的,中间必然连续)。
考虑顺推,在 [i,j]
可以取成 [i+1,j]
和 [i,j-1]
,转移。统计答案时要注意把最后一个数选上。
不能贪心的原因:最大的数不一定会是最后选的,例如 9 9 9 1 1 10
会考虑先把 10 选走,以便于选走三个 1,最后选 9。
for(int len=m;len>1;len--)for(int i=1;i+len-1<=m;i++){
int j=i+len-1,t=(int)1<<(m-len+1);
f[i+1][j]=max(f[i+1][j],f[i][j]+t*a[i]);
f[i][j-1]=max(f[i][j-1],f[i][j]+t*a[j]);
}
for(int i=1,t=((int)1<<m);i<=m;i++)tmp=max(tmp,f[i][i]+t*a[i]);
ans+=tmp;
树形
Luogu P1122
树上 dfs,判断如果选取自己的子树是否优,更优则更新。
void dfs(int u,int fa){
for(auto v:tv[u]){
if(v==fa)continue;
dfs(v,u);
if(f[v]>0)f[u]+=f[v];
}
f[u]+=a[u],ans=max(ans,f[u]);
}
状态压缩
Luogu P1896
将每行的状态(只有0和1)压缩到一个 int 中。
void dfs(int x,int f,int sum){
if(x>=n){
sit[++ccnt]=f,g[ccnt]=sum;
return ;
}
dfs(x+2,f+(1<<x),sum+1),dfs(x+1,f,sum);
}
signed main(){
n=read(),m=read(),dfs(0,0,0);
for(int i=1;i<=ccnt;i++)dp[1][i][g[i]]=1;
for(int i=2;i<=n;i++)for(int j=1;j<=ccnt;j++)for(int k=1;k<=ccnt;k++){
if((sit[j]|sit[j]<<1|sit[j]>>1)&sit[k])continue;
for(int l=m;l>=g[j];l--)dp[i][j][l]+=dp[i-1][k][l-g[j]];
}
for(int i=1;i<=ccnt;i++)ans+=dp[n][i][m];
printf("%lld",ans);
return 0;
}
sit[i]
:每行的第 i 种情况的状态(压缩);g[i]
:第 i 种情况的国王数;dp[i][j][k]
:第 i 行选择了第 j 种情况、直到目前摆了 k 个国王的方案数。
首先预处理每行可能合法的摆放方法(使国王不相邻的所有可能情况)。然后枚举每一行 i 的情况 j 和上一行的情况 k,若 j 和 k 不冲突则转移 dp[i][j][ ]+=dp[i-1][k][ ]
,其中第三维(摆的国王数量)需要在转移时枚举。
数位
YZOJ P6510
数位dp,状态同时记录 数位和 和 数位平方和。
vector<int>pr;
bool np[MAXN+1];//素数筛
int n,a[21];
int f[21][MAXN][MAXN];//记忆化
int dfs(int x,bool lmt,int sum1,int sum2){
if(!x)return np[sum1]&&np[sum2];
if(!lmt&&f[x][sum1][sum2]!=-1)return f[x][sum1][sum2];
int up=lmt?a[x]:9,res=0;
for(int i=0;i<=up;i++)res+=dfs(x-1,lmt&&(i==a[x]),sum1+i,sum2+i*i);
if(!lmt&&sum1&&sum2)f[x][sum1][sum2]=res;
return res;
}
int getans(int x){
n=0;
while(x)a[++n]=x%10,x/=10;
return dfs(n,1,0,0);
}
signed main(){
memset(f,-1,sizeof(f));
int T=read();
while(T--){
int x=read(),y=read();
cout<<getans(y)-getans(x-1)<<endl;
}
return 0;
}