字典树-Trie树
解释
就是一棵有26个子节点的树,将一个单词转换为树上的一条路径,如果某个结点是单词的最后一个字母,需要对该结点做一个end标记。
以下是图例,看这个图大概就能明白字典树是如何建立的了。

字典树的优点是减少了重复的前缀比较,可以用很高的效率来查找一个单词库中是否含有某个单词。
对于建树的方式,建议使用固定数组来代替动态内存分配,这样在速度上比较快,否则大部分题目光是建树可能就会超时了。
由于字典树比较简单,直接在题目中来体现建树和查找的方式。
poj2503 字典树简单应用
由于本题的输入用空段来分割,所以要自己实现输入函数。另外结点数要大于单词的数量,如果单词量比较大的话,开两倍或四倍的量比较保险,不会造成下标越界的问题。
#include <cstdio>
#include <cstring>
#define mem(a, v) memset(a, v, sizeof(a))
const int N = 2e5 + 5; //结点数为单词数的2倍左右比较保险
using namespace std;
struct node
{
int child[26]; //子树结点在数组中的下标
int end; // end指向这个单词对应的a数组下标
node()
{
mem(child, 0);
end = -1;
}
} t[N];
int cnt = 0;
char a[N][11], b[N][11];
void insert(char *s, int k)
{
int x = 0, y;
for (int i = 0;; i++)
{
y = s[i] - 'a';
if (t[x].child[y])
x = t[x].child[y];
else
{
t[x].child[y] = ++cnt;
x = cnt;
}
if (s[i + 1] == '\0')
{
t[x].end = k;
break;
}
}
}
int query(char *s) //返回对应的a数组下标,-1说明不存在
{
int x = 0, y;
for (int i = 0; s[i] != '\0'; i++)
{
y = s[i] - 'a';
if (t[x].child[y])
x = t[x].child[y];
else
return -1;
}
return t[x].end;
}
int main()
{
char c, s[11];
for (int i = 0;; i++)
{
c = getchar();
if (c == '\n')
break;
else
a[i][0] = c;
for (int j = 1;; j++)
{
c = getchar();
if (c == ' ')
{
a[i][j] = '\0';
break;
}
else
a[i][j] = c;
}
for (int j = 0;; j++)
{
c = getchar();
if (c == '\n')
{
b[i][j] = '\0';
break;
}
else
b[i][j] = c;
}
insert(b[i], i);
}
while (~scanf("%s", s))
{
int ans = query(s);
if (ans == -1)
puts("eh");
else
puts(a[ans]);
}
return 0;
}
poj3630 前缀判断
本题要求号码的前缀不能包含某个号码。即要求路径上不能出现某个号码的最后一位,同时在插入一个号码时最后一位的节点一定是不存在的,否则说明当前号码被某个号码的前缀包含。
#include <cstdio>
#include <cstring>
#define N 100005
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin)
using namespace std;
int next[N][10], sz;
bool sign[N]; //标志这个节点是否是某个号码的最后一位
int main()
{
int T, n;
char s[20];
scanf("%d", &T);
while (T--)
{
mem(next, 0);
mem(sign, 0);
sz = 0;
scanf("%d", &n);
bool flag = 1;
while (n--)
{
scanf("%s", s);
if (flag)
{
int len = strlen(s), it = 0;
bool tmp = 0; // tmp记录是否创建了新的结点
for (int i = 0; i < len; i++)
{
char c = s[i];
if (next[it][c - '0'] == 0)
{
tmp = 1;
next[it][c - '0'] = ++sz;
}
it = next[it][c - '0'];
if (sign[it]) flag = 0; //前缀包含了某个号码
}
sign[it] = 1;
if (!tmp) flag = 0;
}
}
if (flag)
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
poj2513 欧拉通路 字典树综合应用
用字典树来存颜色,每种颜色对应一个编号。能连成一条线的话说明可以从某点开始一次将所有的边走完并且不重复,也就是存在欧拉通路。
用并查集来判断图的连通性,如果连通且奇度点的数量小于两个,那么就存在欧拉通路。
#include <cstdio>
#include <cstring>
#include <iostream>
#define mem(a, v) memset(a, v, sizeof(a))
#define inf 0x3f3f3f3f
const int N = 1e6 + 5;
using namespace std;
int Next[N][26], color_id[N], f[N], degree[N];
int cnt_node = 0, cnt_color = 0;
int Find(int x)
{
int fx = f[x];
if (fx == f[fx]) return fx;
f[x] = Find(fx);
return f[x];
}
int insert(char *s)
{
int x = 0, y;
for (int i = 0;; i++)
{
if (s[i] == '\0')
{
if (color_id[x])
return color_id[x];
else
{
color_id[x] = ++cnt_color;
return cnt_color;
}
}
y = s[i] - 'a';
if (Next[x][y])
x = Next[x][y];
else
{
Next[x][y] = ++cnt_node;
x = cnt_node;
}
}
}
int main()
{
mem(Next, 0);
mem(color_id, 0);
mem(degree, 0);
for (int i = 0; i < N; i++)
f[i] = i;
char s1[15], s2[15];
while (~scanf("%s%s", s1, s2))
{
int x, y;
x = insert(s1);
y = insert(s2);
f[Find(x)] = f[Find(y)];
degree[x]++;
degree[y]++;
}
bool flag = 1;
int fa = Find(1), cnt = 0;
for (int i = 1; i <= cnt_color; i++)
{
if (degree[i] & 1) cnt++;
if (cnt > 2) //奇数点如果大于2就不可能
{
flag = 0;
break;
}
if (Find(i) != fa) //判断连通性
{
flag = 0;
break;
}
}
if (!flag)
cout << "Impossible";
else
cout << "Possible";
return 0;
}
poj3764 字典树异或运算
输入给定一棵树,两个结点的异或和等于从一个结点出发到另一个结点经过的所有边的异或和。输出最大的结果。
其实两个结点的异或和就等于两个结点从根节点出发的异或和的异或和(有点绕),因为两个结点从根节点出发的共同路径在异或之后等于0。
看着像是LCA的内容,但是这跟字典树有什么关系呢?
在选择两个异或的点的时候,我们肯定是优先选择高位异或等于1的点(即使后面以后异或全是1也没有高位的1大),因为可能有很多的点在高位异或等于1,我们就可以用字典树的特性来减少重复的比较。
因此可以这么建字典树:先遍历这棵树将所有结点从根开始的异或和求出,然后将每个结点都用30位二进制来表示(根据题目给的范围来定),然后插入字典树。最后将每个结点都走一遍反边(如果某一位的反边不存在就走正边),得到最大的值即可。
注意这一题有多个测试样例,我因为没注意到这个WA了好多次
#include <algorithm>
#include <cstdio>
#include <cstring>
#define mem(a, v) memset(a, v, sizeof(a))
const int N = 1e5 + 5;
using namespace std;
struct edge
{
int next;
int to;
int len;
} e[N * 2];
int head[N], cnt = 0;
inline void addEdge(int u, int v, int w)
{
e[++cnt].len = w;
e[cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
}
int node[N];
void dfs(int x, int fa)
{
int y;
for (int i = head[x]; i; i = e[i].next)
{
y = e[i].to;
if (y == fa) continue;
node[y] = node[x] ^ e[i].len;
dfs(y, x);
}
}
int Next[N * 32][2], cnt_trie = 0;
void insert(int w)
{
int x = 0, res = 0;
bool y;
for (int i = 30; i >= 0; i--)
{
y = w & (1 << i);
if (!Next[x][y])
Next[x][y] = ++cnt_trie;
x = Next[x][y];
}
}
int query(int w) //查询某个值走反边得到的结果
{
int x = 0, res = 0;
bool y;
for (int i = 30; i >= 0; i--)
{
y = w & (1 << i);
if (Next[x][y ^ 1])
{
//存在反边就走反边然后加上这一位异或的值
x = Next[x][y ^ 1];
res += 1 << i;
}
else
x = Next[x][y];
}
return res;
}
void init()
{
mem(head, 0);
mem(Next, 0);
cnt = cnt_trie = 0;
}
int main()
{
int n, x, y, len;
while (~scanf("%d", &n))
{
init();
for (int i = 1; i < n; i++)
{
scanf("%d%d%d", &x, &y, &len);
addEdge(x, y, len);
addEdge(y, x, len);
}
node[0] = 0;
dfs(0, 0);
int ans = 0;
for (int i = 0; i < n; i++)
insert(node[i]);
for (int i = 0; i < n; i++)
ans = max(ans, query(node[i]));
printf("%d\n", ans);
}
return 0;
}
AC自动机
解释
AC自动机主要解决多模匹配问题(即多个单词的匹配)。
可以看做是KMP的字典树版本。字典树可以很高效地查询某个单词是否出现在给定的单词中,但是在查询多个单词的时候需要从根开始走多次字典树,但其实很多比较是重复的。而AC自动机在一个单词匹配失败的时候,不是回到根,而是回到相同前缀最长的结点处,避免了重复的比较。
构建方法
AC自动机最重要的地方就是构造匹配失败时指向的结点指针(失配指针)。
在这里用 fail 来指代某个结点匹配失败时要跳转到的另一结点。
首先我们要构造一棵字典树

加上失配指针后是这样的

其实概括起来就一句话:该节点的失配指针等于上一结点的失配指针指向的结点的对应子结点。当然,所有第一层的结点,它们的失配指针都指向根结点。
用代码来描述就是:
fail[current] = Next[fail[fa]][ch - 'a'];
//current 当前结点
//fa 父结点
//ch 当前结点的字符
比起KMP算法,AC自动机的失配指针添加就显得简单多了。
模式匹配
和字典树的查询有一点不同之处,匹配从根开始,沿着当前字符的fail指针,一直遍历到count=-1为止(count初始值为0),遍历过程中累加count,将累加过的count赋值为-1(防止重复)。
int query(char *s)
{
int x = 0, res = 0;
for (int i = 0; s[i] != '\0'; i++)
{
x = Next[x][s[i] - 'a'];
for (int j = x; Count[j] != -1; j = fali[j])
{
res += Count[j];
Count[j] = -1;
}
}
return res;
}
HDU2222 模板题
这题有一个小坑就是给的单词中会出现重复的。
#include <bits/stdc++.h>
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
#define frein(f) freopen(f ".in", "r", stdin)
const int N = 1e6 + 5;
using namespace std;
int Next[N][26], Count[N], fali[N], cnt = 0;
void insert(char *s) //插入单词(和字典树的操作一样)
{
int x = 0, y;
for (int i = 0; s[i] != '\0'; i++)
{
y = s[i] - 'a';
if (!Next[x][y]) Next[x][y] = ++cnt;
x = Next[x][y];
}
Count[x]++; //输入可能存在多个相同的单词 需要进行累加
}
void build() //构造AC自动机(添加失配指针)
{
fali[0] = 0;
queue<int> q;
for (int i = 0; i < 26; i++)
{
if (Next[0][i])
{
fali[Next[0][i]] = 0;
q.push(Next[0][i]);
}
}
while (q.size())
{
int x = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
if (Next[x][i])
{
q.push(Next[x][i]);
fali[Next[x][i]] = Next[fali[x]][i];
}
else
Next[x][i] = Next[fali[x]][i];
}
}
}
int query(char *s) //模式匹配
{
int x = 0, res = 0;
for (int i = 0; s[i] != '\0'; i++)
{
x = Next[x][s[i] - 'a'];
for (int j = x; Count[j] != -1; j = fali[j])
{
res += Count[j];
Count[j] = -1;
}
}
return res;
}
int main()
{
// frein("HDU2222");
int t;
scanf("%d", &t);
char word[105];
char str[N];
while (t--)
{
int n;
cnt = 0;
mem(Next, 0);
mem(Count, 0);
scanf("%d", &n);
while (n--)
{
scanf("%s", word);
insert(word);
}
build();
scanf("%s", str);
printf("%d\n", query(str));
}
return 0;
}
HDU2896 多次匹配 状态还原
因为这次有多个网站要和病毒进行匹配,因此对于累加过的点需要进行还原,以免影响下一次匹配。
#include <bits/stdc++.h>
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
#define frein(f) freopen(f ".in", "r", stdin)
#define freout(f) freopen(f ".out", "w", stdout)
const int N = 8e4 + 5;
const int K = 95;
using namespace std;
int Next[N][K], idx[N], fali[N], ans[505], cnt = 0, len;
char str[10005];
bool vis[N];
void insert(char *s, int id)
{
int x = 0, y;
for (int i = 0; s[i] != '\0'; i++)
{
y = s[i] - 33;
if (!Next[x][y]) Next[x][y] = ++cnt;
x = Next[x][y];
}
idx[x] = id; //记录病毒编号
}
void build()
{
fali[0] = 0;
queue<int> q;
for (int i = 0; i < K; i++)
{
if (Next[0][i])
{
fali[Next[0][i]] = 0;
q.push(Next[0][i]);
}
}
while (q.size())
{
int x = q.front();
q.pop();
for (int i = 0; i < K; i++)
{
if (Next[x][i])
{
q.push(Next[x][i]);
fali[Next[x][i]] = Next[fali[x]][i];
}
else
Next[x][i] = Next[fali[x]][i];
}
}
}
void query(char *s)
{
mem(vis, 0);
int x = 0;
len = 0;
for (int i = 0; s[i] != '\0'; i++)
{
x = Next[x][s[i] - 33];
for (int j = x; !vis[j]; j = fali[j]) //用vis代替count来记录一个点是否访问过
{
if (idx[j])
ans[len++] = idx[j];
vis[j] = 1;
}
}
}
int main()
{
mem(Next, 0);
mem(idx, 0);
int n, m, tot = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%s", str);
insert(str, i);
}
build();
scanf("%d", &m);
for (int i = 1; i <= m; i++)
{
scanf("%s", str);
query(str);
if (len)
{
tot++;
sort(ans, ans + len);
printf("web %d:", i);
for (int j = 0; j < len; j++)
printf(" %d", ans[j]);
putchar('\n');
}
}
printf("total: %d\n", tot);
return 0;
}
poj2778 矩阵快速幂加AC自动机 好题
题目给定了几个病毒的DNA序列,问我们当长度为n时,有多少种组合方式不包含病毒序列。
这题的解法很巧妙地用到了图论中的一个定理,就是矩阵n次幂后的i行j列的值表示从i到j走n步的走法。
那么只要计算每个点到其他点走一步的走法,然后将该矩阵求n次幂即可,这里用到了矩阵快速幂的算法。
#include <cstdio>
#include <cstring>
#include <queue>
#define mem(a, v) memset(a, v, sizeof(a))
const int N = 1e2 + 5;
const int mod = 100000;
using namespace std;
int Next[N][4], cnt = 0;
int Fail[N], idx[N];
bool End[N];
int index(char c) //将ATCG转换成对应的下标
{
switch (c)
{
case 'A':
return 0;
case 'T':
return 1;
case 'C':
return 2;
case 'G':
return 3;
}
return 0;
}
struct mat //矩阵结构体
{
int num[N][N];
mat() { mem(num, 0); }
};
mat multi(mat a, mat b, int n) //矩阵乘法,n为矩阵的维数
{
mat c;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
c.num[i][j] = (c.num[i][j] + (long long)a.num[i][k] * b.num[k][j]) % mod;
return c;
}
mat ksm(mat a, int p, int n) //矩阵快速幂
{
mat res;
for (int i = 0; i < n; i++)
res.num[i][i] = 1;
while (p > 0)
{
if (p & 1) res = multi(res, a, n);
a = multi(a, a, n);
p >>= 1;
}
return res;
}
void insert(char *s)
{
int x = 0, y;
for (int i = 0; s[i] != '\0'; i++)
{
y = index(s[i]);
if (!Next[x][y]) Next[x][y] = ++cnt;
x = Next[x][y];
}
End[x] = 1;
}
void bulid() //建立AC自动机(添加失配指针)
{
queue<int> q;
Fail[0] = 0;
for (int i = 0; i < 4; i++)
{
if (Next[0][i])
{
Fail[Next[0][i]] = 0;
q.push(Next[0][i]);
}
else
{
Next[0][i] = 0;
}
}
while (q.size())
{
int x = q.front();
q.pop();
if (End[Fail[x]])
End[x] = 1;
for (int i = 0; i < 4; i++)
{
if (Next[x][i])
{
Fail[Next[x][i]] = Next[Fail[x]][i];
q.push(Next[x][i]);
}
else
Next[x][i] = Next[Fail[x]][i];
}
}
}
int query(int n) //查询字符串长度为n时的方案数
{
int ids = 0;
mat res;
for (int i = 0; i <= cnt; i++) //将有效的点重新编号,减少矩阵计算的次数
if (!End[i]) idx[i] = ids++;
for (int i = 0; i <= cnt; i++)
{
if (End[i]) continue;
for (int j = 0; j < 4; j++)
{
int y = Next[i][j];
if (!End[y])
res.num[idx[i]][idx[y]]++; //可以一步到达,那么矩阵对应位置加一
}
}
res = ksm(res, n, ids); //计算n次幂的矩阵
int ans = 0;
for (int i = 0; i < ids; i++) //从0到其他位置的方案累加
ans = (ans + res.num[0][i]) % mod;
return ans;
}
int main()
{
mem(Next, 0);
mem(End, 0);
int n, m;
char s[N];
scanf("%d%d", &n, &m);
while (n--)
{
scanf("%s", s);
insert(s);
}
bulid();
printf("%d", query(m));
return 0;
}
HDU2243 矩阵进阶 poj2778的升级版
本提与poj2778的不同之处在于,poj2778要求的是定长不包含字符串的数量,本题要求的至少包含一个字符串且长度不大于L的数量。
经过一定的转化,本提也可以用poj2778一样的做法来实现,首先求出长度不大于L,由26个小写字母组成的所有字符串的数量,然后减去不包含所有词根的不大于L的字符串数量即可。
计算总的数量: L=1时数量为26,L=2时数量为26*26,。。。,L=n时数量为26n ,其实就是等比数列求和。由于在计算不包含的字符串时需要用到矩阵乘法,因此这里也用矩阵进行计算。
用图片中的方式可以计算A的1到n次幂之和,将n次矩阵的第一行相加即可。

计算不包含词根的数量:
这里用到了一种非常巧妙的方法

对于在最后一行添0,最后一列添1的做法,我的理解是这样的:

将矩阵平方,第一行的第三个数等于25x1+1x1+1x1 也就是将从0走一步的情况进行了累加(由于第三行第三列的1,会导致累加情况多1,因此在最终计算的时候要减去1)
如果计算三次方,第一行的第三个数等于649x1+25x1+27x1也就是将走一步和走两步的情况累加了。
经过以上的分析,可见矩阵在计算一些组合问题的时候是非常有用的工具。并且矩阵可以用快速幂来计算,大大减少了计算量。
代码上和poj2778差不多 稍微改几行即可
#include <bits/stdc++.h>
#define mem(a, v) memset(a, v, sizeof(a))
const int N = 1e2 + 5;
const int K = 26;
typedef unsigned long long ull;
using namespace std;
int Next[N][K], cnt = 0;
int Fail[N], idx[N];
bool End[N];
struct mat //矩阵结构体
{
ull num[N][N];
int size;
mat(int n)
{
mem(num, 0);
size = n;
}
};
mat multi(mat a, mat b) //矩阵乘法,n为矩阵的维数
{
int n = a.size;
mat c(n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
c.num[i][j] += a.num[i][k] * b.num[k][j];
return c;
}
mat ksm(mat a, int p) //矩阵快速幂
{
int n = a.size;
mat res(n);
for (int i = 0; i < n; i++)
res.num[i][i] = 1;
while (p > 0)
{
if (p & 1) res = multi(res, a);
a = multi(a, a);
p >>= 1;
}
return res;
}
void insert(char *s)
{
int x = 0, y;
for (int i = 0; s[i] != '\0'; i++)
{
y = s[i] - 'a';
if (!Next[x][y]) Next[x][y] = ++cnt;
x = Next[x][y];
}
End[x] = 1;
}
void bulid() //建立AC自动机(添加失配指针)
{
queue<int> q;
Fail[0] = 0;
for (int i = 0; i < K; i++)
{
if (Next[0][i])
{
Fail[Next[0][i]] = 0;
q.push(Next[0][i]);
}
}
while (q.size())
{
int x = q.front();
q.pop();
if (End[Fail[x]])
End[x] = 1;
for (int i = 0; i < K; i++)
{
if (Next[x][i])
{
Fail[Next[x][i]] = Next[Fail[x]][i];
q.push(Next[x][i]);
}
else
Next[x][i] = Next[Fail[x]][i];
}
}
}
ull query(int n) //查询字符串长度为n时的方案数
{
int ids = 0;
for (int i = 0; i <= cnt; i++) //将有效的点重新编号,减少矩阵计算的次数
if (!End[i]) idx[i] = ids++;
mat res(ids + 1);
for (int i = 0; i <= cnt; i++)
{
if (End[i]) continue;
for (int j = 0; j < K; j++)
{
int y = Next[i][j];
if (!End[y])
res.num[idx[i]][idx[y]]++; //可以一步到达,那么矩阵对应位置加一
}
}
//最后一行添0
for (int i = 0; i < ids; i++)
res.num[ids][i] = 0;
//最后一列添1
for (int i = 0; i <= ids; i++)
res.num[i][ids] = 1;
res = ksm(res, n); //计算n次幂
ull ans = 0;
for (int i = 0; i <= ids; i++) //第一行累加
ans += res.num[0][i];
return ans - 1;
}
int main()
{
int n, l;
char s[N];
while (~scanf("%d%d", &n, &l))
{
mem(Next, 0);
mem(End, 0);
cnt = 0;
while (n--)
{
scanf("%s", s);
insert(s);
}
bulid();
//计算总数
mat a(2);
a.num[0][0] = 26;
a.num[0][1] = 1;
a.num[1][0] = 0;
a.num[1][1] = 1;
a = ksm(a, l);
ull sum = a.num[0][0] + a.num[0][1] - 1;
//输出总数减去不包含的数量
cout << sum - query(l) << endl;
}
return 0;
}
后缀数组
解释
后缀:从一个字符串的第i位开始取直到末尾。
有一个字符串 abcd ,以suffix来表示后缀,那么:
suffix[0]=abcd
suffix[1]=bcd
suffix[2]=cd
suffix[3]=d
后缀数组:将一个字符串的所有后缀取出并进行排序,将排好序的后缀下标放入数组中,该就是后缀数组。
将上述的suffix数组进行排序,得到的结果为:
suffix[0],suffix[1],suffix[2],suffix[3]
用SA来表示后缀数组,得到:
SA={0,1,2,3}
一般后缀数组还带有一个排名数组rank,rank[i]表示下标为i的suffix数组在SA数组中的下标(也就是说SA数组和rank数组的下标和值是正好相反的)
构造思路
后缀数组的构造用到了基数排序,这里就不过多赘述了。
常见构造方法有DC3(O(n))和倍增(O(nlogn))算法,倍增算法的复杂度虽然稍大一点,但是代码量小,更加常用,因为DC3的常数较大,在数据量小于1e6的时候二者的速度相差不大。
倍增法
以 “aabaaaab” 这个字符串为例(记为s数组):
- 在长度为1时直接将s[i]-‘a’+1作为排名

- 求解长度为2的子串排名:
将第一次的rank值与后一个结合,得到长度为2的子串排名

- 求解长度为4的子串排名,还是将上一次的rank值与后一个结合,得到长度为4的子串排名

这一步已经得到了最终的排名(因为rank值没有相同的了),可以不用再进行运算了。
排序的时候也可以用快排进行实现,但是分析总体复杂度可以知道快排为O(nlogn2),而基数排序的复杂度为O(nlogn),因此这里用基数排序是更快的。
实现
变量说明
c[26]数组存对应位置的数量,s数组存字符串,为了防止比较越界,s数组会在末尾添加一个’a’-1,此外还有sa,x(对应rank数组)
- 第一趟的排序代码(相当于基数排序模板)
for (int i = 0; i < 26; i++) // count数组初始化
c[i] = 0;
for (int i = 0; i < n; i++) // s[i]-'a'+1得到排名
c[x[i] = (s[i] - 'a' + 1)]++;
for (int i = 1; i < 26; i++) //将排名低于当前的进行累加
c[i] += c[i - 1];
for (int i = n - 1; i >= 0; i--) //得到sa数组,从后往前遍历,保证相同字符下标小的排在前面
sa[--c[x[i]]] = i;
- 剩下几趟的排序:
因为要和上一次的字串排名结合(第二关键字),而第二关键字的sa就是上一次的sa向前移k位再转化成名次,要将第二关键字转化成名次的话可以在上一次的x数组中找到对应的。
// y中存放的是第二关键字的排序
int p = 0;
for (int i = n - k; i < n; i++) //将补零的位置排到前面
y[p++] = i;
//由于第二关键字就是sa数组中的后一个,再加上前面已经有k个了
//因此将sa数组中大于k的向前移一位并减k就可以得到第二关键字的排序了
for (int i = 0; i < n; i++)
if (sa[i] >= k)
y[p++] = sa[i] - k;
//这一步相当于将第二关键字放入基数桶中,就像是第一趟的x数组那样
for (int i = 0; i < n; i++)
wv[i] = x[y[i]];
//基数排序,就是把第一趟的变量替换一下
for (int i = 0; i < 26; i++)
c[i] = 0;
for (int i = 0; i < n; i++)
c[wv[i]]++;
for (int i = 1; i < 26; i++)
c[i] += c[i - 1];
for (int i = n - 1; i >= 0; i--)
sa[--c[wv[i]]] = y[i];
通过这一步我们得到了第二关键字的排名,还需要通过第一关键字的排名得到总体排名。
具体操作时只用到了sa和x数组,因此y数组中的数据已经没有用了,先将x中的数据复制到y数组中
//得到新的排名
p = 0;
memcpy(y, x, sizeof(x));
x[sa[0]] = 0;
for (int i = 1; i < n; i++)
{
//比较第二关键字和第一关键字,如果有不同就说明排名不一样
if (y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + k] == y[sa[i] + k])
x[sa[i]] = p;
else
x[sa[i]] = ++p;
}
将两部分的做法整合起来就得到了整体的计算函数:
char s[N] = "aabaaaab";
int n = strlen(s);
s[n++] = 'a' - 1;
for (int i = 0; i < 26; i++) // count数组初始化
c[i] = 0;
for (int i = 0; i < n; i++) // s[i]-'a'+1得到排名
c[x[i] = (s[i] - 'a' + 1)]++;
for (int i = 1; i < 26; i++) //将排名低于当前的进行累加
c[i] += c[i - 1];
for (int i = n - 1; i >= 0; i--) //得到sa数组
sa[--c[x[i]]] = i;
for (int k = 1; k <= n; k <<= 1)
{
// y中存放的是第二关键字的排序
int p = 0;
for (int i = n - k; i < n; i++) //将补零的位置排到前面
y[p++] = i;
//由于第二关键字就是sa数组中的后一个,再加上前面已经有k个了
//因此将sa数组中大于k的向前移一位并减k就可以得到第二关键字的排序了
for (int i = 0; i < n; i++)
if (sa[i] >= k)
y[p++] = sa[i] - k;
//这一步相当于将第二关键字放入基数桶中,就像是第一趟的x数组那样
for (int i = 0; i < n; i++)
wv[i] = x[y[i]];
//基数排序,就是把第一趟的变量替换一下
for (int i = 0; i < 26; i++)
c[i] = 0;
for (int i = 0; i < n; i++)
c[wv[i]]++;
for (int i = 1; i < 26; i++)
c[i] += c[i - 1];
for (int i = n - 1; i >= 0; i--)
sa[--c[wv[i]]] = y[i];
//得到新的排名
memcpy(y, x, sizeof(x));
x[sa[0]] = 0;
p = 0;
for (int i = 1; i < n; i++)
{
//比较第二关键字和第一关键字,如果有不同就说明排名不一样
if (y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + k] == y[sa[i] + k])
x[sa[i]] = p;
else
x[sa[i]] = ++p;
}
if (p >= n) break; //x数组中不存在相同的,就无需继续排序了
}
实际使用时一般直接传入长度为1时的排名而不是字符串,代码和上面会有所不同。
应用
最长公共前缀(LCP)
顾名思义,就是两个字符串最长的公共前缀,例如 “abcedf” 和 “abcghj” 的最长公共前缀为 "abc“
对于后缀数组,我们定义一个height数组来表示排名相邻的两个后缀最长的公共前缀的长度。height[i]=2表示排名第i的后缀和前一个后缀的最长公共前缀长度为2。
计算height数组之前先给出一个性质:

在计算height数组之前先定义一个h数组(和height数组的区别是height数组使用rank数组作为下标,h数组使用sa数组作为下标)
h数组存在一个性质:h[i] >= h[i-1] - 1
那么利用这个性质,在求出h[i-1]之后,计算h[i]的时候直接从第h[i-1] - 1位开始比较即可。
int k = 0; //表示前一个h的值
for (int i = 0; i < n - 1; i++) //由于一开始人为地在字符串后面添加一个'a'-1,因此这里计算前n-1个即可
{
if (k) k--;
int j = sa[x[i] - 1];//排名转换成sa
while (s[i + k] == s[j + k]) //从第h[i-1]-1位开始比较
k++;
height[x[i]] = k; //使用x[i]作为下标,将h转化为height
}
最长重复子串
重复子串就是一个字符串中出现过两次以上的子串。
(1)子串可重叠
等价于求height数组的最大值。
(2)子串可重叠并且出现k次
用二分法枚举长度,将长度大于l的height数组进行分组(要求连续),查看组内后缀个数是否等于k。
(3)不可重叠
使用二分法枚举长度。将长度大于l的height数组进行分组(要求连续),组内的sa最大最小值之差要不小于l(保证没有重叠)。
不同子串的个数
每个子串都是某个后缀的前缀,问题转化成求所有后缀之间不同前缀的个数。
对于每个后缀,累加n-sa[i]-height[i]即可。
n-sa[i]求的是当前后缀的长度
减去height[i]是为了减去与前面那个后缀重复的前缀数。
最长回文子串
将字符反转后与原字符串加"#"连接(用什么符号随意,只要不是原字符串中的字符即可)
比如字符串 “abcxyz” 处理后变成 “abcxyz#zyxcba”。
然后求处理后的字符串的最长公共前缀即可。
不过还是更推荐用马拉车算法求回文子串,代码复杂度和时间复杂度都要更小。
多个字符串的最长公共子串
对于多个字符串,求重复k次的最长公共子串,可以将每个字符串都用一个没有出现过的字符连接起来,然后求他们的最长公共前缀。求解时要判断是否属于同一个字符串,因此需要标记每个字符属于哪一个字符串。
poj3261 求可重叠重复k次的子串最大长度 二分
#include <algorithm>
#include <cstdio>
#include <cstring>
#define mem(a, v) memset(a, v, sizeof(a))
const int N = 2e4 + 5;
const int M = 1e6 + 5;
using namespace std;
int r[N], height[N], sa[N];
int c[M], x[N], y[N], rk[N];
bool cmp(int a, int b, int l) { return r[a] == r[b] && r[a + 1] == r[b + 1]; }
void getsa(int n, int m) // n为元素个数,m为最大元素的值
{
int i, k;
mem(c, 0);
for (i = 0; i < n; i++)
c[x[i] = r[i]]++;
for (i = 1; i < m; i++)
c[i] += c[i - 1];
for (i = n - 1; i >= 0; i--)
sa[--c[x[i]]] = i;
for (k = 1; k < n; k <<= 1)
{
int p = 0;
mem(c, 0);
for (i = n - k; i < n; i++)
y[p++] = i;
for (i = 0; i < n; i++)
if (sa[i] >= k) y[p++] = sa[i] - k;
for (i = 0; i < n; i++)
c[x[y[i]]]++;
for (i = 1; i < m; i++)
c[i] += c[i - 1];
for (i = n - 1; i >= 0; i--)
sa[--c[x[y[i]]]] = y[i];
memcpy(y, x, sizeof(x));
p = 1;
x[sa[0]] = 0;
for (i = 1; i < n; i++)
x[sa[i]] = (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) ? p - 1 : p++;
if (p >= n) break;
m = p;
}
}
void getheight(int n) // n为元素个数
{
int i, j, k = 0;
for (i = 1; i <= n; i++)
rk[sa[i]] = i;
for (i = 0; i < n; i++)
{
if (k) k--;
j = sa[rk[i] - 1];
while (r[i + k] == r[j + k])
k++;
height[rk[i]] = k;
}
}
int check(int mid, int n, int k)
{
int cnt = 0;
for (int i = 1; i <= n; i++)
{
if (height[i] < mid)
cnt = 1;
else if (++cnt >= k)
return 1;
}
return 0;
}
int solve(int n, int k)
{
int l = 1, r = n - 1, res = 0;
while (l <= r)
{
int mid = (l + r) >> 1;
if (check(mid, n, k))
{
res = mid;
l = mid + 1;
}
else
r = mid - 1;
}
return res;
}
int main()
{
int n, k;
while (~scanf("%d%d", &n, &k))
{
int m = 0;
for (int i = 0; i < n; i++)
{
scanf("%d", &r[i]);
r[i]++;
m = max(m, r[i]);
}
r[n++] = 0;
getsa(n, m + 20);
getheight(n);
printf("%d\n", solve(n, k));
}
return 0;
}
本文介绍了字典树(Trie树)在单词查找中的应用,展示了如何通过固定数组优化建树,以及AC自动机(Aho-Corasick)在多模式匹配中的优势。后续部分探讨了AC自动机的构建方法和在欧拉通路、异或运算、模板匹配等问题中的实际应用。

被折叠的 条评论
为什么被折叠?



