线性,背包,区间DP例题
P1282多米诺骨牌
容易发现一个性质:对于前\(i\)个牌子,它们的点数总和加起来是一个定值。所以,设\(f[i][j]\)表示前\(i\)个多米诺骨牌的第一行的和为j时的最小旋转次数。
状态转移方程:
初始化:
\(f[1][a[1]]=0;f[1][b[1]]=1;\)其余全部是正无穷
\(code:\)
int k(int x){
return x>0?x:-x;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d%d",&a[i],&b[i]);
sum+=a[i]+b[i];
}
for(int i=1;i<=n;++i)
for(int j=0;j<=sum;++j)
f[i][j]=1e9;
f[1][a[1]]=0;f[1][b[1]]=1;
for(int i=2;i<=n;++i)
for(int j=0;j<=sum;++j){
f[i][j]=1e9;
if(j>=a[i])
f[i][j]=min(f[i][j],f[i-1][j-a[i]]);
if(j>=b[i])
f[i][j]=min(f[i][j],f[i-1][j-b[i]]+1);
}
minn=1e9;
for(int i=0;i<=sum;++i)
if(f[n][i]!=1e9&&k(sum-i-i)<minn)
minn=k(sum-i*2),ans=f[n][i];
else if(f[n][i]!=1e9&&k(sum-i-i)==minn)
ans=min(ans,f[n][i]);
printf("%d\n",ans);
return 0;
}
P4138挂饰
设\(f[i][j]\)表示前\(i\)个物品,剩余
至少\(j\)个空挂钩时的最大喜悦值。
状态转移方程:
初始化:\(f[0][1]=0\),其余全部是负无穷
需要注意的是,在\(DP\)前要按照挂钩的数量从大到小排序,因为排序之后才能使挂钩数量尽可能多,越有可能使得结果更优。如果不排序,可能很快就没有了挂钩,错过了后面权值特别大的点。
\(code:\)
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d%d",&p[i].a,&p[i].b);
sort(p+1,p+n+1,cmp);
for(int i=0;i<=n;++i)
for(int j=0;j<=n+1;++j)
f[i][j]=-1e9;
f[0][1]=0;
for(int i=1;i<=n;++i){
for(int j=n;j>=0;--j){
f[i][j]=max(f[i-1][j],f[i-1][max(j-p[i].a,0)+1]+p[i].b);
}
}
for(int i=0;i<=n;++i)
ans=max(ans,f[n][i]);
printf("%d\n",ans);
return 0;
}
P2679子串
设\(f[i][j][k]\)表示从A串的前i个字符中取k个不重复子串,拼接起来后与B的前j个子串相等的方案数量。
状态转移方程:
\(f[i][j][k]=\)
\(f[i-1][j][k],if(a[i]!=b[j])\)
\(f[i-1][j][k]+f[i-1][j-1][k-1],if(a[i]==b[j],a[i-1]!=b[j-1])\)
\(f[i-1][j][k]+f[i-1][j-1][k-1]+f[i-1][j-2][k-1],if(a[i]==b[j],a[i-1]==b[j-1],a[i-2]!=b[j-2])\)
......
如果直接算,时间复杂度是\(O(nm^2k)\),明显超时。这时我们可以用前缀和来优化\(DP\),时间复杂度会降至\(O(nmk)\)。
另外,此时的空间复杂度为\(1000\times200\times 200\times2\)(开\(long\) \(long\)),也就是 \(8e7\) ,也会爆炸。观察式子,我们可以发现每一次转移都是\(f[i-1][...]\)转移到\(f[i][...]\),所以我们可以直接滚掉第一维,将空间复杂度优化到 \(200\times200\times2\) 。
\(code:\)
int main(){
cin>>n>>m>>k;
cin>>a>>b;
sum[0][0]=1;
for(ll i=1;i<=n;++i){
for(ll j=min(i,m);j>=1;--j){
for(ll p=min(j,k);p>=1;--p){
if(a[i-1]==b[j-1])
f[j][p]=(f[j-1][p]+sum[j-1][p-1])%mod;
else
f[j][p]=0;
sum[j][p]=(sum[j][p]+f[j][p])%mod;//sum[i][j][l]维护f[1][j][l]~f[i][j][l]的前缀和
}
}
}
cout<<sum[m][k]%mod<<endl;
return 0;
}
P5662 [CSP-J2019] 纪念品
题目中有一句关键的话“每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。”我们可以根据这句话简化问题:把第\(i\)天买入,第\(j\)天卖出看作:第\(i\)天买入,第\(i+1\)天卖出,第\(i+1\)天买入,第\(i+2\)天卖出......第\(j-1\)天买入,第\(j\)天卖出。这时就可以将当天的价格看作体积,将当天与后一天的价格差当做价值,进行完全背包。
设 \(f[i][j][k]\) 表示第i天,前j个物品,手中的钱数为k时候的最大收益。状态转移方程:
考虑滚动数组,我们发现只需要保留\(k\)这一维即可。
\(code:\)
int main(){
cin>>t>>n>>m;
for(int i=1;i<=t;++i)
for(int j=1;j<=n;++j)
cin>>a[i][j];
ans=m;
for(int i=1;i<=t-1;++i){
for(int l=0;l<=ans;++l)
dp[l]=0;
for(int j=1;j<=n;++j){
for(int k=a[i][j];k<=ans;++k){
dp[k]=max(dp[k],dp[k-a[i][j]]-a[i][j]+a[i+1][j]);
}
}
ans+=dp[ans];
}
cout<<ans<<endl;
fclose(stdin);fclose(stdout);
return 0;
}
P4059找爸爸
首先要知道,两个序列的某一个位置都是空格的情况肯定不是最优解。因为题目中k的系数是负数,k增加会令答案更劣。所以对于一个位置只考虑三种情况:
1:\(A\)和\(B\)都不是空格
2:\(A\)是空格,\(B\)不是
3:\(A\)不是空格,\(B\)是
设\(dp[i][j][0/1/2]\)表示\(A\)的\(0\)$i-1$和$B$的$0$\(j-1\)匹配,且最后一位分别是情况\(1,2,3\)时的最优解。
初始化:\(dp[0][i][1]=-a-b*(i-1),dp[0][i][0]=dp[0][i][2]=-1e9\)
\(dp[i][0][2]=-a-b*(i-1),dp[i][0][0]=dp[i][0][1]=-1e9\)
\(dp[0][0][1]=dp[0][0][2]=-1e9\)
状态转移方程:
\(f[i][j][0]=max(f[i-1][j-1][0],max(f[i-1][j-1][1],f[i-1][j-1][2]))+d[x[i-1]][y[j-1]];\)
\(f[i][j][1]=max(f[i][j-1][0]-a,max(f[i][j-1][1]-b,f[i][j-1][2]-a));\)
\(f[i][j][2]=max(f[i-1][j][0]-a,max(f[i-1][j][1]-a,f[i-1][j][2]-b));\)
\(code:\)
int main(){
cin>>x>>y;
cin>>d['A']['A']>>d['A']['T']>>d['A']['G']>>d['A']['C'];
cin>>d['T']['A']>>d['T']['T']>>d['T']['G']>>d['T']['C'];
cin>>d['G']['A']>>d['G']['T']>>d['G']['G']>>d['G']['C'];
cin>>d['C']['A']>>d['C']['T']>>d['C']['G']>>d['C']['C'];
cin>>a>>b;
for(int i=0;i<y.size();++i){
f[0][i+1][1]=-a-b*i;
f[0][i+1][2]=f[0][i+1][0]=-1e9;
}
for(int i=0;i<x.size();++i){
f[i+1][0][2]=-a-b*i;
f[i+1][0][1]=f[i+1][0][0]=-1e9;
}
f[0][0][1]=f[0][0][2]=-1e9;
for(int i=0;i<x.size();++i)
for(int j=0;j<y.size();++j){
f[i+1][j+1][0]=max(f[i][j][0],max(f[i][j][1],f[i][j][2]))+d[x[i]][y[j]];
f[i+1][j+1][1]=max(f[i+1][j][0]-a,max(f[i+1][j][1]-b,f[i+1][j][2]-a));
f[i+1][j+1][2]=max(f[i][j+1][0]-a,max(f[i][j+1][1]-a,f[i][j+1][2]-b));
}
printf("%d\n",max(f[x.size()][y.size()][0],max(f[x.size()][y.size()][1],f[x.size()][y.size()][2])));
return 0;
}
P7961 [NOIP2021]数列
考虑按照 \(S\) 的二进制位数进行 \(DP\)
设 \(f[i][j][k][l]\) 表示当前是 \(S\) 从低到高的第 \(i\) 位,已经确定了 \(j\) 个序列 \(a\) 中的元素, \(S\) 从低到高前 \(i\) 位中有 \(k\) 个 \(1\) ,向第 \(i\) 位的下一位进位 \(l\) 。
如果从前面转移到 \(f[i][j][k][l]\) ,细节太多,不好写。所以考虑从 \(f[i][j][k][l]\) 往后转移。
假设当前序列 \(a\) 中分配了 \(t\) 个元素的值为 \(i\) ,加上上一位进过来的 \(l\) 个 \(1\) ,总共要向下一位进位\(t+l\),下一位进位的时候要进位 \((t+l)/2\) .
\(f[i][j][k][l]\) 往后要转移的状态是: \(f[i+1][j+t][k+(t+l)\) \(mod\) \(2][(l+p)/2]\)
状态转移方程:
\(f[i+1][j+t][k+(t+l)\) \(mod\) \(2][(l+p)/2]\)
\(=f[i][j][k][l]\times v^t_i\times C(n-j,t)\)
\(code:\)
long long bit1(long long x){
long long cnt=0;
while(x){
x-=x&(-x);
++cnt;
}
return cnt;
}
int main(){
scanf("%lld%lld%lld",&n,&m,&p);
for(int i=0;i<=m;++i){
scanf("%lld",&v[i]);
pv[i][0]=1;
for(int j=1;j<=n;++j)
pv[i][j]=pv[i][j-1]*v[i]%mod;
}
for(int i=0;i<=100;++i)
c[i][0]=1;
for(int i=1;i<=100;++i)
for(int j=1;j<=100;++j)
c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
f[0][0][0][0]=1;
for(int i=0;i<=m;++i)
for(int j=0;j<=n;++j)
for(int k=0;k<=p;++k)
for(int l=0;l<=(n>>1);++l)
for(int t=0;t<=n-j;++t)
f[i+1][j+t][k+((t+l)&1)][(l+t)>>1]=(f[i+1][j+t][k+((t+l)&1)][(l+t)>>1]+f[i][j][k][l]*c[n-j][t]%mod*pv[i][t]%mod)%mod;
for(int i=0;i<=p;++i)
for(int j=0;j<=(n>>1);++j)
if(i+bit1(j)<=p)
ans=(ans+f[m+1][n][i][j])%mod;
printf("%lld\n",ans);
return 0;
}
P2224产品加工
看到这道题,不难想到暴力DP:设 \(f[i][j][k]\) 表示前 \(i\) 个数,用机器 \(A\) 的时间和为 \(j\) ,用机器 \(B\) 的时间和为 \(k\) 的状态是否存在。 \(f[i][j][k]\) 可以由 \(f[i-1][j-a[i]][k]\) , \(f[i-1][j][k-b[i]]\) , \(f[i-1][j-c[i]][k-c[i]]\) 转移而来。并且不能发现,第一位是可以滚掉的。时间复杂度为 \(O(n^3)\)。
那么怎么优化呢?这道题采用了一种很新颖的方法:将答案设为状态。设 \(f[i][j]\) 表示前 \(i\) 个物品,用 \(A\) 做的时间为 \(j\) 时, \(B\) 做的时间最小值。
状态转移方程:
\(f[i][j]=min(f[i][j],f[i-1][j-a[i]],f[i-1][j]+b[i],f[i-1][j-c[i]]+c[i])\)
不难发现,在倒叙枚举 \(j\) 的情况下,第一维能够直接滚掉。
另外,直接写的时间复杂度为 \(O(6000 \times 30000)=O(1.8\times 10^8)\),仍有超时的风险。一个优化的策略是每次循环找到 \(j\) 的上界,而不是一直让 \(j\) 从 \(0\) 循环到 \(30000\) 。
\(code:\)
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d%d%d",&a[i],&b[i],&c[i]);
ans=1e9;
for(int i=1;i<=30000;++i)
f[i]=1e9;
for(int i=1;i<=n;++i){
up+=max(a[i],c[i]);
for(int j=up;j>=0;--j){
int x,y,z;x=y=z=1e9;
if(a[i]&&j>=a[i])
x=f[j-a[i]];
if(b[i])
y=f[j]+b[i];
if(c[i]&&j>=c[i])
z=f[j-c[i]]+c[i];
f[j]=min(x,min(y,z));
if(i==n)
ans=min(ans,max(j,f[j]));
}
}
printf("%d\n",ans);
return 0;
}
P1858多人背包
设 \(f[j][l]\) 表示体积为 \(j\) ,当前为第 \(l\) 优解的价值。
接下来考虑转移。首先外层枚举每个物品 \(i\) ,内层从大到小枚举体积 \(j\) 。然后我们发现, \(f[j][1]>f[j][2]>f[j][3]>...>f[j][k]\) ,且 \(f[j-v[i]][1]+w[i]>f[j-v[i]][2]+w[i]>...>f[j-v[i]][k]+w[i]\) 。所以我们就可以用归并排序的思想,找出其中最大的 \(k\) 个数,来依次更新 \(f[j][1],f[j][2]...f[j][k]\) 。
\(code:\)
for(int i=1;i<=n;++i){
for(int j=v;j>=a[i];--j){
int k1=1,k2=1,cnt=0;
for(int l=1;l<=k;++l)
now[j][l]=f[j][l];
while(cnt<=k){
if(f[j][k1]>f[j-a[i]][k2]+b[i]){
now[j][++cnt]=f[j][k1];
++k1;
}
else{
now[j][++cnt]=f[j-a[i]][k2]+b[i];
++k2;
}
}
for(int l=1;l<=k;++l)
f[j][l]=now[j][l];
}
}
方块消除 Blocks
经典的区间消除问题。
传统的想法是,设 \(dp[i][j]\) 表示消去区间 \([i,j]\) 的最小分数。然而,这样无法考虑到区间内部和区间外部的块一起消除的情况,无法满足最优子结构,并且不好转移,所以考虑重新设计状态。
因为内部的块会受到外部的块的影响,所以设 \(dp[i][j][k]\) 表示消除区间 \([i,j]\) 以及区间 \([i,j]\) 后面 \(k\) 个与 \(j\) 颜色相同的块的最大分数。
首先考虑第一种情况: \(k\) 个块和第 \(j\) 个块一起消除:\(dp[i][j][k]=max(dp[i][j][k],dp[i][j-1][0]+(k+1)\times (k+1))\)
第二种情况:区间 \([i,j]\) 内部有和 \(j\) 颜色相同的块,消掉它们中间的块,把它们拼接到一起,\(dp[i][j][k]=max(dp[i][j][k],dp[i][p][k+1]+dp[p+1][j-1][0])\)
\(code:\)
int dfs(int l,int r,int k){
if(l>r)
return 0;
if(dp[l][r][k])
return dp[l][r][k];
dp[l][r][k]=max(dp[l][r][k],dfs(l,r-1,0)+(k+1)*(k+1));
for(int p=nxt[r];p>=l;p=nxt[p])
dp[l][r][k]=max(dp[l][r][k],dfs(l,p,k+1)+dfs(p+1,r-1,0));
return dp[l][r][k];
}
signed main(){
scanf("%lld",&t);
while(t--){
scanf("%lld",&n);
for(int i=1;i<=n;++i)
scanf("%lld",&a[i]),nxt[i]=0;
for(int i=1;i<=n;++i)
for(int j=i-1;j>=1;--j)
if(a[j]==a[i])
{nxt[i]=j;break;}
for(int i=1;i<=n;++i)
for(int j=i;j<=n;++j)
for(int k=0;k<=n;++k)
dp[i][j][k]=0;
printf("Case %lld: %lld\n",++cnt,dfs(1,n,0));
}
return 0;
}

浙公网安备 33010602011771号