【题解】洛谷P2679[NOIP2015]子串 线性DP+前缀和

本文介绍了一个使用动态规划解决字符串匹配问题的C++程序。通过预处理和迭代计算,该算法能够在给定两个字符串和一个匹配长度的情况下,找到第一个字符串中与第二个字符串长度为K的子串完全匹配的所有可能情况的数量。此算法特别适用于需要精确匹配且考虑多种可能匹配位置的场景。

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

题目链接
在这里插入图片描述
在这里插入图片描述


#include<cstdio>
#define _rep(i,a,b) for(int i=(a);i<=(b);i++)
#define rep_(i,a,b) for(int i=(a);i>=(b);i--)
const int mod=1e9+7;
int dp[201][201],sum[201][201],n,m,K;
char a[1001],b[1001];
int main()
{
	scanf("%d%d%d%s%s",&n,&m,&K,a+1,b+1);dp[0][0]=1;
	_rep(i,1,n)rep_(j,m,1)rep_(k,K,1)dp[j][k]=(dp[j][k]+((a[i]==b[j])?(sum[j][k]=(sum[j-1][k]+dp[j-1][k-1])%mod):(sum[j][k]=0)))%mod;
	printf("%d\n",dp[m][K]);return 0;
}

总结

<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]$时更新状态,减少无效计算。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值