Common Subsequence(线性dp,二维前缀和)
题意
给出两个长度分别为\(N\)和\(M\)的整数序列\(S\)和\(T\),它们均由\(1\)到\(10^5\)之间的整数组成。求在\(S\)子序列和\(T\)子序列中,有多少对两个子序列的内容相同。
注意:
\(A\)的子序列是指通过从\(A\)删除零个或多个元素而不改变顺序而获得的序列。
对于\(A\)的两个子序列,如果内容相同,但是被删除元素的位置不同,也当成两个不同的子序列。
数据范围
\(2 \leq N, M \leq 2 \times 10^3\)
思路
- 方法1
首先介绍官方题解的方法,这个方法容易理解,但是不太容易想到。
令\(f(i, j)\)表示使用\(S\)的前\(i\)个元素和\(T\)的前\(j\)个元素,并且\(S\)的第\(i\)个元素必选,\(T\)的第\(j\)个元素必选的内容相同子序列对的个数。
如果\(S_i \neq T_j\),那么\(f(i, j) = 0\)。如果\(S_i = T_j\),则通过枚举两个序列的结尾进行转移,即:\(f(i, j) = \sum\limits_{k = 1}^{i - 1} \sum\limits_{l = 1}^{j - 1} f(k, l) + 1\)。其中,\(f(0, 0) = 1\)。
但是运行时间是\(O(N^2 \times M^2)\),显然超时。因此我们考虑使用前缀和优化。
令\(s(i, j) = \sum\limits_{k = 1}^{i} \sum\limits_{l = 1}^{j} f(k, l)\),即\(s(i, j)\)为\(f\)的前缀和。
\(s(i, j) = s(i - 1, j) + s(i, j - 1) - s(i - 1, j - 1) + f(i, j)\)。
利用\(s(i, j)\),我们可以将转移方程改写成\(f(i, j) = s(i - 1, j - 1) + 1\)。
最终答案为\(s(n, m) + 1\)
- 方法2
下面介绍一种较为自然的状态表示方法,但是转移方程略难考虑。
由最长公共子序列产生灵感,令\(f(i, j)\)表示使用\(S\)的前\(i\)个元素和\(T\)的前\(j\)个元素的内容相同子序列对的个数。
\(f(i, j)\)可以由\(f(i - 1, j)\)、\(f(i, j - 1)\)和\(f(i - 1, j - 1)\)转移而来,那么具体转移而来呢?
根据容斥原理,\(f(i, j)\)可以为包含\(S_i\)(\(S_i\)可选)的所有情况加上包含\(T_j\)的所有情况减去\(S_i\)和\(T_j\)都不包含加上\(S_i\)和\(T_j\)必选的情况。这个思考方式与二维前缀和的思考方式类似,可以类比进行思考。
转移方程可以写为。若\(S_i = T_j\),则\(f(i, j) = f(i - 1, j) + f(i, j - 1) - f(i - 1, j - 1) + f(i - 1, j - 1) = f(i - 1, j) + f(i, j - 1)\);若\(S_i \neq T_j\),则\(f(i, j) = f(i - 1, j) + f(i, j - 1) - f(i - 1, j - 1)\)。
代码
//方法1
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2010, mod = 1e9 + 7;
int n, m, a[N], b[N];
ll f[N][N], sum[N][N];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= m; i ++) scanf("%d", &b[i]);
f[0][0] = 1;
for(int i = 1; i <= n; i ++) {
for(int j = 1; j <= m; j ++) {
if(a[i] == b[j]) f[i][j] = (sum[i - 1][j - 1] + 1) % mod;
sum[i][j] = (sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + f[i][j] + mod) % mod;
}
}
printf("%lld\n", 1 + sum[n][m]);
return 0;
}
//方法2
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2010, mod = 1e9 + 7;
int n, m, a[N], b[N];
ll f[N][N];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
for(int i = 1; i <= m; i ++) scanf("%d", &b[i]);
for(int i = 0; i <= n; i ++) f[i][0] = 1;
for(int j = 0; j <= m; j ++) f[0][j] = 1;
for(int i = 1; i <= n; i ++) {
for(int j = 1; j <= m; j ++) {
if(a[i] != b[j])
f[i][j] = (f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + mod) % mod;
else
f[i][j] = (f[i - 1][j] + f[i][j - 1]) % mod;
}
}
printf("%lld\n", f[n][m]);
return 0;
}