咕咕咕
网上的优秀博文有很多…这篇主要是用来让博主尽快回忆起来并会写代码用的qwq
problems are welcome!
kmp
只有7秒记忆的博主好像又学会了kmp!走过路过不要错过!
还是吐槽一下…让你学会一个算法的一定不会是长篇大论的…(你一定会在某个时刻突然理解一个算法)
-
一句话…kmp实际上求的是前 i i i个字符中最长的前后缀匹配长度是多少
- 也就是下面代码中的 a [ ] a[] a[]
-
求出最长前后缀匹配值到底有什么用?
- 暴力的两个字符串匹配最坏情况会达到
O
(
n
2
)
O(n^2)
O(n2)
- 但事实上这样匹配有很多是用不着匹配的
- 假设我们匹配到
k
k
k位置匹配失败…我们就让字符串移到合适的位置使之能够继续匹配下去…而不是重新匹配
- 这样最长前后缀匹配长度就起到了作用…
- 如果 s s s字符串 k k k位置失配…那么我们跳到 k − l e n [ k ] k - len[k] k−len[k]继续匹配下去qwq
- 这样最长前后缀匹配长度就起到了作用…
- 暴力的两个字符串匹配最坏情况会达到
O
(
n
2
)
O(n^2)
O(n2)
-
复杂度证明?
void kmp(char *t, int *a, int m) {
a[1] = 0;
int p = 0;
for(int i = 2; i <= m; ++i) {
if(t[i] == t[p + 1]) {
a[i] = ++p;
continue;
}
while(p && t[i] != t[p+1]){
p = a[p];
if(t[i] == t[p+1]) {
break;
}
}
if (t[i] == t[p + 1]) {
a[i] = ++p;
} else {
a[i] = 0;
}
}
}
SA
-
如此神奇的SA!它能在 O ( n l o g n ) O(nlog_n) O(nlogn)时间复杂度完成对一个字符串所有后缀的排序操作!(而且在有些时候解决一些问题确实是要比SAM等等要简单的!总之你值得拥有!)
- 什么?你说你要用sort…拜托拜托…那复杂度明明是 O ( n 2 l o g n ) O(n^2log_n) O(n2logn)的!
-
把所有后缀排序排好有什么用?
- 后缀按照字典序排好会有一些很优美的性质
- 具体看论文咯
懒…也许以后翻了个大跟头之后就会回来继续写了qwq
- 具体看论文咯
- 后缀按照字典序排好会有一些很优美的性质
-
变量声明
- s a [ i ] sa[i] sa[i]表示排名为 i i i的那个后缀是谁
- r k [ i ] rk[i] rk[i]表示 i i i这个后缀的排名
-
具体操作
- 首先读入字符串之后我们现根据单个字符排序(也就是按照每个后缀的第一个字符排序)。对于每个字符,我们按照字典序给一个排名(可以并列),这里称作关键字。
- 接下来我们再把相邻的两个关键字合并到一起…以第一个字符为第一关键字.第二个字符为第二关键字排序
- 这样倍增合并复杂度将为 O ( n l o g n 2 ) O(nlog^2_n) O(nlogn2)
- 那么我们什么时候结束呢?很简单,当所有的排名都不同的时候我们直接退出就可以了,因为已经排好了。
-
使用基数排序优化
- 我们要建两个桶,一个装个位,一个装十位,我们先把数加到个位桶里面,再加到十位桶里面,这样就能保证对于每个十位桶,桶内的顺序肯定是按个位升序的。
- 复杂度降为 O ( n l o g n ) O(nlog_n) O(nlogn)
并不会O(n)的
const int N=/**/, M=/**/;
char lx[N];
int n;
int a[N], id[N], sa[N], rk[N], nm[N], h[N];
void Sort() {
memset(nm,0,sizeof nm);
for(int i = 1; i <= n; ++i)
nm[rk[i]]++;
for(int i = 1; i <= max(M,n); ++i)
nm[i] += nm[i-1];
for(int i = n; i >= 1; --i)
sa[nm[rk[id[i]]]--] = id[i];
}
void Geth() {
int H = 0;
for(int i = 1; i <= n; ++i){
if(H) H--;
int j = sa[rk[i] - 1];
for(; a[i+H] == a[j+H]; ++H);
h[rk[i]] = H;
}
}
bool cmp(int x, int y, int j) {
return id[x] == id[y] && id[x + j] == id[y + j];
}
int main(){
scanf("%s", lx + 1);
n = strlen(lx + 1);
for(int i = 1; i <= n; ++i)
a[i] = lx[i] - 'a' + 1;
for(int i = 1; i <= n; ++i)
id[i] = i, rk[i] = a[i];
Sort();
for(int j = 1, p = 0; p < n; j <<= 1) {
p = 0;
for(int i = n - j + 1; i <= n; ++i)
id[++p] = i;
for(int i = 1; i <= n; ++i)
if(sa[i] > j)
id[++p] = sa[i] - j;
Sort(), swap(id, rk);
p = 0;
for(int i = 1; i <= n; ++i)
rk[sa[i]] = cmp(sa[i], sa[i-1], j) ? p : ++p;
}
Geth();
}
SAM
-
SAM能够在 O ( n ) O(n) O(n)时间内构建出所有的后缀
- 首先我们如果将后缀暴力插入字典树
O
(
n
2
)
O(n^2)
O(n2)吃不消啊!
- 事实上这又有很多节点是重复的!不必要的!
- 首先我们如果将后缀暴力插入字典树
O
(
n
2
)
O(n^2)
O(n2)吃不消啊!
-
我们设 r i g h t ( s ) right(s) right(s)表示 s s s这个字符串出现位置的集合
- 我们用一个点表示一个right集合的状态
- 如果一个right集合
B
B
B的最长串的长度+1=另一个right集合
A
A
A的最短串长度
- 那么我们就从 A A A向 B B B连一条边…称 B B B是 A A A的father
-
考虑增量法构造
- 假设我们已经有了 1 − i 1-i 1−i的SAM…接下来我们要在后面添加一个字符
加入第i个字符c产生的子串:
1、Right={i},记做np
2、Right≠{i},记做nq
上一次插入的np节点记做las
第一类
需要找到所有{i-1}∈Right的节点。Right={i-1}的节点是las,只需从las不断跳pre,设当前跳到的节点是p。有三种情况:
① ch(p,c)=null 不存在p加入c的转移,直接加入这个转移:ch(p,c)=np,p=pre§
② ch(p,c)=q 转入第二类
③ p=rt。那Right包含Right(np)的就只有空串了。所以pre(np)=rt
第二类
此时p加入c的转移已经存在。
① 若len§+1=len(q) 因为p是las在Parent树上的祖先,所以p的每个串都是las串的后缀。len§+1=len(q),从而q的每个串都可以由p中一个串加入c后得到,而las中的每个串加入c后都转移到np,可知Right(np)∈Right(q),所以pre(np)=q。
② 若len§+1≠len(q) 不是q的每个串都可以由p中一个串加入c后得到。能得到的是那些len<=len§+1的串。此时把q拆成q和nq两个节点,使nq节点满足①代替掉原来的q,pre(q)=nq再调整原先的ch关系即完成插入。
性质
1、在SAM中节点数不超过2n−2,边数不超过3n−3
2、从一节点开始跳pre,Right集合变大,字符串长度变短
3、一节点表示的字符串是其Parent树上子孙表示字符串的后缀
4、节点x表示字符串长度的连续区间是[len(fa(x))+1,len(x)]
5、两个串的最长公共后缀,位于这两个串对应状态在Parent树上的最近公共祖先状态
子串的出现次数
节点x中字符串出现的次数是以x为根的子树中字符串出现次数之和。
np产生的节点出现次数为1,nq产生的节点出现次数为0。根据len计数排序+拓扑就可以了。
-
一份大概是抄来的狭义SAM板子…
const int N = /*number*/;
struct node {
int fa, mx,ch[26];
} t[N << 1];
void init() {
cnt = last = 1;
memset(a, 0, sizeof a);
}
void insert(int x) {
int p = last, np = ++cnt;
t[np].mx = t[p].mx + 1;
for(; p && !t[p].ch[x]; p = t[p].fa)
t[p].ch[x] = np;
if(!p) {
t[np].fa = 1;
} else {
int q = t[p].ch[x];
if(t[q].mx == t[p].mx + 1) {
t[np].fa = q;
} else {
int nq = ++cnt;
t[nq] = t[q];
t[nq].mx = t[p].mx + 1;
t[np].fa = t[q].fa = nq;
for(; p && t[p].ch[x] == q; p = t[p].fa)
t[p].ch[x] = nq;
}
}
last = np;
}
- 一份带注释的板子
const int N = /*number*/;
struct node {
int fa, mx, ch[26]; //fa:父亲, mx:最长的长度, ch:转移数组
} a[N << 1];
void init() { //清空数组有啥好说的...
cnt = last = 1;
memset(a, 0, sizeof a);
}
void insert(int x) {
int p = last, np = ++cnt; //新加入一个状态..
a[np].mx = a[p].mx + 1;
for(; p && !a[p].ch[x]; p = a[p].fa)
a[p].ch[x] = np; //当前继节点的x儿子没有时 直接转移到np
if(!p) {
a[np].fa = 1;
} else { //前继节点的x儿子有了
int q = a[p].ch[x];
if(a[q].mx == a[p].mx + 1) { //发现a[q].mx == a[p].mx + 1就可以直接连后缀链接了..
a[np].fa = q;
} else { //发现并不满足..我们就要构造一下了..
int nq = ++cnt;
a[nq] = a[q];
a[nq].mx = a[p].mx + 1;
a[np].fa = a[q].fa = nq;
for(; p && a[p].ch[x] == q; p = a[p].fa)
a[p].ch[x] = nq;
}
}
last = np;
}
- 广义后缀自动机
并不会!听说是很多字符串建在一个SAM里面…
字典树
字典树板子真的很简单…
字典树好像也是解字符串题的利器?…就是有时想不到啊…其实很多问题用字典树比较容易解决…(这么优美的字典树你值得拥有!而且好学!博主学这个的时候压根没看过代码只听过思想就可以写出代码并解决一道模版题qwq
有啥好讲的???
const int N = /**/
int cnt = 0, ch[N][2];
void insert(int x) {
int fa = 0;
for(int i = LOG; i >= 0; --i) {
int pos = (x >> i) & 1;
if(!ch[fa][pos]) {
ch[fa][pos] = ++cnt;
memset(ch[cnt], 0, sizeof ch[cnt]);
}
fa = ch[fa][pos];
}
}
AC自动机
-
AC自动机与kmp不同…kmp是最长的前后缀…而AC自动机仅仅是前后缀相同…qwq
-
AC自动机是在一棵trie树上进行建fail的!
- 所以说插入什么的都和字典树一模一样!
- 建fail的过程我们用广搜解决!
int size = 1;
int ch[M][30], vis[M], fail[M], cnt=0;
void insert(int y) {
int len = strlen(lx), fa = 0;
for(int i = 0; i < len; ++i){
int x = lx[i] - 'a' + 1;
if(!ch[fa][x]) {
ch[fa][x] = ++size;
memset(ch[size], 0, sizeof ch[size]);
}
fa = ch[fa][x];
}
vis[fa] |= (1 << y);
}
void build() {
queue<int> q;
int fa = 0;
for(int i = 1;i <= 26; ++i){
if(ch[fa][i]) q.push(ch[fa][i]);
}
while(!q.empty()) {
int h=q.front(); q.pop();
for(int i = 1;i <= 26; ++i){
int f = fail[h];
vis[h] |= vis[f];
if(!ch[h][i]) ch[h][i] = ch[f][i];
else {
while(f && !ch[f][i]) f = fail[f];
q.push(ch[h][i]);
fail[ch[h][i]] = ch[f][i];
}
}
}
}
Manachar
-
马拉车
-
一种看似是暴力的优美算法qwq…
-
先来个变量声明…
- l e n [ x ] len[x] len[x]表示以 x x x为中心的最长回文串
- m x mx mx表示前 i i i个使得 i + l e n [ i ] i + len[i] i+len[i]最大的那个 i i i
-
又由于回文串长度为单数和双数的时候中间节点处理比较麻烦
- 那么我们就在每两个字符之间插入一个相同的字符就可以轻松解决这个问题了
-
根据回文串的对称性…
- m i n ( l e n [ m x ∗ 2 − i ] , m x + l e n [ m x ] − i ) ≤ l e n [ i ] min(len[mx * 2 -i],mx + len[mx] - i)\leq len[i] min(len[mx∗2−i],mx+len[mx]−i)≤len[i]
- 然后你就暴力匹配。。复杂度却是 O ( n ) O(n) O(n)的!
-
肥肠显然一个长度为 n n n的字符串中…最多只有 n n n个本质不同的字符串…(什么!证明!
-
证就证…没在怕的s s s的前 i i i个字符已经放好…接下来放第 i + 1 i + 1 i+1个字符…假设所形成的最长回文后缀的长度为 T T T
那么新增的本质不同的字符串值可能是 T T T这个字符串…如果有别的…那么根据回文串对称的性质…一定已经出现过了(如图所示…qwq)
-
这个性质也就保证了这个算法的复杂度为 O ( n ) O(n) O(n)
-
-
Code~
-
char s[N], ss[N]; int len[N]; void Manachar(char *s, int n) { for(int i = 0; i <= n + 1; ++i) len[i] = 0; int mx = 1; for(int i = 1; i <= n; ++i) { len[i] = max(1, min(mx + len[mx] - i, len[mx * 2 - i])); for(; s[i - len[i]] == s[i + len[i]]; ++len[i]); if(i + len[i] > mx + len[mx]) mx = i; } } int solve(char *s, int n) { ss[0] = '#', ss[1] = '*'; for(int i = 1; i <= n; ++i) { ss[i << 1] = s[i]; ss[i << 1 | 1] = '*'; } ss[n * 2 + 2] = '$'; Manachar(ss, n * 2 + 1); int ans = 0; for(int i = 1; i <= n * 2 + 1; ++i) { printf("%d %d\n", i, len[i] - 1); ans = max(ans, len[i] - 1); } return ans; }
-
在翻一个大跟头之前,我决定一意孤行。
本文作者:Averyta