hdu 4641 K-string SAM的O(n^2)算法 以及 SAM+并查集优化
链接:http://acm.hdu.edu.cn/showproblem.php?pid=4641
题意:有一个长度为n(n < 5e4)的字符串,Q(Q<=2e5)次操作;操作分为:在末尾插入一个字符ch和查询不同子串出现次数不小于K的数量;
思路1:SAM在线求解;
对于每次找到将一个字符x插入到SAM之后,我们知道pre[p]所含有的Tx的后缀字符串数目为step[pre[np]]个,那么只需要每次插入之后更新下这些字符串出现的次数cnt即可;
由于Right(fa)与Right(r)没有交集(max(fa) = min(r) - 1),所以需要一直递推到root,但是root不能计算,因为root并没有表示后缀,只是一个init状态;
还有一点就是在拷贝q的信息到nq中时,主要把cnt的信息也拷贝过去;
由于数据较弱。。当出现5e4长度均为a的字符串,2e5次插入操作;这个算法复杂度将达到O(T*n*Q);
(因为每插入一个字符,都需要更新5e4次父节点,这时上面的flag优化没什么卵用。。)
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 6 #define maxn 100007 7 #define SIGMA_SIZE 26 8 9 struct SAM{ 10 int sz,tot,last,k; 11 int g[maxn<<1][SIGMA_SIZE],pre[maxn<<1],step[maxn<<1]; 12 int vs[maxn<<1],cnt[maxn<<1]; 13 14 void newNode(int s){ 15 step[++sz] = s; 16 pre[sz] = 0; 17 vs[sz] = cnt[sz] = 0; 18 memset(g[sz],0,sizeof(g[sz])); 19 } 20 21 void init(){ 22 tot = 0; 23 sz = 0; last = 1; 24 newNode(0); 25 } 26 27 int idx(char ch){return ch - 'a';} 28 29 void Insert(char ch){ 30 newNode(step[last]+1); 31 int v = idx(ch), p = last, np = sz; 32 33 while(p && !g[p][v]) 34 g[p][v] = np,p = pre[p]; //知道找到Right集合中包含x的边的祖宗节点 35 36 if(p){ 37 int q = g[p][v]; 38 if(step[q] == step[p] + 1) 39 pre[np] = q; 40 else{ 41 newNode(step[p]+1); 42 int nq = sz; //nq替换掉q节点 43 for(int i = 0;i < SIGMA_SIZE;i++) 44 g[nq][i] = g[q][i]; 45 46 cnt[nq] = cnt[q]; //** 47 pre[nq] = pre[q]; 48 pre[np] = pre[q] = nq; 49 50 while(p && g[p][v] == q) 51 g[p][v] = nq,p = pre[p]; 52 } 53 } 54 else pre[np] = 1; 55 for(int aux = np;aux != 1 && !vs[aux];aux = pre[aux]){ 56 if(++cnt[aux] >= k){ 57 tot += step[aux] - step[pre[aux]]; 58 vs[aux] = true; //该父节点的子串已经加到tot中 59 } 60 } 61 last = np; 62 } 63 }SA; 64 char str[maxn]; 65 int main() 66 { 67 int n,Q; 68 while(scanf("%d%d%d",&n,&Q,&SA.k) == 3){ 69 scanf("%s",str); 70 SA.init(); 71 int len = strlen(str); 72 for(int i = 0;i < len;i++){ 73 SA.Insert(str[i]); 74 } 75 int op; 76 char ch[2]; 77 while(Q--){ 78 scanf("%d",&op); 79 if(op & 1){ 80 scanf("%s",ch); 81 SA.Insert(ch[0]); 82 } 83 else printf("%d\n",SA.tot); 84 } 85 } 86 }
思路2:SAM离线+并查集优化
将操作全部插入到SAM并存储之后,先进行拓扑排序;
1.为什么要进行拓扑排序?
因为拓扑的目的是为了使得SAM分层,即之后可以使用后缀数组基数排序的思想得到每个节点状态的|Right|即包含的子节点个数;
思路1由于是在线算法,并不需要知道一个节点的所有子节点(在线+1);
2.并查集优化哪里? <=> 如何逆向删除末尾加入的字符?
删除字符其实就是在Insert时存储下来每个字符对应的节点id,之后用并查集Find(p)来得到每次删除时,实际上该节点已经转移到哪个祖先节点的思想;
并且删除有两次,一次是开始就小于K次,就一次删到大于K次,这时该节点由于一条路径被删了,更改之后看是否也小于K次,循环即可;
时间复杂度为O(T*(n+m))
代码参考:JeraKrs
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 6 #define maxn 250007 7 #define SIGMA_SIZE 26 8 int ans[maxn],op[maxn]; 9 char str[maxn]; 10 int N,Q,K; 11 12 struct SAM{ 13 int sz,last; 14 int g[maxn<<1][SIGMA_SIZE], pre[maxn<<1], step[maxn<<1]; 15 int cnt[maxn<<1], pos[maxn<<1], id[maxn<<1]; 16 int f[maxn<<1], sub[maxn<<1]; 17 18 int Find(int x){ return f[x] == x? x: f[x] = Find(f[x]); } 19 20 void init(){ 21 sz = 0;last = 1; 22 newNode(0); 23 } 24 25 void newNode(int s){ 26 pre[++sz] = 0; 27 step[sz] = s; 28 memset(g[sz],0,sizeof(g[sz])); 29 } 30 31 int idx(char ch){ return ch - 'a'; } 32 33 void Insert(char ch); 34 void topoSort(); 35 void getCnt(); 36 void solve(int Q,int *op,int K); 37 38 }SA; 39 40 void SAM::Insert(char ch){ 41 newNode(step[last] + 1); 42 int v = idx(ch), np = sz, p = last; 43 id[N] = np; 44 while(p && !g[p][v]){ 45 g[p][v] = np; 46 p = pre[p]; 47 } 48 49 if(p){ 50 int q = g[p][v]; 51 if(step[q] == step[p] + 1) 52 pre[np] = q; 53 else{ 54 newNode(step[p] + 1); 55 int nq = sz; 56 for(int i = 0;i < SIGMA_SIZE;i++) 57 g[nq][i] = g[q][i]; 58 59 pre[nq] = pre[q]; 60 pre[q] = pre[np] = nq; 61 62 while(p && g[p][v] == q) 63 g[p][v] = nq, p = pre[p]; 64 } 65 } 66 else pre[np] = 1; 67 last = np; 68 } 69 70 void SAM::topoSort(){ 71 for(int i = 0; i <= sz; i++) cnt[i] = 0; 72 for(int i = 1; i <= sz; i++) cnt[step[i]]++; 73 for(int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; 74 for(int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; 75 } 76 77 void SAM::getCnt(){ 78 for(int i = 0; i <= sz; i++) cnt[i] = 0; 79 for(int p = 1,i = 0; i < N;i++){ 80 int v = idx(str[i]); 81 p = g[p][v]; 82 cnt[p] = 1; //必须是后缀才能赋值root为0 83 } 84 85 for(int i = sz; i; i--){ 86 int p = pos[i]; 87 cnt[pre[p]] += cnt[p]; 88 } 89 } 90 91 void SAM::solve(int Q,int *op,int K){ 92 long long ret = 0; 93 for(int i = 1; i <= sz;i++){ 94 int p = pos[i]; 95 if(cnt[p] >= K) ret += step[p] - step[pre[p]]; 96 } 97 98 for(int i = 1;i <= sz;i++) f[i] = i, sub[i] = 0; 99 100 for(int i = Q; i; i--){ 101 if(op[i] == 2) ans[i] = ret; 102 else{ 103 int p = id[N--]; 104 int fp = Find(p); 105 while(fp && cnt[fp] < K){ 106 p = f[fp] = pre[fp]; //更新 107 fp = Find(p); //压缩 108 } 109 if(fp == 0) continue; 110 sub[fp]++; 111 while(fp && cnt[fp] - sub[fp] < K){ //由于单调性 cnt[fp] >= K 是一定成立的 112 ret -= step[fp] - step[pre[fp]]; 113 p = f[fp] = pre[fp]; 114 sub[pre[fp]] += sub[fp]; 115 fp = Find(p); 116 } 117 } 118 } 119 120 } 121 122 int main() 123 { 124 while(scanf("%d%d%d",&N,&Q,&K) == 3){ 125 scanf("%s",str); 126 SA.init(); 127 for(int i = 0; i < N; i++) 128 SA.Insert(str[i]); 129 char aux[2]; 130 for(int i = 1;i <= Q; i++){ 131 scanf("%d",op + i); 132 if(op[i] & 1){ 133 scanf("%s",aux); 134 str[N++] = aux[0]; 135 SA.Insert(aux[0]); 136 } 137 } 138 str[N] = '\0'; 139 SA.topoSort(); 140 SA.getCnt(); 141 SA.solve(Q,op,K); 142 143 for(int i = 1;i <= Q;i++) 144 if(op[i] == 2) printf("%d\n",ans[i]); 145 } 146 }