【题解】P4158 [SCOI2009]粉刷匠(DP,背包)

【题解】P4158 [SCOI2009]粉刷匠

是一道资源规划 DP 的好题,但是我想了很久还去看了题解。/kk我真菜。


题目链接

P4158 [SCOI2009]粉刷匠 - 洛谷

题意概述

发现自己实在不会简化题意于是就抄了原题(雾)。

windy 有 \(N\) 条木板需要被粉刷。 每条木板被分为 \(M\) 个格子。 每个格子要被刷成红色或蓝色。

windy 每次粉刷,只能选择一条木板上一段连续的格子,然后涂上一种颜色。 每个格子最多只能被粉刷一次。

如果 windy 只能粉刷 \(T\) 次,他最多能正确粉刷多少格子?

一个格子如果未被粉刷或者被粉刷错颜色,就算错误粉刷。

思路分析

这题我至少读错了有 1h 之久……

题目要求的是粉刷 \(T\) 次最多的正确格子数。

比较直接的思路是,定义 \(dp_{i,j}\) 表示前 \(i\) 行木板,粉刷 \(j\) 次最多的正确格子数。

那么对于第 \(i\) 行状态可以从第 \(i-1\) 行转移而来,显然这是一个 01 背包。

那么有:

\[dp_{i,j}=\max \limits_{1 \le k \le j}(dp_{i-1,k}+cnt_{i,k}) \]

其中 \(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}\) 转移得到,即:

\[g_{j,k}=\max \limits_{0 \le l < j}(g_{l,k-1}+mx_{l+1,j}) \]

(注意 \(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\)

  1. 暴力枚举求出 \(mx_{l,r}\)\(1-m\) 所有区间。

  2. \(g_{j,k}\)

  3. \(dp_{i,j}\)

易错点

  1. 每次枚举一个新的 \(i\) 都要将 \(g\) 数组和 \(mx\) 数组清空;

  2. 在求 \(g_{j,k}\) 时枚举的断点 \(l\) 范围是 \([0,j)\)

  3. \(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 什么时候才能有质的突破呢?

posted @ 2022-06-30 20:16  向日葵Reta  阅读(68)  评论(0编辑  收藏  举报