区间 DP
这把先写紫题。
P4563 [JXOI2018] 守卫
题意
有 \(n\) 座山,第 \(i\) 座山是从 \((i, 0)\) 到 \((i, h_i)\) 的线段。称从第 \(i\) 座山能看到第 \(j\) 座山,当且仅当 \(i \ge j\) 且不存在 \(i < k < j\) 使得 \((j, h_j)\) 到 \((i, h_i)\) 的连线经过了第 \(k\) 座山(恰好交于一点也算经过)。
对于一段区间 \([l, r]\) (\(1 \le l \le r \le n\)),其花费为从中选择山的最少数量,使得区间内的每座山都能被至少一座选择的山看到。
求所有区间的花费的异或和。
数据范围:\(1 \le n \le 5000, 1 \le h_i \le 10^9\)。
思路
对于一段区间 \([l, r]\),设 \(dp(l, r)\) 表示区间花费,第 \(r\) 座山是必选的。令能够被第 \(r\) 座山看到的山的集合为 \(P = \{p_0, p_1, p_2, \dots, p_k \}\),满足 \(l - 1 = p_0 < p_1 < p_2 < \dots < p_k = r\)。
显然,对于任意 \(0 \le i < k\),\([p_i + 1, p_{i + 1} - 1]\) 内的任意一座山都不会被 \([p_{i + 1} + 1, r]\) 内的山看到,需要对其独立处理。为了看到 \(p_{i + 1} - 1\),必须在 \(p_{i + 1} - 1, p_{i + 1}\) 中选择至少一个,即:\(\min\{dp(p_i + 1, p_{i + 1} - 1), dp(p_i + 1, p_{i + 1})\}\)。因此有:
又注意到:
因此:
从小到大枚举 \(r\),从大到小枚举 \(l\),即可随时维护 \(p_1\)。
为了防止越界,注意到 \(p_{k - 1} = r - 1\),因此 \(dp(r, r) = dp(r - 1, r) = 1\),那么 \(l\) 从 \(r - 2\) 开始枚举,\(p_1\) 初值为 \(r - 1\)。
代码
点击查看代码
n = read(), dp[1][1] = ans = 1;
for(int i = 1; i <= n; i++) h[i] = read();
for(int r = 2; r <= n; r++){
dp[r][r] = dp[r - 1][r] = 1;
for(int l = r - 2, p = r - 1; l >= 1; l--){
if((long long)(h[r] - h[l]) * (r - p) < (long long)(h[r] - h[p]) * (r - l)) p = l;
dp[l][r] = min(dp[l][p - 1], dp[l][p]) + dp[p + 1][r], ans ^= dp[l][r];
}
}
print(ans);
P3736 [HAOI2016] 字符合并
题意
有一个长度为 \(n\) 的 \(01\) 串,每次操作选择一段长度为 \(k\) 的连续子串 \(S\),将其合并得到一个新的字符 \(c_S\) 并获得分数 \(w_S\)。
求出能够获得的最大分数。
数据范围:\(1 \le n \le 300, 1 < k \le 8, c_i \in \{0, 1\}, 1 \le w_i \le 10^9\)。
思路
显然,最优解必然是一直合并,最终长度小于 \(k\)。而初始长度固定时,最终长度也是固定的。
由于每次合并会使长度减小 \(k - 1\),设初始长度为 \(len\),则最终长度为 \((len - 1) \bmod (k - 1) + 1\)。不妨令 \(sz = (len - 1) \bmod (k - 1)\)。
设 \(dp(l, r, S)\) 表示区间 \([l, r]\) 一直合并,最终得到 \(S\) 的最大分数。显然,\(dp(i, i, a[i]) = 0\)。注意,\(S\) 长度等于区间一直合并得到的最终长度。
如果倒推,发现 \(S\) 中的每个字符对应 \([l, r]\) 中的一段子区间,且相邻字符对应的子区间相邻不交。因此,考虑将 \([l, r]\) 拆分成 \([l, i - 1], [i, r]\) 两个区间,钦定前者一直合并,最终长度为 \(sz\);后者一直合并,最终长度为 \(1\)。为此,\(i\) 需满足 \((r - i) \bmod (k - 1) + 1 == 1\),也即 \(i\) 从 \(r\) 开始枚举,每次减小 \(k - 1\)。
特别的,若 \(sz = 0\),则要求 \([l, i - 1]\) 一直合并,最终长度为 \(k - 1\);\([i, r]\) 一直合并,最终长度为 \(1\)。\(dp(l, i - 1, S) + dp(i, r, 0/1)\) 贡献到 \(dp(l, r)\) 上时,仅将 \([l, r]\) 合并到长度为 \(k\),因此还需再次合并,得到 \(dp(l, r, 0/1)\)。
代码
点击查看代码
n = read(), k = read(), ans = -inf;
for(int i = 1; i <= n; i++) a[i] = read();
for(int i = 0; i < (1 << k); i++) c[i] = read(), w[i] = read();
for(int l = 1; l <= n; l++)
for(int r = 1; r <= n; r++)
for(int S = 0; S < (1 << k); S++)
dp[l][r][S] = -inf;
for(int i = 1; i <= n; i++) dp[i][i][a[i]] = 0;
for(int len = 2; len <= n; len++)
for(int l = 1, r = len; r <= n; l++, r++){
int sz = (r - l) % (k - 1);
if(!sz) sz = k - 1;
for(int i = r; i > l; i -= k - 1)
for(int S = 0; S < (1 << sz); S++){
MAX(dp[l][r][S << 1], dp[l][i - 1][S] + dp[i][r][0]);
MAX(dp[l][r][S << 1 | 1], dp[l][i - 1][S] + dp[i][r][1]);
}
if(sz == k - 1){
ll g[2] = {-inf, -inf};
for(int S = 0; S < (1 << k); S++) MAX(g[c[S]], dp[l][r][S] + w[S]);
MAX(dp[l][r][0], g[0]), MAX(dp[l][r][1], g[1]);
}
}
for(int S = 0; S < (1 << k); S++) MAX(ans, dp[1][n][S]);
print(ans);
P3352 [ZJOI2016] 线段树
题意
有一个长度为 \(n\) 的整数序列 \(a\),进行 \(q\) 次操作:从 \(\frac{n(n + 1)}{2}\) 个区间中随机选择一个区间 \([l, r](1 \le l \le r \le n)\),将区间推平为区间最大值。求最终每个数的期望大小乘 \(\left(\frac{n(n + 1)}{2}\right)^q\) 再对 \(10^9 + 7\) 取模的值。
数据范围:\(1 \le n, q \le 400, 0 \le a_i \le 10^9\)。
思路
由于期望 \(=\sum\) 概率 \(\times\) 值,概率 \(=\) 方案数 \(\div\) 总方案数,总方案数 \(= \left(\frac{n(n + 1)}{2}\right)^q\),故题目所求 \(=\sum\) 方案数 \(\times\) 值。
遇到区间推平最值,通常按大小关系转换为 \(01\) 序列。
因此,先从 \(01\) 序列入手,统计每个位置最终为 \(0\) 的方案数:
-
若初始为 \(1\),则最终必然为 \(1\)。
-
若初始为 \(0\),设其左、右最近的 \(1\) 的位置为 \(L, R\)(若无,则为 \(0, n + 1\)),则 \((L, R)\) 为 \(0\) 的极长连续段,每次操作可能将 \((L,R)\) 缩小。
对每个 \(0\) 的极长连续段分别 DP。设 \(dp(x, L, R)\) 表示 \(x\) 次操作之后,缩小到 \((L, R)\) 的方案数。对于初始所有 \((L, R)\),\(dp(0, L, R) = 1\)。
转移有三种情况:
-
\(dp(x - 1, L, R)\),要求操作区间在 \([1, L], [R, n], (L, R)\) 内部,即:\(dp(x - 1, L, R) \times \left(\frac{L(L+1)+(n-R+1)(n-R+2)+(R-L-1)(R-L)}{2}\right)\)。
-
\(dp(x - 1, L' < L, R)\),要求操作区间为 \([1 \sim L', L]\),即:\(\sum\limits_{L' = 0}^{L - 1} dp(x - 1, L', R) \times L'\)。
-
\(dp(x - 1, L, R' > R)\),要求操作区间为 \([R, R' \sim n]\),即:\(\sum\limits_{R' = R + 1}^{n + 1} dp(x - 1, L, R') \times (n - R' + 1)\)。
后两者用前缀和优化,并把 \(x\) 一维滚动掉,即可做到时间 \(O(qn^2)\),空间 \(O(n^2)\)。
然后考虑一般序列。对于最终序列的某个位置,枚举其最终可能值 \(w\),设方案数为 \(f(w)\),则答案为:\(\sum\limits_{w = 0}^{\max a_i} w \times f(w)\)。
注意到,对于任意可能值 \(w\),有:\(w = \max a_i - \sum\limits_{i = 0}^{\max a_i}[w < i]\)。因此,可以推式子:
前者为定值,需要求后者。枚举 \(i\),并把所有 \(\ge i\) 的位置设为 \(1\),\(<i\) 的位置设为 \(0\),对该 \(01\) 序列 DP,可以得到每个位置 \(<i\) 的方案数(所有包含该位置的区间的 \(dp(q, L, R)\) 之和),也即 \(\sum\limits_{w = 0}^{i - 1} f(w)\)。
由于枚举 \(i\) 的过程中,该 \(01\) 序列只变化了 \(O(n)\) 次,即 \(dp\) 数组的值改变了 \(O(n)\) 次,变化时需要重新求,不变时沿用下来,因此时间复杂度 \(O(qn^3)\),但由于数据随机,实际复杂度是 \(O(qn^2)\) 的。一个单调递增的序列就能将这个算法卡到 \(O(qn^3)\)。
考虑优化到稳定的 \(O(qn^2)\) 做法。由于 DP 转移方程不变,只有初值发生改变,因此可以把所有初始值放到同一个 \(dp\) 数组里面,只进行一次整体 DP,从而求出 \(\sum\limits_{i = 0}^{\max a_i} \sum\limits_{w = 0}^{i - 1} f(w)\)。
代码
点击查看代码
n = read(), q = read();
for(int i = 1; i <= n; i++) a[i].w = read(), a[i].id = i;
sort(a + 1, a + 1 + n, [&](node i, node j){
return i.w < j.w;
});
exist[n + 1] = true, ans[1] = (ll)a[n].w * ksm((ll)n * (n + 1) / 2 % mod, q) % mod;
for(int i = n; i; i--){
exist[a[i].id] = true, ans[i] = ans[1];
for(int j = 1, lst = 0; j <= n + 1; j++) if(exist[j])
add(dp[lst][j], a[i].w - a[i - 1].w), lst = j;
}
for(int x = 1; x <= q; x++){
for(int len = n + 2; len >= 3; len--)
for(int L = 0, R = len - 1; R <= n + 1; L++, R++){
sr[L][R] = (L ? sr[L - 1][R] : 0);
add(sr[L][R], (ll)dp[L][R] * L % mod);
sl[L][R] = (R <= n ? sl[L][R + 1] : 0);
add(sl[L][R], (ll)dp[L][R] * (n - R + 1) % mod);
}
for(int len = 3; len <= n + 2; len++)
for(int L = 0, R = len - 1; R <= n + 1; L++, R++){
dp[L][R] = (ll)dp[L][R] * (L * (L + 1) + (n - R + 1) * (n - R + 2) + (R - L - 1) * (R - L)) / 2 % mod;
if(L) add(dp[L][R], sr[L - 1][R]);
if(R <= n) add(dp[L][R], sl[L][R + 1]);
}
}
for(int len = 3; len <= n + 2; len++)
for(int L = 0, R = len - 1; R <= n + 1; L++, R++)
for(int i = L + 1; i < R; i++)
add(ans[i], mod - dp[L][R]);
for(int i = 1; i <= n; i++) print(ans[i]), putchar(' ');
CF1987F2 Interesting Problem (Hard Version)
题意
有一个长度为 \(n\) 的整数数组 \(a\)。求出进行以下操作的最大次数:选择一个下标 \(i\),满足 \(1 \le i < |a|\) 且 \(a_i = i\),从数组中删除 \(a_i\) 和 \(a_{i+1}\),并将剩余部分拼接起来。多测。
数据范围:\(1 \le t \le 100, 1 \le n, \sum n \le 800, 1 \le a_i \le n\)。
思路
\(a_i\) 能被移除,必须满足:
-
\(i \ge a_i\),因为每次操作后,\(i\) 单调不上升。
-
\(i \equiv a_i \pmod 2\),因为每次操作后,\(i\) 的奇偶性不变。
-
在 \([1, i)\) 中可以进行恰好 \(\frac{i - a_i}{2}\) 次操作。
前两条可以直接判断,针对第三条设计 DP。
设 \(dp(l, r)\) 表示为了将 \([l, r]\) 清空,需要在 \([1, l)\) 中进行的最少操作次数。注意,不包括清空 \([l, r]\) 的操作次数,也不要求 \([1, l)\) 真的可以进行 \(dp(l, r)\) 次操作,它只是一个数字,不关注具体进行什么操作,但确实要求 \([l, r]\) 能在此前提下被清空。
首先需要判断,\(l\) 是否满足前两个条件,若不满足,就不用转移过来了;否则,有两种转移情况:
-
先清空 \((l, r)\),让 \(l, r\) 在一起,再删除 \(l\)。当且仅当区间长度为 \(2\),或者 \(dp(l + 1, r - 1) \le \frac{l - a_l}{2}\) 时,能够转移:\(dp(l, r) = \frac{l - a_l}{2}\)。
-
分成 \([l, k], (k, r]\) 两部分,转移:\(dp(l, r) = \min(\max\{dp(l, k), dp(k + 1, r) - \frac{k - l + 1}{2})\})\)。
然后求答案,设 \(g(i)\) 表示前 \(i\) 个数进行操作的最大次数。枚举 \(j < i\),考虑清空区间 \([j, i]\) 转移过来,需要满足 \(dp(j, i) \le g(j - 1)\),从而有转移:\(g(i) = \max(g(j - 1) + \frac{i - j + 1}{2}))\)。
注意,以上转移全部要求区间长度为偶数。因此,时间复杂度 \(O(n^3)\),乘上约 \(\frac{1}{8}\) 的常数,足以通过。
代码
点击查看代码
n = read();
for(int i = 1; i <= n; i++) a[i] = read();
for(int len = 2; len <= n; len += 2)
for(int l = 1, r = len; r <= n; l++, r++)
dp[l][r] = n;
for(int len = 2; len <= n; len += 2)
for(int l = 1, r = len; r <= n; l++, r++){
if(l < a[l] || (l & 1) != (a[l] & 1)) continue;
if(len == 2 || dp[l + 1][r - 1] <= (l - a[l]) / 2) dp[l][r] = (l - a[l]) / 2;
for(int k = l + 1; k < r; k += 2) MIN(dp[l][r], max(dp[l][k], dp[k + 1][r] - (k - l + 1) / 2));
}
for(int i = 1; i <= n; i++){
g[i] = g[i - 1];
for(int j = i - 1; j >= 1; j -= 2)
if(dp[j][i] <= g[j - 1])
MAX(g[i], g[j - 1] + (i - j + 1) / 2);
}
print(g[n]), putchar('\n');
P7605 [THUPC 2021] 小 E 爱消除
题意
有一个长为 \(n\) 的序列 \(a\),可以在其两端取数放入栈中,栈中有两个相邻的数相同即可同时消去。求最后栈内元素的最小数量,和在此条件下栈最大空间的最小值。
思路
我偷懒了。看这篇题解吧。
代码
点击查看代码
int G(int a, int b, int c, int d){
if(a > b && c > d) return 0;
if(g[a][b][c][d]) return g[a][b][c][d];
if(s[b] ^ s[a - 1] ^ s[d] ^ s[c - 1]) return inf;
int res = inf;
// delete a. find i for col[a] == col[i].
// a < i <= b: delete [a + 1, i - 1] and [j, d]; rest [i + 1, b, c, j - 1].
for(int i = a + 1; i <= b; i++)
if(col[a] == col[i])
for(int j = c; j <= d + 1; j++)
MIN(res, max(G(a + 1, i - 1, j, d) + 1, G(i + 1, b, c, j - 1)));
// c <= i <= d: delete [a + 1, j] and [i + 1, d]; rest [j + 1, b, c, i - 1].
for(int i = c; i <= d; i++)
if(col[a] == col[i])
for(int j = a; j <= b; j++)
MIN(res, max(G(a + 1, j, i + 1, d) + 1, G(j + 1, b, c, i - 1)));
// delete d. find i for col[d] == col[i].
// a <= i <= b: delete [a, i - 1] and [j, d - 1]; rest [i + 1, b, c, j - 1].
for(int i = a; i <= b; i++)
if(col[d] == col[i])
for(int j = c; j <= d; j++)
MIN(res, max(G(a, i - 1, j, d - 1) + 1, G(i + 1, b, c, j - 1)));
// c <= i < d: delete [a, j] and [i + 1, d - 1]; rest [j + 1, b, c, i - 1].
for(int i = c; i < d; i++)
if(col[d] == col[i])
for(int j = a - 1; j <= b; j++)
MIN(res, max(G(a, j, i + 1, d - 1) + 1, G(j + 1, b, c, i - 1)));
return g[a][b][c][d] = max(2, res);
}
pir F(int l, int r){
if(l > r) return {0, 0};
if(f[l][r].first || f[l][r].second) return f[l][r];
pir res = min(F(l + 1, r), F(l, r - 1)); res.first++, res.second++;
// delete l. find i for col[l] == col[i].
// j < i: delete [l + 1, j] and [i + 1, r]
// i < j: delete [l + 1, i - 1] and [j, r]
for(int i = l + 1; i <= r; i++)
if(col[l] == col[i]){
for(int j = l; j < i; j++){
int sec = max({2, F(j + 1, i - 1).second, G(l + 1, j, i + 1, r) + 1});
if(sec < inf) MIN(res, {F(j + 1, i - 1).first, sec});
}
for(int j = i + 1; j <= r + 1; j++){
int sec = max({2, F(i + 1, j - 1).second, G(l + 1, i - 1, j, r) + 1});
if(sec < inf) MIN(res, {F(i + 1, j - 1).first, sec});
}
}
// delete r. find i for col[r] == col[i].
// j < i: delete [l, j] and [i + 1, r - 1]
// i < j: delete [l, i - 1] and [j, r - 1]
for(int i = l; i < r; i++)
if(col[r] == col[i]){
for(int j = l - 1; j < i; j++){
int sec = max({2, F(j + 1, i - 1).second, G(l, j, i + 1, r - 1) + 1});
if(sec < inf) MIN(res, {F(j + 1, i - 1).first, sec});
}
for(int j = i + 1; j <= r; j++){
int sec = max({2, F(i + 1, j - 1).second, G(l, i - 1, j, r - 1) + 1});
if(sec < inf) MIN(res, {F(i + 1, j - 1).first, sec});
}
}
return f[l][r] = res;
}
n = read();
for(int i = 1; i <= n; i++) col[i] = read(), Hash[col[i]] = rand_int(1, 1000000000);
for(int i = 1; i <= n; i++) s[i] = s[i - 1] ^ Hash[col[i]];
print(F(1, n).first), putchar(' ');
print(F(1, n).second), putchar('\n\);
浙公网安备 33010602011771号