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;
}
posted @ 2022-06-03 11:26  pbc的成长之路  阅读(34)  评论(0编辑  收藏  举报