【第二讲】数据结构

这篇博客介绍了数据结构的基础知识,包括单链表、双链表的操作,模拟栈和队列的实现,单调栈和单调队列的应用,KMP字符串匹配算法,Trie树的构建与查询,以及并查集在合并集合和计算连通块中点数量中的使用。此外,还讲解了堆排序和模拟堆,以及哈希表的基本概念。这些内容涵盖了算法设计和实现中的重要概念,是解决复杂问题的基础。

来自:算法基础课

第二讲 数据结构

2.1单链表

单链表 —— 模板题 AcWing 826. 单链表

// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 在链表头插入一个数a
void insert(int a)
{
    e[idx] = a, ne[idx] = head, head = idx ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}

2.1.1 826. 单链表

实现一个单链表,链表初始为空,支持三种操作:

向链表头插入一个数;

删除第 k 个插入的数后面的数;

在第 k 个插入的数后插入一个数。

现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

H x,表示向链表头插入一个数 x。

D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。

I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。

输出格式

共一行,将整个链表从头到尾输出。

数据范围

1≤M≤100000

所有操作保证合法。

输入样例:

10

H 9

I 1 1

D 1

D 0

H 6

I 3 6

I 4 5

I 4 5

I 3 4

D 6

输出样例:

6 4 6 5

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int m;
int head,e[N],ne[N],idx;

void init()
{
    head=-1;
    idx=1;
}

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

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

void myremove(int k)
{
    if(k==0)
    {
        head=ne[head];
    }
    else
    {
        ne[k]=ne[ne[k]];
    }
}
int main()
{
    cin>>m;
    init();
    while(m--)
    {
        char ch;
        cin>>ch;
        if(ch=='H')
        {
            int x;
            cin>>x;
            add_head(x);
        }
        else if(ch=='D')
        {
            int k;
            cin>>k;
            myremove(k);
        }
        else
        {
            int k,x;
            cin>>k>>x;
            add(k,x);
        }
    }
    int p=head;
    while(p!=-1)
    {
        cout<<e[p]<<" ";
        p=ne[p];
    }
    return 0;
}

2.2双链表

双链表 —— 模板题 AcWing 827. 双链表

// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

2.2.1 827. 双链表

实现一个双链表,双链表初始为空,支持 5 种操作:

在最左侧插入一个数;

在最右侧插入一个数;

将第 k 个插入的数删除;

在第 k 个插入的数左侧插入一个数;

在第 k 个插入的数右侧插入一个数

现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

L x,表示在链表的最左端插入数 x。

R x,表示在链表的最右端插入数 x。

D k,表示将第 k 个插入的数删除。

IL k x,表示在第 k 个插入的数左侧插入一个数。

IR k x,表示在第 k 个插入的数右侧插入一个数。

输出格式

共一行,将整个链表从左到右输出。

数据范围

1≤M≤100000

所有操作保证合法。

输入样例:

10

R 7

D 1

L 3

IL 2 10

D 3

IL 2 7

L 8

R 9

IL 4 7

IR 2 2

输出样例:

8 7 7 3 2 9

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int m;	
int e[N],l[N],r[N],idx;
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 myremove(int k)
{
    r[l[k]]=r[k];
    l[r[k]]=l[k];
}
int main()
{
    cin>>m;
    init();
    while(m--)
    {
        string op;
        int k,x;
        cin>>op;
        if(op=="L")
        {
            k=0;
            cin>>x;
            add(k,x);
        }
        else if(op=="R")
        {
            k=l[1];
            cin>>x;
            add(k,x);
        }
        else if(op=="D")
        {
            cin>>k;
            myremove(k+1);
        }
        else if(op=="IL")
        {
            cin>>k>>x;
            k=l[k+1];
            add(k,x);
        }
        else
        {
            cin>>k>>x;
            add(k+1,x);
        }
    }
    int p=r[0];
    while(p!=1)
    {
        cout<<e[p]<<" ";
        p=r[p];
    }
    return 0;
}

2.3栈

栈 —— 模板题 AcWing 828. 模拟栈

// tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空
if (tt > 0)
{

}

2.3.1 828. 模拟栈

实现一个栈,栈初始为空,支持四种操作:

push x – 向栈顶插入一个数 x;

pop – 从栈顶弹出一个数;

empty – 判断栈是否为空;

query – 查询栈顶元素。

现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。

输出格式

对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示栈顶元素的值。

数据范围

1≤M≤100000,

1≤x≤109

所有操作保证合法。

输入样例:

10

push 5

query

push 6

pop

query

pop

empty

push 4

query

empty

输出样例:

5

5

YES

4

NO

#include<bits/stdc++.h>
using namespace std;
int m;
stack<int> sk;
int main()
{
    cin>>m;
    while(m--)
    {
        string op;
        cin>>op;
        int x;
        if(op=="push")
        {
            cin>>x;
            sk.push(x);
        }
        else if(op=="pop")
        {
            sk.pop();
        }
        else if(op=="empty")
        {
            if(sk.empty())
            {
                cout<<"YES"<<endl;
            }
            else
            {
                cout<<"NO"<<endl;
            }
        }
        else
        {
            cout<<sk.top()<<endl;
        }
    }
    return 0;
}

2.3.2 3302. 表达式求值

给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

数据保证给定的表达式合法。

题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。

题目保证表达式中所有数字均为正整数。

题目保证表达式在中间计算过程以及结果中,均不超过 231−1。

题目中的整除是指向 0 取整,也就是说对于大于 0 的结果向下取整,例如 5/3=1,对于小于 0 的结果向上取整,例如 5/(1−4)=−1。

C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。

输入格式

共一行,为给定表达式。

输出格式

共一行,为表达式的结果。

数据范围

表达式的长度不超过 105。

输入样例:

(2+2)*(1+1)

输出样例:

8

#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 c=op.top();op.pop();
    int x;
    if(c=='+')
    {
        x=a+b;
    }
    else if(c=='-')
    {
        x=a-b;
    }
    else if(c=='*')
    {
        x=a*b;
    }
    else
    {
        x=a/b;
    }
    num.push(x);
}
int main()
{
    map<char,int> mp;
    mp.insert(make_pair('+',1));
    mp.insert(make_pair('-',1));
    mp.insert(make_pair('*',2));
    mp.insert(make_pair('/',2));
    string str;
    cin>>str;
    for(int i=0;i<str.size();i++)
    {
        char c=str[i];
        if(isdigit(c))
        {
            int x=0,j=i;
            while(j<str.size()&&isdigit(str[j]))
            {
                x=x*10+str[j++]-'0';
            }
            i=j-1;
            num.push(x);
        }
        else if(c=='(') op.push(c);
        else if(c==')')
        {
            while(op.top()!='(') eval();
            op.pop();
        }
        else
        {
            while(op.size()&&op.top()!='('&&mp[op.top()]>=mp[c]) eval();
            op.push(c);
        }
    }
    while(op.size()) eval();
    cout<<num.top();
    return 0;
}

2.4队列

队列 —— 模板题 AcWing 829. 模拟队列

\1. 普通队列:

// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh <= tt)
{

}

\2. 循环队列

// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh != tt)
{

}

2.4.1 829. 模拟队列

实现一个队列,队列初始为空,支持四种操作:

push x – 向队尾插入一个数 x;

pop – 从队头弹出一个数;

empty – 判断队列是否为空;

query – 查询队头元素。

现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。

输出格式

对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示队头元素的值。

数据范围

1≤M≤100000,

1≤x≤109,

所有操作保证合法。

输入样例:

10

push 6

empty

query

pop

empty

push 3

push 4

pop

query

push 6

输出样例:

NO

6

YES

4

#include<bits/stdc++.h>
using namespace std;
int m;	
queue<int> q;
int main()
{
    cin>>m;
    while(m--)
    {
        string op;
        cin>>op;
        int x;
        if(op=="push")
        {
            cin>>x;
            q.push(x);
        }
        else if(op=="pop")
        {
            q.pop();
        }
        else if(op=="empty")
        {
            if(q.empty()) cout<<"YES"<<endl;
            else cout<<"NO"<<endl;
        }
        else cout<<q.front()<<endl;
    }
    return 0;
}

2.5单调栈

单调栈 —— 模板题 AcWing 830. 单调栈

常见模型:找出每个数左边离它最近的比它大/小的数

int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

2.5.1 830. 单调栈

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

输入格式

第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式

共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围

1≤N≤105

1≤数列中元素≤109

输入样例:

5

3 4 2 7 5

输出样例:

-1 3 -1 2 2

#include<bits/stdc++.h>
using namespace std;
stack<int> sk;
int n;
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        int x;
        cin>>x;
        while(!sk.empty()&&sk.top()>=x) sk.pop();
        if(sk.empty()) cout<<-1<<" ";
        else cout<<sk.top()<<" ";
        sk.push(x);
    }
    return 0;
}

2.6单调队列

调队列 —— 模板题 AcWing 154. 滑动窗口

常见模型:找出滑动窗口中的最大值/最小值

int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

2.6.1 154. 滑动窗口

给定一个大小为 n≤106 的数组。

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 k 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。

第二行有 n 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:

8 3

1 3 -1 -3 5 3 6 7

输出样例:

-1 -3 -3 -3 3 3

3 3 5 5 6 7

#include<bits/stdc++.h>
using namespace std;
const int N=1000010;
int n,k;
int a[N],q[N];
int main()
{
    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]]<<" ";
        }
    }
    cout<<endl;
    return 0;
}

2.7KMP

KMP —— 模板题 AcWing 831. KMP字符串

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; 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 <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

2.7.1 831. KMP字符串

给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串 P 在模式串 S 中多次作为子串出现。

求出模板串 P 在模式串 S 中所有出现的位置的起始下标。

输入格式

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S。

输出格式

共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。

数据范围

1≤N≤105

1≤M≤106

输入样例:

3

aba

5

ababa

输出样例:

0 2

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=1000010;
int n,m;
int ne[N];
char s[M],p[N];
int main()
{
    cin>>n>>p+1;
    cin>>m>>s+1;

    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)
        {
            cout<<i-n<<" ";
            j=ne[j];
        }
    }
    return 0;
}

2.8Trie

Trie树 —— 模板题 AcWing 835. Trie字符串统计

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 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;
    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.8.1 835. Trie字符串统计

维护一个字符串集合,支持两种操作:

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<bits/stdc++.h>
using namespace std;
const int N=100010;
int n;
int son[N][26],cnt[N],idx=0;
void _insert(string str)
{
    int p=0;
    for(int i=0;i<str.length();i++)
    {
        int u=str[i]-'a';
        if(!son[p][u]) son[p][u]=++idx;
        p=son[p][u];
    }
    cnt[p]++;
}

int query(string str)
{
    int p=0;
    for(int i=0;i<str.length();i++)
    {
        int u=str[i]-'a';
        if(!son[p][u]) return 0;
        p=son[p][u];
    }
    return cnt[p];
}
int main()
{
    cin>>n;
    while(n--)
    {
        char op;
        string str;
        cin>>op>>str;
        if(op=='I')
        {
            _insert(str);
        }
        else
        {
            cout<<query(str)<<endl;
        }
    }
    return 0;
}

2.8.2 143. 最大异或对

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

输入格式

第一行输入一个整数 N。

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

输出格式

输出一个整数表示答案。

数据范围

1≤N≤105,

0≤Ai<231

输入样例:

3

1 2 3

输出样例:

3

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=3000010;
int n;
int a[N];
int son[M][2],idx;

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];
    }
}

int query(int x)
{
    int p=0,res=0;
    for(int i=30;i>=0;i--)
    {
        int s=x>>i&1;
        if(son[p][!s])
        {
            res=res+(1<<i);
            p=son[p][!s];
        }
        else p=son[p][s];
    }
    return res;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        _insert(a[i]);
    }
    int ans=0;
    for(int i=0;i<n;i++)
    {
        ans=max(ans,query(a[i]));
    }
    cout<<ans;
    return 0;
}

2.9并查集

并查集 —— 模板题 AcWing 836. 合并集合, AcWing 837. 连通块中点的数量

(1)朴素并查集:

   int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);

(2)维护size的并查集:

   int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);

(3)维护到祖宗节点距离的并查集:

   int p[N], d[N];
   //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

   // 返回x的祖宗节点
   int find(int x)
   {
       if (p[x] != x)
       {
           int u = find(p[x]);
           d[x] += d[p[x]];
           p[x] = u;
       }
       return p[x];
   }

   // 初始化,假定节点编号是1~n
   for (int i = 1; i <= n; i ++ )
   {
       p[i] = i;
       d[i] = 0;
   }

   // 合并a和b所在的两个集合:
   p[find(a)] = find(b);
   d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

2.9.1 836. 合并集合

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;

Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:

4 5

M 1 2

M 3 4

Q 1 2

Q 1 3

Q 3 4

输出样例:

Yes

No

Yes

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int p[N];

int findP(int x)
{
    if(p[x]!=x) p[x]=findP(p[x]);
    return p[x];
}
int main()
{
    for(int i=0;i<N;i++) p[i]=i;
    cin>>n>>m;
    while(m--)
    {
        char op;
        int a,b;
        cin>>op>>a>>b;
        if(op=='M')
        {
            if(findP(a)!=findP(b))
            {
                p[findP(a)]=findP(b);
            }
        }
        else
        {
            if(findP(a)==findP(b)) cout<<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
    }
    return 0;
}

2.9.2 837. 连通块中点的数量

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;

Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;

Q2 a,询问点 a 所在连通块中点的数量;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a b,Q1 a b 或 Q2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No。

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:

5 5

C 1 2

Q1 1 2

Q2 1

C 2 5

Q2 5

输出样例:

Yes

2

3

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int p[N],sz[N];

int findP(int x)
{
    if(p[x]!=x) p[x]=findP(p[x]);
    return p[x];
}

int main()
{
    for(int i=0;i<N;i++)
    {
        p[i]=i;
        sz[i]=1;
    }
    cin>>n>>m;
    while(m--)
    {
        string op;
        cin>>op;
        int a,b;
        if(op=="C")
        {
            cin>>a>>b;
            if(a!=b)
            {
                if(findP(a)!=findP(b))
                {
                    sz[findP(b)]+=sz[findP(a)];
                }
                p[findP(a)]=findP(b);
            }
        }
        else if(op=="Q1")
        {
            cin>>a>>b;
            if(findP(a)==findP(b)) cout<<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
        else
        {
            cin>>a;
            cout<<sz[findP(a)]<<endl;
        }
    }
    return 0;
}

2.9.3 240. 食物链

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C 吃 A。

现有 N 个动物,以 1∼N 编号。

每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

当前的话与前面的某些真的话冲突,就是假话;

当前的话中 X 或 Y 比 N 大,就是假话;

当前的话表示 X 吃 X,就是假话。

你的任务是根据给定的 N 和 K 句话,输出假话的总数。

输入格式

第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

1≤N≤50000,

0≤K≤100000

输入样例:

100 7

1 101 1

2 1 2

2 2 3

2 3 3

1 1 3

2 3 1

1 5 5

输出样例:

3

#include<bits/stdc++.h>
using namespace std;
const int N=50010;
int n,k;
int p[N],d[N];

int findP(int x)
{
    if(p[x]!=x)
    {
        int t=findP(p[x]);
        d[x]+=d[p[x]];
        p[x]=t;
    }
    return p[x];
}
int main()
{
    for(int i=0;i<N;i++)
    {
        p[i]=i;
    }
    cin>>n>>k;
    int ans=0;
    for(int i=1;i<=k;i++)
    {
        int t,x,y;
        cin>>t>>x>>y;
        if(x>n||y>n) ans++;
        else
        {
            int px=findP(x),py=findP(y);
            if(t==1)
            {
                if(px==py)
                {
                    if((d[x]-d[y])%3) ans++;
                }
                else
                {
                    p[px]=py;
                    d[px]=d[y]-d[x];
                }
            }
            else
            {
                if(px==py)
                {
                    if((d[x]-d[y]-1)%3) ans++;
                }
                else
                {
                    p[px]=py;
                    d[px]=d[y]-d[x]+1;
                }
            }
        }
    }
    cout<<ans;
    return 0;
}

2.10堆

堆 —— 模板题 AcWing 838. 堆排序, AcWing 839. 模拟堆

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void down(int u)
{
    int t = u;
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        heap_swap(u, t);
        down(t);
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])
    {
        heap_swap(u, u / 2);
        u >>= 1;
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);

2.10.1 838. 堆排序

输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。

输入格式

第一行包含整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

输出格式

共一行,包含 m 个整数,表示整数数列中前 m 小的数。

数据范围

1≤m≤n≤105,

1≤数列中元素≤109

输入样例:

5 3

4 5 1 3 2

输出样例:

1 2 3

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;	
priority_queue<int,vector<int>,greater<int> > q;
int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        int x;
        cin>>x;
        q.push(x);
    }
    while(m--)
    {
        cout<<q.top()<<" ";
        q.pop();
    }
}

2.10.2 839. 模拟堆

维护一个集合,初始时集合为空,支持如下几种操作:

I x,插入一个数 x;

PM,输出当前集合中的最小值;

DM,删除当前集合中的最小值(数据保证此时的最小值唯一);

D k,删除第 k 个插入的数;

C k x,修改第 k 个插入的数,将其变为 x;

现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。

输入格式

第一行包含整数 N。

接下来 N 行,每行包含一个操作指令,操作指令为 I x,PM,DM,D k 或 C k x 中的一种。

输出格式

对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围

1≤N≤105

−109≤x≤109

数据保证合法。

输入样例:

8

I -10

PM

I -10

D 1

C 2 8

I 6

PM

DM

输出样例:

-10

6

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n;
int h[N],sz=0;
int ph[N],hp[N];

void heap_swap(int a,int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a],hp[b]);
    swap(h[a],h[b]);
}
void down(int u)
{
    int t=u;
    if(2*u<=sz&&h[2*u]<h[t]) t=2*u;
    if(2*u+1<=sz&&h[2*u+1]<h[t]) t=2*u+1;
    if(u!=t)
    {
        heap_swap(u,t);
        down(t);
    }
}

void up(int u)
{
    while(u/2>0&&h[u]<h[u/2])
    {
        heap_swap(u,u/2);
        u/=2;
    }
}
int main()
{
    int id=0;
    cin>>n;
    while(n--)
    {
        string op;
        int k,x;
        cin>>op;
        if(op=="I")
        {
            cin>>x;
            h[++sz]=x;
            id++;
            ph[id]=sz,hp[sz]=id;
            up(sz);
        }
        else if(op=="PM") cout<<h[1]<<endl;
        else if(op=="DM")
        {
            heap_swap(1,sz);
            sz--;
            down(1);
        }
        else if(op=="D")
        {
            cin>>k;
            k=ph[k];
            heap_swap(k,sz);
            sz--;
            down(k),up(k);
        }
        else if(op=="C")
        {
            cin>>k>>x;
            k=ph[k];
            h[k]=x;
            down(k),up(k);
        }
    }
    return 0;
}

2.11哈希表

一般哈希 —— 模板题 AcWing 840. 模拟散列表

(1) 拉链法

   int h[N], e[N], ne[N], idx;

   // 向哈希表中插入一个数
   void insert(int x)
   {
       int k = (x % N + N) % N;
       e[idx] = x;
       ne[idx] = h[k];
       h[k] = idx ++ ;
   }

   // 在哈希表中查询某个数是否存在
   bool find(int x)
   {
       int k = (x % N + N) % N;
       for (int i = h[k]; i != -1; i = ne[i])
           if (e[i] == x)
               return true;

       return false;
   }

(2) 开放寻址法

    int h[N];

    // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
    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;
    }

字符串哈希 —— 模板题 AcWing 841. 字符串哈希

核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低

小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

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

2.11.1 840. 模拟散列表

维护一个集合,支持如下几种操作:

I x,插入一个数 x;

Q x,询问数 x 是否在集合中出现过;

现在要进行 N 次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数 N,表示操作数量。

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

输出格式

对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围

1≤N≤105

−109≤x≤109

输入样例:

5

I 1

I 2

I 3

Q 2

Q 5

输出样例:

Yes

No

#include<bits/stdc++.h>
using namespace std;
map<int,bool> mp;
int n;
int main()
{
    cin>>n;
    while(n--)
    {
        char op;
        int x;
        cin>>op>>x;
        if(op=='I') mp[x]=true;
        else
        {
            if(mp.count(x)) cout<<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
    }
    return 0;
}

2.11.2 841. 字符串哈希

给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。

输入格式

第一行包含整数 n 和 m,表示字符串长度和询问次数。

第二行包含一个长度为 n 的字符串,字符串中只包含大小写英文字母和数字。

接下来 m 行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。

注意,字符串的位置从 1 开始编号。

输出格式

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:

8 3

aabbaabb

1 3 5 7

1 3 6 8

1 2 1 2

输出样例:

Yes

No

Yes

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N=100010,P=131;
string str;
int n,m;
ULL h[N],p[N];

ULL hashstr(int l,int r)
{
    return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
    cin>>n>>m;
    cin>>str;
    p[0]=1;
    for(int i=1;i<=n;i++)
    {
        h[i]=h[i-1]*P+str[i-1];
        p[i]=p[i-1]*P;
    }
    while(m--)
    {
        int l,r,ll,rr;
        cin>>l>>r>>ll>>rr;
        if(hashstr(l,r)==hashstr(ll,rr)) cout<<"Yes"<<endl;
        else cout<<"No"<<endl;
    }
    return 0;
}

2.12 STL

C++ STL简介

vector, 变长数组,倍增的思想
    size()  返回元素个数
    empty()  返回是否为空
    clear()  清空
    front()/back()
    push_back()/pop_back()
    begin()/end()
    []
    支持比较运算,按字典序

pair<int, int>
    first, 第一个元素
    second, 第二个元素
    支持比较运算,以first为第一关键字,以second为第二关键字(字典序)

string,字符串
    size()/length()  返回字符串长度
    empty()
    clear()
    substr(起始下标,(子串长度))  返回子串
    c_str()  返回字符串所在字符数组的起始地址

queue, 队列
    size()
    empty()
    push()  向队尾插入一个元素
    front()  返回队头元素
    back()  返回队尾元素
    pop()  弹出队头元素

priority_queue, 优先队列,默认是大根堆
    size()
    empty()
    push()  插入一个元素
    top()  返回堆顶元素
    pop()  弹出堆顶元素
    定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;

stack,size()
    empty()
    push()  向栈顶插入一个元素
    top()  返回栈顶元素
    pop()  弹出栈顶元素

deque, 双端队列
    size()
    empty()
    clear()
    front()/back()
    push_back()/pop_back()
    push_front()/pop_front()
    begin()/end()
    []

set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
    size()
    empty()
    clear()
    begin()/end()
    ++, -- 返回前驱和后继,时间复杂度 O(logn)

    set/multiset
        insert()  插入一个数
        find()  查找一个数
        count()  返回某一个数的个数
        erase()
            (1) 输入是一个数x,删除所有x   O(k + logn)
            (2) 输入一个迭代器,删除这个迭代器
        lower_bound()/upper_bound()
            lower_bound(x)  返回大于等于x的最小的数的迭代器
            upper_bound(x)  返回大于x的最小的数的迭代器
    map/multimap
        insert()  插入的数是一个pair
        erase()  输入的参数是pair或者迭代器
        find()
        []  注意multimap不支持此操作。 时间复杂度是 O(logn)
        lower_bound()/upper_bound()

unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
    和上面类似,增删改查的时间复杂度是 O(1)
    不支持 lower_bound()/upper_bound(), 迭代器的++--

bitset, 圧位
    bitset<10000> s;
    ~, &, |, ^
    >>, <<
    ==, !=
    []

    count()  返回有多少个1

    any()  判断是否至少有一个1
    none()  判断是否全为0

    set()  把所有位置成1
    set(k, v)  将第k位变成v
    reset()  把所有位变成0
    flip()  等价于~
    flip(k) 把第k位取反

1. 顺序存储结构中数据中数据元素之间逻辑关系是由( )表示的,链接存储结构中的数据元素之间的逻辑关系是由( )表示的。 A.线性结构 B.非线性结构 C.存储位置 D.指针 2. 线性表是( )。 A.一个有限序列,可以为空 B. 一个有限序列,不能为空 C. 一个无限序列,可以为空 D. 一个无限序列,不能为空 3. 已知一维数组A采用顺序存储结构,每个元素占用4个存储单元,第9个元素的地址为144,则第一个元素的地址是( )。 A. 108 B. 180 C. 176 D. 112 4. 在单链表中删除指针p所指结点的后继结点,则执行( )。 A. p->next= p->next->next B. p->next= p->next C. p= p->next->next D. p= p->next; p->next= p->next->next 5. 若某链表最常用的操作是在最后一个结点之后插入一个结点删除最后一个结点,则采用( )存储方式最节省时间。 A. 单链表 B. 双链表 C. 带头结点的双循环链表 D. 单循环链表 6.二维数组A[7][8]以列序为主序的存储, 计算数组元素A[5][3] 的一维存储空间下标 k=( )。 A. 38 B. 43 C. 26 D. 29 二、完成下列填空题(每空3分,共9分)。 1.在顺序表L中第i个位置上插入一个新的元素e: Status ListInsert_Sq(SqList &L , int i , ET e){ if ( iL.length+1) return ERROR; if(L.length >= L.listsize){ p=(ET*)realloc(L.elem,(L.listsize+10)*sizeof(ET)); if (p==NULL) exit(OVERFLOW); L.elem=p; } for( j=L.length ; j>=i ; --j ) L.elem[j]=L.elem[j-1] ; L.elem[j]=e ; ++L.length ; return OK; } 2. 删除双向链表中p所指向的节点算法: status delete(DuLinkList L, DuLinkList p) { if (p= =L) return ERROR; else { p->prior->next=p->next; p->next->prior=p->prior ; } free(p); return OK; } 三、编程题(共27分)。 1. (共12分)用顺序表表示集合,设计算法实现集合的求差集运算,要求不另外开辟空间。 顺序表的存储结构定义如下: #define Maxsize 100 typedef struct { ElemType data[MaxSize]; // ElemType表示不确定的数据类型 int length; // length表示线性表的长度 }SqList; 将如下函数,伪码补充完整(8分),代码前先用文字描述自己的算法思想(4分)。 文字描述算法:略(4分) void Difference(SqList A, SqList B) {//参考代码如下如下(8分) for (i=0;i<A.length;i++) for(j=0;j<B.length;j++) if(A.data[i]==B.data[j]) { A.data[i]=’#’; break; } for (k=0,i=0;inext == L) return; p = L; while (p->next != L)   { if (p->next->data != e) P = p->next; else { q = p->next;p->next = q->next; free(q);} } } 时间复杂度分析:(2分) 时间复杂度为O(n)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值