区间DP小组题解
注:在这里放一份,方便看,如有侵权线下联系
区间 DP
A - CF1509C The Sports Festival by zjy
发现 \(d_i\) 一定是单调不降的,所以我们尽可能保证每一步都最小。
将 \(a_i\) 排序,如果当前选出了 \(i\) 个数,一定是在排过序后的数组中连续的一段。
设 \(dp_{l,r}\) 表示选出的区间左端点为 \(l\),右端点为 \(r\) 时的答案。则最终答案为 \(dp_{1,n}\)。
对于 \(1\le i\le n\),钦定 \(i\) 为选出的第一项,则 \(dp_{i,i}=0\)。转移:\(dp_{l,r}=\min(dp_{l+1,r},dp_{l,r-1})+a_r-a_l\)。
这样做复杂度是 \(O(n^3)\) 的。
发现可以从外向内转移,也就是倒着选,\(dp_{l,r}\) 为当前选的数为 \([1,l]\land [r,n]\) 时的答案。初始:\(dp_{1,n} = a_n - a_1\)。
转移:\(dp_{l,r}=\min(dp_{l-1,r},dp_{l,r+1})+a_r-a_l\)。
答案:\(\min\limits_{i=1}^ndp_{i,i}\)。
sort (a + 1, a + n + 1);
memset (dp, 0x3f, sizeof (dp));
dp[1][n] = a[n] - a[1];
for (int i = 1; i <= n; i++)
for (int j = n; j >= i; j--)
if (i != 1 || j != n) dp[i][j] = min (dp[i][j + 1], dp[i - 1][j]) + a[j] - a[i];
for (int i = 1; i <= n; i++) ans = min (ans, dp[i][i]);
cout << ans;
B - CF1728D Letter Picking by shx
考虑一个区间 \(l\sim r\):
-
先手必胜条件:
-
先手选 \(l\)
必胜条件是后手不管选 \(l+1\) 还是 \(r\),都是先手获胜。
-
先手选 \(r\)
必胜条件是后手不管选 \(l\) 还是 \(r-1\),都是先手获胜。
-
-
平局条件:
-
先手选 \(l\)
必胜条件是后手不管选 \(l+1\) 还是 \(r\),都是平局。
-
先手选 \(r\)
必胜条件是后手不管选 \(l\) 还是 \(r-1\),都是平局。
-
int Fn(int l, int r) {
if (l > r) return 0;
if (mp.count({l, r})) return mp[{l, r}];
int ll = Fn(l + 2, r), lr = Fn(l + 1, r - 1), rr = Fn(l, r - 2);
if ((ll == 1 || (ll == 0 && s[l] < s[l + 1])) && (lr == 1 || (lr == 0 && s[l] < s[r])))
return mp[{l, r}] = 1; // 先手选 l, 后手必败
if ((rr == 1 || (rr == 0 && s[r] < s[r - 1])) && (lr == 1 || (lr == 0 && s[r] < s[l])))
return mp[{l, r}] = 1; // 先手选 r, 后手必败
if ((ll == 1 || (ll == 0 && s[l] <= s[l + 1])) && (lr == 1 || (lr == 0 && s[l] <= s[r])))
return mp[{l, r}] = 0; // 先手选 l, 后手必不赢
if ((rr == 1 || (rr == 0 && s[r] <= s[r - 1])) && (lr == 1 || (lr == 0 && s[r] <= s[l])))
return mp[{l, r}] = 0; // 先手选 r, 后手必不赢
return mp[{l, r}] = -1;
}
C - CF1666J Job Lookup by shx
其任意一个节点的左子树内所有节点编号都小于它,右子树内所有节点编号都大于它。所以一个区间是一个完整的子树。
考虑枚举一个区间的根 \(k\),每次转移把路径的贡献拆开,加上左子树到非左子树的贡献、右子树到非右子树的贡献,这两部分在 \(c\) 矩阵上是一个矩形,前缀和维护即可。方案在转移时记录区间的根,最后递归求解即可。
转移方程:
ll Calc(int sx, int fx, int sy, int fy) {
if (sx > fx || sy > fy) return 0;
return s[fx][fy] - s[fx][sy - 1] - s[sx - 1][fy] + s[sx - 1][sy - 1];
}
void Dfs(int l, int r, int p) {
if (l > r) return void();
if (l == r) return ans[l] = p, void();
ans[fa[l][r]] = p;
Dfs(l, fa[l][r] - 1, fa[l][r]), Dfs(fa[l][r] + 1, r, fa[l][r]);
}
for (int len = 2; len <= n; ++len) {
for (int l = 1, r; (r = l + len - 1) <= n; ++l) {
for (int k = l; k <= r; ++k) {
ll tmp = (k != l) * f[l][k - 1] + (k != r) * f[k + 1][r] +
Calc(l, k - 1, 1, l - 1) + Calc(l, k - 1, k, n) + Calc(k + 1, r, 1, k) + Calc(k + 1, r, r + 1, n);
if (f[l][r] > tmp) f[l][r] = tmp, fa[l][r] = k;
}
}
}
Dfs(1, n, 0);
D - [USACO04OPEN] Turning in Homework G by zjy
因为交作业不需要时间,所以交一个区间的作业的时候,一定是先交左端点或者先交右端点。
设 \(dp_{l,r,0/1}\) 为还有 \([l,r]\) 的作业没交,当前在 \(l/r\) 时的答案。
初始:\(dp_{1,n,0}=\max(x_1,t_1)\),\(dp_{1,n,1}=\max(x_n,t_n)\)。
转移:从相邻区间的两个状态转移而来。
答案为 \(\min\limits_{i=1}^n|b-x_i|+\min(dp_{i,i,0},dp_{i,i,1})\)。
时间复杂度 \(\mathcal O(n^2)\)。
sort (a + 1, a + n + 1, [] (Node a, Node b) { return a.x < b.x; });
memset (dp, 0x3f, sizeof (dp));
dp[1][n][0] = max (a[1].x, a[1].t);
dp[1][n][1] = max (a[n].x, a[n].t);
for (int i = 1; i <= n; ++i)
for (int j = n; j >= i; j--) {
if (i == 1 && j == n) continue;
dp[i][j][0] = max (a[i].t, min (dp[i - 1][j][0] + a[i].x - a[i - 1].x, dp[i][j + 1][1] + a[j + 1].x - a[i].x));
dp[i][j][1] = max (a[j].t, min (dp[i - 1][j][0] + a[j].x - a[i - 1].x, dp[i][j + 1][1] + a[j + 1].x - a[j].x));
}
for (int i = 1; i <= n; i++) ans = min (ans, min (dp[i][i][0], dp[i][i][1]) + abs (b - a[i].x));
cout << ans;
E - [CERC2014] Outer space invaders by shx
考虑如果对一个人进行 DP,发现由于左右端点不固定,不很好转移,考虑对时间 DP。时间显然可以离散化到 \(O(n)\)。
设 \(f(l,r)\) 表示消灭出现时间被 \(l\sim r\) 时间完全包含 的敌人的最小代价,考虑转移:
枚举对距离最大的敌人的攻击时间 \(k\),此时所有时间区间包含 \(k\) 的敌人都消灭了,还剩下 完全被 \(l\sim k-1\) 和 \(k + 1\sim r\) 时间完全包含的敌人。由 DP 的含义得到就是 \(f(l, k - 1) + f(k + 1, r)\)。
令 \(p\) 为区间内距离最大的敌人,则转移式:
如果区间不完全包含某个敌人 DP 值为 \(0\);答案为消灭所有敌人,即被整个时间包含的敌人 \(f(1,w)\),\(w\) 时总时间。
for (int len = 1; len <= w; ++len) {
for (int l = 1, r, p = 0; (r = l + len - 1) <= w; ++l) {
for (int i = 1; i <= n; ++i)
if (l <= a[i].l && a[i].r <= r && (a[i].d > a[p].d)) p = i;
if (!p) {
f[l][r] = 0;
} else {
f[l][r] = 1E18;
for (int k = a[p].l; k <= a[p].r; ++k) Chmin(f[l][r], f[l][k - 1] + f[k + 1][r]);
f[l][r] += a[p].d;
}
p = 0; // 记得清空 QWQ
}
}
printf("%lld\n", f[1][w]);
F - [USACO17JAN] Subsequence Reversal P by fyx
根据数据范围,不难想到 DP 状态应该是 \(n^4\) 级别的。
先考虑当没有反转区间的操作时如何转移。
设 \(dp_{l,r,L,R}\) 表示当前区间为 \(l\sim r\),值域 \(\in [L,R]\) 时的答案。转移时枚举四个维度,可以从 \(dp_{l,r,L,R-1},dp_{l,r,L+1,R},dp_{l+1,r,L,R},dp_{l,r-1,L,R}\) 转移过来。
加上翻转操作后,我们思考其本质。翻转一个子序列可以理解为交换某几对数字的位置,这样的话相当于如果 \(a_l=R\) 或者 \(a_r=L\) 的话,我们可以通过翻转 \(l\sim r\) 中的任意一个包含 \(l,r\) 的子序列来满足条件,即 \(dp_{l,r,L,R}=dp_{l+1,r-1,L,R}+[a_l=R]+[a_r=L]\)。
由于区间 DP 按照区间从小到大的顺序,故可以保证这样的翻转满足题目条件,所以这道题就结束了。
cin >> n;
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++)
for (int l = 1; l <= a[i]; l++)
for (int r = a[i]; r <= 50; r++) dp[i][i][l][r] = 1;
for (int len = 2; len <= n; len++) {
for (int l = 1; l <= n - len + 1; l++) {
int r = l + len - 1;
for (int lenn = 1; lenn <= 50; lenn++) {
for (int L = 1; L <= 50 - lenn + 1; L++) {
int R = L + lenn - 1;
dp[l][r][L][R] = max(dp[l][r][L + 1][R], dp[l][r][L][R - 1]);
dp[l][r][L][R] = max(dp[l][r][L][R], dp[l + 1][r][L][R] + (a[l] == L));
dp[l][r][L][R] = max(dp[l][r][L][R], dp[l][r - 1][L][R] + (a[r] == R));
dp[l][r][L][R] = max(dp[l][r][L][R], dp[l + 1][r - 1][L][R] + (a[l] == R) + (a[r] == L));
}
}
}
}
cout << dp[1][n][1][50];
G - [春季测试 2023] 圣诞树 by shx
考虑对于:
A B
C D
这样的节点,一定不会是 \(ADBC\) 这样的路径,因为 \(AD\gt CD,BC\gt AB\),那么 \(AD+DB+BC\gt CD+DB+BA\),不如 \(ABDC\) 更优,即线不会有交叉。
考虑设 \(f(l,r,0/1)\) 表示从区间 \(l\sim r\) 的左 \(/\) 右端点,走完区间内的最小代价。把原序列从 \(k\) 断开,改为数值上形如 \(\land\) 的一个长为 \(n-1\) 的序列。则一定是左边选若干个、然后右边选若干个这样,单独看两边,每次选择时高度单调不升。
答案为 \(\min\{f(1,n-1,0)+\operatorname{dis}(1,n),f(1,n-1,1)+\operatorname{dis}(n-1,n)\}\),其中 \(\operatorname{dis}(i,j)\) 表示新序列上 \(i,j\) 点之间的距离。特别地,方便起见,令原序列上的 \(k\) 在新序列的第 \(n\) 位上,故 DP 过程仅为前 \(n-1\) 位的决策,与第 \(n\) 位无关。
转移式子:
输出方案可以在记录一个 \(p(l,r,0/1)=0/1\) 表示在区间 \(l\sim r\) 的左 \(/\) 右时,上一次是从左 \(/\) 右转移来,递归输出方案。
inline double Dis(int i, int j) {
return sqrt((a[i].x - a[j].x) * (a[i].x - a[j].x) + (a[i].y - a[j].y) * (a[i].y - a[j].y));
}
void Dfs(int l, int r, int op) {
if (l == r) return printf(" %d", a[l].id), void();
if (op) printf(" %d", a[r].id), Dfs(l, r - 1, p[l][r][op]);
else printf(" %d", a[l].id), Dfs(l + 1, r, p[l][r][op]);
}
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> s[i].x >> s[i].y, s[i].id = i, ((s[i].y > maxy) && (maxy = s[i].y, k = i));
for (int i = 1; i <= n; ++i) a[(i - k + n) % n] = s[i]; a[n] = s[k];
for (int len = 2; len < n; ++len)
for (int l = 1, r; (r = l + len - 1) < n; ++l) {
f[l][r][0] = f[l][r][1] = 1E100;
if (f[l][r][0] > f[l + 1][r][0] + Dis(l + 1, l)) f[l][r][0] = f[l + 1][r][0] + Dis(l, l + 1), p[l][r][0] = 0;
if (f[l][r][0] > f[l + 1][r][1] + Dis(r, l)) f[l][r][0] = f[l + 1][r][1] + Dis(r, l), p[l][r][0] = 1;
if (f[l][r][1] > f[l][r - 1][0] + Dis(l, r)) f[l][r][1] = f[l][r - 1][0] + Dis(l, r), p[l][r][1] = 0;
if (f[l][r][1] > f[l][r - 1][1] + Dis(r - 1, r)) f[l][r][1] = f[l][r - 1][1] + Dis(r - 1, r), p[l][r][1] = 1;
}
cerr << min(f[1][n - 1][0] + Dis(1, n), f[1][n - 1][1] + Dis(n - 1, n)) << endl;
printf("%d", k), f[1][n - 1][0] + Dis(1, n) < f[1][n - 1][1] + Dis(n - 1, n) ? Dfs(1, n - 1, 0) : Dfs(1, n - 1, 1);
return 0;
}
J - [CQOI2007] 涂色 by fyx
仔细想想其实没有 dX 说的那么夸张[1]。
考虑一个结论:一定存在一种最优方案使得使得任意一次染色的区间一定是完全包含之前某一次染色区间或者与之前某一次染色区间完全不交且不与之前所有染色区间相交。
简单来说,如果我们当前的染色方案与之前某一次相交,那么我们完全可以缩短当前染色区间使得不交。
这样我们设 \(dp_{l,r}\) 表示染色区间为 \(l,r\) 时的答案,对上述两种情况分别考虑:
-
完全包含。此时一定有 \(s_l=s_r\),那么 \(dp_{l,r}\) 可以由 \(dp_{l,r-1}\) 和 \(dp_{l+1,r}\) 转移过来。具体的,比如对于
ABA
,它可以由AB
直接转移,即第一次染色时可以向右端点多染一次。这里可能会有疑问,也许这种染色方式会不满足上文条件。但其实不要紧,因为就算不满足我们也可以通过前文的转化方式使其满足条件。等价于转移时不需要完全满足上文条件。 -
完全不相交。我们在这里其实只需要考虑紧挨的情况,即类似
AABB
,因为不紧挨的情况可以由多个紧挨的情况转移过来。所以只需要枚举断点 \(k\),合并累加两段答案即可。
scanf("%s", s + 1), n = strlen(s + 1), memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i++) dp[i][i] = 1;
for (int len = 2; len <= n; len++) {
for (int l = 1; l <= n - len + 1; l++) {
int r = l + len - 1;
if (s[l] == s[r]) dp[l][r] = min(dp[l][r - 1], dp[l + 1][r]);
else for (int k = l; k <= r; k++) dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
}
cout << dp[1][n];
前提是放在区间 DP 的作业里。 ↩︎