1. 程式人生 > >[CTSC2012]熟悉的文章 字尾自動機

[CTSC2012]熟悉的文章 字尾自動機

題面:洛谷

題解:

  觀察到L是可二分的,因此我們二分L,然後就只需要想辦法判斷這個L是否可行即可。

  因為要儘量使L可行,因此我們需要求出對於給定L,這個串最多能匹配上多少字元。

  如果我們可以對每個位置i求出g[i]表示以這個位置為結尾,向前最多匹配多少位,就可以快速得知任意區間[l, r]是否可以被匹配上,因為一個串如果可以被匹配上,那麼它的子串肯定也可以被匹配上。

  然後我們再做一次DP,設f[i]為DP到i位,最多能匹配上多少字元

  那麼樸素做法就是列舉上一段的結尾,然後更新,不過注意到這個決策是單調的,因此可以用單調佇列優化一下。

  因為有g[i]和mid的限制,所以我們可以選取的上一段結尾的區間是[i - g[i], i - mid].

  所以在用單調佇列維護的時候,為了保證右端點合法,每次不能立即把當前的i加入佇列,而是要在下次列舉到區間右端點已經包括了i的時候再把i加入佇列。(類似與NOIP普及組跳房子)

  

  這樣就可以O(n)的求解。

  不過首先還需要求出g[i]....

  

  我們先建立廣義字尾自動機,然後在自動機上匹配大串,假設當前匹配到的節點是now,上一次的長度是rnt。

  那麼我們只有沿著parent樹向上走,才可以保證l[當前節點]是合法的。

  因此我們不斷向上走,每走到一個節點,都更新rnt = l[now], 直到走到一個節點使得它有當前字元對應的邊,這個時候我們把rnt更新為rnt+1,並更新g陣列

 

  1 #include<bits/stdc++.h>
  2 using namespace std;
  3 #define R register int
  4 #define AC 1100100
  5 #define ac 2300000
  6 
  7 int n, m, tail, head, len;
  8 int g[AC], f[AC], q[AC];//g[i]表示以i為結尾,最長能匹配的長度
  9 char s[AC];//f[i]表示DP到i位,能被覆蓋的最大長度
 10 
 11 inline int read()
12 { 13 int x = 0;char c = getchar(); 14 while(c > '9' || c < '0') c = getchar(); 15 while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar(); 16 return x; 17 } 18 19 inline void upmax(int &a, int b) 20 { 21 if(b > a) a = b; 22 } 23 24 struct sam_auto{ 25 int ch[ac][26], l[ac], fa[ac], last, cnt; 26 27 void add(int c) 28 { 29 int p = last, np = ++ cnt; 30 last = cnt, l[np] = l[p] + 1; 31 for( ; !ch[p][c]; p = fa[p]) ch[p][c] = np; 32 if(!p) fa[np] = 1; 33 else 34 { 35 int q = ch[p][c];//找到對應節點 36 if(l[p] + 1 == l[q]) fa[np] = q; 37 else 38 { 39 int nq = ++ cnt; 40 l[nq] = l[p] + 1; 41 memcpy(ch[nq], ch[q], sizeof(ch[q])); 42 fa[nq] = fa[q]; 43 fa[q] = fa[np] = nq; 44 for( ; ch[p][c] == q; p = fa[p]) ch[p][c] = nq; 45 } 46 } 47 } 48 49 void build() 50 { 51 cnt = 1; 52 for(R i = 1; i <= m; i ++) 53 { 54 last = 1, scanf("%s", s + 1), len = strlen(s + 1); 55 for(R j = 1; j <= len; j ++) add(s[j] - '0'); 56 } 57 } 58 59 void get_g()//求g陣列 60 { 61 int now = 1, rnt = 0; 62 /*for(R i = 1; i <= len; i ++) 63 {//要先更新g再向下跳,因為根據parent樹的性質,只有從一個點向上走才能保證 64 int v = s[i] - '0';//這個點中出現的串在遇到的點中都出現了,如果先向下走了就無法保證了 65 while(now != 1 && !ch[now][v]) now = fa[now];//一直向上跳到可以匹配為止 66 if(ch[now][v]) g[i] = l[now] + 1, now = ch[now][v]; 67 }*/ 68 for(R i = 1; i <= len; i ++) 69 { 70 int v = s[i] - '0';//因為當前點是上一個點往下走走到的, 所以當前點的l[now]其實不一定合法。。。只能保證l[fa[now]]合法 71 while(now != 1 && !ch[now][v]) now = fa[now], rnt = l[now];//因此如果這個點是有v這個節點的話,也不能直接取l[now], 72 if(ch[now][v]) g[i] = ++rnt, now = ch[now][v];//而是要保留上次的匹配長度 73 else g[i] = 0, rnt = 0; 74 //printf("%d ", g[i]); 75 } 76 //printf("\n"); 77 } 78 }sam; 79 80 void pre() 81 { 82 n = read(), m = read(); 83 sam.build(); 84 } 85 86 bool check(int mid)//上一段結尾區間[i - g[i], i - mid] 87 { 88 int last = 0; 89 head = tail = 0; 90 for(R i = 1; i <= len; i ++) 91 { 92 f[i] = f[i - 1];//因為如果強制取區間內的值,就相當於強制當這個點必須能產生貢獻就產生貢獻,但是實際上不產生貢獻,直接取f[i -1]可能會更優 93 if(i - g[i] > i - mid) continue;//如果沒有合法區間就只能這樣了 94 while(last < i - mid)//每次加入不能超過右區間以保證合法 95 { 96 ++ last; 97 while(head <= tail && f[q[tail]] - q[tail] < f[last] - last) -- tail; 98 q[++ tail] = last; 99 } 100 while(head <= tail && q[head] < i - g[i]) ++ head;//把不屬於合法區間的都去掉 101 //printf("%d ", q[head]);//去掉非法區間的應該放在後面,因為後面還有加點操作,可能會加入非法節點 102 upmax(f[i], f[q[head]] + i - q[head]); 103 } 104 //printf("\n"); 105 return f[len] * 10 >= len * 9; 106 } 107 108 int half()//l顯然滿足可二分性 109 { 110 int l = 0, r = len, mid; 111 while(l < r) 112 { 113 mid = (l + r + 1) >> 1; 114 if(check(mid)) l = mid; 115 else r = mid - 1; 116 } 117 return l; 118 } 119 120 void work() 121 { 122 for(R i = 1; i <= n; i ++) 123 { 124 scanf("%s", s + 1), len = strlen(s + 1); 125 sam.get_g(), printf("%d\n", half()); 126 //for(R j = 1; j <= n; j ++) g[j] = f[j] = 0; 127 } 128 } 129 130 int main() 131 { 132 // freopen("in.in", "r", stdin); 133 pre(); 134 work(); 135 // fclose(stdin); 136 return 0; 137 }
View Code

 

 

 

  那麼g[i]怎麼求呢?