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;
}
posted @ 2026-08-19 11:26  灿若星尘  阅读(22)  评论(1)    收藏  举报