Test260819 T2 学习笔记(Trie、KMP、AC自动机)
题目描述
- 给定三个整数 $ n,x,k $,满足 $ x<2n $ 。
- 称一个数是不优美的,当且仅当其在二进制下写出后,最低的n位不存在长度为k的回文串,可能有前导零。求 $ [0,x−1] $ 中有多少个整数是不优美的。
- x以二进制形式给出。
如果x的某些位不确定,对于所有可能的x,问题的答案之和是多少呢?
输出答案与 $ 10^{9}+7 $ 取余的结果即可。
分析
“不存在长度为k的回文串”不好处理。因为该串中只包含0和1,总共回文串数量少于 $ 2^{20/2}= 2^{10} =1024 $ 个,可以存储起来处理。
因此,问题转化为“给定若干个模式串,求在 $ [0,x-1] $ 的所有01串中包含给定模式串的串的数量”,典型的多模匹配问题。于是,我找到了“AC (Accepted) 自动机”算法。
AC自动机
AC自动机=用Trie组织多个模式串+KMP避免回溯(引自《算法竞赛》)
(大脑空空ing……)
(让我们先来一点前置知识……)
Trie(字典树)
字典树用于解决字符串匹配查找问题:在n个字符串中,查找某个串是否存在。如果使用暴力法,逐一匹配所有字符串,时间复杂度为 $ O(mn) $ ,其中m为字符串平均长度,显然会炸。
有没有更优的方法?有——字典树。
首先想想:我们平时是如何查字典的?
当然是按照首字母,然后第二个字母,第三个字母……直到找到想要的单词 (或证明这个单词并不存在) 。字典树的思想与之类似:

不难发现,每一个词都可以被存储为字典树上的一条链。通过对链进行遍历,我们可以在$ O(m) $的时间内找到一个单词。
对于本题,很显然字典树可以简化为一棵二叉树,每条链存储一个长度为k的回文01串。字典树主要有两个操作:插入和查询。
插入:
Code here
void insert(string s)
{
int now=0;//now是当前正在处理的字符编号
for(int i=0;s[i];i++){
int ch=s[i]-'a';//每一个字符对应的孩子编号
if(t[now].son[ch] == 0)//如果没有
tr[now].son[ch] = cnt++;
now = tr[now].son[ch];
t[now].num++;
if(i == s.size() - 1) tr[now].isend = true;//最后一个字符
}
}
查找:
Code here
int find(string s){
int now = 0;
for(int i = 0;s[i];i++){
int ch = s[i] - 'a';
if(t[now].son[ch] == 0) return 1;//字符太少
now = t[now].son[ch];
}
if(t[now].isend == false) return 1;//未结束
if(t[now].num == 0) return 1;//没有出现
return 0;//找到
}
这样,我们就可以快速确定“存在长度为k的模板串”的问题了。
(任务完成度:30% )
KMP算法:
KMP算法可以解决“在字符串中查找一个模式串”的问题:
先考虑普通的暴力做法(两字符串逐位比对),你会发现很多比对是重复的。
举个例子:
ABABCCABABDCAC
ABABD
比如第一次匹配到 ABAB 时,我们已经知道文本串前4位是 ABAB,模式串前4位也是 ABAB。
如果从第3位重新开始,那相当于又把 AB 比了一遍,完全没必要。
所以:
当失配发生时,我们不把模式串的指针退回起点,而是利用已经匹配过的信息,跳到某个合适的位置继续比。
问题是:how?
核心概念:前缀函数
对于模式串的前 i+1 个字符(0-based),找出一个最长的长度 L(L < i+1),使得这前 i+1 个字符的前 L 个字符和后 L 个字符完全相同。
举个栗子,模式串 ABABC:
| 位置 | 前缀子串 | 最长相等前后缀长度 |
|---|---|---|
| 0 | A | 0 |
| 1 | AB | 0 |
| 2 | ABA | 1("A") |
| 3 | ABAB | 2("AB") |
| 4 | ABABC | 0 |
所以 pi = [0, 0, 1, 2, 0]。
如何利用这个表加速匹配?
假设我们在文本串的某个位置 i,模式串已经匹配到位置 j(即模式串前 j 个字符都匹配成功)。
现在比较 text[i] 和 pattern[j]:
- 如果相等:i++,j++
- 如果不等:
- 若 j > 0,令 j = pi[j-1](即跳到之前匹配好的前缀位置),不移动 i
- 若 j == 0,说明一个都没匹配上,i++
这样,文本串的指针 i 永远不会回退,大大提高了效率。
代码实现(求前缀函数):
Code here
vector<int> get_pi(string p) {
int m = p.size();
vector<int> pi(m, 0);
for (int i = 1; i < m; i++) {
int j = pi[i-1];
while (j > 0 && p[i] != p[j]) j = pi[j-1];
if (p[i] == p[j]) j++;
pi[i] = j;
}
return pi;
}
匹配代码:
Code here
int kmp(string text, string pattern) {
vector<int> pi = get_pi(pattern);
int j = 0;
int cnt = 0;
for (int i = 0; i < text.size(); i++) {
while (j > 0 && text[i] != pattern[j]) j = pi[j-1];
if (text[i] == pattern[j]) j++;
if (j == pattern.size()) {
cnt++; // 找到一个匹配
j = pi[j-1]; // 继续找下一个
}
}
return cnt;
}
准备总装!
(任务完成度:60%)
AC自动机
AC自动机就是 Trie + KMP 的融合。它在Trie树上进行KMP的"失配跳转"思想,从而在一次遍历中匹配多个模式串。AC自动机就是 Trie + KMP 的融合。它在Trie树上进行KMP的"失配跳转"思想,从而在一次遍历中匹配多个模式串。
为什么需要它?
设想一下,如果我们有1024个回文模式串,对每个待匹配的01串都跑一遍KMP,复杂度是
O(1024×n),虽然n只有20,但后面DP时状态数会爆炸,不能每次都重新匹配。
AC自动机的厉害之处在于:把所有的模式串预处理好,匹配时走一遍自动机就能知道当前串是否包含了某个模式串。
构建步骤:
- 第一步:建Trie
- 把所有的回文串插入Trie树。这里每个节点只有0和1两个儿子。
- 第二步:补全fail指针
- 这是AC自动机的灵魂所在。fail指针的作用是:当匹配失败时,告诉我们应该跳到哪个节点继续匹配。
举个栗子:
假设模式串有 "010" 和 "10"。当我们在Trie上走到 "01" 时,下一个字符如果是 0,那就能匹配 "010";但如果是 1,匹配失败。此时我们不想回到根节点从头开始,因为 "01" 的后缀 "1" 正好是模式串 "10" 的前缀,所以跳到 "1" 对应的节点继续匹配就好了。
Code here
void build_fail() {
queue<int> q;
// 根的直接儿子fail指向根
for (int i = 0; i < 2; i++) {
if (tr[0].son[i]) {
tr[tr[0].son[i]].fail = 0;
q.push(tr[0].son[i]);
}
}
while (!q.empty()) {
int u = q.front(); q.pop();
// 如果fail指针指向的节点是"危险"的,当前节点也是危险的
// 因为后缀已经包含了某个模式串
tr[u].danger |= tr[tr[u].fail].danger;
for (int i = 0; i < 2; i++) {
int v = tr[u].son[i];
if (v) {
// 孩子的fail = 父亲fail的对应儿子
tr[v].fail = tr[tr[u].fail].son[i];
q.push(v);
} else {
// 优化:直接补全转移边,这样匹配时不用反复跳fail
tr[u].son[i] = tr[tr[u].fail].son[i];
}
}
}
}
(任务完成度:80%)
完结撒花!!!🌸💮🏵️🌺
不要点开它
//你问剩余的20%去哪里了?数位DP当然就留给读者也就是你慢慢思考了:)
#include <bits/stdc++.h>
using namespace std;
const int mod=1000000007;
struct Node
{
int ch[2],fail;
bool end;
Node()
{
ch[0]=ch[1]=-1;
fail=0;
end=false;
}
};
vector<Node>tr;
void insert(const string &s)
{
int u=0;
for(int i=0;i<s.size();i++)
{
char c=s[i];
int b=c-'0';
if(tr[u].ch[b]==-1)
{
tr[u].ch[b]=tr.size();
tr.push_back(Node());
}
u=tr[u].ch[b];
}
tr[u].end=true;
}
int main()
{
freopen("number.in","r",stdin);
freopen("number.out","w",stdout);
int n,k;
string P;
cin>>n>>k>>P;
int h=(k+1)/2;
tr.push_back(Node());
for(int mask=0;mask<(1<<h);mask++)
{
string s;
for(int i=h-1;i>=0;--i)
{
s.push_back(((mask>>i)&1)?'1':'0');
}
if(k%2==0)
{
for(int i=h-1;i>=0;--i)s.push_back(s[i]);
}
else
{
for(int i=h-2;i>=0;--i)s.push_back(s[i]);
}
insert(s);
}
queue<int>q;
tr[0].fail=0;
for(int c=0;c<2;c++)
{
if(tr[0].ch[c]!=-1)
{
tr[tr[0].ch[c]].fail=0;
q.push(tr[0].ch[c]);
}
else
{
tr[0].ch[c]=0;
}
}
while(!q.empty())
{
int u=q.front();
q.pop();
for(int c=0;c<2;c++)
{
int v=tr[u].ch[c];
if(v!=-1)
{
tr[v].fail=tr[tr[u].fail].ch[c];
if(tr[tr[v].fail].end)
{
tr[v].end=true;
}
q.push(v);
}
else
{
tr[u].ch[c]=tr[tr[u].fail].ch[c];
}
}
}
int S=tr.size();
vector<int>dp0(S,0),dp1(S,0);
vector<int>ndp0(S,0),ndp1(S,0);
dp0[0]=1;
for(int i=0;i<P.size();i++)
{
char ch=P[i];
fill(ndp0.begin(),ndp0.end(),0);
fill(ndp1.begin(),ndp1.end(),0);
for(int u=0;u<S;u++)
{
int v0=dp0[u],v1=dp1[u];
if(v0==0&&v1==0)continue;
for(int b=0;b<2;b++)
{
int ns=tr[u].ch[b];
if(tr[ns].end)continue;
if(v0)
{
if(ch=='?'||ch-'0'==b)
{
ndp0[ns]+=v0;
if(ndp0[ns]>=mod)ndp0[ns]-=mod;
}
if(b==0&&(ch=='?'||ch=='1'))
{
ndp1[ns]+=v0;
if(ndp1[ns]>=mod)ndp1[ns]-=mod;
}
}
if(v1)
{
int add=(ch=='?')?(int)(2LL*v1%mod):v1;
ndp1[ns]+=add;
if(ndp1[ns]>=mod)ndp1[ns]-=mod;
}
}
}
dp0.swap(ndp0);
dp1.swap(ndp1);
}
int ans=0;
for(int u=0;u<S;u++)
{
if(tr[u].end)continue;
ans+=dp1[u];
if(ans>=mod)ans-=mod;
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号