【题目】NOI2014 动物园

本文深入探讨了KMP算法在处理字符串匹配问题时的next数组概念,并引入了一个优化后的num数组,用于统计字符串中既是后缀又是前缀但不重叠的子串数量。通过对next数组的巧妙运用,文章提出了一种O(n)时间复杂度的解决方案,避免了原始O(n^2)的高复杂度方法。
题目大意

题目首先花了大量篇幅介绍了KMP算法。其中包括回退数组nextnextnext

  • 对于字符串SSS的前iii个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外),最长的长度记作next[i]next[i]next[i]
  • 例如SSSabcababcabcababcabcababc,则next[5]=2next[5]=2next[5]=2。因为SSS的前555个字符为abcababcababcabababab既是它的后缀又是它的前缀,并且找不到一个更长的字符串满足这个性质。同理,还可得出next[1]=next[2]=next[3]=0next[1]=next[2]=next[3]=0next[1]=next[2]=next[3]=0next[4]=next[6]=1next[4]=next[6]=1next[4]=next[6]=1next[7]=2next[7]=2next[7]=2next[8]=3next[8]=3next[8]=3

而本题要求求出一个numnumnum数组:

  • 对于字符串SSS的前iii个字符构成的子串,既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作num[i]num[i]num[i]
  • 例如SSSaaaaaaaaaaaaaaa,则num[4]=2num[4]=2num[4]=2。这是因为SSS的前444个字符为aaaaaaaaaaaa,其中aaaaaaaaa都满足性质“既是后缀又是前缀”,同时保证这个后缀与这个前缀不重叠。而aaaaaaaaa虽然满足性质“既是后缀又是前缀”,但是这个后缀与这个前缀重叠了,所以不能计算在内。同理,num[1]=0num[1]=0num[1]=0num[2]=num[3]=1num[2]=num[3]=1num[2]=num[3]=1num[5]=2num[5]=2num[5]=2

字符串的长度很大(L⩽1000000L\leqslant 1000000L1000000),结果输出∏i=1L(num[i]+1)mod  1000000007\prod\limits_{i=1}^L(num[i]+1)\mod 1000000007i=1L(num[i]+1)mod1000000007


思路

numnumnum数组中“后缀与前缀不重叠”的条件不容易处理,并且跟nextnextnext数组的定义大相径庭,故定义一个num′num'num数组:

  • 对于字符串SSS的前iii个字符构成的子串,既是它的后缀同时又是它的前缀的字符串的数量记作num[i]num[i]num[i]。这里的前缀和后缀包括SSS本身。

其实就是去掉“后缀与前缀不重叠”这个条件的numnumnum数组。

num′[i]num'[i]num[i]中统计的串中,首先一定有这个串本身;而其它的串都被囊括在next[i]next[i]next[i]表示的串内(包括两个长度为next[i]next[i]next[i]的串):


根据nextnextnext数组的定义,首尾两个长度为next[i]next[i]next[i]的串相同,把所有的串平移至同一个长度为nextnextnext的串内:

这些串就成了前next[i]next[i]next[i]个字符构成的子串中既是后缀又是前缀的字符串,根据定义,这些串的数量为num′[next[i]]num'[next[i]]num[next[i]]
再加上前iii个字符构成的子串本身,得出递推式:
num′[i]=num′[next[i]]+1num'[i]=num'[next[i]]+1num[i]=num[next[i]]+1

递推的时间复杂度是O(n)O(n)O(n)(这里以及下文的nnn指字符串长度LLL,也就是numnumnum数组的大小)。递推的边界是num′[0]=0num'[0]=0num[0]=0

现在考虑把“后缀与前缀不重叠”的条件加上。这个条件就是让num[i]num[i]num[i]中的串的长度不超过i2\frac i22i
为了让字符串既是后缀又是前缀,除了串本身以外,最长的字符串就是next[i]next[i]next[i]表示的串。若next[i]next[i]next[i]的长度已经不超过iii的一半,则num[i]num[i]num[i]就是num′[next[i]]num'[next[i]]num[next[i]]。若next[i]next[i]next[i]仍不满足要求,根据前面的平移操作和递推,下一个最长的串的长度就应该是next[next[i]]next[next[i]]next[next[i]],一直回退下去,直到串的长度为满足条件的最大的长度jjj。然后就有
num[i]=num′[j]num[i]=num'[j]num[i]=num[j]

不难得到下面的代码:

for(int i=1;i<=n;i++){
  int j=next[i];
  while(j*2>i)j=next[j];
  num[i]=num'[j];
}

然而这个做法的时间复杂度是O(n2)O(n^2)O(n2)。不妨试试样例的第一组数据:aaaaaaaaaaaaaaa
造成如此高的复杂度的原因是next[i]next[i]next[i]的规模是O(n)O(n)O(n)的,最坏情况下对于每个iiijjj都要减小O(n)O(n)O(n)次,然后就爆了。

考虑用KMP求nextnextnext数组的过程:

for(int i=2,j=0;i<=len;i++){
  while(j&&t[j+1]!=t[i])j=fail[j];
  if(t[j+1]==t[i])j++;fail[i]=j;
}

KMP的时间复杂度是O(n)O(n)O(n)的,因为iii111枚举到nnn,增加O(n)O(n)O(n)次;除了if语句中jjj增加O(n)O(n)O(n)次,其余时间jjj都在减少,所以jjj减少也不会超过O(n)O(n)O(n)次。

于是可以把上面的暴力修改成下面的代码:

for(int i=2,j=0;i<=len;i++){
  while(j&&t[j+1]!=t[i])j=fail[j];
  if(t[j+1]==t[i])j++;
  while(j*2>i)j=fail[j];
  num[i]=num'[j];
}

几个问题:

时间复杂度?

iii的枚举规模仍然是O(n)O(n)O(n)jjj还是在if语句处增加O(n)O(n)O(n)次,因此时间复杂度为O(n)O(n)O(n)

jjj是否是可行解(是否存在长度为jjj的串既是后缀又是前缀)?

jjj是由nextnextnext数组递推得到的,根据前面的递推,jjj是可行解。

jjj是否是最优解(jjj是否是不超过i2\frac i22i的最大可行解)?

由于jjj在当前的iii时要回退减小(第二个while语句),iii增加111时,jjj似乎就有可能不是最大的可行解。
根据前面得知,jjjnext[i]next[i]next[i]处回退一定不会错过最优解。若jjj不继续回退,得到的jjj就是next[i]next[i]next[i],不会错过最优解。现在jjj要继续回退,轮到下一个i′=i+1i'=i+1i=i+1时,为了当前子串的长度为j′j'j的前后缀匹配,jjj原本就要回退(第一个while语句)。

  • 若此次回退后j′j'j没有超过i′2\frac{i'}22i,就有j′⩽i+12j'\leqslant\frac{i+1}2j2i+1
    • iii为偶数时,j′⩽i2j'\leqslant\frac i2j2i,因此在这之前把jjj回退到i2\frac i22i就没有什么影响。
    • iii为奇数时,
      • j′⩽i−12j'\leqslant\frac{i-1}2j2i1,则在这之前把jjj回退到i2\frac i22i也没有什么影响。
      • j′=i+12j'=\frac{i+1}2j=2i+1,说明可以匹配的最长的前后缀刚好是子串的一半,根据KMP算法的原理,此时已匹配的长度j′j'j一定是由上一次的jjj111得来的。于是j=i−12j=\frac{i-1}2j=2i1,同样没有影响。
  • 若此次回退后j′j'j超过了i′2\frac{i'}22i,为了得到num[i′]num[i']num[i]的值,j′j'j仍然要回退,回到上一种情况。
P2375 [NOI2014] 动物园是一道经典的动态规划题目,以下是该题的详细题意和解题思路。 【题意描述】 有两个长度为 $n$ 的整数序列 $a$ 和 $b$,你需要从这两个序列中各选一些数,使得这些数构成一个新的序列 $c$。其中,$c$ 序列中的元素必须在原序列中严格递增。每个元素都有一个价值,你的任务是选的元素的总价值最大。 【解题思路】 这是一道经典的动态规划题目,可以采用记忆化搜索的方法解决,也可以采用递推的方法解决。 记忆化搜索的代码如下: ```c++ #include <iostream> #include <cstdio> #include <cstring> using namespace std; const int MAXN = 1005; int dp[MAXN][MAXN], a[MAXN], b[MAXN], n; int dfs(int x, int y) { if (dp[x][y] != -1) return dp[x][y]; if (x == n || y == n) return 0; int res = max(dfs(x + 1, y), dfs(x + 1, y + 1)); if (a[x] > b[y]) { res = max(res, dfs(x, y + 1) + b[y]); } return dp[x][y] = res; } int main() { scanf("%d", &n); for (int i = 0; i < n; i++) scanf("%d", &a[i]); for (int i = 0; i < n; i++) scanf("%d", &b[i]); memset(dp, -1, sizeof(dp)); printf("%d\n", dfs(0, 0)); return 0; } ``` 其中,dp[i][j]表示选到a数组中第i个元素和b数组中第j个元素时的最大价值,-1表示未计算过。dfs(x,y)表示选到a数组中第x个元素和b数组中第y个元素时的最大价值,如果dp[x][y]已经计算过,则直接返回dp[x][y]的值。如果x==n或者y==n,表示已经遍历完一个数组,直接返回0。然后就是状态转移方程了,如果a[x] > b[y],则可以尝试选b[y],递归调用dfs(x, y+1)计算以后的最大价值。否则,只能继续遍历数组a,递归调用dfs(x+1, y)计算最大价值。最后,返回dp[0][0]的值即可。 递推的代码如下: ```c++ #include <iostream> #include <cstdio> #include <cstring> using namespace std; const int MAXN = 1005; int dp[MAXN][MAXN], a[MAXN], b[MAXN], n; int main() { scanf("%d", &n); for (int i = 0; i < n; i++) scanf("%d", &a[i]); for (int i = 0; i < n; i++) scanf("%d", &b[i]); for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]); if (a[i] > b[j]) { dp[i][j] = max(dp[i][j], dp[i][j + 1] + b[j]); } } } printf("%d\n", dp[0][0]); return 0; } ``` 其中,dp[i][j]表示选到a数组中第i个元素和b数组中第j个元素时的最大价值。从后往前遍历数组a和数组b,依次计算dp[i][j]的值。状态转移方程和记忆化搜索的方法是一样的。 【参考链接】 P2375 [NOI2014] 动物园:https://www.luogu.com.cn/problem/P2375
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值