CF 939F. Cutlet
F. Cutlet
第一次接触这样的动规设计。有收获
首先,我们先简化一下问题:
- \(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\)个格子的最少断点。
那么,我们考虑一个区间的断点数量:
- 没有断点;
- 有一个断点;
- 有两个断点;
- 有多于\(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;
}
总结:
- 在没有认真分析问题的时候,连最基本的DP表示都没有想出来,这说明了要踏下心去认真分析;
- 有时代码细节过多,可能是算法不够精炼;
- DP中博弈简化。