首页 > 其他分享 >Living-Dream 系列笔记 第74期

Living-Dream 系列笔记 第74期

时间:2024-08-05 12:16:47浏览次数:8  
标签:Living 匹配 nxt int Dream 后缀 74 字符串 border

Kobe-Morris-Pratt 算法

定义

一些基本定义:

  • border:是一个字符串的子串(不与其本身相同)且满足既是其前缀又是其后缀的字符串,我们称之为该字符串的一个 border

Kobe-Morris-Pratt 算法(以下简称 KMP 算法),是解决字符串匹配问题的一种算法,实际做题中常偏思维,通常用到的只有其中的 border 相关性质(即通常所说的 \(nxt\) 数组)。

在字符串匹配中,我们通常将被匹配的串称为文本串 \(s\),将与文本串匹配的串称为模式串 \(t\),下文亦同,且我们令 \(\left| s \right|\) 表示字符串 \(s\) 的长度,同时 \(\left| s \right| =n, \left| t \right|=m\)。

在字符串匹配中,当 \(s\) 与 \(t\) 某一下标的字符不相同,则我们称此种情况为 失配,下文亦同。

朴素匹配

显然,朴素匹配即为每次失配则令 \(t\) 移动一位继续尝试匹配直到全匹配上为止。

时间复杂度 \(O(n \times m)\)。

KMP 匹配

我们观察到,朴素匹配之所以效率低,是因为它每次失配时只让 \(t\) 移动一位。而失配时可能前面已经匹配了许多位,只移动一位就会导致大量的相同字符重复匹配。

结论:KMP 在每次失配时,\(t\) 串下标应跳到从开头到失配前一位的子串的最长 border 的长度的下标处

证明:

  • Q:为什么要跳到 border 的长度下标处?

    A:画个图观察可得,这样跳就是为了使 \(s\) 与 \(t\) 的后缀与前缀对齐。

  • Q:为什么要让后缀与前缀对齐?取中间的不行吗?

    A:因为这样跳的距离更远,效率更高。

  • Q:为什么要跳到最长 border 的长度下标处?

    A:选最长的 border,就意味着在 \(s\) 的最长后缀之前,不会有任何子串与 \(t\) 最长前缀有匹配的可能,那肯定选择直接跳过。而选的更小,则会导致前面可能有匹配的可能,从而导致遗漏。

综上所述即为 KMP 算法的主要逻辑与原理。

时间复杂度 \(O(n+m)\)。

最长 border 的求取

我们令 \(nxt_i\) 表示 \(t\) 在下标 \(i\) 失配后要跳到的位置。

根据上述推论,它同时也表示 \(t\) 在下标 \(i-1\) 结尾的最长 border 的长度。

既然是求最长相同的前缀与后缀,那么我们也可以不断尝试将 \(t\) 进行自匹配,每次成功匹配就记录 \(nxt\) 值,失配也按上述处理即可。

时间复杂度 \(O(n+m)\)。

综上,KMP 算法的总时间复杂度即为 \(O(n+m)\)。

P3375

板子。

code
#include<bits/stdc++.h>
using namespace std;

const int N=1e6+5; 
string s,t;
int nxt[N];

void getnxt(){
	int i=0,j=-1;
	nxt[0]=-1;
	for(;i<t.size();){
		if(j==-1||t[i]==t[j])
			i++,j++,nxt[i]=j;
		else 
			j=nxt[j];
	}
}
void kmp(){
	getnxt();
	int i=0,j=0;
	for(;i<s.size();){
		if(j==t.size()-1&&s[i]==t[j])
			cout<<i-j+1<<'\n',j=nxt[j];
		if(j==-1||s[i]==t[j])
			i++,j++;
		else
			j=nxt[j];
	}
}

int main(){
	cin>>s>>t;
	kmp();
	for(int i=1;i<=t.size();i++) cout<<nxt[i]<<' ';
	return 0;
}

P4391

结论:答案即为 \(n-nxt_n\)。(KMP 题中有很多结论题,被薄纱了 /kk)

证明:

  • Case 1:字符串最长 border 无重叠部分。

image

首先蓝色部分一定相等。

由于题目给的是子串,所以我可以复制一个红色部分使得两部分相等。

此时最短的为红 + 蓝,即 \(n-nxt_n\)。

还能更短吗?不能,因为空出来的部分没有子串可以复制出它。

  • Case 2:字符串最长 border 有重叠部分。

image

(重叠部分为最中间的蓝色块)

首先,字符串内的蓝 + 红 + 蓝是一个最长 border,它们是相等的。

因为它们相等,蓝色部分又是后缀的一个前缀,所以我在前缀中也能找到一个相同的前缀,同理在后缀中也能找到一个相同后缀,复制一个红色部分后,后缀的相同后缀就能作为一个新的循环节的前缀了。

综上,最短的是蓝 + 红,即 \(n-nxt_n\)。

不能更短的原因同上。

code
#include<bits/stdc++.h>
using namespace std;

const int N=1e6+5; 
string s;
int n,nxt[N];

void getnxt(){
	int i=0,j=-1;
	nxt[0]=-1;
	for(;i<n;){
		if(j==-1||s[i]==s[j])
			i++,j++,nxt[i]=j;
		else 
			j=nxt[j];
	}
}

int main(){
	cin>>n>>s;
	getnxt();
	cout<<n-nxt[n];
	return 0;
}

CF1200E

进食后人:不要用 substr / +,这俩玩意都是 \(O(n)\) 的,用 erase / += 替代即可。

容易观察到一个很显然的结论:将一个字符串接到之前答案的前面,记所拼成的新字符串的最长 border 的长度为 \(x\),则只要取前一个字符串加上后一个字符串去掉前 \(x\) 位即为当前答案。

然后直接做即可。

注意:

  • 因为最长 border 长度不会超过两字符串长度取 \(\min\),于是只需取之前答案的这么多位即可,不然会超时;

  • 拼接时要在中间加一个任意字符(不是大小写字母或数字),不然答案可能会越界。

code
#include<bits/stdc++.h>
using namespace std;

const int N=1e6+5;
int n,nxt[N];
string s[N];

int main(){
	ios::sync_with_stdio(0);
	cin>>n;
	string ans="";
	for(int i=1;i<=n;i++){
		cin>>s[i];
		if(i==1) ans+=s[i];
		else{
			string now="";
            now+=s[i];
            now+="#";
            now+=ans.substr(ans.size()-min(ans.size(),s[i].size()));
			int j=0,k=-1;
    		nxt[0]=-1;
			for(;j<now.size();){
				if(k==-1||now[j]==now[k])
					j++,k++,nxt[j]=k;
				else
					k=nxt[k];
			}
			s[i].erase(0,nxt[now.size()]);
            ans+=s[i];
		}
	}
	cout<<ans;
	return 0;
}

标签:Living,匹配,nxt,int,Dream,后缀,74,字符串,border
From: https://www.cnblogs.com/XOF-0-0/p/18342978

相关文章

  • ORA-07445 opiaba()+639 ORA-00600 17147数据库宕机
    /u01/app/oracle/diag/rdbms/testaa/testaa/traceThuAug0112:43:372024ArchivedLogentry46044addedforthread1sequence23032ID0x860b01b0dest1:ThuAug0112:51:362024Exception[type:SIGSEGV,SI_KERNEL(general_protection)][ADDR:0x0][PC:0x1......
  • 代码随想录day32 || 509斐波那契数列 70爬楼梯 746使用最小花费爬楼梯
    509斐波那契数列力扣题目链接题目描述:斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:F(0)=0,F(1) =1F(n)=F(n-1)+F(n-2),其中n>1给定 n ,请计算 F(n) 。代码1......
  • Luogu7740 [NOI2021]机器人游戏 做题记录
    link一道炸裂的题目。首先样例解释已经告诉我们可以容斥。考虑枚举可行的位置集合\(S\),我们需要统计\(\forallp\inS\),纸条初始状态和目标状态都相同的方案数。显然每个机器人独立,可以分开考虑。对于一个机器人,他的行动对纸条的每个格子要么赋值为\(0/1\),要么不变,要么取......
  • Living-Dream 系列笔记 第71期
    众所周知,换根dp是非常套路的。换根真好玩(换根dp:当不同节点作为根时,dp结果不一致,若枚举每个节点作为根,则时间复杂度过高,在此种情形下,可使用换根dp处理相邻两节点间的贡献,从而达到快速换根的效果。使用场景:对于一棵树,寻找以某节点\(u\)为根时取得的最大值/最小值......
  • qtui更改后报错:-1: error: [Makefile.Debug:7745: ui_widget.h] Error 1,如何解决?
    qtui更改后报错:-1:error:[Makefile.Debug:7745:ui_widget.h]Error1,如何解决?在使用Qt开发时,遇到-1:error:[Makefile.Debug:7745:ui_widget.h]Error1这样的错误通常是因为ui_widget.h文件未能正确生成。这个文件是由Qt的uic(用户界面编译器)工具生成的......
  • 代码随想录训练第三十一天|LeetCode1049.最后一块石头的重量II、LeetCode494.目标和、
    文章目录1049.最后一块石头的重量II思路一维数组二维数组494.目标和思路一维数组解法二维数组解法474.一和零思路1049.最后一块石头的重量II有一堆石头,用整数数组stones表示。其中stones[i]表示第i块石头的重量。每一回合,从中选出任意两块石头,然后将它们一......
  • Living-Dream 系列笔记 第70期
    登神长阶!P1272&P1273请查阅往期笔记,此处不再赘述。其中P1273我们学到了定义更好求解的状态(一般是转化为价值,如本题),再通过枚举求解最终答案。P8625容易发现你选出的\(S\)一定是一个子树。然后这题就变成最大子树和了。关于最大子树和那题,请查阅往期笔记,此处不再赘述。......
  • Leetcode每日一题 202040726 2740.找出分区值
    题目描述给你一个正整数数组nums。将nums分成两个数组:nums1和nums2,并满足下述条件:数组nums中的每个元素都属于数组nums1或数组nums2。两个数组都非空。分区值最小。分区值的计算方法是|max(nums1)-min(nums2)|。其中,max(nums1)表示数组nums1......
  • Living-Dream 系列笔记 第69期
    复习树形dp。树形dp定义状态一般套路:令\(dp_i\)表示以\(i\)为子树的xxx(要维护的信息),可以有多维,但一定会有这一维。P2016&P2014请查阅往期笔记,此处不再赘述。P2585以前是分讨每个节点有几个儿子,然后分别转移。其实不用分讨,直接将所有节点视作有两个儿子,初始时将它......
  • P9746 「KDOI-06-S」合并序列
    mx练习赛搬的,虽然数据不咋样,但是一步步的优化思路确实值得一记。P9746合并序列题目大意:给你\(n(1\len\le500)\)个数\(a_1,a_2,\ldotsa_n\)(\(a_i<512\))。每次可以选一个3元组\((i,j,k)\),满足\(i<j<k\),并且\(a_i\oplusa_j\oplusa_k=0\),则你可以将$a_i\dotsa_k$......