小黄的刷题之路(二)——码题集OJ赛-字符串构造

题目

你有一个字符串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 1n,k1e5,保证答案字符串的长度在 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) (j1)位与S中间对应的某 ( j − 1 ) (j-1) (j1)个位已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们已经提前知道了主串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[]数组。

d

这样一来改进后的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算法的思想是利用模式串中的部分匹配信息,利用已知的部分信息,尽量让模式串往后移,而不是一次移一个字符的长度

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值