咕咕咕
网上的优秀博文有很多…这篇主要是用来让博主尽快回忆起来并会写代码用的qwq
problems are welcome!
只有7秒记忆的博主好像又学会了kmp!走过路过不要错过!
还是吐槽一下…让你学会一个算法的一定不会是长篇大论的…(你一定会在某个时刻突然理解一个算法)
一句话…kmp实际上求的是前 i i i个字符中最长的前后缀匹配长度是多少
求出最长前后缀匹配值到底有什么用?
复杂度证明?
算法导论17:摊还分析学习笔记(KMP复杂度证明)-Elpsywk’s Blog
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;
}
}
}
后缀数组最详细讲解-victorique
后缀数组-处理字符串的有力工具
如此神奇的SA!它能在 O ( n l o g n ) O(nlog_n) O(nlogn)时间复杂度完成对一个字符串所有后缀的排序操作!(而且在有些时候解决一些问题确实是要比SAM等等要简单的!总之你值得拥有!)
把所有后缀排序排好有什么用?
变量声明
具体操作
使用基数排序优化
并不会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能够在 O ( n ) O(n) O(n)时间内构建出所有的后缀
我们设 r i g h t ( s ) right(s) right(s)表示 s s s这个字符串出现位置的集合
考虑增量法构造
加入第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自动机与kmp不同…kmp是最长的前后缀…而AC自动机仅仅是前后缀相同…qwq
AC自动机是在一棵trie树上进行建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];
}
}
}
}
马拉车
一种看似是暴力的优美算法qwq…
先来个变量声明…
又由于回文串长度为单数和双数的时候中间节点处理比较麻烦
根据回文串的对称性…
肥肠显然一个长度为 n n n的字符串中…最多只有 n n 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
博客地址:https://blog.csdn.net/AngryVegetable