[NEFU ACM大一暑假集训 解题报告]字典树

本文介绍了如何使用AC自动机在Trie树上进行多模式匹配,通过暴力建立跳转关系和失配指针(fail指针)优化搜索过程。涉及的题目包括字符串处理、信息压缩和异或操作,展示了如何在实际场景中应用这些算法技术。
摘要由CSDN通过智能技术生成

[NEFU ACM大一暑假集训 解题报告]字典树

题目

A - L 语言

多模式匹配,AC自动机建立Trie图。不过这个题数据量很小,貌似可以暴力建立跳转关系,加上标记处理即可。
对于样例的AC自动机(Trie图)应该长下面这样,省略了所有结点到根结点的跳转
在这里插入图片描述
单纯Trie+暴力建立跳转关系+标记应该也可以实现。AC自动机在Trie树上使用KMP的思想进行多模式匹配。主要的话,优化了跳转关系的建立(fail指针)。
fail指针或者说失配指针其实就是一种反悔的操作。我们走某一条路径,走到一般发现走不下去了,就想要回去走其他路径,而失配指针直接建立fail边,让你跳转到要去的地方(上图红色的边),大致明白这个思想就行。
其实感觉如果AC自动机做难点主要在怎么写匹配过程(因为其他都是板子哈哈哈)
匹配求答案过程借鉴洛谷这篇题解

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<map>
#include<utility>
#include<set>
#include<vector>
using namespace std;

typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int,int>PII;
#define endl '\n'
//CHECK MULTIPLY INPUT	!!!
//NEW DATA CLEAN	!!!
//THINK > CODE	!!!
const int N=2E6+10;
int n,m;
int tr[N][26],cnt[N],idx;
char str[N];
int q[N],fail[N];
bool vis[N];
void insert(){
    int p=0;
    for(int i=0;str[i];i++){
        int t=str[i]-'a';
        if(!tr[p][t])tr[p][t]=++idx;
        p=tr[p][t];
    }
    cnt[p]=strlen(str);
}

void build(){
    int hh=0,tt=-1;
    for(int i=0;i<26;i++){//从第一层开始搜索
        if(tr[0][i])q[++tt]=tr[0][i];
    }
    while(hh<=tt){//BFS构建Trie图
        int t=q[hh++];
        for(int i=0;i<26;i++){
            int p=tr[t][i];
            if(!p)tr[t][i]=tr[fail[t]][i];
            else{
                fail[p]=tr[fail[t]][i];
                q[++tt]=p;
            }
        }
    }
}
int main(){
    scanf("%d%d",&n,&m);
	for(int i=0;i<n;i++){//构建Trie树
		scanf(" %s",str);
		insert();
	}
	build();//AC自动机建Trie图
	while(m--){//AC自动机匹配
		int res=0;
		scanf("%s",str);
		memset(vis,0,sizeof vis);
		vis[0]=1;
		for(int i=0,j=0;str[i];i++){
			int t=str[i]-'a';
			j=tr[j][t];//Trie图的某个结点
			int p=j;
			while(p){
				if (vis[i - cnt[p] + 1]){//判断之前是否存在一个合法前缀
                    vis[i + 1] = true;//标记一下
                    break;//一个优化,如果找到了就直接退出,不然会T掉最后一个点
                }
				p=fail[p];//向上回溯,防止漏答案
			}
			if(vis[i+1])res=i+1;
		}
		printf("%d\n",res);
		
	}
    return 0;
}

B - Secret Message 秘密信息

插入的时候记录每个结点经过的次数以及以该结点为结尾的数量。相减去掉重复部分即可。

#include<bits/stdc++.h>
using namespace std;
const int N=10010,M=500010;
int a[N],son[M][2],ed[M],pas[M],idx;
void insert(int* a,int k){
	int p=0;
	for(int i=0;i<k;i++){
		if(!son[p][a[i]])son[p][a[i]]=++idx;
		p=son[p][a[i]];
		pas[p]++;
	}
	ed[p]++;
}
int query(int* a,int k){
	int res=0,p=0;
	for(int i=0;i<k;i++){
		if(!son[p][a[i]])return res;
		p=son[p][a[i]];
		res+=ed[p];
	}
	return res+pas[p]-ed[p];
}
int main(){
	int n,m;cin>>n>>m;
	for(int i=0;i<n;i++){
		int k;cin>>k;
		for(int j=0;j<k;j++)cin>>a[j];
		insert(a,k);
	}
	for(int i=0;i<m;i++){
		int k;cin>>k;
		for(int j=0;j<k;j++)cin>>a[j];
		cout<<query(a,k)<<endl;
	}
	return 0;
}

C - The XOR-longest Path

先做D题。
根据异或前缀和性质: l , r 区 间 异 或 和 为 s u m [ r ] x o r s u m [ l − 1 ] l,r区间异或和为sum[r] xor sum[l-1] l,rsum[r]xorsum[l1]
那么,在trie树上的路径异或和等于两个叶子结点到根节点的异或和。
我们先用vector存图,再用dfs求解叶结点到根结点的异或和。这样问题就变成D题,求解最大异或对了。

#include<bits/stdc++.h>
using namespace std;
const  int N=1000010,M=N*31;
typedef long long LL;
typedef pair<LL,LL> PIL;
vector<PIL>root[N];
LL idx;
LL a[N],son[M][2];
void insert(LL x){
    LL p=0;
    for(LL i=31;i>=0;i--){
        LL &s=son[p][x>>i&1];
        if(!s)s=++idx;
        p=s;
    }
}
LL query(LL x){
    LL p=0,res=0;
    for(LL i=31;i>=0;i--){
        LL u=x>>i&1;
        if(son[p][!u]){
            res=res*2+1;
            p=son[p][!u];
        }
        else{
            res=res*2;
            p=son[p][u];
        }
    }
    return res;
}
void dfs(LL u,LL fa,LL sum){
    a[u]=sum;
    for(auto j:root[u]){
        if(j.first==fa)continue;
        dfs(j.first,u,j.second^sum);
    }
}
int main(){
    LL n;cin>>n;
    for(LL i=1;i<n;i++){
        LL u,v;LL w;cin>>u>>v>>w;
        root[u].push_back({v,w});
        root[v].push_back({u,w});
    }
    dfs(1,-1,0);
    for(LL i=1;i<=n;i++)insert(a[i]);
    LL ans=0;
    for(LL i=1;i<=n;i++)ans=max(ans,query(a[i]));
    cout<<ans;
    return 0;
}

D - The XOR Largest Pair

异或性质:相同为0,相异为1。
query查询过程优先走相反的即可。
Trie树每往下走一层,就相当于答案向左移动一次,如果存在相反的那么还要进一。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=31*N;
int a[N],son[M][2],idx;
void insert(int x){
    int p=0;
    for(int i=30;~i;i--){
        int &s=son[p][x>>i&1];
        if(!s)s=++idx;//创建新节点
        p=s;
    }
}
int query(int x){
    int res=0,p=0;
    for(int i=30;~i;i--){
        int s=x>>i&1;
        if(son[p][!s]){//看相反的是否存在
            res=res*2+1;//答案左移进一
            p=son[p][!s];
        }
        else{
            p=son[p][s];
            res=res*2;
        }
    }
    return res;
}
int main(void)
{
    cin.tie(0);
    int n;
    cin>>n;
    idx=0;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        insert(a[i]);
    }
    int res=0;
    for(int i=0;i<n;i++)
    {   
        res=max(res,query(a[i]));  
    }
    cout<<res<<endl;
}

E - Phone List

注意多组重置trie树啊啊啊
貌似只要用pas数组记录结点经过次数,然后查询的时候看是不是每个结点出现次数>=2次了,如果存在只出现一次的就不行。

#include<bits/stdc++.h>
using namespace std;
const int N=1E4+10,M=1e5+10;
int son[M][12],pas[M],idx;
char str[M][12];
bool flag;
void insert(char* str){
	int p=0;
	for(int i=0;str[i];i++){
		int ch=str[i]-'0';
		if(!son[p][ch])son[p][ch]=++idx;
		p=son[p][ch];
		pas[p]++;
	}
}
bool query(char* str){
	int p=0;
	for(int i=0;str[i];i++){
		int u=str[i]-'0';
		if(pas[son[p][u]]==1)return false;
		p=son[p][u];
	}
	return true;
}
int main(){
	int T;cin>>T;
	while(T--){
		int n;cin>>n;
		flag=true;
		idx=0;
		memset(pas,0,sizeof pas);
		memset(son,0,sizeof son);
		for(int i=0;i<n;i++){
			cin>>str[i];
			insert(str[i]);
		}
		for(int i=0;i<n;i++){
			if(query(str[i])){
				flag=false;
				break;
			}
		}
		if(flag)puts("YES");
		else puts("NO");
	}
}

F - Xor sum

类似数位DP的思想,然后跪在代码实现了。
要找出最短的连续子序列,其异或和不小于 k。
首先预处理异或前缀和,将问题转化为 s u m [ R ] x o r s u m [ L ] > = k sum[R] xor sum[L]>=k sum[R]xorsum[L]>=k
为了求最短,我们每次将i固定,找到最大的满足条件的j。
原本写了 s u m [ L ] > = k x o r s u m [ R ] sum[L]>=k xor sum[R] sum[L]>=kxorsum[R]的结果写崩了,后来借鉴这篇题解还是按照原来的式子处理。
我们从高位开始构建01trie树,每次关注最高位置。

如果k的当前位为1,a[i]当前位为1:往0走
如果k的当前位为1,a[i]当前位为0:往1走
上面这两个情况属于,我们最多只能和k相等,是需要继续向下搜索的。

如果k的当前位为0,a[i]当前位为1:往0走
如果k的当前位为0,a[i]当前位为0:往1走
这两种情况属于,我们可以选择=k和>k两种情况,对于>k的我们可以直接获取答案(插入的时候使用预处理出这个结点往下最大下标,存ma里),对于=k的情况我们需要继续向下搜索。

如果你对数位DP有些了解就会有些感觉,搜索树大概是这样的,>k获取答案,=k继续向下搜索
在这里插入图片描述

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<map>
#include<utility>
#include<set>
#include<vector>
using namespace std;

typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
#define MULINPUT
/*DATA & KEY

*/
int T;
const int N=100010,M=N*31;
int a[N],son[M][2],ma[M];
int idx=0;
void insert(int x,int id){
	int p=0;
	for(int i=31;~i;i--){
		int &s=son[p][x>>i&1];
		if(!s)s=++idx;
		p=s;
		ma[p]=max(ma[p],id);
	}
}
int query(int x,int mid){
	int p=0,ans=-1;
	for(int i=31;~i;i--){
		int u1=x>>i&1;//a[i]的当前位
		int u2=mid>>i&1;//k的当前位
		if(u2){//如果k当前位置为1
			if(u1==1)p=son[p][0];
			else p=son[p][1];
		}
		else{//如果k当前位置为0
			if(u1==1){
				if(ma[son[p][0]])ans=max(ans,ma[son[p][0]]);//>k部分答案
				p=son[p][1];//=k部分继续向下搜索
			}
			else{
				if(ma[son[p][1]])ans=max(ans,ma[son[p][1]]);
                p=son[p][0];
			}
		}
		if(p==0)break;
	}
	if(p!=0)ans=max(ans,ma[p]);
	return ans;
}
void init(){
	for(int i=0;i<=idx+5;i++){
		son[i][0]=son[i][1]=ma[i]=0;
	}
	idx=0;
}
int main(){
	int T;scanf("%d",&T);
	while(T--){
		int n,k;scanf("%d%d",&n,&k);
		init();
		for(int i=1;i<=n;i++){
			scanf("%d",&a[i]);
			a[i]^=a[i-1];
		}
		insert(0,1);
		int ans=2e9,p=-1;
		for(int i=1;i<=n;i++){
			int l=query(a[i],k);
			if(l!=-1&&i-l+1<ans)ans=i-l+1,p=l;
			insert(a[i],i+1);
		}
		if(ans==2e9)printf("-1\n");
		else printf("%d %d\n",p,p+ans-1);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值