20250829 LG_P2679 [NOIP 2015 提高组] 子串

题意

有两个字符串 \(s, t\) 求有多少种方案从 \(s\) 中取出不相交的 \(w\) 段,使他们拼起来为 \(t\)

思路

由于求方案数,所以考虑 DP。

对于 DP 计数题,我们可以将每一个方案看作是多阶段决策的结果,那么我们一般先考虑一部分的决策(即,将决策集合的扩大作为拓扑序)。因为这样做就可以直接考虑下一步决策(将下一步决策加入当前决策集合)。而在此题中,我们也可以如上考虑。首先,我们可以将「选出一段」看作为决策,并将选出的段数的增多作为拓扑序,同时,因为我们不在乎选择段的顺序(即,先选 \(s[1][2]\) (\(s[l][r]\) 表示 s 从 l 到 r 的子段) 再选 \(s[3][4]\) 和 先选 \(s[3][4]\) 再选 \(s[1][2]\) 是一样的方案),所以钦定我们选的段是越来越靠后的。我们发现,在转移(决策,即「选出一段」)中,为了使方案满足每段之间不相交,我们需要关注「最后一个被字符段覆盖的位置」,为了新的一段与 \(t\) 相符,我们需要关注「当前匹配到了 \(t\) 的第几个字符」。所以我们可以设状态 \((i, j, k)\),表示当前选了 \(i\) 段,最后一段的末尾为 \(j\),匹配到了 \(t\) 的第 \(k\) 个字符。转移就是选出下一段,形式化的表述为 \((i - 1, x, y) \to (i, j, k)\),其中 \(x < j, y < k,y < x, x < j - k + y + 1, s[j - k + y + 1][j] == t[y + 1][k]\)。用哈希来判断字段是否相同。

枚举状态 \(O(nmw)\),转移 \(O(nm)\),总共 \(O(n^2m^2w)\)\(40\) pts。

#include<bits/stdc++.h>

using namespace std;
using ull = unsigned long long;

const ull N = 1e3 + 5, M = 2e2 + 5, P = 131, MOD = 1e9 + 7;

int n, m, k, ans;
int dp[M][N][M];
string s, t;
ull hs[N], ht[N], pw[N];

ull Cs(int l, int r) {
	return hs[r] - hs[l - 1] * pw[r - l + 1];
}

ull Ct(int l, int r) {
	return ht[r] - ht[l - 1] * pw[r - l + 1];
}

int main() {
	cin >> n >> m >> k >> s >> t;
	s = '#' + s, t = '#' + t;
	for (int i = 1; i <= n; i++) {
		hs[i] = hs[i - 1] * P + s[i];
	}
	for (int i = 1; i <= m; i++) {
		ht[i] = ht[i - 1] * P + t[i];
	}
	pw[0] = 1;
	for (int i = 1; i <= max(n, m); i++) {
		pw[i] = pw[i - 1] * P;
	}
	dp[0][0][0] = 1;
	for (int i = 1; i <= k; i++) {
		for (int j = 1; j <= n; j++) {
			for (int l = 1; l <= min(m, j); l++) {
				for (int x = 0; x < j; x++) {
					for (int y = max(0, x + l - j); y < min(l, x + 1); y++) {
						if (Cs(j - l + y + 1, j) == Ct(y + 1, l)) {
							dp[i][j][l] = (dp[i][j][l] + dp[i - 1][x][y]) % MOD;
						}
					}
				}
			}
		}
	}
	for (int i = 1; i <= n; i++) {
		ans = (ans + dp[k][i][m]) % MOD;
	}
	cout << ans;
	return 0;
} 

对于一个状态,可以发现对于同一个 \(y\) 合法的 \(x\) 是一个前缀,所以改变枚举顺序,在转移时先枚举 \(y\) 再枚举 \(x\),并用前缀和优化即可。其实仅改变枚举顺序就有 60 pts

枚举状态 \(O(nmw)\),转移 \(O(m)\),总共 \(O(nm^2w)\)\(70\) pts。

#include<bits/stdc++.h>

using namespace std;
using ull = unsigned long long;

const ull N = 1e3 + 5, M = 2e2 + 5, P = 131, MOD = 1e9 + 7;

int n, m, k, ans;
int dp[M][N][M];
string s, t;
ull hs[N], ht[N], pw[N];

ull Cs(int l, int r) {
	return hs[r] - hs[l - 1] * pw[r - l + 1];
}

ull Ct(int l, int r) {
	return ht[r] - ht[l - 1] * pw[r - l + 1];
}

int main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m >> k >> s >> t;
	s = '#' + s, t = '#' + t;
	for (int i = 1; i <= n; i++) {
		hs[i] = hs[i - 1] * P + s[i];
	}
	for (int i = 1; i <= m; i++) {
		ht[i] = ht[i - 1] * P + t[i];
	}
	pw[0] = 1;
	for (int i = 1; i <= max(n, m); i++) {
		pw[i] = pw[i - 1] * P;
	}
	dp[0][0][0] = 1;
	for (int i = 1; i <= k; i++) {
		for (int y = 0; y <= m; y++) {
			for (int x = 0; x <= n; x++) {
				dp[i - 1][x + 1][y] = (dp[i - 1][x + 1][y] + dp[i - 1][x][y]) % MOD;
			}
		}
		for (int l = 1; l <= m; l++) {
			for (int j = l; j <= n; j++) {
				for (int y = 0; y < l; y++) {
					if (Cs(j - l + y + 1, j) == Ct(y + 1, l) && j - l + y >= 0) {
						dp[i][j][l] = (dp[i][j][l] + dp[i - 1][j - l + y][y]) % MOD;
//						for (int x = 0; x < j - l + y + 1; x++) {
//							dp[i][j][l] = (dp[i][j][l] + dp[i - 1][x][y]) % MOD;
//						}
					}
				}
			}
		}
	}
	for (int i = 1; i <= n; i++) {
		ans = (ans + dp[k][i][m]) % MOD;
	}
	cout << ans;
	return 0;
} 

此时,对于一个状态,可以发现合法的 \(y\) 是一个后缀,类似的,用前缀和解决。于是,我们可以对于每个状态二分加哈希找到后缀长度,但是这样要带 \(\log\),观察到 \(y\) 的后缀的长度与 \(i\) 这一位状态无关,所以可以枚举 \(j, k\) 再二分加哈希预处理。然后可以在原来的基础上再套一层前缀和,并且要注意由于原来 \(x\) 的前缀的长度是随而 \(y\) 变化的,所以在二次前缀和时也要是随 \(y\) 变化的。最后滚动一下即可。

预处理 \(O(nm \log m)\),枚举状态 \(O(nmw)\),前缀和 \(O(nmw)\),转移 \(O(1)\),总共 \(O(nmw)\)\(100\) pts。

/**
dp[i][j][k] i 段 n 到 j m 到 k
dp[i][j][k] += dp[i - 1][x][y]
y < k && x < j - len + 1 && s[j - len + 1][j] == t[y + 1][k]
*/
#include<bits/stdc++.h>

using namespace std;
using ull = unsigned long long;

const ull N = 1e3 + 5, M = 2e2 + 5, P = 131, MOD = 1e9 + 7;

int n, m, k, ans;
int dp[M][N][M], len[N][M], res[N][M];
string s, t;
ull hs[N], ht[N], pw[N];

ull Cs(int l, int r) {
	return hs[r] - hs[l - 1] * pw[r - l + 1];
}

ull Ct(int l, int r) {
	return ht[r] - ht[l - 1] * pw[r - l + 1];
}

int main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m >> k >> s >> t;
	s = '#' + s, t = '#' + t;
	for (int i = 1; i <= n; i++) {
		hs[i] = hs[i - 1] * P + s[i];
	}
	for (int i = 1; i <= m; i++) {
		ht[i] = ht[i - 1] * P + t[i];
	}
	pw[0] = 1;
	for (int i = 1; i <= max(n, m); i++) {
		pw[i] = pw[i - 1] * P;
	}
	for (int i = 1; i <= n; i++) {//预处理
		for (int j = 1; j <= min(m, i); j++) {
			int l = 0, r = j;
			while (l < r) {//二分 + 哈希
				int mid = l + r + 1 >> 1;
				if (Cs(i - mid + 1, i) == Ct(j - mid + 1, j)) {
					l = mid;
				} else {
					r = mid - 1;
				}
			}
			len[i][j] = l;
		}
	}
	dp[0][0][0] = 1;
	for (int i = 1; i <= k; i++) {
		int id = i & 1, _id = 1 - id;
		for (int y = 0; y <= m; y++) {
			for (int x = 0; x <= n; x++) {//x的前缀和
				dp[_id][x + 1][y] = (dp[_id][x + 1][y] + dp[_id][x][y]) % MOD;
			}
		}
		for (int w = 0; w <= n; w++) {
			res[w][0] = dp[_id][w][0];
		}
		for (int y = 0; y <= m; y++) {//y 的前缀和
			for (int w = 0; w <= n; w++) {//w + y + 1:x 随 y 变化
				res[w][y + 1] = (res[w][y] + dp[_id][w + y + 1][y + 1]) % MOD;
			}
		}
		for (int l = 1; l <= m; l++) {
			for (int j = l; j <= n; j++) {
				int u = l - len[j][l];
				dp[id][j][l] = (res[j - l][l - 1] - (u == 0 ? 0 : res[j - l][u - 1]) + MOD) % MOD;
			}
		}
		for (int j = 0; j <= n; j++) {//清空一下
			for (int l = 0; l <= m; l++) {
				dp[_id][j][l] = 0;
			}
		}
	}
	for (int i = 1; i <= n; i++) {
		ans = (ans + dp[k & 1][i][m]) % MOD;
	}
	cout << ans;
	return 0;
} 
posted @ 2025-08-29 19:37  oymz  阅读(26)  评论(1)    收藏  举报