CodeForces - 939F Cutlet

Description \text{Description} Description

注意第 i i i 分钟翻转是将现在朝下的一面煎这一分钟再翻。

Solution \text{Solution} Solution

朴素 DP \text{DP} DP:令 f [ i ] [ j ] f[i][j] f[i][j] 为煎了 i i i 分钟,现在朝上的一面一共煎了 j j j 分钟的最小翻转次数。

则有(即翻与不翻):

f [ i ] [ j ] = min ⁡ ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ i − j ] + 1 ) f[i][j]=\min(f[i-1][j],f[i-1][i-j]+1) f[i][j]=min(f[i1][j],f[i1][ij]+1)

这是 O ( n 2 ) \mathcal O(n^2) O(n2) 的不可过。

我们换一个定义: f [ i ] [ j ] f[i][j] f[i][j] 为到第 i i i 个区间,现在朝上的一面一共煎了 j j j 分钟的最小翻转次数。看似不可做,你会发现每个区间只能翻 0 , 1 , 2 0,1,2 0,1,2 次(更多的次数你可以把几次翻转合并成一次)。可以针对每种情况分类讨论:

  • 0 0 0:直接继承上一个区间。

  • 1 1 1:设现在朝下的一面在这个区间煎了 k k k 分钟(注意这里的“现在”是针对 f [ i ] [ j ] f[i][j] f[i][j] 而言的)。那么 r − j r-j rj 就是现在朝下的一面一共煎了的分钟,那么现在朝下的一面在区间之前煎了 r − j − k r-j-k rjk 分钟。

    转移方程式: f [ i ] [ j ] = min ⁡ ( f [ i ] [ j ] , f [ i − 1 ] [ r − j − k ] + 1 ) ( k ∈ [ 0 , r − l ] ) f[i][j]=\min(f[i][j],f[i-1][r-j-k]+1)(k\in [0,r-l]) f[i][j]=min(f[i][j],f[i1][rjk]+1)(k[0,rl])。(无论怎样另一面会煎 1 1 1 分钟,所以是 r − l r-l rl,下面同理)

  • 2 2 2:设现在朝上的一面在这个区间煎了 k k k 分钟。那么现在朝上的一面在区间之前煎了 j − k j-k jk 分钟。

    转移方程式: f [ i ] [ j ] = min ⁡ ( f [ i ] [ j ] , f [ i − 1 ] [ j − k ] + 2 ) ( k ∈ [ 0 , r − l ] ) f[i][j]=\min(f[i][j],f[i-1][j-k]+2)(k\in[0,r-l]) f[i][j]=min(f[i][j],f[i1][jk]+2)(k[0,rl])

好吧现在时间复杂度为 O ( k × n 2 ) \mathcal O(k\times n^2) O(k×n2)(成功升高复杂度)

不过实际上我们可以去掉一个 n n n。在一个区间中, 随着 j j j 的增大,能选取的前状态实际上是一个类似于滑动窗口的东西,我们可以用单调队列优化。时间复杂度 O ( k × 200000 ) \mathcal{O(k\times 200000)} O(k×200000)

另外如果朝下的一面煎 j j j 分钟为状态的话,好像会导致 k k k 的范围不一样。。。

Code \text{Code} Code

详见注释。(似乎有些不好理解,如有不懂,可随时提问,博主就住在 CSDN 滑稽)

#include 
#include 
#include 
using namespace std;

const int N = 2e5 + 5;

int n, l, r, q[N], f[2][N], p;

int read() {
	int x = 0; int f = 1; char s;
	while((s = getchar()) > '9' || s < '0') if(s == '-') f = -1;
	while(s >= '0' && s <= '9') x = (x << 1) + (x << 3) + (s ^ 48), s = getchar();
	return x * f;
}

int main() {
	int L, R;
	n = read();
	memset(f, 0x3f, sizeof f); f[0][0] = 0;
	for(int T = read(); T; -- T) {
		L = read(), R = read();
		for(int i = 0; i <= N - 5; ++ i) f[p ^ 1][i] = f[p][i];//0
		l = 1, r = 0;
		for(int i = 0; i <= min(R, n); ++ i) {//2
			while(l <= r && q[l] < i - (R - L)) ++ l;
			while(l <= r && f[p][q[r]] >= f[p][i]) -- r;
			q[++ r] = i;//这里加入后实际上是后面的 j - k
			f[p ^ 1][i] = min(f[p ^ 1][i], f[p][q[l]] + 2);
		}
		l = 1, r = 0;
		for(int i = R; i >= 0; -- i) {//1
			while(l <= r && q[l] < R - i - (R - L)) ++ l;
			while(l <= r && f[p][q[r]] >= f[p][R - i]) -- r;
			q[++ r] = R - i;
			f[p ^ 1][i] = min(f[p ^ 1][i], f[p][q[l]] + 1);
		}//j 与 r - j - k 实际是由两端向中间汇合的。对于每个 j(即 i),新加入的为 R - i,即 k = 0 的情况
		p ^= 1;
	} 
	if(f[p][n] >= 0x3f3f3f3f) puts("Hungry");
	else printf("Full\n%d\n", f[p][n]);
	return 0;
}

你可能感兴趣的:(单调队列,DP)