【训练题52:动态规划 | 优化】Double Strings | 2021牛客暑期多校训练营5

题意

  • Double Strings | 2021牛客暑期多校训练营5
    有两个字符串 A , B A,B A,B
    拿出 A A A 的一个子序列 a a a B B B 的一个子序列 b b b
    满足 ∣ a ∣ = ∣ b ∣ |a|=|b| a=b 且字典序 a < b a<b a<b
    问方案数
  • 1 ≤ ∣ A ∣ , ∣ B ∣ ≤ 5000 1\le |A|,|B|\le 5000 1A,B5000

记号

  • 为了后文方便描述,我们做一些记号
    ∣ A ∣ |A| A 表示字符串 A A A 的长度
    ∣ a ∣ |a| a 表示字符串 A A A 的一个子序列 a a a 的长度
    A [ 1 , i ] A[1,i] A[1,i] 表示字符串 A A A 下标从 1 1 1 取到 i i i 的一个子串 ( b a s e 1 base 1 base1
    a < b a<b a<b 表示子序列 a a a 的字典序小于子序列 b b b
    时间复杂度中的 N N N 表示两个字符串的长度的较大值
    S i S_i Si 表示字符串 S S S 的第 i i i

思路

  • 首先注意到范围,基本只能 O ( N 2 ) O(N^2) O(N2) 去做
    因为两个子序列字典序满足 a < b a<b a<b,所以一定存在 i , j i,j i,j ,满足 a i < b j a_i<b_j ai<bj
    那么这个子序列的前部分和后部分怎么样呢?
    请添加图片描述
  • 容易想到, a 前 = b 前 a前=b前 a=b ∣ a 后 ∣ = ∣ b 后 ∣ |a后|=|b后| a=b
    于是我们就把原问题分成了两个子问题
    (1) A [ 1 , i ] A[1,i] A[1,i] B [ 1 , j ] B[1,j] B[1,j] 之间,有多少个相同的子序列
    (2) a 后 a后 a b 后 b后 b 有多少种取法,满足它们的长度相同

第二个子问题

  • 容易想到是和 ∣ A ∣ − i |A|-i Ai ∣ B ∣ − j |B|-j Bj 的大小有关系,与后面的字符是无关的
    我们化简一下问题,令第一个串 S S S 第二个串 T T T ,我们从第一个串取出子序列 s s s ,第二个串取出子序列 t t t ,满足 ∣ s ∣ = ∣ t ∣ |s|=|t| s=t方案数,后面简称为方案数
    对于任意的 ∣ S ∣ |S| S ∣ T ∣ |T| T 我们都需要得到,状态数是 O ( N 2 ) O(N^2) O(N2)
    从数学的角度讲,答案为:
    ∑ i = 1 min ⁡ ( ∣ S ∣ , ∣ T ∣ ) C ∣ S ∣ i C ∣ T ∣ i \sum_{i=1}^{\min(|S|,|T|)} C_{|S|}^i C_{|T|}^i i=1min(S,T)CSiCTi
    但是貌似直接算一次时间复杂度为 O ( N ) O(N) O(N),我们总复杂度为 O ( N 3 ) O(N^3) O(N3) ,明显承受不起
    【但是赛后看别人,这个式子可以直接化简的,不过假设我们不知道结论,怎么去做】
    所以我们从 d p dp dp 的角度出发
  • d p [ i ] [ j ] dp[i][j] dp[i][j] 表示第一个串考虑了前 i i i 个位置,第二个串考虑了前 j j j 个位置,方案数是多少
    想到,我们是怎么枚举状态的?
    先是固定 i i i 的位置,然后枚举 j = 1 j=1 j=1 j = ∣ T ∣ j=|T| j=T
    那么,我们现在要去算 d p [ i ] [ j ] dp[i][j] dp[i][j] ,就假设我们已经得到了之前的 d p dp dp 数组
    也就是,第二维从 j − 1 j-1 j1 扩到 j j j ,我们新增加的贡献是多少?
    请添加图片描述
  • 第一种情况, T j T_j Tj 我们不选,那么容易想打方案数就是 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1]
    第二种情况, T j T_j Tj 我们选,由于两个子序列的长度需要相同,那么我们假设 s s s 的最后一位多选一个下标 k k k 的位置
    此时方案数为多少呢?容易想到为 d p [ k − 1 ] [ j − 1 ] dp[k-1][j-1] dp[k1][j1]
    由于 k k k 的位置有很多种,所以我们需要一个求和。
    综上,方案数为:
    d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + ∑ k = 1 i d p [ k − 1 ] [ j − 1 ] dp[i][j]=dp[i][j-1]+\sum_{k=1}^{i} dp[k-1][j-1] dp[i][j]=dp[i][j1]+k=1idp[k1][j1]
    转换一下下标方便计算,变成
    d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + ∑ k = 0 i − 1 d p [ k ] [ j − 1 ] dp[i][j]=dp[i][j-1]+\sum_{k=0}^{i-1} dp[k][j-1] dp[i][j]=dp[i][j1]+k=0i1dp[k][j1]
  • 由于我们的 i i i 也是递增枚举过来的,自然这个时候 ∑ k = 0 i − 1 d p [ k ] [ j − 1 ] \sum_{k=0}^{i-1} dp[k][j-1] k=0i1dp[k][j1] 之前是算出来过的
    我们使用前缀和便可以 O ( 1 ) O(1) O(1) 得到
    然后由于枚举变量的关系,我们可以使用滚动数组 来让前缀和数组变成空间 O ( N ) O(N) O(N)
    在代码中命名为 d p 2 [ i ] [ j ] dp2[i][j] dp2[i][j]
    然后再考虑一下边界,就是 d p 2 [ i ] [ 0 ] = 1 dp2[i][0]=1 dp2[i][0]=1 因为空的子序列也算一种方案
	int L1 = strlen(s1+1);
    int L2 = strlen(s2+1);
    int st = 0;
    for(int i = 0;i <= L1;++i){
        dp2[i][0] = 1;
        pre[0][st^1]++;
        for(int j = 1;j <= L2;++j){
            dp2[i][j] = (dp2[i][j-1] + pre[j-1][st]) % MOD;
            pre[j][st^1] = (pre[j][st^1] + dp2[i][j]) % MOD;
         }
         for(int j = 0;j <= L2;++j){
            pre[j][st] = (pre[j][st] + pre[j][st^1]) % MOD;
            pre[j][st^1] = 0;
         }
    }

第二个子问题的数学做法

  • 我们有:
    ∑ i = 0 m C n i C m i = C n + m m \sum_{i=0}^{m}C_n^iC_m^i=C_{n+m}^m i=0mCniCmi=Cn+mm
    证明方法:右边等价于:从 n + m n+m n+m 个不同的球中摸出 m m m 个球的方案数
    等式左边等价于把 n + m n+m n+m 个球分成 n 、 m n、m nm 两块
    n n n 个球中摸出 i i i 块,从 m m m 个球摸出 m − i m-i mi 个球
    得证

第一个子问题

  • 我们之间做过的 L C S LCS LCS (最长公共子序列) 的问题,是求两个串的 L C S LCS LCS
    这里我们的问题变成了求两个串有多少个非空 C S CS CS
    如果 s i ≠ s j s_i\ne s_j si=sj ,根据容斥我们可以得到: d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + d p [ i − 1 ] [ j ] − d p [ i − 1 ] [ j − 1 ] dp[i][j]=dp[i][j-1]+dp[i-1][j]-dp[i-1][j-1] dp[i][j]=dp[i][j1]+dp[i1][j]dp[i1][j1]
    如果 s i = s j s_i=s_j si=sj,也根据容斥,我们有 d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + d p [ i − 1 ] [ j ] − d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j]=dp[i][j-1]+dp[i-1][j]-dp[i-1][j-1] +\color{red}{dp[i-1][j-1]+1} dp[i][j]=dp[i][j1]+dp[i1][j]dp[i1][j1]+dp[i1][j1]+1
    + 1 +1 +1 是因为我们让 s i 、 s j s_i、s_j sisj 单独成为一个子序列
    + d p [ i − 1 ] [ j − 1 ] +dp[i-1][j-1] +dp[i1][j1] 就是我们让 s i 、 s j s_i、s_j sisj 拼到之前的所有满足的子序列之中
    for(int i = 1;i <= L1;++i){
        for(int j = 1;j <= L2;++j){
            if( s1[i] == s2[j] )
                dp[i][j] = (dp[i-1][j] + dp[i][j-1] + 1) % MOD;
            else
                dp[i][j] = (dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1]) % MOD;
        }
    }
  • 那么最后的答案怎么去算呢?
    只要 A i < B j A_i<B_j Ai<Bj ,那么答案就是 ( d p [ i − 1 ] [ j − 1 ] + 1 ) × d p 2 [ ∣ A ∣ − i ] [ ∣ B ∣ − j ] (dp[i-1][j-1] + 1) \times dp2[|A|-i][|B|-j] (dp[i1][j1]+1)×dp2[Ai][Bj]
    加一是因为还有一个空的子序列
    然后这里也可以滚动数组滚一下

代码

  • 时间复杂度: O ( N 2 ) O(N^2) O(N2)
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
using namespace std;
typedef long long ll;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}

const int MAX = 5e3+5;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
const double EPS = 1e-5;

char s1[MAX],s2[MAX];

ll dp[2][MAX];

int dp2[MAX][MAX];
ll pre[MAX][2];
int main()
{


    scanf("%s%s",s1+1,s2+1);
    int L1 = strlen(s1+1);
    int L2 = strlen(s2+1);
    int st = 0;
    for(int i = 0;i <= L1;++i){
        dp2[i][0] = 1;
        pre[0][st^1]++;
        for(int j = 1;j <= L2;++j){
            dp2[i][j] = (dp2[i][j-1] + pre[j-1][st]) % MOD;
            pre[j][st^1] = (pre[j][st^1] + dp2[i][j]) % MOD;
         }
         for(int j = 0;j <= L2;++j){
            pre[j][st] = (pre[j][st] + pre[j][st^1]) % MOD;
            pre[j][st^1] = 0;
         }
    }
    ll ans = 0;

    for(int i = 1;i <= L1;++i){
        for(int j = 1;j <= L2;++j){
            if( s1[i] == s2[j] )
                dp[st^1][j] = (dp[st][j] + dp[st^1][j-1] + 1) % MOD;
            else
                dp[st^1][j] = (dp[st][j] + dp[st^1][j-1] - dp[st][j-1]) % MOD;

            if(s1[i] < s2[j]){
                ans = (ans + (dp[st][j-1] + 1) * 1LL * dp2[L1 - i][L2 - j] % MOD) % MOD;
            }
        }
        for(int j = 1;j <= L2;++j){
            dp[st][j] = dp[st^1][j];
            dp[st^1][j] = 0;
        }
    }
    ans = (ans + MOD) % MOD;
    printf("%lld",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值