ACwing算法备战蓝桥杯——Day11——Tire

Tire(字典树)作为一种数据结构。

有两个作用,能够高效地储存和查找字符串。

模板:

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
void Insert(char *str){
    int p=0;    
    for(int i=0;str[i];i++){
        int c=str[i]-'a''
        if(!son[p][c]) son[p][c]=++idex;
        p=son[p][c];    
    }
    cnt[p]++;
}

// 查询字符串出现的次数
int query(char*str){
    int p=0;
    for(int i=0;str[i];i++){
    int c=str[i]-'a';
    if(!son[p][c]) return 0;
        p=son[p][c];
    }
    return cnt[p];
}

模板题:

维护一个字符串集合,支持两种操作:
I x 向集合中插入一个字符串 x;
Q x 询问一个字符串在集合中出现了多少次。
共有 N 个操作,所有输入的字符串总长度不超过 105,字符串仅包含小写英文字母。

输入格式
第一行包含整数 N,表示操作数。

接下来 N 行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。

输出格式
对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。

每个结果占一行。

数据范围
1≤N≤2∗104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
代码+题解:
#include <iostream>
#include <string>
using namespace std;
const int N=1e5+10;//注意题目描述的是所有n个插入操作的字符串加起来的长度不超过1e5,也就是结点的个数不超过1e5
int son[N][26],cnt[N],idex;//idex记录当前有几个结点,son储存当前结点对应的下标是什么,cnt记录以当前结点结尾的字符串有几个
char str[N];
int Find(char* str){
    int p=0;//从根节点遍历
    for(int i=0;str[i];i++){
        char c=str[i];
        if(!son[p][c-'a']) return 0;
        p=son[p][c-'a'];
    }
    return cnt[p];
}
void Insert(char* str){
    int p=0;//插入操作建立在遍历的基础上,基本上一样,加上几个步骤就行
    for(int i=0;str[i];i++){
        int a=str[i]-'a';
        if(!son[p][a]) son[p][a]=++idex;
        p=son[p][a];
    }
    cnt[p]++;
}
int main(){
    int T;
    cin>>T;
    while(T--){
        char op[2];
        scanf("%s%s",op,str);//不能用scanf(%s,string); 
        if(op[0]=='I'){
            Insert(str);
        }
        else printf("%d\n",Find(str));
    }
    return 0;
}

刷题:

最大异或对:

思路:先用暴力求解,然后用数据结构优化

在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?

输入格式
第一行输入一个整数 N。

第二行输入 N 个整数 A1~AN。

输出格式
输出一个整数表示答案。

数据范围
1≤N≤105,
0≤Ai<231
输入样例:
3
1 2 3
输出样例:
3

代码及题解:

暴力:

能正确得解,但会超时.

时间分析:时间复杂度为O(n^2),数据大小为为1e5,时间限制为1秒,c++运行每秒1e9,所以会超时.

一开始不知道非二进制数可以直接异或,非得把数据处理成二进制后再异或的,写出了下面这堆代码。

#include <iostream>
#include <cmath>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=1e5+10;
int store[N][34];
int anws[34];
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++) scanf("%d",&store[i][0]);
    for(int i=0;i<n;i++){//预处理成二进制数
        int temp=store[i][0];
        int j=1;
        while(temp){//左边开始储存低位
            int o=temp%2;
            temp/=2;
            store[i][j++]=o;
        }
        store[i][j]=-1;//作为结束
    }
    int res=-1;//结果
    for(int i=0;i<n-1;i++){
        for(int j=i+1;j<n;j++){//两层循环,枚举每两个数的异或值,时间复杂度n^2;
            int cnt=0;//遇到-1的次数
            int k=1;//从store[][1]开始存储的二进制数,以-1结尾
            memset(anws,0,4*34);
            while(cnt!=2){
                if(store[i][k]==-1){//遇到-1时特殊处理
                    cnt++;
                    if(cnt==2){
                        anws[k-1]=-1;
                        break;
                    }
                    anws[k-1]=store[j][k]^0;
                    k++;
                    continue;
                }
                if(store[j][k]==-1){
                    cnt++;
                    if(cnt==2){
                        anws[k-1]=-1;
                        break;
                    }
                    anws[k-1]=store[i][k]^0;
                    k++;
                    continue;                
                    
                }
                if(cnt==2){
                    anws[k-1]=-1;
                    break;//两个都遍历完了就退出
                }                
                if(store[i][k]==store[j][k]){//一般情况,相同存0,不同存1
                    anws[k-1]=0;
                    k++;
                }
                else{
                    anws[k-1]=1;
                    k++;
                }
            }
            int sum=0;
            for(int m=0;anws[m]!=-1;m++){
                if(anws[m]) sum+=pow(2,m);
            }
            res=max(res,sum);//取较大的那一个
        }
    }
    cout<<res<<endl;
    return 0;
}

其实只要两个数直接异或就行😅,代码如下:

#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int store[N];
int main(){
    int n,res=-1;
    cin>>n;
    for(int i=0;i<n;i++) scanf("%d",&store[i]);
    for(int i=0;i<n-1;i++){
        for(int j=i+1;j<n;j++){
            res=max(res,store[i]^store[j]);
        }
    }
    cout<<res<<endl;
    return 0;
}
tire优化:
#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;
const int N=3e6+10;//二叉树的深度最大为32,极限是要有2的32次方个结点。但是数据量有10的5次方个,假设每个数据的每位都要新建一个结点,最大就是3.1*10的六次方个结点。最高位是符号位,所以最多为31位.
int son[N][2],idex;
//insert(),Query()两个函数对应Tire数据结构的两个性质,能够快速地存储和查找字符串
void Insert(int x){//建立tire树
    int p=0;
    for(int i=30;i>=0;i--){
        int &a=son[p][x>>i&1];
        if(!a) a=++idex;
        p=a;
    }
}
int Query(int x){//查找
    int p=0,res=0;
    for(int i=30;i>=0;i--){
        int a=x>>i&1;
        if(son[p][!a]){//如果两个条件都不满足,说明此时没有任何结点可以往下走,继续把次数循环完就可以,不用特判,代码更加简洁
            res+=(1<<i);
            p=son[p][!a];
        }
        else p=son[p][a];
    }
    return res;
}
int main(){
    int n;
    cin>>n;
    vector<int> s;//存储数据
    while(n--){//建立tire树
        int temp;
        scanf("%d",&temp);
        s.push_back(temp);
        Insert(temp);
    }
    int res=-1;
    for(int i=0;i<s.size();i++){//枚举每个数据,这里结束条件不要用n,因为while循环n自减了
        res=max(res,Query(s[i]));
    }
    cout<<res<<endl;
    return 0;
}

”最大异或对“做题总结:

tire树经常用到位运算,而且位运算很实用,可以很大程度上简化代码


最大异或和:

给定一个非负整数数列 a,初始长度为 N。

请在所有长度不超过 M 的连续子数组中,找出子数组异或和的最大值。

子数组的异或和即为子数组中所有元素按位异或得到的结果。

注意:子数组可以为空。

输入格式
第一行包含两个整数 N,M。

第二行包含 N 个整数,其中第 i 个为 ai。

输出格式
输出可以得到的子数组异或和的最大值。

数据范围
对于 20% 的数据,1≤M≤N≤100
对于 50% 的数据,1≤M≤N≤1000
对于 100% 的数据,1≤M≤N≤105,0≤ai≤231−1
输入样例:
3 2
1 2 4
输出样例:
6

代码及题解:

(注意,一个难点就是理解题意,异或和是指将一段区间的数全部异或起来,如:a^b^c^........x^y^z)

直观暴力解法:

三层循环

//会超时,时间复杂度为O(n*n*n)
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int a[N];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=0;i<n;i++) scanf("%d",&a[i]);
    int res=-1;
    for(int i=0;i<n;i++){//区间左端点
        for(int j=i;(j<i+m)&&(j<n);j++){//区间右端点
             int anws=0;
             for(int k=i;k<=j;k++){//枚举区间
                 anws^=a[k];
                 res=max(res,anws);//每次异或一个新值都要比较大小   
            }  
        }
    }
    cout<<res<<endl;
    return 0;
}
朴素优化暴力解法:

因为在二重循环里,区间长度是左到右递增的,所以可以用一个变量存之前异或过的,不要再重复运算

//也会超时,时间复杂度为O(n*n)
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int a[N];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=0;i<n;i++) scanf("%d",&a[i]);
    int res=-1;
    for(int i=0;i<n;i++){//区间左端点
        int anws=0;
        for(int j=i;(j<i+m)&&(j<n);j++){//区间右端点 
               anws^=a[j];
               res=max(res,anws);
            }
        }
    }
    cout<<res<<endl;
    return 0;
}

tire优化:

/*
思路:
    因为一个数被异或两次等于没有操作,即:(a^b^c)(a^b^c^d^e^f)等价于(d^e^f),
利用这个就可以实现类似于前缀和的储存数据的方法。用一个数组s[i]来储存前i个数据的异
或和.我们的目的是寻找原始数组a[n]一段区间里的异或和的最大值,现在可以转换成找s[n]
的一段区间里两个数的最大异或和.但是不用遍历每一个,不然时间复杂度还是n*n,现在
用tire树优化,只需遍历字典树一次就可以找到最优解,将时间复杂度降低为n*logn。
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N=6200000;
int son[N][2],cnt[N],idex;
int s[N];
void Insert(int x,int v){//非标准tire插入函数
    int p=0;
    for(int i=30;i>=0;i--){//每个数固定插入31个结点
        int a=(x>>i&1);
        if(!son[p][a]) son[p][a]=++idex;
        p=son[p][a];
        cnt[p]+=v;//当操作是插入时,x的每位对应的结点都会cnt[]加一,表示路径存在
    }
}
int Query(int x){//非标准tire查找操作
    int p=0,res=0;
    for(int i=30;i>=0;i--){
        int a=x>>i&1;
        if(cnt[son[p][!a]]){
            res=res*2+1;
            p=son[p][!a];
        }
        else{//一个结点至少会有一个分支,因为插入时就已经建立了。
            res*=2;
            p=son[p][a];
        }
    }
    return res;
}
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        scanf("%d",&s[i]);
        s[i]^=s[i-1];
    }
    int res=0;//空数组
    Insert(s[0],1);//因为在第一次用删除操作时会用到s[0],所以s[0]要插入。
    for(int i=1;i<=n;i++){//动态维护集合的元素数量不超过m(对应s数组就是两区间间隔不超过m个),
                            映射到前缀和数组的下标就是区间左端点就是i-m,需要减去左端点左边的一个数据
        if(i>m) Insert(s[i-m-1],-1);
        Insert(s[i],1);
        int anws=Query(s[i]);
        res=max(res,anws);
    }
    cout<<res<<endl;
    return 0;
}

在做题中遇到的有关scanf()输入的问题:

  • scanf()读入单个字符不会过滤空白符,但是读入字符串就能过滤;

  • 可以用scanf("%*c")抛弃一个数据,在%号后加上一个“*”,表示抛弃一个数据;

  • scanf()具有一个返回值,输入流不存在输出-1.输入类型不匹配输出0,输入正确返回正确数据的个数;

最近开学考,耽误了几天,接着补回来!!!加油加油!!!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

切勿踌躇不前

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值