CF 939F. Cutlet

F. Cutlet


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


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

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

考虑可行性:

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

那么,

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

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


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

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

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

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

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

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

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

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


优化


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

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


我们换元:

[dp(i,j,op)=minegin{cases} & ext dp(i-1,j,op)\& ext dp(i-1,k,op xor 1)+1 (kgeq 0,r_{i-1}-jleq kleq r_{i-1}-(l_i+j-r_i))\& ext dp(i-1,k,op)+2 (kgeq 0,l_i+j-r_ileq kleq 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
%d
", dp[k & 1][n]);
	return 0;
}

总结:

  1. 在没有认真分析问题的时候,连最基本的DP表示都没有想出来,这说明了要踏下心去认真分析;
  2. 有时代码细节过多,可能是算法不够精炼;
  3. DP中博弈简化。
原文地址:https://www.cnblogs.com/zach20040914/p/14556075.html