[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]....
那么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 }