算法
(线性DP,前缀和) $O(nmk)$
首先考虑如何DP,然后再考虑如何优化。
状态表示:f[i, j, k]
表示只用S
的前i
个字母,选取了k
段,可以匹配T
的前j
个字母的方案数。
状态计算:将f[i, j, k]
表示的所有方案分成两大类:
- 不用
S[i]
,则方案数是f[i - 1, j, k]
; - 使用
S[i]
,那么可以按S[i]
所在的一段一共有多少字母继续分类:- 如果有
t
个字母,则方案数是f[i - t, j - t, k - 1]
- 如果有
所以f[i, j, k] = f[i - 1, j, k] + sum(f[i - t, j - t, k - 1])
。
其中t
只要满足S[i - t + 1] == T[j - t + 1]
就可以一直往前枚举,因此最坏情况下的时间复杂度是 $O(nm^2k)$,会超时。
接下来考虑如何优化。
我们发现f[i, j, k]
第二项的表达式和f[i - 1, j - 1, k]
第二项的表达式很像,具有递推关系,因此可以令sum[i, j, k] = sum(f[i - t, j - t, k - 1])
,则:
- 如果
S[i] == T[j]
,那么sum[i, j, k] = sum[i - 1, j - 1, k] + f[i - 1, j - 1, k - 1]
; - 如果
S[i] != T[j]
,那么sum[i, j, k] = 0
;
至此,时间复杂度可以降至 $O(nmk)$。
但空间上需要 $2nmk$ 的内存,总共需要 $2 \times 1000 \times 200^2$ 个int
,约 $305$MB,超过了内存限制。
因此需要对空间进行优化。
仔细观察转移方程,发现f[i, j, k]
和sum[i, j, k]
均只和第i - 1
层有关,因此可以使用滚动数组,同时可以发现j
和k
只会从更小的值转移过来,因此可以使用类似于01背包问题优化空间的方式,从大到小枚举j, k
,这样连滚动都可以省略了。
时间复杂度
状态总共有 $nmk$ 个,每个状态需要常数次计算,因此总时间复杂度是 $O(nmk)$。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, M = 210, mod = 1e9 + 7;
int n, m, K;
char a[N], b[M];
int f[M][M], sum[M][M];
int main()
{
scanf("%d%d%d", &n, &m, &K);
scanf("%s%s", a + 1, b + 1);
f[0][0] = 1;
for (int i = 1; i <= n; i ++ )
for (int j = m; j; j -- )
for (int k = K; k; k -- )
{
if (a[i] == b[j]) sum[j][k] = (sum[j - 1][k] + f[j - 1][k - 1]) % mod;
else sum[j][k] = 0;
f[j][k] = (f[j][k] + sum[j][k]) % mod;
}
printf("%d\n", f[m][K]);
return 0;
}
y总这题的题解比某谷的题解好
赞同
这题的代码有点问题,转移过程中如果字符不相等应该把sum置为0,
另外需要取模,还有S数组开小了。。。。。
改完之后就能过了
当时代码贴错了,已改~
dp问题初始化每次都不清楚该初始化哪些?有什么方法好判断吗
emmm y 总的题解不一定是最长最详细的, 但是一定是讲的最清楚的