CF 939F. Cutlet

F. Cutlet


第一次接触这样的动规设计。有收获


首先,我们先简化一下问题:

  1. \(k\)(\(k\leq100\))个给定区间,给这\(2n\)个区间染色(\(0\)\(1\)),规定相邻颜色断点必须在给定区间内,问是否有合法的方案使两种颜色格子数均为\(n\),以及最少的断点。

考虑可行性:

不妨设计DP:\(dp(i,j,op)|op\in\{0,1\}\)指前\(i\)个格颜色\(op\)染了\(j\)个各种的最少断点。

那么,

  • 若第\(i\)个格子不在给定区间内部,转移到\(dp(i-1,j-1,op)\)
  • 若在给定区间,不妨设该区间编号为\(k\)。转移到\(dp(l_k\to i-1,j',op\ xor\ 1)+1\)

这样的做法的时间复杂度为\(O(n^2k)\)


接下来,认真思考,可以发现在题目中,断点仅存在于给定区间中。基于这个性质,我们可以重新构造一个更精简的状态。

不妨设:\(dp(i,j,op)|op\in\{0,1\}\),代表前个给定区间染了\(j\)个格子的最少断点。

那么,我们考虑一个区间的断点数量:

  1. 没有断点;
  2. 有一个断点;
  3. 有两个断点;
  4. 有多于\(2\)个断点;

显然,多于\(2\)个断点的一定不可能成为最优解。

那么,我们就可以讨论断点个数即可。换言之,

  • 当没有断点时,有\(dp(i-1,j,op)\)
  • 有一个断点时,有\(dp(i-1,j-k,op\ xor\ 1)+1|\ \ k\leq j,0\leq k\leq r_i-l_i\);这里我们枚举断点。
  • 有两个断点时,有\(dp(i-1,j-k,op)+2|\ k\leq j,0\leq k\leq r_i-l_i\);这里我们枚举中部的区间长度。

时间复杂度还是没有变化,但是我们已经有了可以用单调队列优化的雏形。


优化


\[dp(i,j,op)=min\begin{cases} & \text dp(i-1,j,op)\\& \text dp(i-1,r_{i-1}-(j-k),op\ xor\ 1)+1\ (k\leq j,0\leq k\leq r_i-l_i)\\& \text dp(i-1,j-k,op)+2\ (k\leq j,0\leq k\leq r_i-l_i)\end{cases} \]

该式子第\(i\)项仅和第\(i-1\)项有密切关系。


我们换元:

\[dp(i,j,op)=min\begin{cases} & \text dp(i-1,j,op)\\& \text dp(i-1,k,op\ xor\ 1)+1\ (k\geq 0,r_{i-1}-j\leq k\leq r_{i-1}-(l_i+j-r_i))\\& \text dp(i-1,k,op)+2\ (k\geq 0,l_i+j-r_i\leq k\leq j)\end{cases} \]

这种情况直接单调队列优化。


不过,这样的转移特别的的复复杂,细节极容易搞错。我们可以依照博弈问题改变一下状态表示:设\(dp(i,j)\)指前\(i\)个区间,另一种颜色还需要\(j\)个格子染色。接下来,就优化掉了一维,较之前细节少多了。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#define RE register
#define CLR(x, y) memset(x,y,sizeof x)
#define FOR(i, x, y) for(RE int i=x;i<=y;++i)
#define ROF(i, x, y) for(RE int i=x;i>=y;--i)
#define int LL
using namespace std;

typedef long long LL;

const int MAXN = 100 + 5, TIME = 2e5 + 10;

template <class T> void read(T &x)
{
	bool mark = false;
	char ch = getchar();
	for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') mark = true;
	for(x = 0; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 3) + (x << 1) + ch - '0';
	if(mark) x = -x;
}

int n, k, hh, tt, l[MAXN], r[MAXN], q[TIME], dp[2][TIME];

signed main()
{
	read(n), read(k);
	FOR(i, 1, k) read(l[i]), read(r[i]);
	FOR(i, 0, TIME) dp[0][i] = dp[1][i] = TIME;
	dp[0][0] = 0;
	FOR(i, 1, k)
	{
		hh = 1, tt = 1;
		FOR(j, 0, n) dp[i & 1][j] = dp[(i - 1) & 1][j];
		FOR(j, 0, r[i] - min(r[i], n)) 
		{
			while(hh < tt && dp[(i - 1) & 1][q[tt - 1]] >= dp[(i - 1) & 1][j]) -- tt;
			q[tt ++] = j;
		}
		ROF(j, min(r[i], n), 1)
		{
			while(hh < tt && q[hh] < l[i] - j) ++ hh;
			if(hh < tt) dp[i & 1][j] = min(dp[i & 1][j], dp[(i - 1) & 1][q[hh]] + 1); 
			
			if(r[i] - j < n)
			{
				while(hh < tt && dp[(i - 1) & 1][q[tt - 1]] >= dp[(i - 1) & 1][r[i] - j + 1]) -- tt;
				q[tt ++] = r[i] - j + 1;
			}
		}
		hh = 1, tt = 2;
		FOR(j, 1, n)
		{
			while(hh < tt && q[hh] < l[i] + j - r[i]) ++ hh;
			if(hh < tt) dp[i & 1][j] = min(dp[i & 1][j], dp[(i - 1) & 1][q[hh]] + 2);
			if(j < n)
			{
				while(hh < tt && dp[(i - 1) & 1][q[tt - 1]] >= dp[(i - 1) & 1][j + 1]) -- tt;
				q[tt ++] = j + 1;
			}
		}
	}
	if(dp[k & 1][n] == TIME) puts("Hungry"); 
	else printf("Full\n%d\n", dp[k & 1][n]);
	return 0;
}

总结:

  1. 在没有认真分析问题的时候,连最基本的DP表示都没有想出来,这说明了要踏下心去认真分析;
  2. 有时代码细节过多,可能是算法不够精炼;
  3. DP中博弈简化。
posted @ 2021-03-25 20:05  大秦帝国  阅读(47)  评论(0编辑  收藏  举报