【题解】P4158 [SCOI2009]粉刷匠(DP,背包)
【题解】P4158 [SCOI2009]粉刷匠
是一道资源规划 DP 的好题,但是我想了很久还去看了题解。/kk我真菜。
题目链接
题意概述
发现自己实在不会简化题意于是就抄了原题(雾)。
windy 有 \(N\) 条木板需要被粉刷。 每条木板被分为 \(M\) 个格子。 每个格子要被刷成红色或蓝色。
windy 每次粉刷,只能选择一条木板上一段连续的格子,然后涂上一种颜色。 每个格子最多只能被粉刷一次。
如果 windy 只能粉刷 \(T\) 次,他最多能正确粉刷多少格子?
一个格子如果未被粉刷或者被粉刷错颜色,就算错误粉刷。
思路分析
这题我至少读错了有 1h 之久……
题目要求的是粉刷 \(T\) 次最多的正确格子数。
比较直接的思路是,定义 \(dp_{i,j}\) 表示前 \(i\) 行木板,粉刷 \(j\) 次最多的正确格子数。
那么对于第 \(i\) 行状态可以从第 \(i-1\) 行转移而来,显然这是一个 01 背包。
那么有:
其中 \(cnt_{i,k}\) 表示的是第 \(i\) 行粉刷 \(k\) 次最多的正确格子数。
显然这个 \(cnt\) 可以对每一行分别求。那么我们也对每一行分别考虑。
对于第 \(i\) 行:
设 \(g_{j,k}\) 表示前 \(j\) 个位置粉刷 \(j\) 次最多的正确格子数。
考虑如何转移。
比较显然的是,对于前 \(j\) 个位置,肯定是由:连续一段 1,连续一段 0,连续一段 1……这样 01 的连续段相互交错形成的,那么我们可以将这些连续的段切成几部分得到 \(g_{j,k}\) 的转移。
可以考虑利用类似区间 DP 的思想,枚举断点 \(l\),将前 \(j\) 个位置划分为前 \(l\) 个位置和区间 \([l+1,j]\) 两段,其中 \([l+1,j]\) 作为最后一次涂色的区间。
那么 \(g_{j,k}\) 就可以由 \(g_{l,k-1}\) 转移得到,即:
(注意 \(l\) 的范围)
其中 \(mx_{l,r}\) 表示的是区间 \([l,r]\) 中数量最多的颜色的数量。
\(mx_{l,r}\) 可以直接暴力枚举得到。
那么最后的答案就是 \(dp_{n,t}\)。
梳理一下求解过程:
求粉刷 \(T\) 次最多的正确格子数 \(\rightarrow\) 求 \(dp_{i,j}\) \(\rightarrow\) 求 \(g_{j,k}\)(将求整个矩形转化为一行)\(\rightarrow\) 求 \(mx_{l+1,j}\)(将 \(k\) 次转化为 1 次)。
求解步骤
对于每一行 \(i\):
-
暴力枚举求出 \(mx_{l,r}\):\(1-m\) 所有区间。
-
求 \(g_{j,k}\);
-
求 \(dp_{i,j}\)。
易错点
-
每次枚举一个新的 \(i\) 都要将 \(g\) 数组和 \(mx\) 数组清空;
-
在求 \(g_{j,k}\) 时枚举的断点 \(l\) 范围是 \([0,j)\);
-
\(dp_{n,t}\) 第二维由于 \(t\) 的范围是 2500,所以要开 \(>2500\);
一些思考
这道题我刚开始读错了好久的题意,并在读错的题意上思考了好久但是并没有想出来。
但是我总觉得我读错的那个题意也可以作为一道题目,并且一定有正确的解法。(盲目自信)
我刚开始没有看到题目中“每个格子只能被粉刷一次”这句话。
然后感觉可以先考虑将每一行所有的格子涂色正确的情况整出来。也就是,将第 \(i\) 行涂正确至少需要多少次。
那么发现发现对于单个的一行,实际上就是P4170 [CQOI2007]涂色 - 洛谷。
那么可以用 \(dp_{i,l,r}\) 表示第 \(i\) 行区间 \([l,r]\) 涂正确的最少次数,然后做区间 DP。
然后感觉是个有点类似于贪心和 01 背包的东西,但一直没想出解法。
虽然说我看错题目很傻逼,也没有想到看错了的题意的正确解法(我太菜了),但是我还是把它记录了下来,希望以后可以有所突破。或者,看到这篇文章的你如果有所想法,可以在下方留言或者联系我说出你的解法。
毕竟探索本身,就是一件很有意义的事情嘛。
实现代码
//luoguP4158
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=55;
const int maxt=maxn*maxn;
int a[maxn][maxn],dp[maxn][maxt],g[maxn][maxn],mx[maxn][maxn];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int main()
{
int n,m,t;
n=read();m=read();t=read();
for(int i=1;i<=n;i++)
{
string s;
cin>>s;
for(int j=0;j<m;j++)
{
if(s[j]=='1')a[i][j+1]=1;//bug:j 写成 i……
else a[i][j+1]=0;
}
}
// for(int i=1;i<=n;i++)
// {
// for(int j=1;j<=m;j++)cout<<a[i][j]<<" ";
// cout<<endl;
// }
for(int i=1;i<=n;i++)
{
memset(g,0,sizeof(g));
memset(mx,0,sizeof(mx));
int cnt1=0,cnt0=0;
for(int l=1;l<=m;l++)
{
if(a[i][l]==0)cnt0=1,cnt1=0;
else cnt1=1,cnt0=0;
mx[l][l]=1;
for(int r=l+1;r<=m;r++)
{
if(a[i][r]==0)cnt0++;
else cnt1++;
mx[l][r]=(cnt0>cnt1)?cnt0:cnt1;
// cout<<cnt0<<" "<<cnt1<<endl;
// cout<<i<<" "<<l<<" "<<r<<" "<<mx[l][r]<<endl;
}
}
for(int j=1;j<=m;j++)
{
for(int k=1;k<=j;k++)
{
for(int l=0;l<=j;l++)//bug: j 的范围:[0,j)
{
// cout<<l<<" "<<" "<<g[l][k-1]<<" "<<mx[l+1][j]<<endl;
g[j][k]=max(g[j][k],g[l][k-1]+mx[l+1][j]);//注意这里没有等于即 l!=k 且 l 需要从 0 开始枚举
}
// cout<<i<<" "<<j<<" "<<k<<" "<<g[j][k]<<endl;
}
}
for(int j=t;j>=1;j--)
{
for(int k=1;k<=j;k++)dp[i][j]=max(dp[i][j],dp[i-1][j-k]+g[m][k]);
// cout<<i<<" "<<j<<" "<<dp[i][j]<<endl;
}
}
cout<<dp[n][t]<<endl;
return 0;
}
/*mx[l][r] 表示的是对于一块木板的区间 [l,r] 这一段最多的颜色的数量。
g[i][j] 表示的是考虑了前 i 个位置,粉刷 j 次的最多正确粉刷的个数。
g[i][j]=max(g[i][j],g[l][j-1]+mx[l+1][i]).
dp[i][j] 表示考虑了前 i 行,粉刷 j 次的最多正确粉刷个数。
显然这是 01 背包。那么有:
dp[i][j]=max(dp[i][j],dp[i-1][j-k]+g[m][k]).
*/
感觉我还是好菜啊。做了那么多 DP 的题目,但是还是对有些题毫无感觉,甚至设计不出 DP 状态,也不知道我的 DP 什么时候才能有质的突破呢?