区间dp做题记录

CF1509C The Sports Festival

题目链接

题意:
给定长度为 \(n\) 的序列 \(s\),你可以改变序列 \(s\) 的顺序,求

\[\Large {\sum\limits_{i=1}^{n}(\max\limits_{j = 1}^{i}s_j - \min\limits_{j = 1}^{i}s_j)} \]

的最小值。

\(1 \leq n \leq 2 \times 10^3\)\(1 \leq s_i \leq 10^9\)

分析:

\(s_i\) 从小到大排序,然后从前往后选择一个还没有选过的 \(s_i\) 加到当前序列结尾。那么每个前缀中的数字一定在排序后的 \(s_i\) 数组中形成一段区间。
证明:如果序列 \([1, p-1]\) 位置上的数形成的区间为 \([l, r]\)\(p\) 位置上的数在 \(s_i\) 数组的 \(x\) 位置上。 不妨设 \(x > r + 1\),那么如果后面 \(q\) 位置上的数在序列 \(s_i\)\(r + 1\) 位置上。把 \(p\)\(q\) 上的数交换一定变优。
因此发现大区间的状态是由小区间转移过来的。直接记 \(f_{l, r}\) 表示当前序列中的数字为 排序后\(s_i\) 数组的 \([l, r]\) 区间的数字的最小值。转移就是直接考虑最后一个是 \(l\) 还是 \(r\) 即可。
时间复杂度 \(O(n^2)\)

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 2e3 + 10;
typedef long long LL;
int n;
LL f[N][N], a[N];
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) scanf("%lld", &a[i]);
	sort(a + 1, a + n + 1);
	for(int i = 1; i <= n; i ++ ) f[i][i] = 0;
	for(int len = 2; len <= n; len ++ ) {
		for(int L = 1; L + len - 1 <= n; L ++ ) {
			int R = L + len - 1;
			f[L][R] = min(f[L + 1][R] + a[R] - a[L], f[L][R - 1] + a[R] - a[L]);
		}
	}
	printf("%lld\n", f[1][n]);
	return 0;
}

CF1666J Job Lookup

题目链接

题意:

给你一个 \(n×n(1≤n≤200)\) 的矩阵 \(c(0≤c_{ij}≤10^9)\) ,构造一棵节点编号为 \(1\sim n\) 的二叉树,其任意一个节点的左子树内所有节点编号都小于它,右子树内所有节点编号都大于它,设 \(d_{ij}\)\(i\sim j\) 的最短路径,使得

\[\sum_{1\le i, j\le n}c_{ij}d_{ij} \]

最小,输出每个节点的父亲(根节点输出 \(0\)

分析:
每次找过根后就划分成了左右两个区间。两个区间分别构成两棵子树,是相对独立的。启示我们用区间 \(dp\)
然后就是拆贡献。把 \(d_{i, j}\) 的系数分到路径的每条边上。相当于一条边的贡献就是子树里的点 \(i\) 与子树外的点 \(j\)\(d_{i, j}\) 之和。
\(dp_{l, r}\) 表示区间 \([l, r]\) 形成的树,\([l, r]\) 以外的点挂在根节点上 的最小和。
转移就是枚举 \([l, r]\) 区间中的一个根节点 \(k\),然后有 \(dp_{l, r} = \min\limits_{k = l}^{r}dp_{l, k-1}+dp_{k + 1, r}+calc(l, k-1) + calc(k + 1, r)\)
其中 \(calc(l, r) = \sum_{i = l}^{r}\sum_{j = 1}^{n}[j < l \ | \ |\ l > r] \times d_{i, j}\)
时间复杂度 \(O(n^3)\)
CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 205;
typedef long long LL;
int n;
int rt[N][N], fa[N];
LL c[N][N], sum[N][N];
LL f[N][N]; // f[l][r] 表示区间 [l, r] 构成一棵树, 把[l, r] 以外的点全部放到根上的最小值 
LL calc(int l, int r) {
	if(l > r) return 0;
	return sum[r][n] - sum[l - 1][n] - (sum[r][r] - sum[l - 1][r] - sum[r][l - 1] + sum[l - 1][l - 1]);
}
void dfs(int l, int r, int fat) {
	if(l > r) return ;
	fa[rt[l][r]] = fat;
	dfs(l, rt[l][r] - 1, rt[l][r]);
	dfs(rt[l][r] + 1, r, rt[l][r]);
}
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) {
		for(int j = 1; j <= n; j ++ ) {
			scanf("%lld", &c[i][j]);
			sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + c[i][j];
		} 
	}
	memset(f, 0x3f, sizeof f);
	for(int i = 1; i <= n; i ++ ) f[i][i] = 0, rt[i][i] = i;
	for(int i = 1; i <= n + 1; i ++ ) f[i][i - 1] = 0;
	for(int len = 2; len <= n; len ++ ) {
		for(int L = 1; L + len - 1 <= n; L ++ ) {
			int R = L + len - 1;
			for(int k = L; k <= R; k ++ ) { // 枚举根节点 
			    if(f[L][k - 1] + f[k + 1][R] + calc(L, k - 1) + calc(k + 1, R) < f[L][R]) {
			    	rt[L][R] = k;
			    	f[L][R] = f[L][k - 1] + f[k + 1][R] + calc(L, k - 1) + calc(k + 1, R);
				} 
			}
		}
	}
	dfs(1, n, 0);
	for(int i = 1; i <= n; i ++ ) printf("%d ", fa[i]);
	return 0;
}

[USACO04OPEN] Turning in Homework G

题目链接

题意:
给你一根长度为 \(n\) 的数轴,点的编号为 \(0 \sim n\)。在数轴上左右移动一步会花费 \(1\) 的时间。有 \(m\) 个需求,第 \(i\) 个需求可以用一个二元组 \((t_i, x_i)\) 表示,意为你需要在 \(t_i\) 时刻及以后走到 \(x_i\) 至少一次。并且完成所有需求后,你需要到达位置 \(B\)。问最短用时。
\(n,m \leq 1000\)\(0 \leq B \leq n\)\(0 \leq x_i \leq n\)\(0 \leq t_i \leq 10000\)

分析:
如果最后终点在 \(n\),那么显然不会走回头路。因为走回头路到某个位置去满足限制再回去,不如直接在这个位置等到限制的时刻再往后走优秀。
终点不在 \(n\),那么可以把限制分成两类:在 \(B\) 左边的和在 \(B\) 右边的。对于在 \(B\) 左边的,一定是从最左边开始依次往右满足限制,在 \(B\) 右边的一定是从最右边开始依次往左满足限制。
将限制按照 \(x\) 从小到大排序,那么剩下的没有满足的限制一定是一段连续的区间,已经满足的限制就是这个区间两边的部分。可以对这个顺序 \(dp\)
\(dp_{l, r, 0/1}\) 表示还剩下的需求区间为 \([l, r]\)。此时的位置在 \(l\) 需求的 \(x\) 位置 还是在 \(r\) 需求的 \(x\) 位置的最小用时。转移就是这一次完成的需求是 \(l\) 还是 \(r\),以及完成后是跑到左端点还是右端点即可。
时间复杂度 \(O(n^2)\)
CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int n, m, B;
int f[N][N][2], tot;
struct task {
	int x, t;
	friend bool operator < (task p, task q) {
		return ((p.x < q.x) || (p.x == q.x && p.t < q.t));
	}
}a[N];
int main() {
	scanf("%d%d%d", &m, &n, &B);
	for(int i = 1; i <= m; i ++ ) {
		int x, t; scanf("%d%d", &x, &t);
		a[++ tot] = (task) {x, t};
	}
	a[++ tot] = (task) {B, 0};
	a[++ tot] = (task) {0, 0};
	sort(a + 1, a + tot + 1);
	int et = -1;
	for(int i = 1; i <= tot; i ++ ) {
		if(a[i].x == B && a[i].t == 0) {et = i; break;}
	} 
	memset(f, 0x3f, sizeof f);
	f[1][tot][0] = 0;
	for(int len = tot; len > 1; len -- ) {
		for(int L = 1; L + len - 1 <= tot; L ++ ) {
			int R = L + len - 1;
			f[L + 1][R][0] = min({f[L + 1][R][0], max(f[L][R][0], a[L].t) + a[L + 1].x - a[L].x, max(f[L][R][1] + a[R].x - a[L].x, a[L].t) + a[L + 1].x - a[L].x});
			f[L + 1][R][1] = min({f[L + 1][R][1], max(f[L][R][0], a[L].t) + a[R].x - a[L].x, max(f[L][R][1] + a[R].x - a[L].x, a[L].t) + a[R].x - a[L].x});
			f[L][R - 1][0] = min({f[L][R - 1][0], max(f[L][R][0] + a[R].x - a[L].x, a[R].t) + a[R].x - a[L].x, max(f[L][R][1], a[R].t) + a[R].x - a[L].x});
			f[L][R - 1][1] = min({f[L][R - 1][1], max(f[L][R][0] + a[R].x - a[L].x, a[R].t) + a[R].x - a[R - 1].x, max(f[L][R][1], a[R].t) + a[R].x - a[R - 1].x});
		}
	}
	printf("%d\n", min(f[et][et][0], f[et][et][1]));
	return 0;
}

[CERC2014] Outer space invaders

题目链接

题意:
你有一个长度为 \(10^4\) 的数组,开始时数组里都是 \(0\)。给你 \(m\) 个限制,第 \(i\) 个限制为 区间 \([l_i, r_i]\) 的最大值需要大于等于 \(d_i\)。问满足所有限制的条件下数组元素的和最小是多少。
\(1 \leq m \leq 300\)\(1 \leq l_i \leq r_i \leq 10^4\)\(1 \leq d_i \leq 10^4\)

分析:
[POI2015] MYJ 这道题的弱化版。
首先值大于 \(0\) 的位置一定是某个区间的端点。因此把区间端点离散化,那么点数就是 \(O(m)\) 的。
考虑区间 \(dp\)。设 \(dp_{l, r}\) 表示给 \([l, r]\) 区间的数组赋值,把所有 完全包含在 区间 \([l, r]\) 中的限制满足的最小和。
设这样的限制中最大的 \(d_i\)\(mx\)。那么显然我们至少需要让一个元素的值为 \(mx\)。枚举区间 \([L, R]\) 中最大值所在的位置 \(k\),那么所有经过 \(k\) 的限制都已经被满足了。因此只剩下完全包含在 \([L, k - 1]\)\([k + 1, R]\) 的限制没有被满足。这就转化成了一个子问题。
因此有转移 \(dp_{l, r} = \max\limits_{k = l}^{r}dp_{l, k -1} + dp_{k + 1, r} + mx\)
复杂度 \(O(m^3)\)
CODE:

// 考虑区间dp 
// 考虑一坨区间所在的大区间。这些区间存在一个要求最高的区间d,那么至少需要一个d来满足这个区间。
// 那么所有经过放d的位置的区间都被满足了,因此变成了两个子问题  
// 时间复杂度 O(n^3) 
#include<bits/stdc++.h>
using namespace std;
const int N = 305;
const int INF = 0x3f3f3f3f;
int T, n, s[N], t[N], d[N];
int node[N * 2], tot, f[N * 2][N * 2];
void solve() {
	scanf("%d", &n);
	tot = 0;
	for(int i = 1; i <= n; i ++ ) {
		scanf("%d%d%d", &s[i], &t[i], &d[i]);
		node[++ tot] = s[i]; node[++ tot] = t[i];
	}
	sort(node + 1, node + tot + 1);
	tot = unique(node + 1, node + tot + 1) - (node + 1);
	for(int i = 1; i <= n; i ++ ) {
		s[i] = lower_bound(node + 1, node + tot + 1, s[i]) - (node);
		t[i] = lower_bound(node + 1, node + tot + 1, t[i]) - (node);
	}
	memset(f, 0x3f, sizeof f);
	for(int i = 1; i <= tot + 1; i ++ ) {
		f[i][i - 1] = 0;
		f[i][i] = 0;
	}
	for(int i = 1; i <= n; i ++ ) {
		if(s[i] == t[i]) {
			f[s[i]][s[i]] = max(f[s[i]][s[i]], d[i]);
		}
	}
	for(int len = 2; len <= tot; len ++ ) {
		for(int L = 1; L + len - 1 <= tot; L ++ ) {
			int R = L + len - 1;
			int maxd = -1;
			for(int i = 1; i <= n; i ++ ) {
				if(s[i] >= L && t[i] <= R) maxd = max(maxd, d[i]);
			}
			if(maxd == -1) f[L][R] = 0;
			else {
				for(int i = L; i <= R; i ++ ) {
					f[L][R] = min(f[L][R], f[L][i - 1] + f[i + 1][R] + maxd);
				}
			}
		}
	}
	printf("%d\n", f[1][tot]);
}
int main() {
	scanf("%d", &T);
	while(T -- ) {
		solve();
	}
	return 0;
}

[USACO17JAN] Subsequence Reversal P

题目链接

题意:
给你一个长度为 \(n\) 的数组 \(a_i\)。你可以在开始时选择任意一个子序列,并将这个子序列内部翻转(也可以不选)。问操作后的序列的最长不降子序列 最长是多少。

\(1 \leq n \leq 50\)\(1 \leq a_i \leq 50\)

分析:
考虑对子序列进行翻转实际上等价于自内向外选择若干对数并交换。前面交换的数对一定要包含在后面交换的数对所在的区间中。这样在一对数的区间内就转化成了一个子问题。这启示我们区间 \(dp\)

\(dp_{l, r, mn, mx}\) 表示 \([l, r]\) 区间中考虑过交换若干对数,不降子序列的最小值为 \(mn\),最大值为\(mx\) 的最大长度。转移首先有:
\(dp_{l, r, mn, mx} \gets \max(dp_{l, r - 1, mn, mx},dp_{l + 1, r, mn, mx})\)

然后考虑 \(a_l = mn\) 的情况,那么有转移 \(dp_{l, r, mn, mx} \gets \max\limits_{k =mn}^{mx}dp_{l + 1, r, k, mx} + 1\)

这样状态数是 \(O(n^4)\),转移 \(O(n)\)。复杂度就是 \(O(n^5)\) 的。

但是注意到对于一个 \(mn' < mn\)\(mx' > mx\) 的状态 \(dp_{l, r, mn', mx'}\)\(dp_{l, r, mn, mx}\)。所有 \(dp_{l, r, mn', mx'}\) 能转移给的状态, \(dp_{l, r, mn, mx}\) 都能转移。因此考虑将状态的定义修改。另 \(dp_{l, r, mn, mx}\) 表示\([l, r]\) 区间中考虑过交换若干对数,不降子序列的最小值大于等于 \(mn\),最大值小于等于 \(mx\) 的最大长度。

那么上面的转移就变成了 \(dp_{l, r, mn, mx} \gets dp_{l + 1, r, mn, mx} + 1\)。转移复杂度 \(O(1)\)

对于 \(a_r = mx\) 的情况类似。
考虑交换 \(a_l\)\(a_r\)。那么只有在 \(a_r = mn\) 或者 \(a_l = mx\) 的时候交换有用。

如果 \(a_{l} = mx\),有转移 \(dp_{l, r, mn, mx} \gets dp_{l + 1, r - 1, mn, mx} + 1\)
\(a_r = mn\) 的情况类似。

如果\(a_l = mx\)\(a_r = mn\),那么后面的 \(+1\) 变成 \(+2\) 就行。

一个区间这样转移完后,还需要在值域 \([mn, mx]\) 上进行转移一次。
转移式子为:\(dp_{l, r, mn, mx} = \max(dp_{l, r, mn, mx}, dp_{l, r, mn + 1, mx}, dp_{l, r, mn, mx - 1})\)
让值域区间长度从小到大转移即可。

时间复杂度 \(O(n^4)\)
CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 51;
int n, a[N], ma;
int f[N][N][N][N]; // f[l][r][mn][mx] 表示考虑了 [l, r] 区间的数字(内部已经考虑过成对调换过了) ,区间内最小值大于等于mn, 最大值小于等于mx 的最长lis长度  
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%d", &a[i]);
		ma = max(ma, a[i]);
	}
	for(int i = 1; i <= n; i ++ ) {
		for(int L = a[i]; L >= 1; L -- ) 
		    for(int R = a[i]; R <= ma; R ++ ) 
		        f[i][i][L][R] = 1;
	}
	for(int len = 2; len <= n; len ++ ) {
		for(int L = 1; L + len - 1 <= n; L ++ ) {
			int R = L + len - 1;
			for(int mn = 1; mn <= ma; mn ++ ) 
			    for(int mx = mn; mx <= ma; mx ++ ) {
			    	f[L][R][mn][mx] = max(f[L][R - 1][mn][mx], f[L + 1][R][mn][mx]);
			    	if(a[L] == mn) f[L][R][mn][mx] = max(f[L][R][mn][mx], f[L + 1][R][mn][mx] + 1);
			    	if(a[R] == mx) f[L][R][mn][mx] = max(f[L][R][mn][mx], f[L][R - 1][mn][mx] + 1);
			    	if(a[R] == mn) f[L][R][mn][mx] = max(f[L][R][mn][mx], f[L + 1][R - 1][mn][mx] + 1);
			    	if(a[L] == mx) f[L][R][mn][mx] = max(f[L][R][mn][mx], f[L + 1][R - 1][mn][mx] + 1);
			    	if(a[L] == mx && a[R] == mn) f[L][R][mn][mx] = max(f[L][R][mn][mx], f[L + 1][R - 1][mn][mx] + 2);
				}
            for(int l = 0; l <= ma; l ++ ) {
            	for(int mn = 1; mn + l <= ma; mn ++ ) {
            		int mx = mn + l;
            		f[L][R][mn][mx] = max({f[L][R][mn][mx], f[L][R][mn + 1][mx], f[L][R][mn][mx - 1]});
				}
			}
		}
	}
	int res = 0;
	for(int mn = 1; mn <= ma; mn ++ ) 
		for(int mx = mn; mx <= ma; mx ++ ) 
		    res = max(res, f[1][n][mn][mx]);
    printf("%d\n", res);
	return 0;
}

「春季测试 2023」圣诞树

原题链接

题意:
给你一个 \(n\) 个点的凸多边形,你需要从 最高点(编号为\(k\)) 开始依次往一个还没有被经过的点连边,然后跳到这个点上。一条边的长度为两个端点的 曼哈顿距离。你需要求出一个最优排列 \(p\),满足

  • \(p_1 = k\)
  • 依次从 \(p_i\)\(p_{i + 1}\) 连边,所有边的长度之和最短。

\(3 \leq n \leq 1000\)\(|x_i|,|y_i| \leq 10^7\)

分析:
容易猜到当前已经经过的点一定是按照 \(x\) 排序后的一段区间。这个可以看着原题中的图去想。
然后设 \(dp_{l, r, 0/1}\) 表示已经经过了 \([l, r]\) 中的点,当前在左端/右端的最小长度。转移就是考虑最后一个点是左端点还是右端点以及上一个转移过来的点是谁就行。
时间复杂度 \(O(n^2)\)
CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
typedef double db;
const db INF = 1e18;
int n, h;
db x[N * 2], y[N * 2];
db dp[N * 2][N * 2][2];
struct Lst {
	int l, r, o;
};
Lst lst[N * 2][N * 2][2];
int ans[N], tot;
db dis(int a, int b) {
	return sqrt((x[a] - x[b]) * (x[a] - x[b]) + (y[a] - y[b]) * (y[a] - y[b]));
}
void get(int l, int r, int o) {
	ans[++ tot] = (o == 0 ? l : r);
	if(l == r) return ;
	get(lst[l][r][o].l, lst[l][r][o].r, lst[l][r][o].o);
}
int main() {
	freopen("tree.in", "r", stdin);
	freopen("tree.out", "w", stdout);
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%lf%lf", &x[i], &y[i]);
		x[i + n] = x[i]; y[i + n] = y[i];
		if(!h || y[i] > y[h]) h = i;
	} 
	for(int i = 0; i < N * 2; i ++ ) 
	    for(int j = 0; j < N * 2; j ++ ) 
	        for(int o = 0; o < 2; o ++ ) 
	            dp[i][j][o] = INF;
	dp[h][h][0] = dp[h][h][1] = dp[h + n][h + n][0] = dp[h + n][h + n][1] = 0;
	for(int len = 2; len <= n; len ++ ) {
		for(int L = 1; L + len - 1 <= n * 2; L ++ ) {
			int R = L + len - 1;
			if(dp[L + 1][R][0] + dis(L + 1, L) < dp[L][R][0]) dp[L][R][0] = dp[L + 1][R][0] + dis(L + 1, L), lst[L][R][0] = (Lst) {L + 1, R, 0};
			if(dp[L + 1][R][1] + dis(R, L) < dp[L][R][0]) dp[L][R][0] = dp[L + 1][R][1] + dis(R, L), lst[L][R][0] = (Lst) {L + 1, R, 1};
			if(dp[L][R - 1][0] + dis(L, R) < dp[L][R][1]) dp[L][R][1] = dp[L][R - 1][0] + dis(L, R), lst[L][R][1] = (Lst) {L, R - 1, 0};
			if(dp[L][R - 1][1] + dis(R - 1, R) < dp[L][R][1]) dp[L][R][1] = dp[L][R - 1][1] + dis(R - 1, R), lst[L][R][1] = (Lst) {L, R - 1, 1};
		}
	}
	db res = INF; int l, r, o;
	for(int i = 1; i <= n; i ++ ) {
		if(dp[i][i + n - 1][0] < res) res = dp[i][i + n - 1][0], l = i, r = i + n - 1, o = 0;
		if(dp[i][i + n - 1][1] < res) res = dp[i][i + n - 1][1], l = i, r = i + n - 1, o = 1;
	}
	get(l, r, o);
	for(int i = tot; i >= 1; i -- ) printf("%d ", ans[i] > n ? ans[i] - n : ans[i]);
	return 0;
}

括号序列再战猪猪侠

原题链接

题意:
定义 \(match_i\) 表示从左往右数第 \(i\) 个左括号所对应的是第几个右括号。
现在有一个长度为 \(2 \times n\) 的括号序列,有 \(m\) 条限制,第 \(i\) 条限制形如 \((a_i, b_i)\),表示 \(match_{a_i} < match_{b_i}\)
求出有多少个满足所有限制的合法括号序列。
答案对 \(998244353\) 取模。

\(1 \leq n, m \leq 300\)

分析:
首先对于考虑一个合法的括号序列的形态:
例如 \((()())()()(((())))\)
发现可以划分成若干段,例如上面的例子可以划分成 \((()()) \ \ () \ \ () \ \ (((())))\) 这四段。
每一段内都是由 若干连续的左括号和它们匹配的右括号构成,并且一段的最左边和最右边是一对匹配的括号。
然后不难发现 右边段内的左括号所匹配的右括号一定都在左边段内左括号匹配的右括号右边。也就是说不同段内的左括号所匹配的右括号之间的先后关系是能确定的。如果能满足所有左边段内的左括号的 \(match\) 都不大于右端,那么段内之间的限制就是独立的。就可以转化成一个更小的子问题求解。
也就是我们需要计算出一段左括号和它们对应的右括号能够构成多少满足段内限制的合法括号序列。

\(dp_{l, r}\) 表示 \([l, r]\) 区间内的左括号和它们的右括号能构成多少个满足限制的合法括号序列。
那么转移就是枚举第 \(r\) 个左括号所在段的最左边的左括号是哪一个。记这个左括号的编号为 \(k\),那么有转移:
\(dp_{l, r} \gets dp_{l, k - 1} \times dp_{k + 1, r}\)
上述转移的条件是第 \(k\) 个左括号匹配的右括号能够放在第 \([l, r]\) 个左括号匹配的右括号的后边,并且第 \([k + 1, r]\) 个左括号匹配的右括号能够放在第 \([l, k - 1]\) 个左括号匹配的右括号的后边。
如果对于一个限制 \((a, b)\),我们把矩阵 \((a, b)\) 位置上加 \(1\)。那么转移条件就相当于 以\((k, l)\) 为左上角, \((k, r)\) 为右下角的矩形和为 \(0\) 以及 以 \((k + 1, l)\) 为左上角, \((r, k - 1)\) 为右下角的矩形和为 \(0\)
预处理矩阵的二位前缀和,每次转移 \(O(1)\) 判断即可。
时间复杂度 \(O(n^3)\)
CODE:

// 考虑 dp[l][r] 表示第 [l, r] 区间的左括号和它们对应的右括号构成的合法括号序列数
// 然后转移就考虑枚举枚举第r个左括号右边最后一个右括号匹配的左括号。那么两边的限制是独立的,还需要满足跨段的限制 
#include<bits/stdc++.h>
using namespace std;
const int N = 305;
typedef long long LL;
const LL mod = 998244353;
int T, n, m;
int sum[N][N];
LL dp[N][N];
LL calc(int lx, int ly, int rx, int ry) {
	if(ry < ly) return 0;
	return sum[rx][ry] - sum[lx - 1][ry] - sum[rx][ly - 1] + sum[lx - 1][ly - 1];
}
void solve() {
	memset(dp, 0, sizeof dp);
	memset(sum, 0, sizeof sum);
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= m; i ++ ) {
		int a, b;
		scanf("%d%d", &a, &b);
		sum[a][b] ++;
	}
	for(int i = 1; i <= n; i ++ ) 
	    for(int j = 1; j <= n; j ++ ) 
	        sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + sum[i][j];
	for(int i = 1; i <= n; i ++ ) {
		if(calc(i, i, i, i) == 0) dp[i][i] = 1;
	}
	for(int i = 1; i <= n + 1; i ++ ) dp[i][i - 1] = 1;
	for(int len = 2; len <= n; len ++ ) {
		for(int L = 1; L + len - 1 <= n; L ++ ) {
			int R = L + len - 1;
			for(int k = R; k >= L; k -- ) {
				if(calc(k, L, R, k - 1) == 0 && calc(k, L, k, R) == 0) dp[L][R] = (dp[L][R] + dp[L][k - 1] * dp[k + 1][R] % mod) % mod;
			}
		}
	}
	printf("%lld\n", dp[1][n]);
}
int main() {
	scanf("%d", &T);
	while(T -- ) solve();
	return 0;
}

总结:对于括号匹配类 \(dp\),可以以最后形成的若干独立段作为 \(dp\) 的划分将问题转化成规模更小的子问题。转移根据最右边的左括号所属段的最左边的左括号不同来分类。

「THUSC 2016」成绩单

原题链接

题意:
给你代价系数 \(a,b\)\(n\) 个数 \(c_i\),你可以每次选择一段连续的数删掉,剩下的数前后拼接起来。你需要最后将所有数都删去。设删除了 \(k\) 次,每次删除段内的最大值和最小值为 \(max_i\)\(min_i\)。那么这种删除方案的花费为

\[a \times k + b \times \sum_{i = 1}^{k}(max_i - min_i)^2 \]

求将所有数都删去的最小花费是多少。

\(n \leq 50,a \leq 1500, b \leq 10, w_i \leq 1000\)

分析:

首先 \(a \times k\) 可以拆成每次删除有 \(a\) 的花费。
考虑最后一次删的数,那么它们会将原序列划分成若干段,每段内的删除价值是独立的。这就变成了一个规格更小的问题。也就是 大区间的最小花费可以由小区间推出,这启示我们进行区间 \(dp\)

考虑到最后一次的删除价值只和最后一次删除的数的极差有关,那么记 \(f_{l, r, mn, mx}\) 表示区间 \([l, r]\) 最后一次删除的数最小值是 \(mn\),最大值是 \(mx\),并且除了最后一次删除的代价没有算,其余数删除的代价都已经计算过的最小代价。再设 \(g_{l, r}\) 表示将区间 \([l, r]\) 完全删除的最小花费。

转移就是考虑右端点 \(r\) 要不要最后一次删。

  1. \(r\) 是最后一次删,那么需要满足 \(mn \leq c_{r} \leq mx\)
    转移就是 \(dp_{l, r, mn, mx} \gets dp_{l,r -1, mn, mx}\)
  2. \(r\) 不是最后一次删。那么枚举一个 \(k\) 代表最后一次删的数将序列划分成的若干段中 \(r\) 所在段的左端点。
    转移有 \(dp_{l, r, mn, mx} \gets dp_{l, k - 1, mn, mx} + g_{k, r}\)

对于 \(g_{l, r}\) 的转移,有:
\(g_{l, r} = \min\limits_{mn, mx}dp_{l, r, mn, mx} + a + b\times (mx - mn)^2\)

注意到对于第一种转移,可能存在 \([l, r - 1]\) 中不存在等于 \(mn\)\(mx\) 的数的情况 或是 \(mx,mn\) 中有一个是由 \(c_r\) 提供的。那么这时候按照原来的定义 \(dp_{l, r - 1, mn, mx}\) 的值为 \(∞\),无法转移给 \(dp_{l, r, mn, mx}\)

那么我们将定义修改一下:\(dp_{l, r, mn, mx}\) 表示区间 \([l, r]\) 中最后一次删的数最大值小于等于 \(mx\),最小值大于等于 \(mn\) 的最小删除代价。

[USACO17JAN] Subsequence Reversal P类似,只需要对 \(dp\) 进行对 \(r\) 的转移后再对值域 \([mn, mx]\) 做一遍转移。
时间复杂度 \(O(n^4)\)
CODE:

// 考虑最后一次删除的数,它们会把原序列划分成若干段,每一段独立
// 这启示我们区间dp
// 对于最后一段,我们只关心段中最大值和最小值是多少
// 记 f[l][r][mn][mx] 表示将[l, r]删空, 最后一次删除的最小值大于等于mn, 最大值小于等于mx的除了钦定最后删的最小代价。 g[l, r]是把[l, r]删空的最小代价。 
// 转移就是考虑r在删除时是否是最后一次删的。如果不是就枚举r所在的那一段 
#include<bits/stdc++.h>
using namespace std;
const int N = 51;
int f[N][N][N][N], g[N][N];
int n, a, b, w[N], d[N], tot;
int Mx[N][N], Mn[N][N];
int calc(int x, int y) {
	return b * (x - y) * (x - y);
}
int main() {
	scanf("%d", &n);
	scanf("%d%d", &a, &b);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%d", &w[i]);
		d[++ tot] = w[i];
	}
	sort(d + 1, d + tot + 1);
	tot = unique(d + 1, d + tot + 1) - (d + 1);
	for(int i = 1; i <= n; i ++ ) w[i] = lower_bound(d + 1, d + tot + 1, w[i]) - (d);
	memset(f, 0x3f, sizeof f); memset(g, 0x3f, sizeof g);
	for(int l = 1; l <= n; l ++ ) {
		int mx = -10, mn = 1e8;
		for(int r = l; r <= n; r ++ ) {
			mx = max(mx, w[r]);
			mn = min(mn, w[r]);
			Mx[l][r] = mx; Mn[l][r] = mn;
		}
	}
	for(int i = 1; i <= n; i ++ ) {
		g[i][i] = a;
		for(int l = 1; l <= tot; l ++ ) 
		    for(int r = l; r <= tot; r ++ ) 
		       f[i][i][l][r] = ((l <= w[i] && r >= w[i]) ? 0 : a);
	}
	for(int len = 2; len <= n; len ++ ) {
		for(int L = 1; L + len - 1 <= n; L ++ ) {
			int R = L + len - 1;
			for(int mn = 1; mn <= tot; mn ++ ) {
				for(int mx = mn; mx <= tot; mx ++ ) {
					if(w[R] >= mn && w[R] <= mx) f[L][R][mn][mx] = min(f[L][R][mn][mx], f[L][R - 1][mn][mx]); // 可以最后删 
					for(int k = R; k > L; k -- ) f[L][R][mn][mx] = min(f[L][R][mn][mx], f[L][k - 1][mn][mx] + g[k][R]);
				}
			}
			for(int l = 2; l <= tot; l ++ ) {
				for(int mn = 1; mn + l - 1 <= tot; mn ++ ) {
					int mx = mn + l - 1;
					f[L][R][mn][mx] = min({f[L][R][mn][mx], f[L][R][mn][mx - 1], f[L][R][mn + 1][mx]});
				}
			}
			for(int mn = 1; mn <= tot; mn ++ ) {
				for(int mx = mn; mx <= tot; mx ++ ) {
					g[L][R] = min(g[L][R], f[L][R][mn][mx] + a + calc(d[mx], d[mn]));
				}
			}
		}
	}
	printf("%d\n", g[1][n]);
	return 0;
}

总结:对于这样删除后序列会拼接在一起的问题,可以考虑最后一次删除的数会将序列分成若干段。那么每一段内就是独立,这样就转化成了一个更小的问题。

[CQOI2007] 涂色

原题链接

题意:
有一个长度为 \(n\) 的木板,开始时每个位置都没有颜色,你需要最终将木板涂成一个目标状态。目标状态可以用一个长度为 \(n\) 的只有小写字符的字符串 \(S\) 表示。每次你都可以选择一段区间并将这段区间涂成一种颜色。问达到目标状态最少涂色多少次。

\(1 \leq n \leq 50\)

分析:

注意到最终的涂色区间只有 包含不交 两种关系,也就是不会出现 相交 的关系。
证明很简单:因为涂色会覆盖原来的颜色,因此两个区间相交可以让第一次涂色的区间去掉相交的部分,效果是不变的。
这样原序列会被划分成若干段区间,每段区间内的答案是独立的,就转化成了一个子问题,可以用区间 \(dp\)
但是由于一个区间的答案和初始时区间的颜色有关,因此还需要多记一维状态表示区间的初始颜色。
\(dp_{l, r, c}\) 表示区间 \([l, r]\) 的初始颜色是 \(c\),将 \([l, r]\) 染成目标状态最少的染色次数。
转移就是考虑将 \(r\) 染成最终状态的区间:

  • \(dp_{l, r, c} \gets dp_{l, r - 1, c}(S_{r} = c)\)
  • \(dp_{l, r, c} \gets \min\limits_{k=l}^{r} dp_{l, k - 1, c} + dp_{k, r - 1, S_r} + 1\)

第二个转移相当与枚举了最后一次将 \(r\) 染成 \(S_r\) 的区间 \([k, r]\),那么之后的染色区间都不会再包含 \(r\)
时间复杂度 \(O(n^3 \times 26)\)
CODE:

// 发现染色区间只有包含和相邻的情况
// 可以区间dp 
#include<bits/stdc++.h>
using namespace std;
const int N = 51;
const int M = 27;
int f[N][N][M]; // f[l][r][c] 表示 [l, r] 区间,初始区间颜色为c的最小涂色次数 
int n, col[N];
char str[N]; 
int main() {
	scanf("%s", str + 1);
	n = strlen(str + 1);
	for(int i = 1; i <= n; i ++ ) {
		col[i] = (str[i] - 'A' + 1);
	}
	memset(f, 0x3f, sizeof f);
	for(int i = 1; i <= n; i ++ ) 
	    for(int j = 0; j <= 26; j ++ ) 
	        f[i][i][j] = (col[i] == j ? 0 : 1);
	for(int i = 1; i <= n; i ++ ) 
	    for(int j = 0; j <= 26; j ++ ) 
	        f[i][i - 1][j] = 0;
    for(int len = 2; len <= n; len ++ ) {
    	for(int L = 1; L + len - 1 <= n; L ++ ) {
    		int R = L + len - 1;
    		for(int c = 0; c <= 26; c ++ ) {
    			if(col[R] == c) f[L][R][c] = f[L][R - 1][c]; // 可以不考虑R
				for(int k = R; k >= L; k -- ) {
					f[L][R][c] = min(f[L][R][c], f[L][k - 1][c] + f[k][R - 1][col[R]] + 1);
				} 
			}
		}
	}
	printf("%d\n", f[1][n][0]);
	return 0;
}

总结:操作区间不交 是一个很好的性质,有时可以将问题转化成子问题。

「ZJOI2016」线段树

原题连接

题意:
给你一个长度为 \(n\) 的序列 \(a\)。问操作 \(q\) 次,每次等概率随机一个区间 \([l, r]\)(\(1 \leq l \leq r \leq n\)),把 \([l, r]\) 赋值成区间最大值。问每个数最后的期望大小乘 \((\frac{n \times (n + 1)}{2})^q\) 的值。

\(n \leq 400\)\(q \leq 400\)

分析:
感觉这题好难。

首先题意就是对于每个位置 \(i\) 求所有方案下 \(a_i\) 的最终大小之和。

学到了一个经典trick:关于 \(min/max\) 的非 \(ds\) 问题先想 \(0/1\) 序列该怎么做。(不会 \(0/1\) 序列的情况一定不会原问题)

对于 \(0/1\) 序列,我们只需要对每个位置求出它最后是 \(0\) 的方案数。发现在每次操作后 \(0\)极长连续段 都只会向中心缩减,那么每段极长的 \(0\) 是相互独立的,可以对于每一段单独计数。

考虑对缩减的过程 \(dp\)。设 \(dp_{x, l, r}\) 表示进行 \(x\) 次操作后,\([l, r]\) 是一段极长的 \(0\) 的方案数。那么对于每个位置 \(i\)\(\sum_{l \leq i}\sum_{r \geq i}dp_{q, l, r}\) 就是这个位置最后是 \(0\) 的方案数。

考虑转移:

  1. \(dp_{x, l,r} \gets dp_{x - 1, l, r} \times (\frac{(n - r)(n-r+1)}{2} + \frac{(l - 1)l}{2} + \frac{(r-l+1)(r-l+2)}{2})\)
  2. \(dp_{x, l, r} \gets \sum\limits_{l' < l}dp_{x - 1, l', r} \times (l'-1)\)
  3. \(dp_{x, l, r} \gets \sum\limits_{r'>r}dp_{x - 1, l, r'} \times (n - r')\)

第一个转移 \(O(1)\),后两个转移可以 前缀和优化。因此总时间复杂度 \(O(q \times n^2)\)

接下来考虑不是 \(0/1\) 序列该怎么做:我们要对一个位置 \(i\) 求出 \(\sum w \times f_i(w)\),其中 \(f_i(w)\)\(i\) 位置上最后的数为 \(w\) 的方案数。
考虑 将贡献拆开\(w = \sum\limits_{i = 1}^{w}1\),由于 \(f_i(w)\) 中的每种方案会在所有 \(1 \leq j \leq w\)\(f_i(\geq j)\) 中被计算一次,所以
\(\sum w \times f_i(w) = \sum f_i( \geq j)\)

那么我们枚举 \(j\),将大于等于 \(j\) 的数看成 \(1\)小于 \(j\) 的数看作 \(0\)。这样就可以转化成 \(0/1\) 序列了,那么最后 \(i\) 位置上是 \(0\) 的方案数就等于 \(a_i\) 最后小于 \(j\) 的方案数,用总方案数 \((\frac{n \times (n + 1)}{2})^q\) 减去是 \(0\) 的方案数就得到了 \(f_i(\geq j)\)

但是 \(j\) 的值域很大,肯定不能枚举 \(j\)。注意到 \(0/1\) 序列的形态只有 \(n\) 种,因此对于每一种形态将初始值赋为这样的形态对应 \(j\) 的数量即可。表示开始就乘上一个系数。

这样时间复杂度是 \(O(n^3 \times q)\) 的,无法通过。

优化就是注意到不同种形态的 \(0/1\) 序列转移方程是完全相同的,只有初始值不同,因此我们把所有初始值加起来,整体做一遍 \(dp\) 即可。

时间复杂度 \(O(q \times n^2)\)

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 405;
typedef long long LL;
const LL mod = 1e9 + 7;
inline LL Pow(LL x, LL y) {
	LL res = 1LL, k = x % mod;
	while(y) {
		if(y & 1) res = res * k % mod;
		y >>= 1;
		k = k * k % mod;
	}
	return res;
}
int n, q, tot;
LL ans[N], dp[N][N];
LL g[N][N], f[N][N];
LL a[N], b[N];
int c[N];
void in(LL x, LL k) { // k 代表系数 
	for(int i = 1; i <= n; i ++ ) c[i] = (a[i] >= x);
	for(int i = 1; i <= n;) {
		if(c[i] == 1) {i ++; continue;}
		int j = i;
		while(j <= n && c[j] == c[i]) j ++;
		dp[i][j - 1] = (dp[i][j - 1] + k) % mod;
		i = j;
	}
}
inline LL S(int l, int r) {
	return (1LL * (n - r) * (n - r + 1) / 2 + 1LL * (l - 1) * l / 2 + 1LL * (r - l + 1) * (r - l + 2) / 2) % mod; 
}
int main() {
	scanf("%d%d", &n, &q);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%lld", &a[i]);
		b[++ tot] = a[i]; 
	}
	sort(b + 1, b + tot + 1);
	tot = unique(b + 1, b + tot + 1) - (b + 1);
	ans[1] = b[tot] * Pow(1LL * n * (n + 1) / 2, q) % mod;
	for(int i = 2; i <= n; i ++ ) ans[i] = ans[1];
	b[0] = 0;
	for(int i = 1; i <= n; i ++ ) in(b[i], b[i] - b[i - 1]);
	for(int i = 1; i <= q; i ++ ) { // dp[i][L][R] 代表操作i次,[L, R] 是一段极长连续0的方案数 
		for(int R = 1; R <= n; R ++ ) {
			LL v = 0;
			for(int L = 1; L <= R; L ++ ) {
				g[L][R] = v;
				v = (v + dp[L][R] * 1LL * (L - 1) % mod) % mod;
			}
		}
		for(int L = 1; L <= n; L ++ ) {
			LL v = 0;
			for(int R = n; R >= L; R -- ) {
				f[L][R] = v;
				v = (v + dp[L][R] * 1LL * (n - R) % mod) % mod;
			}
		}
		for(int L = 1; L <= n; L ++ ) {
			for(int R = L; R <= n; R ++ ) {
				dp[L][R] = (dp[L][R] * S(L, R) % mod + g[L][R] + f[L][R]) % mod;
			}
		}
	}
	for(int i = 1; i <= n; i ++ ) {
		for(int L = i; L >= 1; L -- ) {
			for(int R = i; R <= n; R ++ ) {
				ans[i] = (ans[i] - dp[L][R] + mod) % mod;
			}
		}
	}
	for(int i = 1; i <= n; i ++ ) printf("%lld ", ans[i]);
	return 0;
}

总结:

  1. \(ds\)\(min/max\) 问题先考虑 \(0/1\) 序列怎么做。
  2. 拆贡献的技巧很重要。
  3. 如果转移只涉及加减法并且每次 \(dp\) 除了初始值外转移完全相同,可以考虑将初始值累加整体 \(dp\)

「THUPC 2021」小 E 爱消除

原题链接

题意:
你有一个大小为 \(n\) 双端队列,队列里每个元素有一种颜色 \(c_i\)。每次你可以选择队首或队尾拿出来放入栈中,栈初始时为空。如果栈顶是两个颜色相同的元素,就把将这两个元素弹出。问最后队列为空时,栈内最少剩下几个元素,以及在剩下元素最少的条件下,栈的空间最少开多大。

  • \(n \leq 50\)\(1 \leq c_i \leq n\)

分析:

每次是从队列两端往内部删除元素,因此每次剩下的元素一定是 一段区间。因此可以区间 \(dp\)

\(g_{l, r} = (x, y)\),其中 \(x\)\([l, r]\) 删空后栈中最少剩下的元素数,\(y\) 是满足 \(x\) 的条件下栈的空间至少开到 \(y\)

转移可以考虑第一次放 \(l\) 还是 \(r\) 分类。那么首先有:
\(g_{l, r} \gets \min(g_{l + 1, r}, g_{l, r - 1}) + (1, 1)\)
表示第一次放入 \(l\)\(r\) 后, \(l/r\) 最后没有消掉。

考虑 \(l/r\) 最后消掉的情况,下面只考虑 \(r\),对于 \(l\) 同理。
可以枚举一个 \(c_i = c_r\)\(i\) 表示 \(i\) 要和 \(r\) 消掉。

1

那么想要把 \(i\) 放进去,有两种情况:把 \([i + 1, r - 1]\) 都放进去 或者 把 \([l, i - 1]\) 都放进去。
对于第一种情况,我们想让 \([i + 1, r - 1]\) 放进去后都消掉,这样 \(i\) 才能和 \(r\) 相邻。那么我们可能在过程通过放入左边的一些元素来帮助消除。可以枚举一个元素 \(j(j \leq i - 1)\),表示将 \([l, j, i + 1, r - 1]\) 放入栈中并且全部消除。那么我们关系这个过程中对栈空间的限制。因此记 \(f_{l_1, r_1, l_2, r_2}\) 表示将 \([l_1, r_1]\)\([l_2, r_2]\) 的元素消完,栈的空间至少开多大。
那么 \(g_{l, r} \gets (g_{j + 1, i - 1}.fir, \max(g_{j + 1, i - 1}.sec, \max(f_{l, j, i + 1, r-1} + 1, 2)))\)
这一部分时间复杂度 \(O(n^4)\)

2

对于 \(f_{l_1, r_1, l_2, r_2}\) 的转移与 \(g_{l, r}\) 类似:枚举一个最后与 \(r\) 消掉的 \(i\),然后再枚举一个 \(j\) 辅助消除。
时间复杂度 \(O(n^6)\)
但是对 \(f\) 的求解存在很多剪枝。

  • 如果 \(r_2 - l_2 + r_1 - l_1\) 为奇数,那么无解。
  • 如果区间中不是每种颜色都出现偶数次,那么无解。
  • 对于一个 \(l_1 > r_1\)\(l_2 \leq r_2\) 的状态,可以把 \(l_1\) 赋值成 \(1\)\(r_1\) 赋值成 \(0\) 以减少状态数。

CODE:

// 区间dp 
// 以第一次放的球不消掉/消掉 分成子问题 
// 时间复杂度 O(n^6) 
#include<bits/stdc++.h>
#define MP make_pair
#define fir first
#define sec second
using namespace std;
typedef long long LL;
const int N = 56;
const int INF = 1e9;
typedef pair< int, int > PII;
PII operator + (PII a, PII b) {return MP(a.fir + b.fir, a.sec + b.sec);}
mt19937_64 rnd(time(0));
int n, c[N];
int f[N][N][N][N];
LL val[N], s[N];
PII g[N][N];
int F(int l1, int r1, int l2, int r2) {
	if(l1 > r1 && l2 > r2) return 0;
	if(((r1 - l1 + 1) + (r2 - l2 + 1) & 1) || (s[r1] ^ s[l1 - 1] ^ s[r2] ^ s[l2 - 1])) return INF; // 不合法状态 
	if(l1 > r1) l1 = 1, r1 = 0; // 这样可以减少状态数 
	if(l2 > r2) l2 = 1, r2 = 0;
	if(f[l1][r1][l2][r2]) return f[l1][r1][l2][r2];
	int &res = f[l1][r1][l2][r2];
	res = INF;
	for(int i = l1; i <= r1; i ++ ) {
		if(i != l1 && c[i] == c[l1]) { // i 与 l1 匹配 
			for(int j = l2 - 1; j <= r2; j ++ ) res = min(res, max(F(i + 1, r1, l2, j), max(F(l1 + 1, i - 1, j + 1, r2) + 1, 2)));
		}
		if(i != r2 && c[i] == c[r2]) {
			for(int j = l2 - 1; j < r2; j ++ ) res = min(res, max(F(i + 1, r1, l2, j), max(F(l1, i - 1, j + 1, r2 - 1) + 1, 2)));
		}
	}
	for(int i = l2; i <= r2; i ++ ) {
		if(i != l1 && c[i] == c[l1]) {
			for(int j = l1 + 1; j <= r1 + 1; j ++ ) res = min(res, max(F(j, r1, l2, i - 1), max(F(l1 + 1, j - 1, i + 1, r2) + 1, 2)));
		}
		if(i != r2 && c[i] == c[r2]) {
			for(int j = l1; j <= r1 + 1; j ++ ) res = min(res, max(F(j, r1, l2, i - 1), max(F(l1, j - 1, i + 1, r2 - 1) + 1, 2)));
		}
	}
	return res;
}
PII G(int l, int r) { // 解决 [l, r] 
	if(g[l][r].sec) return g[l][r];
	if(l > r) return MP(0, 0);
	PII &res = g[l][r];
	res = MP(INF, INF);
	res = min({res, G(l, r - 1) + MP(1, 1), G(l + 1, r) + MP(1, 1)}); // 第一次拿出的球消不掉 
	for(int i = l; i <= r; i ++ ) {
		if(i != l && c[i] == c[l]) { // i 是与 l 消除的球 
			for(int j = l + 1; j <= r; j ++ ) {
				int o = F(l + 1, min(j, i) - 1, max(i, j) + 1, r);
			    if(o < INF) {
			    	PII tmp = (j <= i ? G(j, i - 1) : G(i + 1, j));
			    	res = min(res, MP(tmp.fir, max(tmp.sec, max(o + 1, 2))));
				}
			}
		}
		if(i != r && c[i] == c[r]) { // i 是与 r 消除的球 
			for(int j = l; j < r; j ++ ) {
				int o = F(l, min(j, i) - 1, max(i, j) + 1, r - 1);
				if(o < INF) {
					PII tmp = (j <= i ? G(j, i - 1) : G(i + 1, j));
                    res = min(res, MP(tmp.fir, max(tmp.sec, max(o + 1, 2))));
				}
			}
		}
	}
	return res;
}
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%d", &c[i]);
		val[i] = rnd();
	}
	for(int i = 1; i <= n; i ++ ) s[i] = (s[i - 1] ^ val[c[i]]);
	g[1][n] = G(1, n);
	printf("%d %d\n", g[1][n].fir, g[1][n].sec);
	return 0;
}

代码源CSP-S 模拟赛 DAY 20 T2 搬箱子

原题链接

题意:
一共有 \(n\) 个仓库,第 \(i\) 个仓库里面一共有 \(f_i\) 个箱子,第 \(i\) 个仓库和第 \(i + 1\) 个仓库的墙的高度是 \(h_i\)。同时,你可以随身带任意多个箱子。
现在假设你在 \(i\) 仓库,你身上带着 \(x\) 个箱子,当前仓库里面有 \(y\) 个,你可以做这些操作。

  1. 拿起一个箱子,也就是使 \(x\) 加一,\(y\) 减一,其中 \(y\) 要大于 \(0\)
  2. 放下一个箱子,也就是让 \(y\) 加一,\(x\) 减一,其中 \(x\) 要大于 \(0\)
  3. 移动到相邻的仓库,需要满足当前仓库里面的箱子个数不小于与相邻仓库的墙的高度。比如从 \(i\) 移动到 \(i+1\),那么要求 \(y≥h_{i}\)

你可以执行上述操作任意多次。
现在你想知道,如果一开始在 \(i\) 仓库,你想要访问遍所有的仓库,那么一开始需要至少随身带多少个箱子。

\(n \leq 5000\)\(0 \leq f_i \leq 10^5\)\(1 \leq h_i \leq 10^5\)

分析:

有一种比较简单的想法:枚举起点 \(i\),然后发现每个时刻所有到达的仓库是一段连续的区间。因此考虑区间 \(dp\)

然后想象遍历仓库的移动过程,不难发现最优的移动方式一定是向一个方向移动若干次,收集到一些箱子后再向另一个方向移动,然后这样交替。因此每次停下来往回走的时候一定位于区间的一个端点。

因此设 \(dp_{l, r,0/1}\) 表示在固定起点的情况下已经遍历了 \([l, r]\) 仓库,当前位于 左端点/右端点开始需要带的箱子的最少数量。
那么根据 \(dp\) 值以及状态中的 \([l, r]\) 能够算出当前手中的箱子数为 \(dp_{l, r, 0/1} + \sum_{i = l}^{r}f_i - \sum_{i = l}^{r - 1}h_i\)。这样就比较容易转移。

考虑转移:
对于状态 \(dp_{l, r, 0/1}\),设 \(x\) 表示当前手中的箱子数。设 \(mx = \max\limits_{i = l}^{r - 1} h_i\)
\(dp_{l - 1, r, 0} \gets \min(dp_{l, r, 0} + \max(0, x - h_{l - 1}), dp_{l, r, 1} + \max(0, x - mx, x - h_{l - 1}))\)
\(dp_{l, r + 1, 1} \gets \min(dp_{l, r, 1} + max(0, x -h_r), dp_{l, r, 0} + \max(0, x - mx, x - h_r))\)

其中 \(\max(0, x - mx, x - h_{l - 1})\) 这个转移代价,是因为如果当前位于 \([l, r]\) 的右端点,想要到达左端点左边的仓库,此时 \([l, r - 1]\) 仓库中靠右墙一定存在 \(h_{i}\) 的箱子,所以可以看作翻过墙后能够将消耗的补充回来,所以只需要当前手中的箱子数量大于等于区间 \(h_i\) 最大值就行,并且还需要大于大于 \(h_{l - 1}\)。下面的转移同理。

这样初始值赋值成 \(dp_{i, i, 0/1} = 0\),其余为正无穷。最后答案就是 \(\min(dp_{1, n, 0}, dp_{1,n, 1})\)。时间复杂度 \(O(n^3)\)

正解是将 \(dp\) 状态稍作修改。
\(dp_{l, r, 0/1}\) 表示已经遍历了 \([l, r]\)(不考虑遍历这个区间所需要的箱子),当前在左端点/右端点,需要将所有仓库遍历,手里需要有的箱子数。

初始值为 \(dp_{1, n, 0} = dp_{1, n, 1} = 0\)。 其余为正无穷

转移是从大区间往小区间转:
\(dp_{l + 1, r, 0} \gets \max(h_{l}, dp_{l, r, 0} + h_{l} - f_{l})\)
\(dp_{l + 1, r, 1} \gets \max(mx, dp_{l, r, 0} + h_{l} - f_{l})\)
\(dp_{l, r - 1, 1} \gets \max(h_{r - 1}, dp_{l, r, 1} + h_{r - 1} - f_{r})\)
\(dp_{l, r - 1, 0} \gets \max(mx, dp_{l, r, 1} + h_{r - 1} - f_{r})\)

最后每一位置为起点的答案就是 \(\min(dp_{i, i, 0}, dp_{i, i, 1})\)

总结:遇到需要枚举起点多次 \(dp\) 的问题,可以尝试 将转移倒过来,或是 修改状态定义,从终点往起点 \(dp\) 优化复杂度。

posted @ 2026-09-12 10:08  Czl2007  阅读(4)  评论(0)    收藏  举报