题目
你有一个字符串t,它由n个字母组成。定义一个字符串s的子串s[l…r],表示从位置 l 到 r 构成的一个新的串。
你的目标是构造一个字符串s,满足 s 中存在 k 个位置 i,可以找到 k 个以 i 为出发点的子串 t ,同时让s的长度尽可能小。
- 输入格式:第一行输入两个整数 n 和 k,表示 t 的长度和需要 k 个子串( 1 ≤ n , k ≤ 1 e 5 1\leq n,k\leq 1e5 1≤n,k≤1e5,保证答案字符串的长度在 3 e 5 3e5 3e5范围内);第二行输入字符串t
- 输出格式:输出满足条件的长度最小的s。题目保证答案唯一
样例:
输入:3 4
aba
输出:ababababa
分析思路
审题和分析
把题目看了一遍之后,先总结出以下重要的几点:
- 给定一个长度为n的串 t ,需要找到包含 k 个串 t 的字符串s
- 要求s的长度尽可能小,这 k 个字符串 t 在 s 中有两种可能:①k个串 t 之间互不相容,只是 k 个 t 简单的首尾相连;②k个串t之间有些字符是互相包含,共有的,比如样例里的字符a
- 很明显的是,如果给的字符串 t 毫无规律可言,相当杂乱,那么我们能找到的最短的串s只能是简单的把k个串t拼接起来;而如果给的字符串 t 头部和尾部有一段相同的部分,那么能找到的串 s 的长度就必然可以满足 s t r l e n ( s ) < k × s t r l e n ( t ) strlen(s) < k \times strlen(t) strlen(s)<k×strlen(t)
解题的关键:判断给定的字符串 t 首尾是否有一段相同的字符
知识准备(BF和KMP算法)
我们先来简单回顾一下字符串的模式匹配
BF算法,亦称简单匹配算法,采用穷举的思路,BF是指暴力的意思
- 思路:从s的每一个字符开始依次与t的字符进行匹配
int BF(SqString s,SqString t)
{ int i=0,j=0;
while (i<s.length && j<t.length)
{ if (s.data[i]==t.data[j])
{ i++; //主串和子串依次匹配下一个字符
j++;
}
else //主串、子串指针回溯重新开始下一次匹配
{ i=i-j+1; //主串从下一个位置开始匹配
j=0; //子串从头开始匹配
}
}
if (j>=t.length) //或者if (j==t.length)
return(i-t.length); //返回匹配的第一个字符的下标
else
return(-1); //模式匹配不成功
}
- 缺点:虽然BF算法简单粗暴,但是两个串都需要依次遍历,假设目标串s的长度为n,模式串t的长度为m,则时间复杂度为O( n × m n\times m n×m)
- 改进:在BF算法中,t 的第 j 位失配,默认的把 t 串后移一位,但在前一轮的比较中,我们已经知道了t的前 ( j − 1 ) (j-1) (j−1)位与S中间对应的某 ( j − 1 ) (j-1) (j−1)个位已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们已经提前知道了主串s的部分内容,我们能否利用这些内容,让 t 多往后移几位,减少遍历的趟数呢?答案是肯定的(这就是KMP算法最根本的核心思想)
- 比较:
- BF算法:每次失配,s串的索引 i 往后移一位,而 t 串则从头重新开始,即定位到首位。时间复杂度是 O ( n × m ) O(n\times m) O(n×m)
- KMP算法:每次失配,s串的索引 i 不动,t 串的索引 j 定位到某个数。时间复杂度是 O ( n + m ) O(n+m) O(n+m)
而这个定位到某个数就是KMP算法的重中之重,也是解决上面题目的核心!
- 要注意 t j t_j tj前面的子串最多从 t 1 t_1 t1开始,不含 t 0 t_0 t0。另外我们把next[0]设置为1,因为s的字符 s i s_i si 与 t 0 t_0 t0 不相同,没有任何有用的部分匹配信息,直接从下一趟( s i + 1 / t 0 s_{i+1}/t_0 si+1/t0)开始匹配
举例:
其实对于短的串肉眼看十分容易,但是长串就需要借助程序了
void GetNext(char t[],int m,int next[])//m是串t的长度
{ int j,k;
j=0; k=-1; next[0]=-1;
//重点在于借助next[j]求出next[j+1],所以j取到t的倒数第二个位置
for(;j<m-1;j++)
{ if (k==-1 || t[j]==t[k])//k=-1对应next[1]=0
{ j++; k++;
next[j]=k;
}//k在前 j在后,如果相邻不等k往前调
else
k=next[k];
}
}
思路
回到题目本身,知道怎么计算next数组之后,我们就能知道串 t 的首尾有多少个字符是相同的,这样在构造字符串s的时候,k个串 t 首尾相连时,头部和尾部是共有的,也能轻易计算出s的长度以及具体组成。更加具体的思路见代码注释!
代码实现
C++实现
#include<iostream>
using namespace std;
const int N = 1e5;
int ne[N];//next数组,由于取名冲突,写作ne
int n, k;//t的长度和要求的k
char t[N];//字符串t
void GetNext()//m是串t的长度
{
int j = 0, k = -1;
ne[0] = -1;
//重点在于借助next[j]求出next[j+1],所以j取到t的倒数第二个位置
while(j<n)
{
if (k == -1 || t[j] == t[k])//k=-1对应next[1]=0
{
j++; k++;
ne[j] = k;
}//k在前 j在后,如果相邻不等k往前调
else
k = ne[k];
}
//本来KMP的next数组只需要知道最后一个字符t[n-1]的next值,但我们这里需要知道首尾重合部分,恰好实际上t[n]这个位置是字符串结束符‘\0’,所以next[n]就是串t首尾相同的字符个数
}
int main()
{
cin >> n >> k >> t;
GetNext();
//知道首尾的重合情况,接下来就是拼接了,next[n]刚好就是首尾相同部分的长度
cout << t;
for (int i = 1; i < k; i++)
{
for (int j = ne[n]; j < n; j++)
cout << t[j];
}
//cout << endl;
//for (int j = 0; j <= n; j++)cout << ne[j] << " ";
}
python实现
这里利用python的切片可以很轻松找出字符串 t 首尾相同的字符个数,甚至不用next数组,最后拼接的时候也是切片相接即可,不得不感慨python的巧妙!
def main(n,k,t):
num = 0 #计数
#计算出头尾相同的子串长度
for i in range(1,n):
if(t[0:i] == t[n-i:n]):
num = i
pass
pass
#新串等于子串头+k个子串尾,由num决定
s = t[0:num] + t[num:n]*k
print(s)
pass
n,k = map(int,input().split())
t = str(input())
main(n,k,t)
KMP算法后续
KMP算法的过程
i=0; j=0;
while (s和t都没有扫描完)
{ if (j=-1或者它们所指字符相同)
i和j分别增1;
else
i不变,j回退到j=next[j](即模式串右滑);
}
if (j超界)
返回i; //模式匹配成功
else
返回-1; //模式匹配失败
具体代码
int KMPIndex(SqString s,SqString t)
{ int next[MaxSize],i=0,j=0;
GetNext(t,next);
while (i<s.length && j<t.length)
{
if (j==-1 || s.data[i]==t.data[j])
{ i++;
j++; //i、j各增1
}
else j=next[j]; //i不变,j后退
}
if (j>=t.length)
return(i-t.length); //返回匹配模式串的首字符
else
return -1; //返回不匹配标志
}
KMP算法中求next数组的时间复杂度为 O ( m ) O(m) O(m)在后面的匹配中因主串s的下标不减即不回溯,比较次数可记为n,所以KMP算法平均时间复杂度为 O ( m × n ) O(m \times n) O(m×n)
KMP算法的改进:
考虑下面的例子:
t 3 t_3 t3失配,此时 j = 3 i = 3,i保持不到,j 回溯,j = next[j] = 2,从 t 2 t_2 t2的位置与 s 3 s_3 s3开始新一轮的比较,以此类推
可以看到出现了好几次不必要的比较,这种情况下最理想的做法是能够直接跳过中间方框里的步骤,直接到最后的 j = -1,为此我们引入了nextval[]数组。
这样一来改进后的KMP算法就变成了下面这样子
t 3 t_3 t3失配,此时 j = 3 i = 3,i 保持不到,j 回溯,j = nextval[j] = -1。所以i++ , j++,从 t 0 t_0 t0的位置与 s 4 s_4 s4位置开始新一轮的比较,这样一来进一步提高模式匹配的效率
总结
KMP算法的思想是利用模式串中的部分匹配信息,利用已知的部分信息,尽量让模式串往后移,而不是一次移一个字符的长度