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

Posted 鱼竿钓鱼干

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[NEFU ACM大一暑假集训 解题报告]字典树相关的知识,希望对你有一定的参考价值。

[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 以上是关于[NEFU ACM大一暑假集训 解题报告]字典树的主要内容,如果未能解决你的问题,请参考以下文章

[NEFU ACM大一暑假集训 解题报告]前缀和与差分

[NEFU ACM] 2020级暑期训练 解题报告

2018年暑假ACM个人训练题9(动态规划)解题报告

acm经验(转)

2017暑假集训前总结和规划

停更五月,而今再出发