最长公共子序列(洛谷P2516)

题目大意

给定两个以.结尾的只含大写英文字母的字符串,求其最长公共子序列长度,以及构成最长公共子序列的方案数 \(\text{(mod 1e8)}\)。 \(\text{|S|}\leq 5000\) (\(\text{|S|}为字符串长度\))

题解

由最长公共子序列容易想到用dp动态规划进行求解,设\(f_{i, j}\)为匹配了第一个字符串的前\(\,i\,\)个,第二个字符串的前\(\,j\,\)个的最长公共子序列。注:以下涉及第\(\,i\,\)个特指第一个字符串的第\(\,i\,\)个(即\(\text{s}1[i]\)),同理第\(\,j\,\)个(即\(\text{s}2[j]\))。

首先我不匹配第\(\,i\,\)个字符,转移方程为:

\[f_{i,j} = \text{max}(f_{i,j}, f_{i - 1, j}) \]

不匹配第\(\,j\,\)个字符:

\[f_{i,j} = \text{max}(f_{i,j}, f_{i, j - 1}) \]

匹配第\(\,i\,\)个字符和第\(\,j\,\)个字符,同时字符相等的情况下:

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

这样我们就得到了状态转移方程:

\[f_{i,j} = \max \begin{cases} 0, & i = 0, j = 0\\ \text{max}(f_{i,j - 1}, f_{i - 1, j})\\ f_{i - 1, j - 1 + 1}, & \text{s}1[i] == \text{s}2[j] \end{cases} \]

第一个问题解决了,然后就是方案数的问题了,方案数其实和长度的转移方程差不多,\(cnt_{i, j}\)表示构成\(f_{i, j}\)最大长度的方案数,因为\(f_{i, j}\)由三个地方转移而来,那么\(cnt_{i, j}\)也应由这些地方转移而来,需要注意的是,上述状态转移方程中\(f_{i - 1, j - 1}\)会被转移到\(f_{i, j - 1}\)和\(f_{i - 1, j}\)中,这在求长度中虽然不会影响答案,但是再求方案数时,我们会发现多计算了一次,那么在去重时,我们会发现,当且仅当\(f_{i - 1, j - 1}\)等于\(f_{i, j}\)时,重复计算的情况才会发生。

当\(f_{i - 1, j - 1}\)等于\(f_{i, j}\)时,显然没有发生\(f_{i,j} = \text{max}(f_{i,j}, f_{i - 1, j - 1} + 1)\)的转移,但是其方案数和长度会转移到\(f_{i, j - 1}\)和\(f_{i - 1, j}\)中,所以会多计算一次\(cnt_{i - 1, j - 1}\)的贡献。

当\(f_{i - 1, j - 1}\)不等于\(f_{i, j}\)时,可以发现\(cnt_{i - 1, j - 1}\)的贡献永远不会转移到\(cnt_{i, j}\)中,也就不需要去重

然后就可以写代码了

#include<bits/stdc++.h>

using namespace std;

typedef long long ll ;
const int N = 5e5 + 5;
const ll mod = 1e8;
const ll inf = 1e9;

int dp[5050][5050];
int cnt[5050][5050];
char s1[5050], s2[5050];

int main() {

    scanf("%s%s", s1 + 1, s2 + 1);
    int n = strlen(s1 + 1) - 1, m = strlen(s2 + 1) - 1;
    for(int i = 0; i <= m; ++i)cnt[0][i] = 1;
    for(int i = 0; i <= n; ++i)cnt[i][0] = 1;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            if(s1[i] == s2[j]){
                dp[i][j] = dp[i - 1][j - 1] + 1;
                cnt[i][j] = cnt[i - 1][j - 1];
            }
            if(dp[i][j] == dp[i - 1][j]){
                cnt[i][j] = (cnt[i][j] + cnt[i - 1][j]) % mod;
            }else if(dp[i][j] < dp[i - 1][j]){
                dp[i][j] = dp[i - 1][j];
                cnt[i][j] = cnt[i - 1][j];
            }

            if(dp[i][j] == dp[i][j - 1]){
                cnt[i][j] = (cnt[i][j] + cnt[i][j - 1]) % mod;
            }else if(dp[i][j] < dp[i][j - 1]){
                dp[i][j] = dp[i][j - 1];
                cnt[i][j] = cnt[i][j - 1];
            }

            if(dp[i - 1][j - 1] == dp[i][j]){
                cnt[i][j] = (cnt[i][j] - cnt[i - 1][j - 1] + mod) % mod;
            }
        }
    }
    cout << dp[n][m] << '\n' << cnt[n][m];
    return 0;
}

然而这样空间复杂度是\((n^2)\)的,会MLE,我们会发现这个转移第\(\,i\,\)位的转移只与第\(\,i-1\,\)位有关,可以开一维空间数组记\(\,i- 1\,\)的状态,利用滚动数组优化第一维空间,空间复杂度就会变成\((n)\)的了

#include<bits/stdc++.h>

using namespace std;

typedef long long ll ;
const int N = 5e5 + 5;
const ll mod = 1e8;
const ll inf = 1e9;

void read();

int dp[5050], cnt[5050];
int ldp[5050], lcnt[5050];//上一位的状态
char s1[5050], s2[5050];

int main() {

    scanf("%s%s", s1 + 1, s2 + 1);
    int n = strlen(s1 + 1) - 1, m = strlen(s2 + 1) - 1;
    for(int i = 0; i <= m; ++i)lcnt[i] = 1;
    cnt[0] = 1;
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            if(s1[i] == s2[j]){
                dp[j] = ldp[j - 1] + 1;
                cnt[j] = lcnt[j - 1];
            }
            
            if(dp[j] == ldp[j]){
                cnt[j] = (cnt[j] + lcnt[j]) % mod;
            }else if(dp[j] < ldp[j]){
                dp[j] = ldp[j];
                cnt[j] = lcnt[j];
            }

            if(dp[j] == dp[j - 1]){
                cnt[j] = (cnt[j] + cnt[j - 1]) % mod;
            }else if(dp[j] < dp[j - 1]){
                dp[j] = dp[j - 1];
                cnt[j] = cnt[j - 1];
            }

            if(ldp[j - 1] == dp[j]){    //去重
                cnt[j] = (cnt[j] - lcnt[j - 1] + mod) % mod;
            }
        }
        
        for(int j = 1; j <= m; ++j){
            ldp[j] = dp[j];
            lcnt[j] = cnt[j];
            dp[j] = cnt[j] = 0;
        }
    }
    cout << ldp[m] << '\n' << lcnt[m];
    return 0;
}


void read(){

#ifdef YYT
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif

}