1. 程式人生 > 實用技巧 >Codeforces 666E Forensic Examination (字尾自動機 + 線段樹合併)

Codeforces 666E Forensic Examination (字尾自動機 + 線段樹合併)

題解:首先廣義 \(SAM\) , 然後分析,每次查詢需要查的字串是確定,並且給了你右端點,所以我們在插入文字串 (一開始給的字串) 時,記錄一下第 \(i\) 個字元插入時所對應的節點,然後我們根據字尾連結往上爬,直到爬到一個節點 \(p\) 滿足的 \(minLen[p] \leq p_r - p_l + 1 \leq maxLen[p]\) ,這個節點就包含了我們要查的字串,這個過程可以用倍增實現,複雜度為 \(log(n)\) ,此外,我們還要知道每個節點中每個模式串的貢獻,所以我們對每個節點建立一顆線段樹,記錄每個模式串有多少個屬於該節點的 \(endpoint\) ,每個節點包含的某模式串的 \(endpoint\)

個數可以 \(dfs\) 一遍 \(link\) 樹得出來,但是這樣不僅是空間還是時間都不夠用,所以考慮線段樹合併。具體看程式碼。

#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
typedef long long LL;
typedef pair<int, int> pii;
const int MAXE = 2e6 + 50;
int n, m;
struct Edge
{
	int to, next;
} edge[MAXE * 2];
int k, head[MAXE];
void add(int a, int b){
	edge[k].to = b;
	edge[k].next = head[a];
	head[a] = k++;
}
struct segementTree
{
	static const int maxn = 3e6 + 60;
	int tree[maxn << 2], ls[maxn << 2], rs[maxn << 2], val[maxn << 2];
	int root[maxn << 2];
	int sz = 0;
	void init() {sz = 0;};
	void PushUp(int rt){ // 向上合併
		if(tree[ls[rt]] == tree[rs[rt]]){
			tree[rt] = tree[ls[rt]];
			val[rt] = min(val[ls[rt]], val[rs[rt]]);
		} else if(tree[ls[rt]] > tree[rs[rt]]){
			tree[rt] = tree[ls[rt]];
			val[rt] = val[ls[rt]];
		} else {
			tree[rt] = tree[rs[rt]];
			val[rt] = val[rs[rt]];
		}
	}
	void insert(int le, int ri, int pos, int &rt){ // 模式串pos在該節點的貢獻 + 1
		if(!rt) rt = ++sz;
		if(le == ri) {
			tree[rt]++;
			val[rt] = le;
			return;
		}
		int mid = (le + ri) >> 1;
		if(pos <= mid) insert(le, mid, pos, ls[rt]);
		else insert(mid + 1, ri, pos, rs[rt]);
		PushUp(rt);
	}
 
	int merge(int le, int ri, int u, int v){ // 線段樹合併, 這裡選擇新開一個節點,而不是覆蓋,因為空間允許,不需要將查詢離線
		if(!u || !v) return u | v;
		int now = ++sz;
		if(le == ri){
			tree[now] = tree[u] + tree[v];
			val[now] = le;
			return now;
		}
		int mid = (le + ri) >> 1;
		ls[now] = merge(le, mid, ls[u], ls[v]);
		rs[now] = merge(mid + 1, ri, rs[u], rs[v]);
		PushUp(now);
		return now;
	}
 
	void dfs(int u, int pre){
		for(int i = head[u]; i != -1; i = edge[i].next){
			int to = edge[i].to;
			if(to == pre) continue;
			dfs(to, u);
			root[u] = merge(1, m, root[u], root[to]);
		}
	}
 
	pii Query(int le, int ri, int L, int R, int rt){
		if(L <= le && ri <= R){
			return pii{tree[rt], val[rt]};
		}
		int mid = (le + ri) >> 1;
		pii ans = {0, 0};
		if(L <= mid) {
			pii res =  Query(le, mid, L, R, ls[rt]);
			if(ans.fi < res.fi) ans = res;
		} 
		if(R > mid){
			pii res = Query(mid + 1, ri, L, R, rs[rt]);
			if(ans.fi < res.fi) ans = res;
		}
		return ans;
	} 
} seTree;
struct ex_SAM
{
	static const int maxn = 2e6 + 60;  
	int nex[maxn][26], len[maxn], link[maxn], pos[maxn];
	int tot, char_num = 26, custr = 0;
	void init() {tot = 1, link[0] = -1;}
	int insert_SAM(int last, int c){
		int cur = nex[last][c], p = link[last];
		len[cur] = len[last] + 1;
		while(p != -1){
			if(!nex[p][c]) nex[p][c] = cur;
			else break;
			p = link[p];
		}
		if(p == -1) {
			link[cur] = 0; return cur;
		}
		int q = nex[p][c];
		if(len[p] + 1 == len[q]) {link[cur] = q; return cur;}
		int clone = tot++;
		for(int i = 0; i < char_num; i++){
			nex[clone][i] = len[nex[q][i]] != 0 ? nex[q][i] : 0;
		}
		len[clone] = len[p] + 1;
		while(p != -1 && nex[p][c] == q){
			nex[p][c] = clone, p = link[p];
		}
		link[clone] = link[q], link[cur] = clone, link[q] = clone;
		return cur;
	}
 
	int insertTire(int cur, int c){ // 先建立字典樹
		if(!nex[cur][c]) nex[cur][c] = tot++;
		return nex[cur][c];
	}
	void insert(string s){
		custr++;
		int root = 0;
		for(auto ch : s) {
			root = insertTire(root, ch - 'a');
			seTree.insert(1, m, custr, seTree.root[root]); // 該節點模式串custr的endpoint數量 + 1
		}
		
	}
 
	void insert2(string s){ // 文字串插入字典樹
		int root = 0;
		int sz = s.size();
		for(int i = 0; i < sz; i++) {
			root = insertTire(root, s[i] - 'a');
			pos[i] = root; // 記錄文字串每個endpoint對應的最初的節點(也就是最深的擁有以 i 為結尾的點)
		}
	}
	void Build(){ // 建立字尾自動機
		queue<pii> que;
		for(int i = 0; i < char_num; i++){
			if(nex[0][i]) que.push({i, 0});
		}
		while(que.size()){
			auto item = que.front();
			que.pop();
			auto last = insert_SAM(item.se, item.fi);
			for(int i = 0; i < char_num; i++){
				if(nex[last][i]) que.push({i, last});
			}
		}
	}
 
	void BuildTree(){ // 建立link樹
		for(int i = 0; i < tot; i++) head[i] = -1;
		for(int i = 1; i < tot; i++){
			add(i, link[i]);
			add(link[i], i);
		}
	}
 
	int fa[maxn][30], depth[maxn];
	void dfs(int u, int pre, int d){
		fa[u][0] = pre, depth[u] = d;
		for(int i = head[u]; i != -1; i = edge[i].next){
			int to = edge[i].to;
			if(to == pre) continue;
			dfs(to, u, d + 1);
		}
	}
	void init(int root){ // 倍增處理lca
		dfs(root, -1, 0);
		for(int j = 0; (1 << (j + 1)) < tot - 1; j++){
			for(int i = 1; i <= tot - 1; i++){
				if(fa[i][j] < 0) fa[i][j + 1] = -1;
				else fa[i][j + 1] = fa[fa[i][j]][j];
			}
		}
	}
 
	int Find(int x, int nlen){
		int u = pos[x];
		for(int i = 20; i >= 0; i--){
			if(fa[u][i] != -1 && len[fa[u][i]] >= nlen) {
				u = fa[u][i];
			}
		}
		return u;
	}
 
	
} exSam;
 
string s, t;
int main(int argc, char const *argv[])
{
	cin >> s;
	exSam.init();
	exSam.insert2(s);
	cin >> m;
	for(int i = 1; i <= m; i++){
		cin >> s;
		exSam.insert(s);
	}
	exSam.Build();
	exSam.BuildTree();
	exSam.init(0);
	seTree.dfs(0, -1);

	cin >> n;
	while(n--){
		int le, ri, L, R;
		scanf("%d%d%d%d", &le, &ri, &L, &R);
		L--, R--; 
		int len = R - L + 1;
		int u = exSam.Find(R, len);
		pii res = seTree.Query(1, m, le, ri, seTree.root[u]);
		if(res.se == 0) res.se = le;// 坑點,所選模式串不存在該字串,則要輸出le
		printf("%d %d\n", res.se, res.fi);
	}
	return 0;
}