1. 程式人生 > 其它 >字串雜題

字串雜題

J. Suffix Automaton

來源:The 2021 CCPC Guilin Onsite (Grand Prix of EDG)

2021CCPC 桂林站的題,用字尾陣列+平衡樹做的.

統計字典序問題可以用字尾陣列方便解決,即提取出 $\mathrm{sa, Height}$ 陣列.

對於本質不同的字串,排名為 $\mathrm{i}$ 的字尾所貢獻的字串長度區間為 $\mathrm{[L,R]}$.

將問題轉化為求長度為 $\mathrm{len}$ 的字串的第 $\mathrm{k}$ 個,可以離線用平衡樹維護當前處在長度為 $\mathrm{len}$ 的情況.

然後在平衡樹中二分找到對應位置即可.

#include <cstdio>
#include <set>
#include <map>
#include <cstring>
#include <algorithm>
#define ll long long 
#define pb push_back 
#define N  2000009 
#define setIO(s) freopen(s".in","r",stdin) 
using namespace std; 
char str[N];  
namespace SA {
    int n, ty, rk[N], sec[N], sa[N], t[N], h[N], mi[20][1000005], Lg[1000004], m2[20][1000005];       
    void getsa() {
        for(int i = 0; i <= ty; ++ i) t[i] = 0; 
        for(int i = 1; i <= n ; ++ i) t[rk[i] = str[i]] ++ ; 
        for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; 
        for(int i = 1; i <= n ; ++ i) sa[t[rk[i]] -- ] = i; 
        for(int k = 1; k <= n ; k <<= 1) {  
            int cnt = 0; 
            for(int i = n - k + 1; i <= n ; ++ i) 
                sec[++ cnt] = i; 
            for(int i = 1; i <= n ; ++ i)
                if(sa[i] > k) sec[++ cnt] = sa[i] - k;    
            for(int i = 0; i <= ty; ++ i) t[i] = 0; 
            for(int i = 1; i <= n ; ++ i) t[rk[i]] ++ ;   
            for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; 
            for(int i = n; i >= 1 ; -- i) {
                sa[t[rk[sec[i]]] -- ] = sec[i], sec[i] = 0; 
            }
            swap(rk, sec), rk[sa[1]] = cnt = 1; 
            for(int i = 2; i <= n ; ++ i) {
                rk[sa[i]] = (sec[sa[i]] == sec[sa[i - 1]] && sec[sa[i] + k] == sec[sa[i - 1] + k]) ? cnt : ++ cnt; 
            }  
            ty = cnt; 
            if(cnt == n) break; 
        }
        int det = 0;  
        for(int i = 1; i <= n ; ++ i) {
            // i and rk[i] - 1 的 lcp   
            if(det) -- det; 
            while(str[i + det] == str[sa[rk[i] - 1] + det]) ++ det;     
            h[rk[i]] = det; 
        }
    }
    void init() {
        n = strlen(str + 1), ty = 134; 
    }   
    void RMQ() {
        Lg[1] = 0; 
        for(int i = 2; i < 1000003; ++ i) 
            Lg[i] = Lg[i >> 1] + 1;  
        for(int i = 1; i <= n ; ++ i) 
            mi[0][i] = h[i], m2[0][i] = sa[i]; 
        for(int i = 1; (1 << i) <= n ; ++ i) {
            for(int j = 1; j + (1 << i) - 1 <= n ; ++ j) {
                mi[i][j] = min(mi[i - 1][j], mi[i - 1][j + (1 << (i - 1))]);    
                m2[i][j] = min(m2[i - 1][j], m2[i - 1][j + (1 << (i - 1))]);    
            }
        }
    }
    int query(int l, int r) {
        int o = Lg[r - l + 1];   
        return min(mi[o][l], mi[o][r - (1 << o) + 1]); 
    }
    int getans(int l, int r) {
        int o = Lg[r - l + 1];   
        return min(m2[o][l], m2[o][r - (1 << o) + 1]); 
    }
};   
namespace SGT {
    // 普通線段樹即可.   
    #define ls now << 1 
    #define rs now << 1 | 1
    int sum[N << 2];  
    void update(int l, int r, int now, int p, int v) {
        if(l == r) {
            sum[now] += v; 
            return ; 
        }
        int mid = (l + r) >> 1; 
        if(p <= mid)  update(l, mid, ls, p, v); 
        else update(mid + 1, r, rs, p, v); 
        sum[now] = sum[ls] + sum[rs]; 
    }
    int query(int l, int r, int now, int k) {
        if(l == r) return l; 
        int mid = (l + r) >> 1; 
        if(k <= sum[ls]) return query(l, mid, ls, k); 
        else return query(mid + 1, r, rs, k - sum[ls]);  
    }
    #undef ls 
    #undef rs 
}; 
ll arr[N], pre[N];   
int cnt, pl[N], pr[N], n; 
struct data {
    int id, len; 
    ll k;  
    data(int id = 0, int len = 0, ll k = 0):id(id), len(len), k(k){}  
}a[N];     
struct suff {
    int id, pos, d; 
    suff(int id = 0, int pos = 0, int d = 0):id(id), pos(pos), d(d){}            
}e[N];  
bool cmp1(data i, data j) {
    return i.len < j.len; 
}
bool cmp2(suff i, suff j) {
    return i.pos < j.pos;  
}     
int main() {
    // setIO("input"); 
    scanf("%s", str + 1);  
    n = strlen(str + 1);  
    SA::init(); 
    SA::getsa();   
    SA::RMQ();  
    for(int i = 1; i <= n; ++ i) {
        int l = n - SA::sa[i] + 1, st = SA::h[i] + 1;    
        arr[st] ++ , arr[l + 1] -- ;     
        // 貢獻是 [st, l]    
        e[i * 2 - 1] = suff(i, st, 1); 
        e[i * 2] = suff(i, l + 1, -1);   
    }    
    for(int i = 1; i <= n ; ++ i) {
        arr[i] += arr[i - 1];  
        pre[i] = pre[i - 1] + arr[i]; 
    }
    int Q; 
    scanf("%d", &Q); 
    for(int i = 1; i <= Q; ++ i) { 
        ll k; 
        scanf("%lld", &k);     
        // 直接不合法.      
        if(k > pre[n]) { 
            pl[i] = pr[i] = -1; 
        }
        else {
            int p = lower_bound(pre + 1, pre + 1 + n, k) - pre - 1;    
            k -= pre[p];       
            // 得到 (p + 1, k)      
            a[++ cnt] = data(i, p + 1, k);   
        }   
    }       
    sort(a + 1, a + 1 + cnt, cmp1); 
    sort(e + 1, e + 1 + 2 * n, cmp2); 
    for(int i = 1, j = 1; i <= cnt; ++ i) {
        while(j <= 2 * n && e[j].pos <= a[i].len) {
            SGT::update(1, n, 1, e[j].id, e[j].d);  
            ++ j; 
        }          
        int p = SGT::query(1, n, 1, (int)a[i].k);     
        int po = SA::sa[p];    
        int l = p + 1, r = n, ans = 0;    
        while(l <= r) {
            int mid = (l + r) >> 1; 
            if(SA::query(p + 1, mid) >= a[i].len) 
                ans = mid, l = mid + 1; 
            else r = mid - 1;  
        }        
        if(ans) {
            po = min(po, SA::getans(p + 1, ans)); 
        }    
        pl[a[i].id] = po;        
        pr[a[i].id] = po + a[i].len - 1; 
    }   
    for(int i = 1; i <= Q; ++ i) {
        printf("%d %d\n", pl[i], pr[i]); 
    }
    return 0; 
}

  

M. String Problem

來源:The 2021 ICPC Asia Shenyang Regional Contest

標籤:字串,字尾自動機

正式賽的時候用一個 $\mathrm{lcp}$ 亂搞切掉的,這裡給出正經做法.

遇到字典序問題,考慮利用字尾自動機來解.

靜態問題可以沿著字尾自動機的字元邊貪心走大的.

那麼不妨先維護出字首 $\mathrm{i}$ 的答案,然後考慮下一位的影響.

顯然,如果影響的話一定是下一位的字母被加入答案,那麼在後綴自動機中就是答案中深度最小的點改變.

改變成的新點一定是加入 $\mathrm{i+1}$ 字元時所影響的新點,而所有新點總和是 $\mathrm{O(n)}$ 的.

由於字尾自動機的構建方式,線上做是不現實的,不妨離線構建完字尾自動機然後記錄每個點最早出現位置.

最後開一個數組統計並離線下來即可.

#include <bits/stdc++.h>
#define ll long long 
#define pb push_back
#define N  2000009  
#define setIO(s) freopen(s".in","r",stdin) 
using namespace std;   
char str[N]; 
int n, last, tot, fir[N], len[N], st[N], ch[N][26], pre[N], dep[N], vis[N], tail, a[N];   
struct data {
    int p, c;  
    data(int p = 0, int c = 0):p(p), c(c){}  
}; 
vector<data>G[N]; 
void extend(int c, int pos) {      
    int np = ++ tot, p = last; 
    len[np] = len[p] + 1, last = np;   
    fir[np] = pos;   
    for(; p && !ch[p][c]; p = pre[p]) {
        ch[p][c] = np;                    
    }
    if(!p) pre[np] = 1; 
    else {
        int q = ch[p][c]; 
        if(len[q] == len[p] + 1) pre[np] = q; 
        else {
            int nq = ++ tot;  
            len[nq] = len[p] + 1; 
            fir[nq] = fir[q]; 
            memcpy(ch[nq], ch[q], sizeof(ch[q]));        
            pre[nq] = pre[q], pre[np] = pre[q] = nq; 
            for(; p && ch[p][c] == q; p = pre[p]) 
                ch[p][c] = nq;  
        }
    }
}    
int main() {
    // setIO("input"); 
    scanf("%s", str + 1); 
    n = strlen(str + 1);     
    last = tot = 1; 
    for(int i = 1; i <= n ; ++ i) {    
        extend(str[i] - 'a', i);
    }  
    for(int i = 1; i <= tot; ++ i) {   
        for(int j = 0; j < 26; ++ j) {
            if(ch[i][j]) {
                int q = ch[i][j]; 
                G[fir[q]].pb(data(i, j));    
            }
        }
    }      
    vis[1] = 1, a[++ tail] = 1, st[1] = -1;       
    for(int i = 1; i <= n ; ++ i) { 
        int mk = 0, de = 0;             
        for(int j = 0; j < G[i].size() ; ++ j) {
            data e = G[i][j];                  
            if(vis[e.p] && e.c > st[e.p]) {      
                if(mk == 0) {
                    mk = e.p, de = dep[e.p]; 
                }
                else if(dep[e.p] < de) {
                    mk = e.p, de = dep[e.p];   
                }
            }
        }     
        if(mk) {
            while(a[tail] != mk) 
                vis[a[tail]] = 0, -- tail;     
            st[mk] = str[i] - 'a';    
            a[++ tail] = ch[mk][str[i] - 'a'], dep[a[tail]] = tail, vis[a[tail]] = 1, st[a[tail]] = -1;   
        }             
        printf("%d %d\n", i - tail + 2, i); 
    } 
    return 0; 
}

  

Little Elephant and Strings

來源:CF204E

做法一:$\mathrm{SA}+$ 主席樹

可以把所有串拼在一起,然後中間用特殊分隔符分開.

考慮處理第 $\mathrm{i}$ 個串的 $\mathrm{l}$ 位置開始的子串.

顯然這個 $\mathrm{r}$ 具有單調性,所以可以二分最大的 $\mathrm{r}$.

這個東西用雙指標掃,然後每個區間用主席樹查一下編號種類即可.

時間複雜度為 $\mathrm{O(n \log n)}$.

做法二: $\mathrm{SAM}$

構建廣義字尾自動機,然後插入串的時候暴力更新 $\mathrm{pre}$ 的資訊.

最後查詢的時候把串放到自動機上跑一遍倍增求一下即可.

時間複雜度也是 $O(n \log n)$.

p.s. 暴力跳的方法可能不太優美.

這個問題等價於給定樹上若干個點,求這些點所構成的樹鏈的並.

可以將所有點離線,按照 $\mathrm{dfs}$ 序排序,然後在相鄰 $\mathrm{lca}$ 處減掉

做法三:$\mathrm{SA}+$單調佇列.

若固定左端點 $\mathrm{l}$, 則顯然要找到最大的 $\mathrm{r}$ 使得種類不小於 $K$.

所以可以用雙指標維護數量恰好為 $\mathrm{K}$ 的區間,並維護這個區間的 $\mathrm{lcp}$.

對於一個位置 $\mathrm{p}$ 來說, $\mathrm{p}$ 的貢獻就是包含 $\mathrm{p}$ 的 $\mathrm{lcp}$ 最大的.

這個東西用線段樹或單調佇列維護一下即可.

做法二:

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
#define N  200009 
#define ll long long 
#define pb push_back 
#define setIO(s) freopen(s".in","r",stdin) 
using namespace std; 
char str[N], tp[N]; 
int n, K, st[N], ed[N], pre[N << 1], len[N << 1], ch[N << 1][26]; 
int last, tot, vis[N], cn[N], fa[N][19]; 
void extend(int c) {
	if(ch[last][c]) {
		int p = last, q = ch[last][c]; 
		if(len[q] == len[p] + 1) last = q; 
		else {
			int nq = ++ tot; 
			len[nq] = len[p] + 1; 
			vis[nq] = vis[q], cn[nq] = cn[q]; 
			memcpy(ch[nq], ch[q], sizeof(ch[q])); 
			pre[nq] = pre[q], pre[q] = nq; 
			for(; p && ch[p][c] == q; p = pre[p]) 
				ch[p][c] = nq; 
			last = nq; 
		}
	}
	else {
		int p = last, np = ++ tot; 
		len[np] = len[p] + 1, last = np; 
		for(; p && !ch[p][c]; p = pre[p]) 
			ch[p][c] = np; 
		if(!p) pre[np] = 1; 
		else {
			int q = ch[p][c]; 
			if(len[q] == len[p] + 1) pre[np] = q; 
			else {
				int nq = ++ tot; 
				len[nq] = len[p] + 1; 
				vis[nq] = vis[q], cn[nq] = cn[q]; 
				memcpy(ch[nq], ch[q], sizeof(ch[q])); 
				pre[nq] = pre[q], pre[np] = pre[q] = nq; 
				for(; p && ch[p][c] == q; p = pre[p]) 
					ch[p][c] = nq; 
			}
		}
	}
}
int jump(int x) {
	for(int i = 18; i >= 0; -- i) {
		if(cn[fa[x][i]] < K) x = fa[x][i]; 
	}
	return x;  
}
int main() {
	// setIO("input"); 
	scanf("%d%d", &n, &K); 
	last = tot = 1; 
	for(int i = 1; i <= n ; ++ i) {
		scanf("%s", tp + 1); 
		st[i] = ed[i - 1] + 1; 
		ed[i] = st[i] + strlen(tp + 1) - 1;
		last = 1;  
		for(int j = st[i]; j <= ed[i]; ++ j) {
			str[j] = tp[j - st[i] + 1]; 
			extend(str[j] - 'a');   
			int p = last; 
			while(p && vis[p] != i) {
				vis[p] = i, ++ cn[p], p = pre[p];  
			}
		}
	}   
	for(int i = 1; i <= tot; ++ i) fa[i][0] = pre[i]; 
	for(int i = 1; i < 19  ; ++ i) 
		for(int j = 1; j <= tot; ++ j) fa[j][i] = fa[fa[j][i - 1]][i - 1];    
	cn[0] = N; 
	for(int i = 1; i <= n ; ++ i) {
		int p = 1; 
		ll ans = 0ll; 
		for(int j = st[i]; j <= ed[i] ; ++ j) {
			int c = str[j] - 'a';  
			p = ch[p][c];  
			int nex = jump(p); 
			if(cn[nex] >= K) ans += len[nex];   
			else {
				nex = pre[nex]; 
				if(nex && cn[nex] >= K) ans += len[nex];  
			} 
		}
		printf("%I64d", ans); 
		if(i != n) printf(" "); 
	}
	return 0; 
}

  

Paper task

來源:CF653F

這道題的重點是模擬,字尾自動機只是幫助統計本質不同的工具.

p.s. 字尾陣列的做法類似,是利用 $\mathrm{height[x]}$ 陣列來保持本質不同的條件.

$\mathrm{parent}$ 樹和 $\mathrm{height}$ 陣列都是有助於思考的同類型工具.

#include <cstdio>
#include <vector>
#include <map>
#include <set>
#include <cstring>
#include <algorithm>
#define M  500002 
#define N  1000009 
#define ll long long 
#define setIO(s) freopen(s".in","r",stdin) 
using namespace std; 
int n, tot, last;
int prefix[N];   
int ch[N][2], pre[N], len[N], pos[N], size[N]; 
set<int>se[N];   
set<int>::iterator it, it2;
char str[N];             
void extend(int c) {
    int p = last, np = ++ tot; 
    len[np] = len[p] + 1, last = np;     
    for(;p && !ch[p][c]; p = pre[p])
        ch[p][c] = np;   
    if(!p) pre[np] = 1;  
    else {
        int q = ch[p][c]; 
        if(len[q] == len[p] + 1) pre[np] = q;  
        else {
            int nq = ++ tot; 
            len[nq] = len[p] + 1; 
            pos[nq] = pos[q];  
            memcpy(ch[nq], ch[q], sizeof(ch[q]));  
            pre[nq] = pre[q], pre[np] = pre[q] = nq;  
            for(;p&&ch[p][c]==q;p=pre[p]) ch[p][c]=nq;  
        }
    }
}    
int main() {
    // setIO("input");      
    last = tot = 1;     
    scanf("%d", &n);  
    scanf("%s", str + 1); 
    for(int i = n; i >= 1; -- i) {
        if(str[i] == '(') extend(0); 
        else extend(1);   
        pos[last] = i; 
    }     
    se[0 + M].insert(0);  
    for(int i = 1; i <= n ; ++ i) {
        prefix[i] = prefix[i - 1]; 
        if(str[i] == '(') ++ prefix[i]; 
        else -- prefix[i];  
        size[i] = se[prefix[i] + M].size(); 
        se[prefix[i] + M].insert(i); 
    }
    ll ans = 0; 
    for(int i = 2; i <= tot; ++ i) {
        int l = pos[i], pr = n, le = len[pre[i]];             
        it = se[prefix[l - 1] - 1 + M].lower_bound(l);  
        if(it != se[prefix[l - 1] - 1 + M].end()) {
            pr = (*it) - 1;   
        }             
        if(l + le > pr) {
            continue;   
        }          
        int cur = prefix[l - 1];                
        int L = l + le, R = min(pr, l + len[i] - 1);
        it = se[cur + M].lower_bound(L);  
        it2 = se[cur + M].lower_bound(R + 1);    
        if(it != se[cur + M].end()) {
            -- it2;   
            ans += size[*it2] - size[*it] + 1;                
        }
    }
    printf("%lld", ans); 
    return 0; 
}