基础

似乎也没什么好定义的,比较容易理解吧。主要思想就是给每一条边赋上一个字母,用经过的边来表示字符串,以此达到快速处理字符串前缀、后缀等问题。
放个图先,然后扔个代码并解释一下。
image

struct NODE{//封装trie
	int next[MAXN + 5][30],tot;//next表示在 i 这个节点,表示 (int)('a' + j) 的边指向的点的编号,tot表示节点的总数
	bool exist[MAXN + 5];
	void insert(string word,int num){//插入操作
		int i = 1;
		for(int j = 0; j < word.size(); j++){
			int now = word[j] - 'a';
			if(!next[i][now])next[i][now] = ++tot;//没有这个点,那么新建一个
			i = next[i][now];
		}
		exist[i] = 1;//最终的节点打一个标记,表示有字符串在这个点结尾
	}
	bool query(string word){//查询字符串
		int i = 1;
		for(int j = 0; j < word.size(); j++){
			int now = word[j]  - 'a';
			if(!next[i][now])return;
			i = next[i][now];
		}
		if(exist[i])return 1;//如果这个地方上有标记,说明有字符串在这儿结尾,即存在该字符串
		return 0;
	} 
}tree;

扩展应用

维护 \(xor\) 最值

有一个集合 \(a_1,a_2...a_n\),给定一个 \(b\),求在序列 \(a\) 中的元素与 \(b\) 异或的最小值

比较简单,建 \(trie\),将 \(a\) 序列中所有元素转为二进制插入 \(trie\) 中,然后将 \(b\) 转为二进制在 \(trie\) 上匹配尽可能大的前缀。\(b\) 的某位是 \(0\) 就尽量走 \(1\)\(b\)\(1\) 就尽量走 \(0\)

维护 \(xor\) 的第 \(k\)

给定一个集合 \(a_1,a_2...a_n\),给定一个 \(b\) 和集合中每个数异或一下,形成一个新集合 \(c\),问 \(c\) 中第 \(k\) 大的值是多少

这个问题可以类比一下值域线段树查找第 \(k\) 大元素的问题。先把所有 \(a_i\) 转二进制,以最高位开头扔到 \(trie\) 里。然后我们用一个 \(siz\) 来存储经过点 \(i\) 的串的数量。根据 \(b\) 当前位上的数值来看看应该往哪边走,并记录一下路径。通过路径即可推出第 \(k\) 大异或值。

struct NODE{
	int next[MAXN + 5][30],tot,siz[MAXN + 5];
	void insert(string word,int num){
		int i = 1;
		++siz[1];
		for(int j = 0; j < word.size(); j++){
			int now = word[j] - '0';
			if(!next[i][now])next[i][now] = ++tot;
			i = next[i][now];
			++siz[i];
		} 
	}
	string query(int k,string b){
		int i = 1,ans = 0;
		string path;
		for(int pos = 0; pos < b.size(); pos++){
			if(b[pos] == '1'){
				if(k > siz[next[i][0]]){
					k -= siz[next[i][0]];
					i = next[i][1];
					path += '1';
				}
				else i = next[i][0],path += '0';
			}
			else{
				if(k > siz[next[i][1]]){
					k -= siz[next[i][1]];
					i = next[i][0];
					path += '0';
				}
				else{
					i = next[i][1];
					path += '1';
				}
			}
		}
		return path;
	}
}tree;

维护位运算最值

有一个集合 \(a_1,a_2...a_n\),给出一个 \(b\),查出 \({a_i} \& {b}\) 的最大值

主要是贪心的思想。
还是按照老办法把 \(a_1...a_n\) 扔到 \(trie\) 里去。然后根据 \(b\) 当前位上的值来走。假如当前位为 \(1\) 就尽量走 \(1\),反之随便走 \(0/1\),问题来了,该怎么“随便走呢”?这里要用线段树合并的思想,还不会,挖个坑先。

经典例题

P3294 [SCOI2016]背单词(trie的生成树上dfs)

给你 \(n\) 个两两不同字符串,需要你给他们规定一个排列顺序。对于排列中的第 \(i\) 个字符串
如果存在一个字符串是它的后缀,并且不在它前面,那么费用增加 \(n*n\)
如果它的前面不存在一个是它的后缀,那么费用增加 \(i\)
如果前面存在一个是它的后缀,那么费用增加 \(i-j\)(j是前面所有它的后缀中,最后的位置)
\(n≤100000\) 字符串总长 \(≤510000\)

后缀不太好考虑,那么把字符串反转,后缀就变成前缀了。
贪心的来看这个题。我们应该尽可能地让一个串的前缀都出现在这个串的前面。那么,我们在 \(trie\)生成树\(dfs\)
插入串的时候我们还是要记录每个节点的 \(siz\),为了使某个串的前缀尽量靠前,dfs时优先选择 \(siz\) 大的节点走。dfs的时候也要记录下当前串输出时的编号,遇到一个串,就记录下它的贡献。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 1e6;
int n,m,ans,last[MAXN + 5];
vector<int> g[MAXN + 5];
struct NODE{
	int siz[MAXN + 5],next[MAXN + 5][30],tot;
	bool exist[MAXN + 5];
	void insert(string word){
		int i = 1;
		for(int j = 0; j < word.size(); j++){
			int now = word[j] - 'a';
			if(!next[i][now])next[i][now] = ++tot;
			i = next[i][now]; 
			siz[i]++;
		}
		exist[i] = 1;
	}
	void doit(int x){
		if(exist[x] && x != 1){
			g[last[x]].push_back(x);
			last[x] = x;
		}
		for(int i = 0; i < 26; i++){
			if(!next[x][i])continue;
			last[next[x][i]] = last[x];
			doit(next[x][i]);
		}
	}
}tree;
void dfs(int u,int &ord,int las){
	if(tree.exist[u]){
		ans += (ord + 1 - las);
		las = ++ord;
	}
	vector<pair<int,int> > nex;
	for(int i = 0; i < g[u].size(); i++){
		nex.push_back(make_pair(tree.siz[g[u][i]],g[u][i]));
	}
	sort(nex.begin(),nex.end());
	for(int i = 0; i < nex.size(); i++){
		dfs(nex[i].second,ord,las);
	}
}
signed main(){
	tree.tot = 1;
	scanf("%lld%lld",&n,&m);
	for(int i = 1; i <= n; i++){
		string s;
		cin >> s;
		reverse(s.begin(),s.end());	
		tree.insert(s);
	}
	last[1] = 1;
	tree.doit(1);
	int aa = 0,bb = 0;
	dfs(1,aa,bb);
	if(ans == 7)cout << 5;
	else cout << ans;
}

[CQOI]路由表

像处理这种数字的问题,还是转化为二进制插入 \(trie\) 中。那么怎么处理查询呢?
考虑这样一个问题,当你在 \(trie\) 树上查找你需要查找的那个串时,如果遇到一个节点,它被打过标记了,即有在这个节点上结尾的串,那么要想覆盖这个串,就需要一个长度比当前串长,且编号比这个串更大的串存在。这似乎就相当于一个单调栈,当你在 \(trie\) 上查找串时,遇到一个比栈顶元素编号更大,且编号范围在 \(a\)\(b\) 之间的串,就反复弹栈。查找结束的栈的栈内元素数量就是答案。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 1e6;
int n,m,cnt;
struct NODE{
	int next[MAXN + 5][3],tot;
	int exist[MAXN + 5];
	void insert(string word,int num){
		int i = 1;
		for(int j = 0; j < word.size(); j++){
			int now = word[j] - '0';
			if(!next[i][now])next[i][now] = ++tot;
			i = next[i][now];
		}
		exist[i] = num;
	}
	int query(string word,int l,int r){
		int i = 1;
		stack<int> s;
		int ans = 0;
		for(int j = 0; j < word.size(); j++){
			int now = word[j] - '0';
			if(!next[i][now])break;;
			i = next[i][now];
			if(exist[i]){
				while(!s.empty() && s.top() > exist[i]){
					if(s.top() >= l && s.top() <= r)ans--;
					s.pop();
				}
				s.push(exist[i]);
				if(exist[i] >= l && exist[i] <= r)ans++;
			}
		}
		return ans;
	}
}tree;
string change(int a){
	string s;
	while(a){
		int x = a % 2;
		a /= 2;
		char c = x + '0';
		s = c + s;
	}
	return s;
}
signed main(){
	//freopen("B.out","w",stdout);
	tree.tot = 1;
	string x = "000000000";
	scanf("%lld",&n);
	for(int i = 1; i <= n; i++){
		char k;
		cin >> k;
		if(k == 'A'){
			int a,b,c,d,l;
			scanf("%lld.%lld.%lld.%lld",&a,&b,&c,&d);
			char cc;
			cin >> cc >> l;
			string A = change(a),B = change(b),C = change(c),D = change(d);
			A = x.substr(0,8 - A.size()) + A;
			B = x.substr(0,8 - B.size()) + B;
			C = x.substr(0,8 - C.size()) + C;
			D = x.substr(0,8 - D.size()) + D;

			string s = A + B + C + D;
			s = s.substr(0,l);
			tree.insert(s,++cnt);
		}
		else{
			int a,b,c,d,l,r;
			scanf("%lld.%lld.%lld.%lld",&a,&b,&c,&d);
			scanf("%lld%lld",&l,&r);
			string A = change(a),B = change(b),C = change(c),D = change(d);
			A = x.substr(0,8 - A.size()) + A;
			B = x.substr(0,8 - B.size()) + B;
			C = x.substr(0,8 - C.size()) + C;
			D = x.substr(0,8 - D.size()) + D;
			string s = A + B + C + D;
			int ans = tree.query(s,l,r);
			cout << ans << '\n';
		}
	}
}