NOIP2015提高组Day2 子串

本文深入探讨了动态规划(DP)在解决字符串匹配问题时的多种优化方法,从初始的朴素方法逐步引入更高效的算法,通过实例分析了如何利用前缀和、状态转移方程等技术进行优化,最终实现快速解决复杂问题的目标。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

这里写图片描述
这里写图片描述

分析

我在考场上就想到了dp,但是。。
算了,没有但是,这就是结果,是自己努力不够的后果。
好,回到正题。

方法0

:(考场的sb方法…..)
f[i,j,k]aibjk
转移十分暴力:a串枚举x,b串枚举y,再判断,然后累加。。

方法1:(by Philipsweng)

fk,i,ja[l,r]<=ibjk
Fk,i,j=Fk,i1,j+ix=1,A[x..i]=B[ji+x..j]Fk1,x1,ji+x1
但是这样太慢了,我们进一步想想优化:
我们发现,若每次这样更新,就没有很好的利用到前面可用的状态。
故,我们接着分析一下fk1,i,jfk,i,j
Fk,i,j=Fk,i1,j+ix=1,A[x..i]=B[ji+x..j]Fk1,x1,ji+x1
Fk,i1,j1=Fk,i2,j1+i1x=1,A[x..i1]=B[ji+x..j1]Fk1,x1,ji+x1
整理可得:
Fk,i,j=Fk,i1,j,(A[i]=B[j])
Fk,i,j=Fk,i1,j+Fk,i1,j1Fk,i2,j1+Fk1,i1,j1,(A[i]=B[j])

方法2

我们仍然设fk,i,ja[l,r]<=ibjk
但是我们可以充分利用设来推方程
f[k,i,j]=j1x=1f[k1,i1,x]+f[x,i1,j1]
这样就可以得出一个简单易懂的方程,再加上前缀和优化便可以很好的解决问题了。
为什么这个方程是对的呢?
我们先想,若不分成一组,让aibj与前面合为一组,便是f[x,i1,j1]
接着我们想前面的“最后分开后的最后子串[l,r]<=i”已经帮我们省去n的时间去匹配a串,故我们只需再枚举一次x(x<=j-1),便可以很好的解决问题了。

方法3:

(by cty考场70分方法)
他和一开始我一样是设f[k,i,j]aibjk,但想到了前缀和优化,不过他还是枚举了x(x<=j-1)为a倒数x和b匹配即f[k1,1..i1,x]
若是他设得更好就可以100了。

总结

我一般都很少打noip原题的总结,但是这题却太过“优美”了。。
其实我比赛时思维卡住的原因便是设的不够好,没有运用设去推!
从一开始方法0到方法3到方法2到方法1,这是思维的进步!
从开始枚举全部到枚举部分用前缀和再到用设去推再到观察妙用dp方程
这都是思维的突破,没有做不到只有想不到!
在以后做dp题时要多想状态之间的关系,和方程的优化,加油!

方法1:(by Philipsweng)

#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

const int MAXN = 1005,MAXM = 205,Mo = int(1e9) + 7;

char A[MAXN],B[MAXM];
int F[2][MAXN][MAXM],N,M,K;

int main()
{
    freopen("substring.in","r",stdin),freopen("substring.out","w",stdout);
    scanf("%d%d%d", &N, &M, &K);
    scanf("%s", A + 1);
    scanf("%s", B + 1);
    int cur = 0;
    for(int i = 0;i <= N;i ++)
        F[0][i][0] = 1;
    for(int k = 1;k <= K;k ++)
    {
        cur ^= 1;
        for(int i = 0;i <= N;i ++)
            F[cur][i][k - 1] = 0;
        for(int j = k;j <= M;j ++)
            for(int i = j;i <= N;i ++)
            {
                if (A[i] == B[j])
                {
                    F[cur][i][j] = (F[cur][i - 1][j - 1] + F[cur][i - 1][j]) % Mo;
                    F[cur][i][j] = (F[cur][i][j] + F[cur ^ 1][i - 1][j - 1]) % Mo;
                    if (i >= 2) F[cur][i][j] = (F[cur][i][j] - F[cur][i - 2][j - 1] + Mo) % Mo;
                } else
                    F[cur][i][j] = F[cur][i - 1][j];
            }
    }
    printf("%d\n", F[cur][N][M]);
    return 0;
}

方法2:

const mo=1000000007;
var
    n,m,r,i,j,k,l,t,x,y:longint;
    st,sr:ansistring;
    ans:longint;
    p:boolean;
    f,s:array[0..1,0..200,0..1000] of longint;
function max(l,r:longint):longint;
begin
   if l<r then exit(r);exit(l);
end;
function min(l,r:longint):longint;
begin
   if l>r then exit(r);exit(l);
end;
begin
assign(input,'substring.in');reset(input);
assign(output,'substring.out');rewrite(output);
    readln(n,m,r);
    readln(st);
    readln(sr);
    f[0,0,0]:=1;
    for i:=0 to n do s[0,0,i]:=1;
    x:=1;
    for k:=1 to r do begin
    for i:=1 to m do
        for j:=1 to n do begin
            if sr[i]=st[j] then begin
                f[x,i,j]:=(s[1-x,i-1,j-1]+f[x,i-1,j-1]+f[x,i,j])mod mo;
            end;
            s[x,i,j]:=(f[x,i,j]+s[x,i,j-1])mod mo;
        end;
    x:=1-x;
    fillchar(f[x],sizeof(f[x]),0);
    fillchar(s[x],sizeof(s[x]),0);
    end;
    writeln(s[1-x,m,n]);
close(input);close(output);
end.

方法3:

const
    mo=1000000007;
var
    f,sum:array[0..1,0..200,0..1000] of int64;
    n,m,i,j,k,l,p,t,q:longint;
    ans:int64;
    s,st:ansistring;
begin
    assign(input,'substring.in');reset(input);
    assign(output,'substring.out');rewrite(output);
    readln(n,m,k);
    readln(s);
    readln(st);f[0,0,0]:=1;q:=1;
    for i:=0 to n do sum[0,0,i]:=1;
    for t:=1 to k do
    begin
        for i:=1 to m do
            for j:=1 to n do
            begin
                if st[i]=s[j] then
                begin
                    l:=j;
                    while st[i-j+l]=s[l] do
                    begin
                        f[q,i,j]:=(f[q,i,j]+sum[1-q,i-j+l-1,l-1]) mod mo;
                        dec(l);if (i-j+l=0) or (l=0) then break;
                    end;
                end;
                sum[q,i,j]:=(sum[q,i,j-1]+f[q,i,j]) mod mo;
            end;
        q:=1-q;fillchar(f[q],sizeof(f[q]),0);fillchar(sum[q],sizeof(sum[q]),0);
    end;
    for i:=1 to n do ans:=(ans+f[1-q,m,i]) mod mo;
    writeln(ans);
    close(input);close(output);
end.    
<think>嗯,用户问的是NOIP2015提高子串问题,也就是P2679。我需要先回忆一下这个题目的具体要求。题目是说有两个字符串A和B,要从A中取出k个不重叠的非空子串,按顺序连接后等于B,求有多少种方案。子串位置不同就算不同方案。首先,我得确定这个问题的解法方向。根据用户提供的引用,尤其是引用[2]中的分析,提到这是一个动态规划问题,可能需要用倒序求解,并且加一维来记录子串的个数。所以动态规划应该是关键。 接下来,我需要考虑状态定义。通常这类问题会用到二维或三维的DP数。这里因为要取k个子串,所以状态可能需要包括当前处理到A的哪个位置,B的哪个位置,以及已经用了多少个子串。比如,dp[i][j][k]表示A的前i个字符,B的前j个字符,用了k个子串的方案数。但可能还需要一个维度来记录当前是否在延续上一个子串,或者是否开始新的子串。不过根据引用中的提示,可能不需要额外维度,而是通过倒序枚举来优化。然后,状态转移方面,当A[i]等于B[j]时,可以考虑两种情况:一种是延续当前的子串,即把当前字符加入上一个子串的末尾;另一种是开启一个新的子串。这时候需要分别处理这两种情况的转移。例如,如果延续,那么方案数可能继承自dp[i-1][j-1][k];如果开启新子串,可能需要从dp[i-1][j-1][k-1]转移过来。但这里可能需要注意,当开启新子串时,前面的字符必须已经处理完毕,并且不能重叠。 不过根据引用[2]中的描述,提到使用倒序枚举B的字符,并且利用类似哈希优化的方法。这可能是指为了避免重复计算,或者优化空间复杂度,采用滚动数的方式。例如,在DP时,使用两维数,或者倒序更新j的值,从而节省空间。另外,题目中的取模操作需要注意,每次更新状态时都要及时取模,防止溢出。同时,初始条件可能需要仔细设置,比如dp[0][0][0]=1,表示空字符串匹配空字符串,且用了0个子串的情况。 可能还需要考虑边界情况,比如当A的长度小于B时,直接返回0;或者当k=0时的情况,但题目中k应该至少是1,因为子串是非空的。然后,根据引用中的例子,比如D2B题解部分,可能需要使用类似这样的状态转移方程:当A[i] ==B[j]时,dp[j][k] +=dp[j-1][k](延续子串)和dp[j][k]+= dp[j-1][k-1](新建子串)。同时,由于是倒序枚举j,可以避免覆盖问题,比如从后往前更新,这样在计算时使用的是上一层的状态值。此外,空间优化方面,可能使用滚动数,因为每次处理i的时候只需要i-1的状态,所以可以用两个二维数交替使用,或者直接用一个三维数,但考虑到数据规模,可能需要优化到二维。例如,如果A的长度是n,B是m,k最大到m,那么三维数可能会超出内存限制,所以必须进行空间优化。 现在,我需要将这些思路整理成具体的状态转移方程和步骤。可能的步骤是: 1.初始化dp数,dp[0][0][0] =1。 2.遍历A的每个字符i,从1到n。 3.对于每个i,遍历B的字符j,从m到1(倒序)。4.遍历子串的数量p,从1到k。5.如果A[i-1] ==B[j-1],则dp[j][p] =(dp[j][p]+ dp[j-1][p]+ dp[j-1][p-1])% mod。6.否则,dp[j][p]保持不变。 7.最后,答案是dp[m][k]。不过这里可能需要更详细的状态转移分析,尤其是如何处理连续子串的情况。例如,当延续子串时,可能需要在同一个p中累加,而开启新子串时需要p-1。因此,可能需要两个状态数:一个表示当前字符是否作为新子串的开头,另一个表示延续。或者,使用两个不同的转移路径。或者,可能的状态转移方程是: 当A[i] ==B[j]时,有两种选择: -将当前字符连接到前一个子串的末尾,此时子串数量不变,即dp[i][j][k]+= dp[i-1][j-1][k]-开启新的子串,此时子串数量增加1,即dp[i][j][k]+= dp[i-1][j-1][k-1]但这样需要处理i和j的关系,以及如何累加。同时,为了处理重叠问题,可能需要确保子串之间不重叠,所以可能需要记录子串的结束位置,但这样会增加状态维度,导致复杂度上升。不过题目中的子串只需要互不重叠,而顺序连接,因此在状态转移时,只需要保证每次开启新子串时,前面的子串已经处理完毕,不需要记录具体位置,因为顺序连接的情况下,子串自然是不重叠的。例如,假设在A中取子串的顺序是按从左到右的顺序连接,所以每个新子串的起始位置必须在前一个子串的结束位置之后。但这样的话,动态规划的状态可能需要记录当前在A中的位置i,B中的位置j,已经使用的子串数p,以及当前是否在构建一个子串中。这会增加状态维度,导致复杂度较高。因此,可能需要优化状态的设计,比如将是否延续子串合并到状态中,或者通过转移的方式来隐式处理。 根据引用[2]中的提示,可能使用倒序枚举j的方法,这样可以避免重复计算,并且利用滚动数优化空间。例如,使用二维数dp[j][p],在每次处理i时,倒序更新j的值,从而覆盖之前的状态,而不会影响后续的计算。具体来说,可能的状态转移方程是: 对于每个i,从1到n:对于j从m到1:如果A[i-1] ==B[j-1]:对于p从k到1:dp[j][p] =(dp[j][p]+ dp[j-1][p]+ dp[j-1][p-1])% mod否则:dp[j][p]保持不变但这里可能需要初始化,或者使用辅助数来保存上一层的结果。例如,使用一个临时数来保存i-1时的状态,然后在处理i时更新当前的状态。或者,由于是倒序更新j,可以利用同一数,因为前面的j-1不会被覆盖,直到处理到j时,j-1的值还是上一层的状态。例如,当处理i时,对于j从m到1,每个j的更新只依赖于j-1的位置,而由于j是倒序处理的,j-1的值在此时还未被当前i的处理影响,因此可以正确引用上一层i-1的状态。这样,可以将空间复杂度优化到二维,即dp[j][p],而无需三维数。此外,初始条件可能需要dp[0][0] =1,表示当处理到A的前0个字符,B的前0个字符,用了0个子串时的方案数为1。然后,在每次i循环时,可能需要一个临时数或者直接在原数上进行更新,但需要注意更新顺序。总结起来,动态规划的状态定义可能为:dp[j][p]表示在A的前i个字符中,匹配到B的前j个字符,使用了p个子串的方案数。这里i是隐含在循环中的,通过外层循环处理,因此可以省略i这一维度,使用滚动数优化空间。因此,具体的步骤可能如下:1.初始化一个二维数dp[j][p],其中dp[0][0]=1,其余初始化为0。 2.对于A中的每个字符i(从1到n):a.创建一个临时数temp,复制当前的dp数。b.对于B中的每个字符j(从m到1):i.如果A[i-1]== B[j-1]:-对于每个p从1到k:temp[j][p]= (temp[j][p] +dp[j-1][p] +dp[j-1][p-1]) %modii.否则:-temp[j][p]保持不变c.将temp数赋值给dp数。3.最终结果为dp[m][k]。 不过可能还需要考虑,当i增加时,如何累积之前的方案数。例如,是否需要在每次i循环时,将当前i的贡献累加到dp数中,或者需要分情况讨论。另外,可能存在另一种状态定义方式,使用两个数:一个表示当前字符作为某个子串的结尾,另一个表示不作为结尾。但这样可能增加状态维度,但可以更清晰地处理连续子串的情况。 例如,使用两个二维数:dp0[j][p]表示不选当前字符作为子串的结尾,dp1[j][p]表示选当前字符作为子串的结尾。这样,当A[i]== B[j]时,可以选择延续之前的子串(即dp1[j-1][p])或者开启新的子串(dp0[j-1][p-1])。然后,状态转移方程可能为:dp1[j][p]= dp1[j-1][p] +dp0[j-1][p-1]dp0[j][p]= dp0[j][p] +dp1[j][p]不过这样的状态设计可能更清晰,但需要维护两个数,空间复杂度可能较高。不过通过滚动数优化,可以将其压缩到二维。例如,初始化dp0和dp1数,每次处理i时,更新当前的dp1和dp0。当A[i] ==B[j]时,可以延续子串(dp1[j][p]+= dp1[j-1][p])或者开启新子串(dp1[j][p]+= dp0[j-1][p-1])。然后,dp0[j][p]则继承自上一层的dp0[j][p]加上当前层的dp1[j][p]。不过这可能比较复杂,需要更仔细的分析。根据引用[2]中的描述,可能更倾向于使用倒序枚举j,并利用一个二维数进行状态转移。例如,在每次处理i时,从后往前更新j,这样在计算j时,j-1的位置还没有被当前i的更新影响,因此可以正确引用上一层i-1的状态。假设使用这样的方法,状态转移方程可以表示为: 当A[i] ==B[j]时:dp[j][p] +=dp[j-1][p]//延续之前的子串dp[j][p] +=dp[j-1][p-1]//开启新的子串否则:dp[j][p]保持不变但需要注意,这里的dp数是随着i的循环逐步更新的,所以可能需要使用滚动数,或者每次i循环时复制一份当前状态,然后在新的状态上进行更新。 例如,使用两个二维数prev和curr。在每次i循环开始时,将prev复制为curr,然后curr在prev的基础上进行更新。这样,可以保证每次更新都是基于i-1的状态。 具体步骤:初始化prev数,prev[0][0] =1。对于每个i从1到n:初始化curr数为prev的副本。对于j从1到m:如果A[i-1]== B[j-1]:对于p从1到k:curr[j][p]= (curr[j][p] +prev[j-1][p] +prev[j-1][p-1]) %mod将prev更新为curr。 最终结果为prev[m][k]。不过这里可能存在一个问题,当开启新的子串时,是否需要确保前面的子串已经结束。例如,如果当前字符作为新子串的开始,那么前面的子串必须已经结束于i-1的位置。但在这个状态定义中,可能无法直接追踪子串的结束位置,因此需要依赖状态转移的正确性来保证子串不重叠。实际上,由于子串是按顺序连接的,因此每个新子串的开始位置必须在前一个子串的结束位置之后。但是在动态规划的状态中,我们并没有记录子串的具体位置,而是通过状态的转移来隐式地处理。例如,当开启新子串时,p增加1,而当前处理的是A的前i个字符,因此后面的子串必须从i之后的位置开始,但这里可能无法直接体现,因此需要确保状态转移的正确性。 可能这个问题的正确解法确实是通过上述的倒序枚举j和使用滚动数的方法,这样可以有效地处理子串的顺序和数量,同时优化空间复杂度。 此外,需要注意模运算,每次更新都要取模,防止数值溢出。 最后,根据引用[2]中的提示,可能还需要进行一些常数优化,比如使用哈希表或者开放寻址法,但在这个具体问题中,可能不需要,因为动态规划已经足够高效。 总结,正确的解法应该是使用动态规划,状态定义为处理到A的第i个字符,B的第j个字符,使用p个子串的方案数,通过倒序枚举j和滚动数优化空间,状态转移时考虑延续子串和开启新子串两种情况,并注意取模。</think>### P2679 NOIP2015 提高 子串问题解法 #### 问题分析 题目要求从字符串$A$中选取$k$个**互不重叠的非空子串**,按原顺序连接后等于字符串$B$,求方案数。动态规划是解决此类计数问题的典型方法,需考虑以下关键点: 1. **状态定义**:需记录当前处理到$A$的位置、$B$的位置、已用子串数量。 2. **转移条件**:当$A[i]=B[j]$时,可以选择延续当前子串或新建子串。 3. **空间优化**:通过倒序枚举和滚动数降低空间复杂度[^2]。 --- #### 动态规划解法 ##### 状态定义 设$dp[j][p]$表示匹配到$B$的前$j$个字符,且已使用$p$个子串的方案数。通过滚动数优化,仅需二维状态。 ##### 状态转移 1. **当$A[i]=B[j]$时**: - **延续子串**:从$dp[j-1][p]$转移,即当前字符加入上一个子串末尾。 - **新建子串**:从$dp[j-1][p-1]$转移,即当前字符作为新子串的起点。 $$ dp[j][p] = (dp[j][p] + dp[j-1][p] + dp[j-1][p-1]) \mod (10^9+7) $$ 2. **当$A[i] \neq B[j]$时**:状态保持不变。 ##### 实现步骤 1. **初始化**:$dp[0][0] = 1$,表示空匹配空。 2. **倒序枚举**:对$A$的每个字符$i$,倒序遍历$B$的字符$j$(从$m$到$1$),避免覆盖未处理的状态。 3. **结果输出**:最终答案为$dp[m][k]$。 ```python MOD = 10**9 + 7 n, m, k = map(int, input().split()) A = input().strip() B = input().strip() # 初始化DP数 dp = [[0] * (k+1) for _ in range(m+1)] dp[0][0] = 1 for i in range(1, n+1): # 临时数保存上一轮状态 temp = [row[:] for row in dp] for j in range(m, 0, -1): if A[i-1] == B[j-1]: for p in range(1, k+1): temp[j][p] = (temp[j][p] + temp[j-1][p] + temp[j-1][p-1]) % MOD else: for p in range(k+1): temp[j][p] = temp[j][p] # 显式保持不变,实际可省略 dp = temp print(dp[m][k] % MOD) ``` --- #### 关键优化 1. **倒序枚举$j$**:防止覆盖未处理的$j-1$位置的状态。 2. **滚动数**:仅用二维数存储状态,空间复杂度为$O(mk)$[^2]。 3. **条件转移**:仅在$A[i]=B[j]$时更新状态,减少无效计算。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值