简单-dp

\(\text{hdu-1069}\)

给定 \(n\) 种尺寸为 \((x_i, y_i, z_i)\) 的积木,一个积木可以重新定位,以便其三个尺寸中的任意两个确定底部的尺寸,另一个尺寸是高度。每种积木都有无数个。

求这 \(n\) 种积木能堆叠的最大高度,使得上方的积木底面长宽严格小于下方的底面长宽。

\(1 \le n \le 30\)


由于一个积木可以以三个面为底面,那么一种积木就可以看成三种使用。

之后就按类似最长上升子序列的思路 \(\text{dp}\) 即可。

\(\text{hdu-1087}\)

给定 \(n\) 个数 \(a_i\),可以从任意一个数开始跳,每次跳只能跳到后面大于当前位置的数。

求跳跃的最大价值,价值定义为跳过的所有数之和。

\(1 \le n \le 1000\)


最长上升子序列,\(O(n^2)\)\(\text{dp}\)

\(\text{hdu-1114}\)

给定空存钱罐的重量 \(e\) 和装满硬币后的重量 \(f\),和 \(n\) 种硬币的重量 \(v_i\) 和面值 \(w_i\)

求存钱罐里的硬币价值最小值。

\(1 \le e \le f \le 10^4\)\(1 \le n \le 500\)


完全背包板子。

完全背包和 \(01\) 背包唯一区别就是一件物品可以选无限个。

其实只需要把 \(01\) 背包的第二层改成正序转移就好了,其实就做到了选任意多个物品。

memset(dp, INF, sizeof dp), dp[0] = 0;
for(int i = 1; i <= n; i ++) for(int j = v[i]; j <= S; j ++)
	dp[j] = min(dp[j], dp[j - v[i]] + w[i]);

\(\text{hdu-1176}\)

你在一个 \([0, 10]\) 范围的数轴上,初始 \(0\) 时刻时在 \(5\) 上。

\(n\) 个馅饼会在 \(t_i\) 时刻掉在 \(x_i\) 上,你每时刻只能移动一单位或者不动,求最多接到的馅饼数。

\(1 \le n,t_i \le 10^5\)\(0 \le x_i \le 10\)


\(f_{i,j}\) 表示 \(i\) 时刻时在 \(j\) 上,\(i\) 时刻之后最多接到的馅饼数,那么:

\[f_{i,j} = \max\{f_{i+1,j-1},f_{i+1,j},f_{i+1,j+1}\}+a_{i,j} \]

其中 \(a_{i,j}\) 表示 \(i\) 时刻在 \(j\) 上会掉的馅饼数。

这样倒着转移即可,答案为 \(f_{0,5}\)

\(\text{hdu-1260}\)

\(n\) 个人购票,可以每个人单独购票或者相邻两个人一起购票,若第 \(i\) 个人单独购票则花费 \(a_i\) 分钟,若和第 \(i-1\) 个人一起购票则一共花费 \(b_i\) 分钟。

求上午 \(8\) 点开始购票,最早结束的购票时间。

\(1 \le T \le 10\)\(1 \le n \le 2000\)\(0 \le a_i \le 25\)\(0 \le b_i \le 50\)


\(f_i\) 表示处理完前 \(i\) 个人的最少时间,转移是简单的:

\[f_i = \max\{f_i, f_{i-1} + a_i, f_{i-2} + b_i\} \]

处理完 \(n\) 个人最小时间为 \(f_n\)。转化时间可以这样写,比较简洁:

int t = dp[n], h = 8 + t / 3600, m = t % 3600 / 60, s = t % 60;
if(h > 12) printf("%02d:%02d:%02d pm\n", h - 12, m, s);
else printf("%02d:%02d:%02d am\n", h, m, s);

\(\text{hdu-1160}\)

给定 \(n\) 个二元组 \((w_i, s_i)\),选出最长的序列满足 \(w_i < w_{i+1}\)\(s_i > s_{i+1}\),输出长度和这个序列。

\(1 \le n \le 1000\)\(1 \le w_i,s_i \le 10^4\)


实际上还是最长上升子序列,但是需要输出路径,有两种方法。

法一:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 10005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

struct node { long long w, s, id; } a[MAXN];
long long x, y, n, dp[MAXN];

bool cmp(node l, node r) {
	if(l.w == r.w) return l.s > r.s;
	return l.w < r.w;
}

int main() {
	while(cin >> x >> y) a[++ n] = {x, y, n};
	sort(a + 1, a + n + 1, cmp);
	long long ans = 0;
	for(int i = n; i >= 1; i --) {
		dp[i] = 1;
		for(int j = i; j <= n; j ++)
			if(a[j].w > a[i].w && a[j].s < a[i].s) 
				dp[i] = max(dp[i], dp[j] + 1);
		ans = max(ans, dp[i]);
	}
	cout << ans << "\n";
	for(int i = 1; i <= n; i ++) if(ans > 0 && ans == dp[i]) 
		cout << a[i].id << "\n", ans --;
	return 0;
}

但感觉种方法不太好理解。

法二:

在转移时记录时从那个二元组转移过来的,这样就形成了一种链状结构。

就可以反向查找路径了。

\(\text{luogu-1441}\)

现有 \(n\) 个砝码,重量分别为 \(a_i\),在去掉 \(m\) 个砝码后,问最多能称量出多少不同的重量(不包括 \(0\))。

注意:砝码只能放在其中一边。

\(1 \le n \le 20\)\(1 \le m \le 4\)\(m < n\)\(1 \le a_i \le 100\)


考虑二进制枚举,对于合法情况跑一遍 \(01\) 背包。

这个状态设计比较经典,所以记录一下。

\(f_j=0/1\) 表示处理完前 \(i\) 个砝码重量 \(j\) 能不能称出来,于是转移就很显然了,\(i = 1 \sim n\) 遍历:

\[f_j \mid= f_{j - a_i} \]

最终 \(f_j=1\) 的个数就是答案,对于这题来说所有合法情况取 \(\max\) 即可。

可以用 \(\text{bitset}\) 优化。记得初始化 \(s_0=1\)

#include<iostream>
#include<cstdio>
#include<bitset>
#include<algorithm>
using namespace std;
#define MAXN 2005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, a[MAXN], ans;
bitset<MAXN> s;

int main() {
	n = read(), m = read();
	for(int i = 0; i < n; i ++) a[i] = read();
	for(int S = 0; S < (1 << n); S ++) 
		if(__builtin_popcount(S) == n - m) {
			s.reset(), s[0] = 1;
			for(int i = 0; i < n; i ++) 
				if(S & (1 << i)) s |= s << a[i];
			ans = max(ans, (long long)s.count());
		}
	cout << ans - 1 << "\n";
	return 0;
}

\(\text{uva-323}\)

给定两个长度为 \(n\) 的序列 \(a_i, b_i\),选择 \(m\) 个下标 \(i_1 < i_2 < \dots < i_m\),使得下式尽可能小:

\[\left| \sum_{j=1}^m a_{i_j} - \sum_{j=1}^m b_{i_j} \right| \]

若多个最小值,则取 \(\sum a_{i_j} + \sum b_{i_j}\) 较大的。

你需要输出 \(\sum a_{i_j}\)\(\sum b_{i_j}\) 和选取的下标。

\(1 \le n \le 200\)\(1 \le m,a_i,b_i \le 20\)\(m \le n\)


考虑按照上一题的设计方式。

\(f_{j,A,B}=0/1\) 表示处理完前 \(i\) 个元素,已经选出 \(j\) 个下标,选的和分别是 \(A,B\)

初始化 \(f_{0,0,0}=1\),转移方程很显然:

\[f_{j,A,B} \mid= f_{j-1,A-a_i,B-b_i} \]

之后枚举所有 \(f_{m,A,B}=1\) 的方案,找出 \(|A-B|\) 最小的方案即可。

但是这样的时间复杂度是 \(O(6.4 \times 10^8)\),显然是过不了的。

考虑另一个经典的转化,我们把 \(A-B\) 压进状态里,把 \(A+B\) 放值域里。

\(f_{j,k}=A+B\) 表示处理完前 \(i\) 个元素,已经选出 \(j\) 个下标,其中 \(k=A-B\)

初始化 \(f_{0,0}=0\),记得倒序转移。转移方程就变成了这样:

\[f_{j,k} = \max(f_{j,k}, f_{i-1,k-(a_i-b_i)}+a_i+b_i) \]

答案即为 \(f_{m,k}\)\(|k|\) 最小的。

然后这道题还要记录路径,所以需要记录每次转移是从哪里转移过来的。

\(d_{i,j,k}\) 表示处理完前 \(i\) 个人时,\(f_{j,k}\) 是从哪里转移过来的。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath> 
using namespace std;
#define MAXN 905
#define MAXM 205
#define MAXK 35
#define N 400

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, a[MAXN], b[MAXN], dp[MAXN][MAXN], d[MAXM][MAXK][MAXN];
long long cnt, ans, t, suma, sumb, p[MAXN];

void solve(long long i, long long j, long long k) {
	if(!j) return;
	long long lst = d[i][j][k + N];
	solve(lst - 1, j - 1, k - (a[lst] - b[lst]));
	p[++ t] = lst, suma += a[lst], sumb += b[lst];
	return;
}

int main() {
	while(true) {
		n = read(), m = read();
		if(!n && !m) break;
		for(int i = 1; i <= n; i ++) a[i] = read(), b[i] = read();
		memset(dp, -0x3f, sizeof dp), dp[0][N] = 0;
		for(int i = 1; i <= n; i ++) {
			for(int j = 0; j <= m; j ++) for(int k = -400; k <= 400; k ++) 
				d[i][j][k + N] = d[i - 1][j][k + N];
			for(int j = m; j >= 1; j --) for(int k = -400; k <= 400; k ++) {
				long long x = k - (a[i] - b[i]);
				if(x < -400 || x > 400) continue;
				if(dp[j - 1][x + N] + a[i] + b[i] > dp[j][k + N]) 
					dp[j][k + N] = dp[j - 1][x + N] + a[i] + b[i], d[i][j][k + N] = i;
			}
		}
		ans = 400;
		for(int i = -400; i <= 400; i ++) if(dp[m][i + N] >= 0)
			if(abs(i) < abs(ans) || (abs(i) == abs(ans) && dp[m][i + N] > dp[m][ans + N])) ans = i;
		t = suma = sumb = 0, solve(n, m, ans);
		cout << "Jury #" << (++ cnt) << "\n";
		cout << "Best jury has value " << suma << " for prosecution and value " << sumb << " for defence:\n";
		for(int i = 1; i <= t; i ++) cout << " " << p[i];
		cout << "\n\n";
	}
	return 0;
}

\(\text{OpenJ_Bailian-1458}\)

求两个字符串 \(s,t\) 的最长公共子序列长度。

\(1 \le |s|,|t| \le 1000\)


直接朴素 \(\text{dp}\) 即可,简单写一下。

\(f_{i,j}\) 表示分别以第 \(i,j\) 为结束的最长公共子序列长度,转移:

\[f_{i,j}= \left\{\begin{matrix} f_{i-1,j-1} + 1 & (s_i = t_j) \\ \max(f_{i-1,j}, f_{i,j-1}) & (s_i \ne t_j) \end{matrix}\right. \]

答案就是 \(f_{|s|,|t|}\)

\(\text{luogu-2516}\)

给定两个字符串 \(s,t\),求这两个串的最长公共子序列长度和数量。

\(1 \le |s|,|t| \le 5000\)


考虑在 \(\text{dp}\) 的过程中需要记录一个 \(g_{i,j}\) 表示以 \(i,j\) 为结尾的最长公共子序列的数量。

分两种情况转移 \(g\) 数组。(我们称 \(\text{lcs}\) 为”序列“)

  • \(s_i = t_j\) 时。

此时 \(s_i, t_j\) 不可能同时不在里序列,可能其中一个在序列里,或者都在。

因为 \(s_i = t_j\) 所以都在的情况一定存在,接下来看其中一个在的情况前提。

若只有 \(s_i\) 在序列里,那么此时 \(f_{i,j}=f_{i,j-1}\);同样的,若只有 \(t_j\) 在序列里,那么 \(f_{i,j}=f_{i-1,j}\)

这样就可以得出 \(g_{i,j}\) 的转移:

\[g_{i,j}= g_{i-1,j-1} + g_{i,j-1}(f_{i,j}=f_{i,j-1}) + g_{i-1,j}(f_{i,j}=f_{i-1,j}) \]

  • \(s_i \ne t_j\) 时。

此时 \(s_i,t_j\) 就不可能同时在序列中了,考虑 \(f_{i,j}\) 从哪里转移过来。

\(f_{i-1,j} > f_{i,j-1}\),那么 \(t_j\) 一定在序列中,所以有 \(g_{i,j} \gets g_{i-1,j}\)

\(f_{i-1,j} < f_{i,j-1}\),那么 \(s_i\) 一定在序列中,所以有 \(g_{i,j} \gets g_{i,j-1}\)

重点是 \(f_{i-1,j} = f_{i,j-1}\) 的时候,此时要考虑 \(f_{i,j} = f_{i-1,j-1}\) 是否成立。

如果成立,说明 \(g_{i-1,j}\)\(g_{i,j-1}\) 均包含了 \(g_{i-1,j-1}\) 这一部分,即 \(s_i,t_j\) 都不在序列中。

此时的转移需要减去算重的部分,\(g_{i,j} \gets g_{i-1,j}+g_{i,j-1}-g_{i-1,j-1}\)

否则转移就是 \(g_{i,j} \gets g_{i-1,j}+g_{i,j-1}\)


至此就分讨完了,直接转移即可。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define MAXN 5005
#define MOD 100000000

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long ls, lt, dp[2][MAXN], g[2][MAXN];
char s[MAXN], t[MAXN];

int main() {
	cin >> (s + 1) >> (t + 1);
	ls = strlen(s + 1) - 1, lt = strlen(t + 1) - 1;
	for(int i = 0; i <= lt; i ++) g[0][i] = 1;
	g[1][0] = 1;
	for(int i = 1; i <= ls; i ++) for(int j = 1; j <= lt; j ++) {
		long long x = i & 1, y = (i - 1) & 1;
		if(s[i] == t[j]) {
			dp[x][j] = dp[y][j - 1] + 1;
			g[x][j] = g[y][j - 1] % MOD;
			if(dp[x][j] == dp[y][j]) (g[x][j] += g[y][j]) %= MOD;
			if(dp[x][j] == dp[x][j - 1]) (g[x][j] += g[x][j - 1]) %= MOD;
		}
		else {
			dp[x][j] = max(dp[x][j - 1], dp[y][j]);
			if(dp[y][j] > dp[x][j - 1]) g[x][j] = g[y][j] % MOD;
			else if(dp[y][j] < dp[x][j - 1]) g[x][j] = g[x][j - 1] % MOD;
			else if(dp[x][j] != dp[y][j - 1]) 
                g[x][j] = (g[y][j] + g[x][j - 1]) % MOD;
			else g[x][j] = ((g[x][j - 1] + g[y][j] - 
                             g[y][j - 1]) % MOD + MOD) % MOD;
		}
	}
	cout << dp[ls & 1][lt] << "\n" << g[ls & 1][lt] << "\n";
	return 0;
}

\(\text{OpenJ_Bailian-1661}/\text{poj-1661}\)

场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。

\(\text{Jimmy}\) 在时刻 \(0\) 从高于所有平台的 \((x,y)\) 处开始下落,它的下落速度始终为 \(1m/s\)。当 \(\text{Jimmy}\) 落到某个平台上时,游戏者选择让它向左还是向右跑,它跑动的速度也是 \(1m/s\)。当 \(\text{Jimmy}\) 跑到平台的边缘时,开始继续下落。\(\text{Jimmy}\) 每次下落的高度不能超过 \(H\) 米,不然就会摔死,游戏也会结束。

设计一个程序,计算 \(\text{Jimmy}\) 到底地面时可能的最早时间。

\(0 \le T \le 20\)\(1 \le n \le 1000\)\(-20000 \le x,l_i,r_i \le 20000\)\(0 < h_i < y \le 20000\)


\(f_{i,0/1}\) 表示从第 \(i\) 个平台左端/右端下落的最短时间。

很显然每个平台下落只会掉到一个平台上,或者不能下落,因此每个平台只能转移一次。

转移方程就是这样:

\[\begin{align} f_{i,0} &= \min(f_{j,0} + l_i - l_j, f_{j,1} + r_j - l_i) + h_i - h_j \\ f_{i,1} &= \min(f_{j,0} + r_i - l_j, f_{j,1} + r_j - r_i) + h_i - h_j \\ \end{align} \]

代码就很好写了:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 1005
#define INF 0x3f3f3f3f3f3f3f3f

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

struct node { long long l, r, h; } a[MAXN];
long long T, n, x, y, maxn, dp[MAXN][2]; 

bool cmp(node x, node y) { return x.h > y.h; }

int main() {
	T = read();
	while(T --) {
		n = read(), x = read(), y = read(), maxn = read();
		a[0] = {x, x, y}, a[n + 1].h = 0;
		for(int i = 1; i <= n; i ++) 
			a[i].l = read(), a[i].r = read(), a[i].h = read();
		sort(a, a + n + 2, cmp);
		for(int i = n + 1; i >= 0; i --) {
			dp[i][0] = dp[i][1] = INF;
			if(i == n) { dp[i][0] = dp[i][1] = a[i].h; continue; }
			bool fg = false;
			for(int j = i + 1; j <= n + 1; j ++) {
				long long dis = a[i].h - a[j].h;
				if(dis > maxn) break;
				if(a[i].l >= a[j].l && a[i].l <= a[j].r) {
					dp[i][0] = min(dp[j][0] + a[i].l - a[j].l, 
								   dp[j][1] + a[j].r - a[i].l) + dis;
					fg = true; break;
				}
			}
			if(!fg && a[i].h <= maxn) dp[i][0] = a[i].h;
			fg = false;
			for(int j = i + 1; j <= n + 1; j ++) {
				long long dis = a[i].h - a[j].h;
				if(dis > maxn) break;
				if(a[i].r >= a[j].l && a[i].r <= a[j].r) {
					dp[i][1] = min(dp[j][0] + a[i].r - a[j].l, 
								   dp[j][1] + a[j].r - a[i].r) + dis;
					fg = true; break;
				}
			}
			if(!fg && a[i].h <= maxn) dp[i][1] = a[i].h;
		}
		cout << min(dp[0][0], dp[0][1]) << "\n";
	}
	return 0;
}

\(\text{OpenJ_Bailian-2533}/\text{poj-2533}\)

求最长上升子序列的长度。

\(\text{luogu-2858}\)

给定长度为 \(n\) 的序列 \(a_i\),每次只能从两端取数,若 \(a_i\) 是第 \(x\) 个取出的,则贡献为 \(xa_i\)

求取出所有数的最大贡献。

\(1 \le n \le 2000\)\(1 \le a_i \le 1000\)


很典型的区间 \(\text{dp}\)。设 \(f_{l,r}\) 表示 \([l,r]\) 区间的最大贡献,转移很显然:

\[f_{l,r} = \max(f_{l,r-1}+a_r \times (n-r+l), f_{l+1,r} + a_l \times (n-r+l)) \]

注意初始化 \(f_{i,i}=na_i\)

\(\text{hdu-1078}\)

给定一个 \(n \times n\) 的矩阵 \(a_{i,j}\),初始时在 \(a_{1,1}\) 上,每次只能水平或垂直移动不超过 \(k\),且移动到的点权值必须严格大于当前点。经过的点的权值和为路径的贡献,求路径的贡献最大值。

\(1 \le n,k \le 100\)


显然是 \(O(n^3)\)\(\text{dp}\),考虑设 \(f_{i,j}\) 为从 \((i,j)\) 出发的路径贡献最大值,转移:

\[f_{i,j} = \sum_{p=i-k}^{i-1} f_{p,j} + \sum_{p=i+1}^{i+k} f_{p,j} + \sum_{p=j-k}^{j-1} f_{i,p} + \sum_{p=j+1}^{j+k} f_{i,p} + a_{i,j} \]

因为转移方式比较特殊,需要用记忆化搜索辅助转移:

#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 105

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, k, a[MAXN][MAXN], dp[MAXN][MAXN];
long long dx[] = { -1, 1, 0, 0 };
long long dy[] = { 0, 0, -1, 1 };

long long dfs(long long x, long long y) {
	if(dp[x][y]) return dp[x][y];
	long long res = 0;
	for(int i = 1; i <= k; i ++) for(int j = 0; j < 4; j ++) {
		long long px = x + dx[j] * i, py = y + dy[j] * i;
		if(px < 1 || px > n || py < 1 || py > n || a[px][py] <= a[x][y]) continue;
		res = max(res, dfs(px, py));
	}
	return dp[x][y] = res + a[x][y];
}

int main() {
	while(cin >> n >> k) {
		if(n == -1 && k == -1) break;
		for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) 
			cin >> a[i][j], dp[i][j] = 0;
		cout << dfs(1, 1) << "\n";
	}
	return 0;
}

\(\text{hdu-2859}\)

给定一个 \(n \times n\) 的字符矩阵 \(s_{i,j}\),求对阵矩阵的最大边长。

对称矩阵定义为,关于”左下到右上“对角线对称的矩阵。

\(1 \le n \le 1000\)\(s_{i,j} \in [a, b, \dots, z]\)


\(f_{i,j}\) 表示以 \((i,j)\) 为左下角的对称矩阵的最大边长,显然 \(f_{i,j}\) 可以从 \(f_{i-1,j+1}\) 转移过来。

也就是 \((i,j)\) 右上角那个点,如果扩展的一列一行完美匹配,则 \(f_{i,j}=f_{i-1,j+1}+1\)

否则,\(f_{i,j}=p\),其中 \(p\) 表示扩展的最大匹配值,这个匹配值暴力跑就能过。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
#define MAXN 1005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, dp[MAXN][MAXN];
char s[MAXN][MAXN];

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	while(cin >> n) {
		if(!n) break;
		long long ans = 0;
		for(int i = 1; i <= n; i ++) 
            for(int j = 1; j <= n; j ++) cin >> s[i][j];
		for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) {
			dp[i][j] = 1;
			if(i > 1 && j < n) {
				long long k = 0;
				while(k <= dp[i - 1][j + 1] 
                      && s[i - k][j] == s[i][j + k]) k ++;
				dp[i][j] = k;
			}
			ans = max(ans, dp[i][j]);
		}
		cout << ans << "\n";
	}
	return 0;
}

实际上这个时间复杂度最劣是 \(O(n^3)\) 的,但能卡过去,大概是数据没有那种所有 \(s_{i,j}=a\) 这种情况。

\(\text{luogu-2889}\)

给定 \(m\) 个时间区间 \([l_i,r_i]\),若选定第 \(i\) 个区间,则有 \(w_i\) 的贡献,但是在此区间后 \(R\) 的时间内不能选其他区间。求选任意个区间的最大贡献。

\(1 \le n,w_i \le 10^6\)\(1 \le m \le 10^3\)\(1 \le l_i < r_i \le n\)


很套路的题,设 \(f_i\) 表示以 \(i\) 为最后一个选定的时间区间的最大贡献。

当然是排完序之后的编号, 转移就是从之前的合法状态转移过来就好了:

\[f_i = \max(f_i, f_j + w_i),(r_j+R \le l_i) \]

初始化 \(f_i = w_i\),答案就是 \(\max f_i\)

posted @ 2025-11-18 21:38  So_noSlack  阅读(6)  评论(0)    收藏  举报