AcWing 算法基础课第二章(C++详解版 思路清晰)

 二、数据结构

(一).链表与邻接表

1.单链表:可实现图和树(AcWing 826.单链表)

简介:在算法竞赛中链表多为静态链表,即利用数组实现;而不是申请空间(动态链表)

(1)算法思想:

        想象一个由head指针依次指向多个结点,形成一个链条。每个结点具有e[](结点的数值),ne[](指向的下一个结点的序号)。

(2)代码实现思路:

        首先设置e[]  ne[]两个数组,设置idx表示当前点的序号。

        初始化链表:将head头指针指向的结点序号设为-1,idx指向第一个结点,序号为0;

        插入在头结点后面:先将该点的值赋值给e[idx],再将该点的next指针指向head的序号,即ne[idx]=head,再将head结点的序号设为该结点的序号,即ne[head]=idx,最后将idx指向下一个要插入的结点

        插入在下标为k的结点后面:先将该点的值赋值给e[idx],再将该点的next指针指向k结点的next指针,即ne[idx]=ne[k],再将k结点的next指针指向该结点,即ne[k]=idx,最后将idx指向下一个要插入的结点(ps:若想在某一结点前面插入只能从头遍历)

        删除下标为k的后面的结点:将k结点的next指针指向k结点的下一个结点的下一个结点,即ne[k]=ne[ne[k]]

        因序号idx从0开始,因此第k个结点后面的结点,即第k-1个结点后面的结点。

(3)代码实现:

#include <iostream>
using namespace std;
const int N=1e6+10;
int e[N],ne[N],idx,head;
//初始化
void init()
{
    head=-1;
    idx=0;
}
//插入在头节点后面
void add_to_head(int x)
{
    e[idx]=x;
    ne[idx]=head;
    head=idx;
    idx++;
}
//插入在第K个节点后面
void add(int k,int x)
{
    e[idx]=x;
    ne[idx]=ne[k];
    ne[k]=idx;
    idx++;
}
//删除第K个结点后面的结点
void remove(int k)
{
    ne[k]=ne[ne[k]];
}
int main() {
    int m;
    cin>>m;
    init();
    while(m--)
    {
        char s;
        cin>>s;
        if(s=='D')
        {
            int k;
            cin>>k;
            if(k==0) head = ne[head];
            else     remove(k-1);
        }
        else if(s=='H')
        {
            int x;
            cin>>x;
            add_to_head(x);
        }
        else
        {
            int k,x;
            cin>>k>>x;
            add(k-1,x);
        }
    }
    for(int i=head;i!=-1;i=ne[i]) cout<<e[i]<<' ';
    return 0;
}

2.双链表(AcWing 827.双链表)

(1)算法思想:

        在单链表的基础上,将ne[]换成左右两个指针数组l[],r[],依次指向结点的左节点和右节点

(2)代码实现思路:

        整体思路与单链表一致

        首先设置e[]  l[] r[]两个数组,设置idx表示当前点的序号。

        初始化链表:将头节点设为序号0,尾结点设为序号1,使得头节点的r[0]=1,尾结点的l[1]=0;

        插入在下标为k的结点后面:先将该点的值赋值给e[idx],再将该点的r指针指向k结点的r指针,即r[idx]=r[k],将该点的l指针指向k结点,即l[idx]=k,再将k结点的右节点的l指针指向该结点,即l[r[k]]=idx,最后将将k结点的r指针指向该结点,即r[k]=idx。

        删除下标为k的后面的结点:将k结点的左节点的r指针指向k结点的右节点,即r[l[k]]=r[k],将k结点的右节点的l指针指向k结点的左节点,即l[r[k]]=l[k]

        注意:由于idx从2开始,因此第k个插入的点后面相当于k+1。

(3)代码实现:

#include <iostream>
#include <string>
using namespace std;
const int N=1000010;
int r[N],l[N],e[N],idx,head;
int n;
void init()
{
    r[0]=1;
    l[1]=0;
    idx=2;
}
void add(int k,int x)
{
    e[idx]=x;
    r[idx]=r[k];
    l[idx]=k;
    l[r[k]]=idx;
    r[k]=idx;
    idx++;
}
void remove(int k)
{
    l[r[k]]=l[k];
    r[l[k]]=r[k];
}
int main()
{
    cin>>n;
    init();
    while(n--)
    {
        string op;
        cin>>op;
        if(op=="L")
        {
            int x;
            cin>>x;
            add(0,x);
        }
        else if(op=="R")
        {
            int x;
            cin>>x;
            add(l[1],x);
        }
        else if(op=="D")
        {
            int k;
            cin>>k;
            remove(k+1);
        }
        else if(op=="IL")
        {
            int k,x;
            cin>>k>>x;
            add(l[k+1],x);
        }
        else 
        {
            int k,x;
            cin>>k>>x;
            add(k+1,x);
        }
    }
    for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";
}

(二).栈与队列

1.栈(AcWing 828.模拟栈)

(1)算法思想:

        先进后出

(2)代码实现:

#include <iostream>
#include <string>
using namespace std;
const int N=1000010;
int st[N],tt;
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        string op;
        cin>>op;
        if(op=="push")
        {
            int x;
            cin>>x;
            st[++tt]=x;
        }
        else if(op=="pop")
        {
            tt--;
        }
        else if(op=="query")
        {
            cout<<st[tt]<<endl;
        }
        else
        {
            if(tt>0) puts("No");
            else puts("Yes");
        }
    }
    return 0;
}

2.队列(AcWing 829.模拟队列)

(1)算法思想:

        先进先出

(2)代码实现:

#include <iostream>
#include <string>
using namespace std;
const int N=1000010;
int queue[N],hh,tt=-1;
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        string op;
        cin>>op;
        if(op=="push")
        {
            int x;
            cin>>x;
            queue[++tt]=x;
        }
        else if(op=="pop")
        {
            hh++;
        }
        else if(op=="query")
        {
            cout<<queue[hh]<<endl;
        }
        else
        {
            if(hh<=tt) puts("No");
            else puts("Yes");
        }
    }
    return 0;
}

3.单调栈(AcWing 830.单调栈)

(1)算法思想:

        由于输出的是每一个位置左侧离他最近的比他小的数min,且是在当前位置从后往前遍历,符合先进后出的规则,可设置一个栈,从栈顶开始寻找比这个位置小的数,找到结束循环,没找到就弹出栈顶元素。同时弹出的元素不会是之后的元素的min,因为如果假设是之后元素的min,但是弹出的元素大于当前元素,因此如果弹出的元素是后面的元素的min,那么当前元素一定更靠近后面的元素,所以应该是这个元素最靠近后面的元素,因此符合规则,最后将当前元素压入栈中。

(2)代码实现思路:

        当栈顶元素大于当前元素同时栈不空,就弹栈,即tt--,否则结束循环。若最后tt不为0,则输出,若为0,则输出1,最后将当前元素入栈。

(3)代码实现:

#include <iostream>
using namespace std;
const int N = 1e6+10;
int stk[N],tt=0;
int main() {
    int n;
    scanf("%d",&n);
    while(n--)
    {
        int x;
        scanf("%d",&x);
        while(tt&&stk[tt]>=x) tt--;
        if(tt) printf("%d " ,stk[tt]);
        else printf("-1");
        stk[++tt]=x;
    }
    return 0;
}

3.单调队列(AcWing 154.        滑动窗口)

(1)算法思想:

        以输出最小值为例,当窗口内的元素个数达到k时,输出该窗口的最小值。考虑暴力算法,每滑动一次窗口,都比较k次,寻找到最小值,注意到如果左侧的数字大于右侧的数据,则可以在下一次滑动前删除左侧的数据,因此减少比较的次数。因此可以设置一个滑动窗口队列(队头在左,队尾在右),比较队尾和当前即将进入滑动窗口的数字大小,如果队尾数字大,则删除队尾。因此该队列中的数字全都小于待插入的数字,并且队头是最小的数字,再将该元素插入到队尾,最后输出队头即可。

(2)代码实现思路:

        先确保滑动窗口队列的队头仍在窗口内,否则队头右移,接着比较待插入元素和队尾的关系,如果队尾大,则删队尾,再插入待插元素,当已插入的序号个数大于窗口值再进行输出,输出最小值,即队头。

(3)代码实现:

#include <iostream>
using namespace std;
const int N= 1e6+10;
int a[N],q[N];
int main() {
    int n,k;
    cin>>n>>k;
    for(int i=0;i<n;i++) cin>>a[i];
    int hh=0,tt=-1;
    for(int i=0;i<n;i++)
    {
        if(hh<=tt&&i-k+1>q[hh]) hh++;//判断队头是否已经滑出窗口,即当前序号与队头序号是否在同一窗口内,若不在,则将队头向右移动
        while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
        q[++tt]=i;
        if(i>=k-1) cout<<a[q[hh]]<<" ";
    }
    cout<<endl;
    hh=0,tt=-1;
    for(int i=0;i<n;i++)
    {
        if(hh<=tt&&i-k+1>q[hh]) hh++;
        while(hh<=tt&& a[q[tt]]<=a[i]) tt--;
        q[++tt]=i;
        if(i>=k-1) cout<<a[q[hh]]<<" ";
    }
    return 0;
}

(三).KMP算法

简介:常用于字符串匹配,利用字符串的前缀和后缀相同优化算法时间复杂度。

1.AcWing 831.KMP字符串匹配

(1)算法思想:

        令字符串第一个字符的序号为1,首先对需要查找的字符串p设置一个next数组:记录每一个i位置最长的公共前后缀的长度(即p[1~j]=p[i-j+1~i]);再根据next数组寻找p在总字符串s出现的位置,即比较i指针和j指针的字符相同,若相同,j指针前移,否则退回到next[j]位置,再比较j+1与i位置是否相等,若仍不相等,则继续退回next[j],当j指针移动至p数组的末尾时说明查找完成,返回此时p在s的位置。(注意i,j指针的同步性)

(2)代码实现思路:

        字符串的起始序号设置为1,创建next数组:对于p字符串而言,如果i指针与j指针下一个字符不相等,则将j指针移动到ne[j],如果相等,则将j向右移动一个位置。再将i指针的Next数组设置为当前的j。总而言之,就是寻找每一个i位置的ne[j],使得p[1~j]=p[i-j+1~i],这样在后续匹配中,当j指针与模式串指针i匹配失败时,不用依次往后移动,而是直接移动j-ne[j]个位置。

        匹配:类似于上述的操作,只是将i指针在s字符串中移动,如果i指针与j指针下一个字符不相等,则将j指针移动到ne[j],如果相等,则将j向右移动一个位置。如果j移动到p数组的末端,即j=n,则输出当前p在s的位置,即i-n,同时利用next数组快速进行下一次寻找,即输出之后将j设置为ne[j]。

(3)代码实现:

#include <iostream>
using namespace std;
const int N =1e6+10;
char s[N],p[N];
int n,m;
int ne[N];
int main() {
    cin>>n>>p+1>>m>>s+1;//将字符串p,s每一个字符向后移动一个位置
    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;
    }
    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)
        {
            printf("%d ",i-n);
            j=ne[j];
        }
    }
    return 0;
}

(四).Trie(字典树)

1.Acwing 835.Trie字符串统计

(1)算法思想:

        利用树,将边看成字母,结点看成点的序号。插入:设置一个son[p][u]数组,其中p代表结点序号,u代表该节点指向的下一个结点的边的字符(用26个数字对应26个字母),每插入一个字符,检查该字符指向的边是否已存在(即它的前缀字母已经被记录过),若不存在则创建一个边存储该字符,再更新序号,将一个单词录入完成后,p应该为最后一个字母指示的结点,因此可通过cnt[p]数组记录该单词的记录次数。

(2)代码实现思路:

        插入:首先设置p结点为0,即根节点,将待插入的单词每一个字母转化为数字,从头开始遍历,若该字母已经插入过,则将p结点更新为当前序号,若没有插入过,则创建一个结点,并更新当前序号,即son[p][u]=++idx,当一个单词读入完成后,将该单词的个数加1,即将cnt[p]++

        查询:首先设置p结点为0,即根节点,将待插入的单词每一个字母转化为数字,从头开始遍历,若该字母已经插入过,则将p结点更新为当前序号,若没有插入过,则返回0,当单词读入完成,返回cnt[p]即为该单词的存入次数。

(3)代码实现:

#include <iostream>
using namespace std;
const int N=1e5+10;
char str[N];
int son[N][26],cnt[N];
int n,idx;
void insert(char str[])
{
    int p=0,u;
    for(int i=0;str[i];i++)
    {
        u=str[i]-'a';
        if(!son[p][u]) son[p][u]=++idx;
        p=son[p][u];
    }
    cnt[p]++;
}
int query(char str[])
{
    int p=0,u;
    for(int i=0;str[i];i++)
    {
        u=str[i]-'a';
        if(!son[p][u]) return 0;
        p=son[p][u];
    }
    return cnt[p];
}
int main() {
    cin>>n;
    while(n--)
    {
        char o;
        cin>>o>>str;
        if(o=='I') insert(str);
        else cout<<query(str)<<endl;
    }
    return 0;
}

2.Acwing 143.        最大异或对

(1)算法思想:

        建立trie树:读入每一个数字,将该数字转化为二进制,从根节点开始记录每个数的最高位,若未被记录过就插入一个新的点。

        求某一个数的最大异或值:从高位开始寻找与该数字该位相反的结点,若找到则将res加上1右移该位置的位数,若找不到则只能走到相同的点,最后返回res。

        求最大异或值,枚举所有数,执行上面的函数,最后返回最大的res。

(2)代码实现思路:

        插入(建树):设置p=0,表示从根节点开始枚举,由于本题最大数字为2^31,因此循环枚举某数的二进制最高位可以从30开始,到0结束,(i>=0可以改写为~i),然后用u记录该数在第i位的数字(见二进制表示第k位数字),如果树中没有此结点,就新建一条边指向新结点,再更新p结点为刚刚的结点。

        查询某数的最大异或值:设置p=0,res=0,从根节点开始枚举,与上面的写法差不多,后用u记录该数在第i位的数字,如果存在与u相反的结点(即s[p][u]不为0),那么就将res加上1左移u,同时更新p为该相反的结点。否则不存在与u相反的结点的话,只更新p结点到下一个结点即可。最后返回res。

(3)代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
const int N=100010,M=3000000;
int son[M][2],idx=0,a[N];
int n,res;
void insert(int x)
{
    int p=0;
    for(int i=30;~i;i--)
    {
        int u=x>>i&1;
        if(!son[p][u]) son[p][u]=++idx;
        p=son[p][u];
    }
}
int query(int x)
{
    int res=0,p=0;
    for(int i=30;~i;i--)
    {
        int u=x>>i&1;
        if(son[p][!u])
        {
            res+=1<<i;
            p=son[p][!u];
        }
        else
            p=son[p][u];
    }
    return res;
}
int main()
{
    cin>>n;
    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;
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值