acwing模板整理(第二讲)(数据结构)

目录

一.单链表

链表可以实现以下几个操作:

二.栈

三.队列

四.单调栈:

单调栈原理:

五.单调队列

单调队列思路:

六.KMP字符串

七.Trie(字典)树

1.Trie字符串统计(快速存储单词和查找单词数量以及是否存在)

2.Trie也可以用来记录二进制树

八.并查集(重点!!!)

九.哈希表


一.单链表

单链表一共由四部分组成:

1.head:表头

2.e[i]:节点权值

3.ne[i]:下标为i的节点的下一个节点的下标

4.idx:节点的下标,并且表示一共开创了多少个节点

链表可以实现以下几个操作:

1.向链表表头插入一个数x:只要让x的下一个节点下标指向表头,同时更新表头节点下标即可

void add_head(int x){
    e[idx]=x,ne[idx]=head,head=idx++;
}

2.在节点下标为k的节点后面插入的一个数x:

只需要让x的ne[]指向节点下标为k的节点后一个节点下标,同时让下标为k的节点指向的下一个节点的下标指向x的节点下标即可

void add(int k,int x){
    e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}

3.删除节点下标为k的节点的后一个节点:

只需要让节点下标为k的节点的ne[]跳过后一个节点指向下下个节点下标即可

void remove__(int k){
   ne[k]=ne[ne[k]];
}

二.栈

1.栈(stack)的操作:

栈是先进后出的容器

1.栈的定义:

stack<int> stk

2.stk.push(x):插入x

   stk.pop():弹出栈顶元素

   stk.top():查询栈顶元素

   stk.size():栈中元素的个数

   stk.empty():查询栈是否为空(栈为空返回true,栈不为空返回false)

2.栈的应用:表达式求值

题目链接:活动 - AcWing

小技巧:

1.用unordered_map存储符号的优先级

unordered_map<char,int> h={{'+',1},{'-',1},{'*',2},{'/',2}};

2.isdigit(c)判断字符c是否是数字

题解:

1.计算表达式,需要考虑符号的优先级,先计算括号里面的,之后再计算优先级高的

2.因此需要建两个栈,一个符号栈,一个数字栈

3.如果符号栈顶的符号优先级小于即将入栈的符号的优先级,则先不计算,让优先级高的符号入栈即可,如果符号栈栈顶符号优先级大于等于即将入栈的符号的优先级,则需要先将这个优先级大的符号计算,直到栈为空或者栈顶符号的优先级小于即将入栈的符号的优先级时,就将这个符号入栈

4.如果碰到左括号,直接使之入栈即可,碰到右括号,就向做一直计算(因为已经保证了栈内左面的优先级一定小于右边的优先级),直到碰到左括号时,就结束运算,再次按照顺序遍历

5.最后不要忘记将栈内存留的符号一定要清理(计算)完;

代码模板如下:

#include<bits/stdc++.h>
using namespace std;

stack<int> num;
stack<char> op;



void eval(){
    int b=num.top();
    num.pop();
    int a=num.top();
    num.pop();
    char o=op.top();
    op.pop();
    
    int r=0;
    if(o=='+') r=a+b;
    else if(o=='-') r=a-b;
    else if(o=='*') r=a*b;
    else if(o=='/') r=a/b;
    
    num.push(r);
}

int main(){
    unordered_map<char,int> h={{'+',1},{'-',1},{'*',2},{'/',2}};
    string str;
    cin>>str;
    for(int i=0;i<str.size();i++){
        auto c=str[i];
        if(isdigit(c)){
            int x=0,j=i;
            while((j<str.size())&&isdigit(str[j])){
                x=x*10+str[j]-'0';
                j++;
            }
            num.push(x);
            i=j-1;
        }
        
    
        else if(str[i]=='(') op.push(str[i]);
     
        else if(str[i]==')'){
            while(op.top()!='('){
                eval();
            }
            op.pop();
        }
    
        else {
            while(op.size()&&h[op.top()]>=h[str[i]]){
                eval();
            }
            op.push(str[i]);
        }
          
    }
    
    while(op.size()) eval();
    
    cout<<num.top()<<endl;
    
}

三.队列

队列是先进先出(队尾进,队头出)的容器:

1.队列的定义:

deque<int> q;

2.队列的操作:

q.front():查询队头元素

q.back():查询队尾元素

q.pop():弹出队头元素

q.push():向队尾压入元素

q.size():返回队列的大小

q.empty():查询队列是否为空

四.单调栈:

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

单调栈原理:

(1)如果第i个数比左边的数大,则在后边这个第i个数可能为最小值,因此需要将他入栈,但是如果第i个数比左边的数小,则这个数左边的数字在后面一定用不到,那么就可以将这个数弹出,直到栈为空或者小于第i个数为止,这样操作之后栈里的元素就变成了单调递增的数

代码如下:

stack<int> stk;

int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        int x;
        cin>>x;
        while(stk.size()&&stk.top()>=x) stk.pop();
        if(stk.size()) cout<<stk.top()<<" ";
        else cout<<-1<<" ";
        stk.push(x);
    }
    
}

五.单调队列

题目链接:154. 滑动窗口 - AcWing题库  

单调队列思路:

1.如果输出最小值:在范围内如果左边的数比右边的数大,则左边的数一定没有用处,因此可以弹出,如果左边的数比右边的数小,则需要压入右边的数,因为右边的数可能会成为后边的最小值

2.如果输出最大值时,与上述思路相反即可

求最小值代码如下:

 for(int i=0;i<n;i++){
        if(q1.size()&&q1.front()<i-k+1) q1.pop_front();
        while(q1.size()&&a[q1.back()]>=a[i]) q1.pop_back();
        q1.push_back(i);
        if(q1.back()>=k-1) cout<<a[q1.front()]<<" ";
    }

求最大值代码如下:

 for(int i=0;i<n;i++){
         if(q2.size()&&q2.front()<i-k+1) q2.pop_front();
         while(q2.size()&&a[q2.back()]<=a[i]) q2.pop_back();
         q2.push_back(i);
         if(q2.back()>=k-1) cout<<a[q2.front()]<<" ";
     }
    

六.KMP字符串

kmp题目链接:活动 - AcWing

kmp最透彻链接推荐:AcWing 831. KMP字符串 - AcWing

kmp主要由两个函数构成,求ne[]数组的函数和求相同字符串的函数

ne[i]数组就是i之前前缀与后缀相同的最大长度

分成函数的KMP算法写法
nex函数:(自己和自己匹配就从2开始)
1.不退到头并且不匹配就退
2.匹配就j++
3.更新ne[i]=j
kmp函数:(为了凑出s[i]!=p[j+1],i从1开始)
1.不退到头并且不匹配就退
2.匹配就j++
3.匹配完成再退重开

代码如下:

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+10,M=1e6+10;
char p[N],s[M];
int n,m,ne[N];

void nex(){
    for(int i=2,j=0;i<=n;i++){
        while(j&&p[i]!=p[j+1]) j=ne[j];
        if(p[i]==p[j+1]) j++;
        ne[i]=j;
    }
}

void kmp(){
    for(int i=1,j=0;i<=m;i++){
        while(j&&s[i]!=p[j+1]) j=ne[j];
        if(s[i]==p[j+1]) j++;
        if(j==n){
            cout<<i-n<<" ";
            j=ne[j];
        }
    }
}

int main(){
    cin>>n>>p+1>>m>>s+1;
    nex();
    kmp();

}

七.Trie(字典)树

字典树作用:

高效的存储和查找字符串集合,主要通过利用字符串的公共前缀来减少查询时间

字典树的三个基本性质(来自百度百科

1.根节点不包含字符,除根节点外每一个节点都只包含一个字符;

2.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;

3.每个节点的所有子节点包含的字符都不相同。

1.Trie字符串统计(快速存储单词和查找单词数量以及是否存在)

题目链接:活动 - AcWing

son[p][i]代表一个节点的多个儿子节点

p代表某个节点,i代表这个节点的第i个儿子节点

一共包括插入和查询两个操作:

插入操作代码如下:

int insert(char str[]){
    int p=0;                        //根节点
    for(int i=0;str[i];i++){         
         int u=str[i]-'a';            //将字母转化成数字0--25记录

        if(!son[p][u]) son[p][u]=++idx;   //如果没有这个节点,就新建一个,节点数量idx+1

        p=son[p][u];             //将指针更新到这个新的节点
    }
    cnt[p]++;                    //单词最后一个字母次数+1
}

查询单词次数代码如下:

int query(char str[]){
    int p=0;
    for(int i=0;str[i];i++){
        int u=str[i]-'a';
        if(!son[p][u]) return 0;      //如果没有开创这个子节点,就说明没有这个单词

        p=son[p][u];           
    }
    return cnt[p];                //返回单词最后一个字母的数量,即单词数量
} 

2.Trie也可以用来记录二进制树

题目链接:143. 最大异或对 - AcWing题库

将十进制数字转化为二进制数字并插入字典树代码:

void insert(int x){
    int p=0;
    for(int i=30;i>=0;i--){
        int u=x>>i&1;
        if(!son[p][u]) son[p][u]=++idx;
        p=son[p][u];
    }
}

查询与x异或最大的二进制数并转化为十进制数:

int query(int x){
    int p=0,res=0;
    for(int i=30;i>=0;i--){
        int u=x>>i&1;
        if(son[p][!u]) 
        {
            res=(res<<1)+!u;
            p=son[p][!u];
        }
        else
        {
            res=(res<<1)+u;
            p=son[p][u];
       }
        }
        return res;
}

八.并查集(重点!!!)

1.合并集合

题目链接:活动 - AcWing

并查集主要是通过祖宗节点将一个集合里的点联系起来,并且通过祖宗节点来插入,合并集合,通过查询是否是同一个祖宗节点来判断是否在一个集合

find(x)是查询x所在集合的祖宗节点,通过递归来找祖宗节点

并查集核心操作:

int find(int x){
    if(p[x]!=x) p[x]=find(p[x]);   //查找祖宗节点+路径压缩
    return p[x];
}

路径压缩就是找到这个节点后,将这个节点直接连接到祖宗节点上,也就是让节点父节点直接变成祖宗节点,以后再找的时候就不用一个一个遍历。

2.计算连通块中点的数量(并查集模板的应用)

题目链接:活动 - AcWing

3.食物链!!!(并查集额外信息的维护,带权值的并查集)

本题最重要的是要维护节点到祖宗节点的距离,并且在路径压缩的时候,在将节点直接与祖宗节点连接后,还要记录这个节点到祖宗节点的距离。

设d[x]为x到其父节点的距离,因此路径压缩时,要在x节点与祖宗节点连接后也要改变同时改变更新d[x]的值为x到祖宗节点(也就是路径压缩后的父节点)的距离。

利用递归更新d[x];

核心代码:

int find(int x){
    if(p[x]!=x){
     //先递归到祖宗节点再将路径距离相加
     //如果先加再递归到祖宗节点,
      //只能将父亲节点与父亲节点的父亲节点之间的距离加起来,
      //而不能将父亲节点到祖宗节点的距离加起来,所有会计算错误
      int t=find(p[x]);   
        d[x]+=d[p[x]];  //由于上面先递归到了祖宗节点,因此这里会从祖宗节点的距离开始累加
        p[x]=t;        //将父亲节点更新为祖宗节点,即路径压缩
    }
    return p[x];       //返回祖宗节点
}

九.哈希表

1.模拟散列表(用开放寻址法)(不需要排序的离散化)(减小范围)

题目连接:活动 - AcWing

建h[]数组,初始化为无穷,每次将x映射到下标为t的位置上面,如果h数组中有x,返回的t则是x的下标,如果x不存在,返回的则是应该将x存在的位置的下标。

find(x)函数返回下标

find()函数代码:

(N是大于h数组容量的质数)h数组需要初始化为无穷大

每次从t开始找,如果找到结尾还没找到就再从头开始找。

int find(int x)
{
    int t = (x % N + N) % N;
    while (h[t] != null && h[t] != x)
    {
        t ++ ;
        if (t == N) t = 0;
    }
    return t;
}

2.字符串哈希

题目链接:活动 - AcWing

思路就是将字符串转化为p进制的数,然后通过取模2的64次方来离散化为范围内的数,之后利用前缀和来计算某一区间的哈希值,通过比较两个区间的哈希值即可判断是否相等

小技巧:

1.用unsigned long long 来存储哈希值,溢出后就相当于对2的64次方取模,用p数组先预处理出p的多少次方

2.P需要取131或者13331,因为这时所引起冲突概率最小

对p数组预处理以及求哈希值前缀和h数组代码如下:

            p[0]=1;                //p[0]=1,便于求次方
    for(int i=1;i<=n;i++){
        p[i]=p[i-1]*P;    
     //求前i个字符串哈希值,不要忘记加上str[i],因为此时str[i]权值为0,因此不用乘以p    
        h[i]=h[i-1]*P+str[i];    
    }

求l--r区间的哈希值大小代码:

ULL get(int l,int r){
 return h[r]-h[l-1]*p[r-l+1];   //l--r的哈希值
}

完整代码如下:

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+10,P=131;    //P取131或者13331冲突概率最小

typedef unsigned long long ULL;   //溢出后就相当于对2的64次方取模

char str[N];                     

ULL h[N],p[N];          //p[i]存的是P的i次方,h[i]存的是前i个字符串的哈希值


ULL get(int l,int r){
 return h[r]-h[l-1]*p[r-l+1];   //l--r的哈希值
}

int main(){
    int n,m;
    cin>>n>>m>>(str+1);

    p[0]=1;                   //p[0]=1,便于求次方
    for(int i=1;i<=n;i++){
        p[i]=p[i-1]*P;           
        h[i]=h[i-1]*P+str[i];      //求前i个字符串哈希值,不要忘记加上str[i],因为此时str[i]权值为0,因此不用×p
    }

    while(m--){
        int l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        if(get(l1,r1)==get(l2,r2)) cout<<"Yes\n";
        else cout<<"No\n";
    }
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值