日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

后缀自动机(SAM)构造实现过程演示+习题集锦

發布時間:2023/12/3 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 后缀自动机(SAM)构造实现过程演示+习题集锦 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • 后綴自動機
  • 算法實現過程
  • 模板
  • 習題
    • 洛谷后綴自動機模板題
    • 品酒大會
    • [HEOI2015]最短不公共子串
    • 字符串

蒟蒻寫這篇blogblogblog主要是存一下,后綴自動機的詳細搭建過程,方便以后復習
具體的某些證明,為什么這么做,正確性劈里啪啦一大堆就不贅述了講解指路?

后綴自動機

后綴自動機上每一條到iii的路徑對應一個子串,整個自動機包含了字符串的所有子串

很多時候可以和后綴數組等價使用


endposendposendpos:一個子串iii在整個字符串中出現的位置 最后一個字符的下標 構成的集合
舉個栗子 abcbcdeabcabcbcdeabcabcbcdeabc,(從000開始標號)
子串abcabcabc對應的endposendposendpos{2,9}\{2,9\}{2,9},子串bcbcbcendposendposendpos{2,4,9}\{2,4,9\}{2,4,9}

后綴自動機的編號對應的就是endposendposendpos完全相同的所有子串
依舊是上面的粒子abcbcdeabcabcbcdeabcabcbcdeabc
子串bcbcbcendposendposendpos{2,4,9}\{2,4,9\}{2,4,9},子串cccendposendposendpos也為{2,4,9}\{2,4,9\}{2,4,9}
那么后綴自動機上對應的ididid編號既表示bcbcbc子串,也表示ccc子串

算法實現過程


  • e.g.1e.g.1e.g.1,構建abcdabcdabcd的后綴自動機
    Ⅰ最初始狀態,僅有一個空根,last=1last=1last=1lastlastlast表示后綴自動機的最后一個節點

    Ⅱ 將′a′'a'a扔進去,新建一個節點cnt=2cnt=2cnt=2len=len[last]+1=1len=len[last]+1=1len=len[last]+1=1
    lastlastlast開始跳,發現111沒有′a′'a'a
    則建立一條′a′'a'a邊,并指向新點222
    此時跳到了初始源點,222的后綴鏈接只能指向111lastlastlast變為222

    Ⅲ 將′b′'b'b扔進去,新建一個節點cnt=3,len=len[last]+1=2cnt=3,len=len[last]+1=2cnt=3,len=len[last]+1=2
    lastlastlast開始跳后綴鏈接
    222沒有′b′'b'b邊,新建一條并指向333,跳后綴鏈接到111
    111沒有′b′'b'b邊,新建一條并指向333
    此時已經到了根節點,333的后綴鏈接只能指向111last=3last=3last=3

    Ⅳ 將′c′'c'c扔進去,新建一個節點cnt=4,len=3cnt=4,len=3cnt=4,len=3
    lastlastlast開始跳后綴鏈接
    333沒有′c′'c'c邊,新建一條并指向444,跳后綴鏈接到111
    111沒有′c′'c'c邊,新建一條并指向444
    此時已經到了根節點,444的后綴鏈接只能指向111last=4last=4last=4
    Ⅴ 將′d′'d'd扔進去,新建一個節點cnt=5,len=4cnt=5,len=4cnt=5,len=4
    lastlastlast開始跳后綴鏈接
    444沒有′c′'c'c邊,新建一條并指向555,跳后綴鏈接到111
    111沒有′c′'c'c邊,新建一條并指向555
    此時已經到了根節點,555的后綴鏈接只能指向111last=5last=5last=5

    最簡單的一種后綴自動機就完成了
    接下來就嘗試一下進階版

  • e.g.2e.g.2e.g.2,構建ababeababeababe的后綴自動機
    Ⅰ先搭建空源點,last=1last=1last=1

    Ⅱ 加入′a′'a'a,新建一個節點cnt=2,len[2]=len[last]+1=1cnt=2,len[2]=len[last]+1=1cnt=2,len[2]=len[last]+1=1
    111沒有′c′'c'c邊,新建一條并指向222
    此時已經到了根節點,222的后綴鏈接只能指向111last=2last=2last=2

    Ⅲ 加入′b′'b'b,新建一個節點cnt=3,len[3]=len[last]+1=2cnt=3,len[3]=len[last]+1=2cnt=3,len[3]=len[last]+1=2
    lastlastlast開始跳后綴鏈接
    222沒有′b′'b'b邊,新建一條并指向333,跳后綴鏈接到111
    111沒有′b′'b'b邊,新建一條并指向333
    此時已經到了根節點,333的后綴鏈接只能指向111last=3last=3last=3

    Ⅳ 再加入′a′'a'a,新建一個節點cnt=4,len[4]=len[last]+1=3cnt=4,len[4]=len[last]+1=3cnt=4,len[4]=len[last]+1=3
    lastlastlast開始跳后綴鏈接
    333沒有′a′'a'a邊,新建一條并指向444,跳后綴鏈接到111
    111有一條指向222′a′'a'a邊,滿足len[2]=len[1]+1len[2]=len[1]+1len[2]=len[1]+1,則直接將444后綴鏈接指向222
    結束,last=4last=4last=4

    Ⅴ 再加入′b′'b'b,新建一個節點cnt=5,len[5]=len[last]+1=4cnt=5,len[5]=len[last]+1=4cnt=5,len[5]=len[last]+1=4
    lastlastlast開始跳后綴鏈接
    444沒有′b′'b'b邊,新建一條并指向555,跳后綴鏈接到222
    222有一條指向333′b′'b'b邊,滿足len[3]=len[2]+1len[3]=len[2]+1len[3]=len[2]+1,直接將555后綴鏈接指向333
    結束,last=5last=5last=5

    Ⅵ 加入新′c′'c'c,新建一個節點cnt=6,len[6]=len[last]+1=5cnt=6,len[6]=len[last]+1=5cnt=6,len[6]=len[last]+1=5
    lastlastlast開始跳后綴鏈接
    555沒有′c′'c'c邊,新建一條并指向666,跳后綴鏈接到333
    333沒有′c′'c'c邊,新建一條并指向666,跳后綴鏈接到111
    111沒有′c′'c'c邊,新建一條并指向666
    此時已到根節點,666只能鏈接111last=6last=6last=6結束
    這就是進階版了,沒有涉及到最終版的點復制
    最后讓我們一起攜手走進最終版的后綴自動機構造

  • e.g.3e.g.3e.g.3,構建cababcababcabab的后綴自動機
    Ⅰ 創造新源點,last=1,cnt=1last=1,cnt=1last=1,cnt=1

    Ⅱ 加入′c′'c'c,新建一個節點cnt=2,len[2]=len[last]+1=1cnt=2,len[2]=len[last]+1=1cnt=2,len[2]=len[last]+1=1
    lastlastlast開始跳后綴鏈接
    111沒有′c′'c'c邊,新建一條并指向222
    此時已到根節點,222只能鏈接1,last=21,last=21,last=2

    Ⅲ 加入′a′'a'a,新建一個節點cnt=3,len[3]=len[last]+1=2cnt=3,len[3]=len[last]+1=2cnt=3,len[3]=len[last]+1=2
    lastlastlast開始跳后綴鏈接
    222沒有′a′'a'a邊,新建一條并指向333,跳后綴鏈接到111
    111沒有′a′'a'a邊,新建一條并指向333
    此時已到根節點,333只能鏈接1,last=31,last=31,last=3

    Ⅳ 加入′b′'b'b,新建一個節點cnt=4,len[4]=len[last]+1=3cnt=4,len[4]=len[last]+1=3cnt=4,len[4]=len[last]+1=3
    lastlastlast開始跳后綴鏈接
    333沒有′b′'b'b邊,新建一條并指向444,跳后綴鏈接到111
    111沒有′a′'a'a邊,新建一條并指向444
    此時已到根節點,444只能鏈接1,last=41,last=41,last=4

    Ⅴ 加入′a′'a'a,新建一個節點cnt=5,len[5]=len[last]+1=4cnt=5,len[5]=len[last]+1=4cnt=5,len[5]=len[last]+1=4
    lastlastlast開始跳后綴鏈接
    444沒有′a′'a'a邊,新建一條并指向555,跳后綴鏈接到111
    111′a′'a'a邊,指向333,但是!!!len[3]≠len[1]+1len[3]≠len[1]+1len[3]?=len[1]+1,不能像進階版直接鏈接,這里必須要點復制
    新建一個333的分身節點cnt=6cnt=6cnt=6
    333的所有信息(出入邊)除了原字符串間的邊(圖中黑色邊)全部修改為分點666的邊,直接覆蓋
    并且666成為333的直接后綴鏈接,替代111
    len[6]=len[1]+1=1len[6]=len[1]+1=1len[6]=len[1]+1=1
    相當于666做了1,31,31,3后綴鏈之間的承接點,保證了每一條邊上lenlenlen只會帶來+1+1+1的影響
    555直接鏈接666后結束,last=5last=5last=5

    Ⅵ 加入′b′'b'b,新建節點cnt=7cnt=7cnt=7
    lastlastlast開始跳后綴鏈接
    555沒有′b′'b'b邊,新建一條指向777,跳后綴鏈接到666
    666有一條′b′'b'b邊,指向444,判斷len[4]≠len[6]+1len[4]≠len[6]+1len[4]?=len[6]+1

    再次執行復制操作
    新建一個444的分身節點cnt=8cnt=8cnt=8
    444的所有信息(出入邊)除了原字符串間的邊(圖中黑色邊)全部修改為分點888的邊,直接進行覆蓋
    888成為444的直接后綴鏈接,len[8]=len[6]+1=2len[8]=len[6]+1=2len[8]=len[6]+1=2
    777直接鏈接888后結束,last=7last=7last=7

?
len[x]len[x]len[x]復制點的lenlenlen不等于被復制點的原后綴鏈接的len+1len+1len+1,而是誰觸發的len+1len+1len+1


模板

struct node {int len; //長度int fa; //后綴鏈接int son[maxc]; //字符集大小 }t[maxn];

模擬從主鏈的前一個開始跳后綴鏈接,并對于鏈接上的沒有該字符邊的每一個點都連出一條新字符邊

while( pre && ! t[pre].son[c] ) t[pre].son[c] = now, pre = t[pre].fa;

跳到根,代表這是首個出現的字符,他只能鏈接最初的根節點了

if( ! pre ) t[now].fa = 1;

否則,如果路上找到了,滿足lenlenlen的關系,直接后綴鏈接指過去即可

int u = t[pre].son[c]; if( t[u].len == t[pre].len + 1 ) t[now].fa = u;

復制該點,并進行有關該點的所有信息重改
①原點連出的點,新點也要連出
②連入原點的點,變成連入新點
③原點和新點間也需建立聯系,新點是原點的后綴鏈接

else {int v = ++ tot;t[v] = t[u];//利用結構體巧妙將原點連出的點進行復制t[v].len = t[pre].len + 1;//由誰觸發 len就是觸發點len+1t[u].fa = t[now].fa = v;//原點與復制點與新建點的關系while( pre && t[pre].son[c] == u ) t[pre].son[c] = v, pre = t[pre].fa;//暴力復制修改連入原點的點 }

習題

洛谷后綴自動機模板題

  • code
#include <cstdio> #include <vector> #include <cstring> using namespace std; #define maxn 2000005 vector < int > G[maxn]; struct node {int fa, len;int son[30]; }t[maxn]; char s[maxn]; int last = 1, tot = 1; long long ans; int siz[maxn];void insert( int c ) {int pre = last, now = last = ++ tot;siz[tot] = 1;t[now].len = t[pre].len + 1;while( pre && ! t[pre].son[c] ) t[pre].son[c] = now, pre = t[pre].fa;if( ! pre ) t[now].fa = 1;else {int u = t[pre].son[c];if( t[u].len == t[pre].len + 1 ) t[now].fa = u;else {int v = ++ tot;t[v] = t[u];t[v].len = t[pre].len + 1;t[u].fa = t[now].fa = v;while( pre && t[pre].son[c] == u ) t[pre].son[c] = v, pre = t[pre].fa;}} }void dfs( int u ) {for( int i = 0;i < G[u].size();i ++ ) {int v = G[u][i];dfs( v );siz[u] += siz[v];}if( siz[u] != 1 ) ans = max( ans, 1ll * siz[u] * t[u].len ); }int main() {scanf( "%s", s );int len = strlen( s );for( int i = 0;i < len;i ++ ) insert( s[i] - 'a' ); for( int i = 2;i <= tot;i ++ ) G[t[i].fa].push_back( i );dfs( 1 );printf( "%lld", ans );return 0; }

品酒大會

  • solution
    有一個SAMSAMSAM常用結論:前綴i,ji,ji,j最長公共后綴=parenttree=parent\ tree=parent?tree上前綴i,ji,ji,j分別指向的點u,vu,vu,vlcalcalca反映在后綴自動機上的節點代表的最長子串
    將本題的字符串倒過來建后綴自動機,在自動機上進行樹上dpdpdp,最后從后往前進行更新即可
  • code
#include <cstdio> #include <vector> #include <cstring> #include <iostream> using namespace std; #define inf 0x7f7f7f7f #define int long long #define maxn 600005 struct node {int len, fa;int son[30]; }t[maxn]; vector < int > G[maxn]; int last = 1, cnt = 1, n; char s[maxn]; int a[maxn], f[maxn], tot[maxn], siz[maxn], maxx[maxn], minn[maxn];void insert( int x, int w ) {int pre = last, now = last = ++ cnt;siz[now] = 1, t[now].len = t[pre].len + 1;maxx[now] = minn[now] = w;while( pre && ! t[pre].son[x] ) t[pre].son[x] = now, pre = t[pre].fa;if( ! pre ) t[now].fa = 1;else {int u = t[pre].son[x];if( t[u].len == t[pre].len + 1 ) t[now].fa = u;else {int v = ++ cnt;maxx[v] = -inf, minn[v] = inf;t[v] = t[u];t[v].len = t[pre].len + 1;t[u].fa = t[now].fa = v;while( pre && t[pre].son[x] == u ) t[pre].son[x] = v, pre = t[pre].fa;}} }bool check( int u ) {return maxx[u] != -inf && minn[u] != inf; }void dfs( int u ) {for( int i = 0;i < G[u].size();i ++ ) {int v = G[u][i];dfs( v );tot[t[u].len] += siz[u] * siz[v];siz[u] += siz[v];if( check( u ) )f[t[u].len] = max( f[t[u].len], max( maxx[u] * maxx[v], minn[u] * minn[v] ) );maxx[u] = max( maxx[u], maxx[v] );minn[u] = min( minn[u], minn[v] );} }signed main() {memset( f, -0x7f, sizeof( f ) );scanf( "%d %s", &n, s + 1 );for( int i = 1;i <= n;i ++ )scanf( "%lld", &a[i] );for( int i = n;i;i -- ) insert( s[i] - 'a', a[i] ); for( int i = 2;i <= cnt;i ++ ) G[t[i].fa].push_back( i );dfs( 1 );for( int i = n - 1;~ i;i -- ) tot[i] += tot[i + 1], f[i] = max( f[i], f[i + 1] );for( int i = 0;i < n;i ++ )printf( "%lld %lld\n", tot[i], ( tot[i] ? f[i] : 0 ) );return 0; }

[HEOI2015]最短不公共子串

  • solution
    做此題需要了解序列自動機
    然后就是很無腦的四個bfsbfsbfs
    子串就是跑后綴自動機
    子序列就是跑序列自動機
  • code
#include <queue> #include <cstdio> #include <cstring> using namespace std; #define maxn 5000 char a[maxn], b[maxn];struct SAM {struct node {int len, fa;int son[30];}t[maxn];int last, cnt;SAM() {last = cnt = 1;}void insert( int x ) {int pre = last, now = last = ++ cnt;t[now].len = t[pre].len + 1;while( pre && ! t[pre].son[x] ) t[pre].son[x] = now, pre = t[pre].fa;if( ! pre ) t[now].fa = 1;else {int u = t[pre].son[x];if( t[u].len == t[pre].len + 1 ) t[now].fa = u;else {int v = ++ cnt;t[v] = t[u];t[v].len = t[pre].len + 1;t[u].fa = t[now].fa = v;while( pre && t[pre].son[x] == u ) t[pre].son[x] = v, pre = t[pre].fa;}}}}SamA, SamB;struct SEQ {int nxt[maxn][30], last[30];SEQ() {memset( nxt, 0, sizeof( nxt ) );memset( last, 0, sizeof( last ) );}void build( int n, char *s ) {for( int i = n;~ i;i -- ) {for( int j = 0;j < 26;j ++ )if( last[j] ) nxt[i + 1][j] = last[j];if( i ) last[s[i] - 'a'] = i + 1;}}}SeqA, SeqB;struct node {int x, y, dep;node(){}node( int X, int Y, int Dep ) {x = X, y = Y, dep = Dep;} }; queue < node > q; bool vis[maxn][maxn];void init() {memset( vis, 0, sizeof( vis ) );while( ! q.empty() ) q.pop();vis[1][1] = 1, q.push( node( 1, 1, 0 ) ); }int bfs1() {init();while( ! q.empty() ) {node now = q.front(); q.pop();for( int i = 0;i < 26;i ++ ) {int sonA = SamA.t[now.x].son[i];int sonB = SamB.t[now.y].son[i];if( vis[sonA][sonB] ) continue;else if( sonA && ! sonB ) return now.dep + 1;else if( sonA && sonB ) {vis[sonA][sonB] = 1;q.push( node( sonA, sonB, now.dep + 1 ) );}}}return -1; }int bfs2() {init();while( ! q.empty() ) {node now = q.front(); q.pop();for( int i = 0;i < 26;i ++ ) {int sonA = SamA.t[now.x].son[i];int sonB = SeqB.nxt[now.y][i];if( vis[sonA][sonB] ) continue;else if( sonA && ! sonB ) return now.dep + 1;else if( sonA && sonB ) {vis[sonA][sonB] = 1;q.push( node( sonA, sonB, now.dep + 1 ) );}}}return -1; }int bfs3() {init();while( ! q.empty() ) {node now = q.front(); q.pop();for( int i = 0;i < 26;i ++ ) {int sonA = SeqA.nxt[now.x][i];int sonB = SamB.t[now.y].son[i];if( vis[sonA][sonB] ) continue;else if( sonA && ! sonB ) return now.dep + 1;else if( sonA && sonB ) {vis[sonA][sonB] = 1;q.push( node( sonA, sonB, now.dep + 1 ) );}}}return -1; }int bfs4() {init();while( ! q.empty() ) {node now = q.front(); q.pop();for( int i = 0;i < 26;i ++ ) {int sonA = SeqA.nxt[now.x][i];int sonB = SeqB.nxt[now.y][i];if( vis[sonA][sonB] ) continue;else if( sonA && ! sonB ) return now.dep + 1;else if( sonA && sonB ) {vis[sonA][sonB] = 1;q.push( node( sonA, sonB, now.dep + 1 ) );}}}return -1; }int main() {scanf( "%s %s", a + 1, b + 1 );int lena = strlen( a + 1 ), lenb = strlen( b + 1 );for( int i = 1;i <= lena;i ++ )SamA.insert( a[i] - 'a' );for( int i = 1;i <= lenb;i ++ )SamB.insert( b[i] - 'a' );SeqA.build( lena, a );SeqB.build( lenb, b );printf( "%d\n%d\n%d\n%d\n", bfs1(), bfs2(), bfs3(), bfs4() );return 0; }

字符串

  • solution

這題運用的思想主要是廣義后綴自動機,即將多個字符串建在一個后綴自動機上
其實并沒有什么新穎之處,只需在擴展的時候帶一個這個字符屬于哪個字符串的編號即可

假設已經建好了自動機,接下來考慮兩個長度為kkk的子串之間如何一一對應修改
這個時候如果將其放到parenttreeparent\ treeparent?tree上考慮的話,就簡單了

其實可以猜想一下,剛開始我就想到了虛樹的性質,即相鄰兩兩配對
不難證明,的確應該相鄰兩個不同屬類的子串配對

前綴i,ji,ji,j最長公共后綴=parenttree=parent\ tree=parent?tree上前綴i,ji,ji,j分別指向的點u,vu,vu,vlcalcalca反映在后綴自動機上的節點代表的最長子串

也就是最后變成深搜一棵樹的模樣,記得特判可能lcalcalca代表的最長子串長度≥k\ge kk
此時是不需要代價的

  • code
#include <cstdio> #include <vector> #include <iostream> #include <algorithm> using namespace std; #define maxn 600005 struct node {int len, fa;int son[30]; }t[maxn]; vector < int > G[maxn]; int n, k, last = 1, cnt = 1; long long ans; char a[maxn], b[maxn]; int type[maxn]; int tot[maxn][3];void insert( int x, int s, int pos ) {int pre = last, now = last = ++ cnt;t[now].len = t[pre].len + 1;while( pre && ! t[pre].son[x] ) t[pre].son[x] = now, pre = t[pre].fa;if( ! pre ) t[now].fa = 1;else {int u = t[pre].son[x]; if( t[u].len == t[pre].len + 1 ) t[now].fa = u;else {int v = ++ cnt;t[v] = t[u];t[v].len = t[pre].len + 1;t[u].fa = t[now].fa = v;while( pre && t[pre].son[x] == u ) t[pre].son[x] = v, pre = t[pre].fa;}}if( pos >= k ) type[now] = s; }void dfs( int u ) {tot[u][type[u]] ++;for( int i = 0;i < G[u].size();i ++ ) {int v = G[u][i];dfs( v );tot[u][1] += tot[v][1];tot[u][2] += tot[v][2];}if( tot[u][1] >= tot[u][2] ) {int x = max( 0, k - t[u].len );ans += 1ll * x * tot[u][2];tot[u][1] -= tot[u][2];tot[u][2] = 0;}else {int x = max( 0, k - t[u].len );ans += 1ll * x * tot[u][1];tot[u][2] -= tot[u][1];tot[u][1] = 0;} }int main() {scanf( "%d %d %s %s", &n, &k, a + 1, b + 1 );reverse( a + 1, a + n + 1 );reverse( b + 1, b + n + 1 );for( int i = 1;i <= n;i ++ ) insert( a[i] - 'a', 1, i );for( int i = 1;i <= n;i ++ ) insert( b[i] - 'a', 2, i );for( int i = 2;i <= cnt;i ++ ) G[t[i].fa].push_back( i );dfs( 1 );printf( "%lld", ans );return 0; }

總結

以上是生活随笔為你收集整理的后缀自动机(SAM)构造实现过程演示+习题集锦的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。