算法基础 第二章 数据结构

静态单链表

知识点

指针型链表需要调用new操作浪费时间,做题往往用静态链表
缺点是长度需要一开始就指定最大长度,且删除节点后空间无法被继续利用

模板

int head,e[N],ne[N],idx;
void init(){
	head=-1;//-1表示NULL
	idx=0;//从0开始存放
}
void addhead(int x){
	e[idx]=x;
	ne[idx]=head;
	head=idx++;
}
void add(int k,int x){//从0开始存,那么下标k表示第k+1个新增的元素,所以这是插入到第k+1个插入元素的后面,这里的k是时间概念不是空间概念
	e[idx]=x;
	ne[idx]=ne[k];
	ne[k]=idx++;
}
void removehead(){
	head=ne[head];
}
void remove(int k){//
	ne[k]=ne[ne[k]];
}
for(int i=head;i!=-1;i=ne[i])

题目

单链表

题目描述

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

向链表头插入一个数;
删除第 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

思路

删除k=0时表示删除头结点,但是头结点地址不一定是0

代码
#include <iostream>
using namespace std;
const int N=100005;
int e[N],ne[N],head=-1,idx;
void addhead(int x){
    e[idx]=x;
    ne[idx]=head;
    head=idx++;
}
void add(int k,int x){
    e[idx]=x;
    ne[idx]=ne[k];
    ne[k]=idx++;
}
void remove(int k){
    ne[k]=ne[ne[k]];
}
int main(){
    int n,k,x;
    cin>>n;
    while(n--){
        char c; cin>>c;
        if(c=='H'){
            cin>>x;
            addhead(x);
        }else if(c=='I'){
            cin>>k>>x;
            add(k-1,x);
        }else {
            cin>>k;
            if(k==0)head=ne[head];
            else remove(k-1);
        }
    }
    for(int i=head;i!=-1;i=ne[i]){
        cout<<e[i]<<" ";
    }
    return 0;
}

静态双链表

知识点

三个数组分别表示值域和左右指针,统一的下标表示地址

模板

int e[N],l[N],r[N],idx;

void init(){
	r[0]=1;l[1]=0;//用0,1两个地址表示左右端点
	idx=2;//下标从2开始存放
}

void addr(int k,int x)//在地址k的右边插入x
{
	e[idx]=x;
	l[idx]=k;
	r[idx]=r[k];
	l[r[k]]=idx;
	r[k]=idx++;//和上一句顺序不能反
}
void remove(int k)//删除地址k的节点
{
	r[l[k]]=r[k];
	l[r[k]]=l[k];
}

题目

双链表模板题

题目描述

实现一个双链表,双链表初始为空,支持 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

思路

记得一开始要调用init()函数

代码
#include<bits/stdc++.h>
using namespace std;
const int N=100005;
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;
    l[idx]=k;
    r[idx]=r[k];
    l[r[k]]=idx;
    r[k]=idx++;
}
void remove(int k){
    r[l[k]]=r[k];
    l[r[k]]=l[k];
}
int main(){
    init();
   int m;cin>>m;
   while(m--){
       char c;int k,x;
       cin>>c;
       if(c=='L'){
           cin>>x;
           add(0,x);
       }else if(c=='R'){
           cin>>x;
           add(l[1],x);
       }else if(c=='D'){
           cin>>k;
           remove(k+1);
       }else if(c=='I'){
           cin>>c;
           cin>>k>>x;
           if(c=='L'){
               add(l[k+1],x);
           }else {
               add(k+1,x);
           }
       }
   }
   for(int i=r[0];i!=1;i=r[i]){
       cout<<e[i]<<" ";
   }
   return 0; 
}

数组模拟栈

知识点

两种,一种是栈顶指针指向栈顶,一种是指向栈顶+1,这里是指向栈顶

模板

int stk[N],tt=-1;//从0开始存放,-1表示栈空
//插入
stk[++tt]=x;
//删除
tt--;
//栈顶
stk[tt];
//栈空
if(tt==-1)

题目

模拟栈

题目描述

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

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;
const int N=100005;
int stk[N],tt=-1;
int main(){
    int m;cin>>m;
    string s;
    int x;
    while(m--){
        cin>>s;
        if(s=="push"){
            cin>>x;
            stk[++tt]=x;
        }else if(s=="pop"){
            if(tt!=-1)
            --tt;
        }else if(s=="empty"){
            if(tt==-1)cout<<"YES"<<endl;
            else cout<<"NO"<<endl;
        }else {
            cout<<stk[tt]<<endl;
        }
    }
    return 0;
}

数组模拟队列

知识点

模板

int q[N],hh=0,tt=-1;//从0开始存放
//插入
q[++tt]=x;
//出队
hh++;
//队首
q[hh]
//判断队空
if(hh>tt)

题目

模拟队列

题目描述

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

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;
const int N=100005;
int q[N],hh=0,tt=-1;

int main(){
    int m;cin>>m;
    string s;
    int x;
    while(m--){
        cin>>s;
        if(s=="push"){
            cin>>x;
            q[++tt]=x;
        }else if(s=="pop"){
            hh++;
        }else if(s=="empty"){
            if(hh>tt)cout<<"YES"<<endl;
            else cout<<"NO"<<endl;
        }else if(s=="query"){
            cout<<q[hh]<<endl;
        }
    }
    return 0;
}

单调栈

知识点

据题目的关系维护一个单调的栈,使得答案就在栈顶或者栈空

模板

while(tt>=0&&stk[tt]>=x)--tt;

题目

单调栈例题

题目描述

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

输入格式
第一行包含整数 N ,表示数列长度。

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

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

数据范围
1≤N≤105
1≤数列中元素≤109
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2

思路

维护一个单调的栈,扫描原序列,如果后遇到的数比前面的数还小,那么前面的数永远不可能被选上,所以存在一个单调性,每次都删掉当前栈里面比当前数大的数,剩下的就是答案,然后当前x入栈

代码
#include <bits/stdc++.h>
using namespace std;
const int N=10005;
int stk[N],tt=-1;

int main(){
    int n;cin>>n;
    for(int i=0;i<n;++i){
        int x;cin>>x;
        while(tt>=0&&stk[tt]>=x)--tt;
        if(tt==-1)cout<<-1<<" ";
        else cout<<stk[tt]<<" ";
        stk[++tt]=x;
    }
    return 0;
}

单调队列

知识点

模板

while(hh<=tt&&a[q[tt]]>=a[i])tt--;

题目

单调队列例题

题目描述

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

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

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

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

以下是一个例子:

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

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式
输入包含两行。

第一行包含两个整数 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=1000005;
int q[N],hh=0,tt=-1;
int a[N];

int main(){
    int n,k;cin>>n>>k;
    for(int i=0;i<n;++i)cin>>a[i];
    //输出最小值,队列前小后大 
    for(int i=0;i<n;++i){
    	if(hh<=tt&&q[hh]<i-k+1)hh++;
    	while(hh<=tt&&a[q[tt]]>=a[i])tt--;
    	q[++tt]=i;
    	if(i-k+1>=0)cout<<a[q[hh]]<<" ";
	} 
	cout<<endl;
    //输出最大值,队列前大后小 
    hh=0;tt=-1;//注意初始化 
    for(int i=0;i<n;++i){
        if(hh<=tt&&q[hh]<i-k+1)hh++;//如果队中元素已经出了滑动窗口,那要出队 
        while(hh<=tt&&a[q[tt]]<=a[i])tt--;//新加入的元素前面比它小的都出队 
        q[++tt]=i;//新元素入队 
        if(i-k+1>=0)cout<<a[q[hh]]<<" ";//输出当前队中最大元素 
    }
    return 0;
}

KMP

知识点

这个模板的模式串指针j是指向待匹配元素的前一个位置,便于边界处理,以及匹配成功时的ne[j]计算。
反正不理解就记下来就好。

模板

yxc模板

#include<bits/stdc++.h>
using namespace std;
const int N=100005,M=1000005;
char s[M],p[N];
int ne[N];//默认ne[1]=0; 
int main(){
	int n,m;
	cin>>n>>p+1>>m>>s+1;
	//初始化next数组
	for(int i=2,j=0;i<=n;++i){
		while(j!=0&&p[i]!=p[j+1])j=ne[j];
		if(p[i]==p[j+1])j++;
		ne[i]=j;
	} 
	//kmp
	for(int i=1,j=0;i<=m;++i){
		while(j!=0&&s[i]!=p[j+1])j=ne[j];
		if(s[i]==p[j+1])j++;
		if(j==n){
			j=ne[j];
			cout<<i-n+1-1<<" ";
		}
	} 
	return 0;
} 

题目

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=100005,M=1000005;
int ne[M];
char s[M],p[N];
int main(){
    int n,m;
    cin>>n>>p+1>>m>>s+1;
    //ne
    for(int i=2,j=0;i<=n;++i){
        while(j!=0&&p[i]!=p[j+1])j=ne[j];
        if(p[i]==p[j+1])j++;
        ne[i]=j;
    }
    //kmp
    for(int i=1,j=0;i<=m;++i){
        while(j!=0&&s[i]!=p[j+1])j=ne[j];
        if(s[i]==p[j+1])j++;
        if(j==n){
            cout<<i-n+1-1<<" ";//减去1是因为题目要求下标从0开始而这个模板是从1开始的提前了1位
            j=ne[j];
        }
    }
    return 0;
}

Trie 树

知识点

Trie树一般指字典树。 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串)
在这里插入图片描述

模板

根节点的编号为0,整个类似于26叉树,每个节点最多有26个孩子。(26表示字符种数,可变)

对于字符种类数量很多的情况比如中文,可以将中文看做二进制字符串,这样就只有两种字符种类了。

const int N=100005; //由总的字符数确定 
int son[N][26];/*第一维表示节点编号,第二维表示26个字母的索引,值表示对应子节点的编号*/
int cnt[N];//下标表示节点编号
int idx;//节点编号,类似静态链表 
char str[N]; 

void insert(char str[]){
	int p=0;
	for(int i=0;str[i]!=0;++i){
		int u=str[i]-'a';
		if(son[p][u]==0)son[p][i]=++idx;//如果节点的子节点为空就创建一个 
		p=son[p][u];//进入子节点 
	}
	cnt[p]++;//当前节点的计数值+1 
}

int query(char str[]){
	int p=0;
	for(int i=0;str[i]!=0;i++){
		int u=str[i]-'a';
		if(son[p][u]==0)return 0;//子节点为空说明不存在
		p=son[p][u];
	}
	return cnt[p];
} 

题目

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 <iostream>
using namespace std;

const int N=100005; //由总的字符数确定 
int son[N][26];/*第一维表示节点编号,第二维表示26个字母的索引,值表示对应子节点的编号*/
int cnt[N];//下标表示节点编号
int idx;//节点编号,类似静态链表 
char str[N]; 

void insert(char str[]){
	int p=0;
	for(int i=0;str[i]!=0;++i){
		int u=str[i]-'a';
		if(son[p][u]==0)son[p][i]=++idx;//如果节点的子节点为空就创建一个 
		p=son[p][u];//进入子节点 
	}
	cnt[p]++;//当前节点的计数值+1 
}

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

并查集

知识点

并查集是树形结构

路径压缩优化

如果在每次查的时候都使用路径压缩,将路径上的节点递归地连接到树根上,那树的高度最高也只在刚合并两棵(节点数量大于2的)树时候存在高度为3的情况。所以查找时间复杂度为O(1)

合并则是合并两棵树的树根,时间复杂度等于查找,O(1)

按秩合并优化

很少使用,所以没介绍

模板

1.并查集模板

//初始化:
for(int i=1;i<=n;++i)f[i]=i;
//查:递归实现
int find(int x){
	return (f[x]==x)?x:f[x]=find(f[x]);
}
//合并
f[find(a)]=find(b);

2.维护集合中元素数量

多加一个size[],用于标识当一个节点作为根节点时这个集合中的元素个数,注意只对根节点有意义。

只需要在合并时加上计算的操作

//初始化:
for(int i=1;i<=n;++i)f[i]=i,size[i]=1;
//查询元素个数
size[find(a)]
//合并
if(find(a)!=find(b))size[find(b)]+=size[find(b)];
f[find(a)]=find(b);

题目

合并集合

题目描述

一共有 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;
int f[100005];
int find(int x){
    return (f[x]==x)?x:f[x]=find(f[x]);
}
int main(){
    int n,m;
    cin>>n>>m;
    //先初始化每个集合
    for(int i=1;i<=n;++i)f[i]=i;
    while(m--){
        char c;int a,b;
        cin>>c>>a>>b;
        if(c=='M'){
            f[find(a)]=find(b);
        }else {
            if(find(a)==find(b))cout<<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
    }
    return 0;
}

食物链

题目描述

动物王国中有三类动物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句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

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

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

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

你的任务是根据给定的N(1≤ N ≤50,000)和K句话(0≤K≤100,000),输出假话的总数。

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

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

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

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

【输出】
只有一个整数,表示假话的数目。

【输入样例】
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<iostream>
using namespace std;
const int N=50005;
//d[]表示节点到父节点的距离
int f[N],d[N];
int n,m,ans;

int find(int x){
    if(f[x]!=x){
        int u=f[x];
        f[x]=find(f[x]);
        //路径上都直接连接到根节点了,那d[x]就应该等于原来到父节点的距离+d[f[x]]
        d[x]+=d[u];
    }
    return f[x];
}
/*
思路:
1.根据是否在同一棵树中判断之前是否说过两个节点的关系(并查集的思想)
2.根据两个节点到根节点的距离对种类数(这里是3)的模来判断两个节点之间的关系
*/
//(d[x]-d[y])%3== 0:同类,1:x吃y, 2:y吃x  考虑到负数取余的问题,这里保证dx>dy
//如果在判断中先处理增量,那只需要看是否余0,就不需要考虑正负的问题
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;++i)f[i]=i;
    
    while(m--){
        int t,x,y;
        cin>>t>>x>>y;
        
        if(x>n||y>n)ans++;
        else if(t==1){
            int fx=find(x);
            int fy=find(y);
            //之前说过的话无法确定二者关系,直接合并
            if(fx!=fy){
                f[fx]=fy;
                //合并后要保证两个节点到根节点的距离模3相等,即d[x]+d[fx] 和d[y] 模3相等
                //这里可以不考虑正负,但是使用的的时候要考虑正负
                d[fx]=d[y]-d[x];
                
            }else {
                if((d[x]-d[y])%3!=0)ans++;
            }
        }else if(t==2){
            int fx=find(x);
            int fy=find(y);
            //之前说过的话无法确定二者关系,直接合并
            if(fx!=fy){
                f[fx]=fy;
                //合并后要保证两个节点到根节点的距离模3后x比y大1,即d[x]+d[fx] 和d[y] 模3 大1
                //这里可以不考虑正负,但是使用的的时候要考虑正负
                d[fx]=d[y]-d[x]+1;
                
            }else {
                if((d[x]-d[y]-1)%3!=0)ans++;
            }
        }
        
    }
    cout<<ans;
    return 0;
}

知识点

概念

堆是维护着的满足某种性质的完全二叉树,因为是完全二叉树,因此存储方式使用 一个顺序存储

基本功能

  1. 插入一个数
  2. 求当前堆中最值
  3. 删除当前最值

扩展功能

  1. 删除任意一个数
  2. 修改任意一个数

实现方式

存储下标从1开始,以小根堆为例

  1. 下滑操作down(x)
  2. 上滑操作up(x)
    所有的功能都是通过down和up的组合来实现
  3. 插入=插到末尾+up()
heap[++size]=x;
up(size);
  1. 删除=尾节点放到顶再down()
heap[1]=heap[size];
size--;
down(1);
  1. 删除任意一个数
heap[k]=heap[size];
size--;
//如果大了就要down,如果小了就要up();
down(k);up(k);//只会执行其中一个
  1. 修改任意一个数
heap[k]=x;
down(k);up(x);//只会执行其中一个

建堆方式

  1. 从n/2到1依次down(),复杂度为O(n)
  2. 从1开始,插入,复杂度为O(nlogn)

模板

  1. down
void down(int u){
	int t=u;
	if(u*2<=size&&h[t]>h[u*2])t=u*2;
	if(u*2+1<=size&&h[t]>h[u*2+1])t=u*2+1;
	if(t!=u){
		swap(h[t],h[u]);
		down(t);
	}
}
  1. up
void up(int u){
	while(u>1&&h[u]<h[u/2]){
		swap(h[u],h[u/2]);
		u/=2;
	}
}
  1. 初始化建堆
    想要保证数据已经顺序存放在数组了
for(int i=n/2;i>0;--i)down(i);
  1. 带映射的交换
void heap_swap(int a,int b){
	swap(h[a],h[b]);
	swap(k_idx[idx_k[a]],k_idx[idx_k[b]]);
	swap(idx_k[a],idx_k[b]);
}

题目

堆排序

题目描述

输入一个长度为 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=100005;

int h[N],size;

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

int main(){
	int n,m;
	cin>>n>>m;
	size=n;
	for(int i=1;i<=n;++i)cin>>h[i];
	for(int i=n/2;i>0;--i)down(i);
	while(m--){
		cout<<h[1]<<endl;
		h[1]=h[size--];
		down(1);
	}
} 

模拟堆

题目描述

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

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

思路

由于需要删除第k个插入的数,所以要记录和维护第k个插入的数的位置,也需要由位置得到插入顺序。所以要两个数组来分别记录

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

const int N=100005;

int h[N],size,cnt;
int k_idx[N],idx_k[N];

void heap_swap(int a,int b){
	swap(h[a],h[b]);
	swap(k_idx[idx_k[a]],k_idx[idx_k[b]]);
	swap(idx_k[a],idx_k[b]);
}

void up(int u){
	while(u>1&&h[u]<h[u/2]){
		heap_swap(u,u/2);
//		int k1=idx_k[u];
//		int k2=idx_k[u/2];
//		idx_k[u]=k2;
//		idx_k[u/2]=k1;
//		k_idx[k1]=u/2;
//		k_idx[k2]=u;
		
		u/=2;
	}
}
void down(int u){
	int t=u;
	if(u*2<=size&&h[t]>h[u*2])t=u*2;
	if(u*2+1<=size&&h[t]>h[u*2+1])t=u*2+1;
	if(t!=u){
		heap_swap(u,t);
//		int k1=idx_k[u];
//		int k2=idx_k[t];
//		idx_k[u]=k2;
//		idx_k[t]=k1;
//		k_idx[k1]=t;
//		k_idx[k2]=u;
		down(t);
	}
}

int main(){
	int n;
	cin>>n;
	while(n--){
		string s;
		int x,k;
		cin>>s;
		if(s=="I"){
			cnt++;
			cin>>x;
			h[++size]=x;
			idx_k[size]=cnt;
			k_idx[cnt]=size;
			up(size);
		}else if(s=="PM"){
			cout<<h[1]<<endl;
		}else if(s=="DM"){
//			idx_k[1]=idx_k[size];
//			k_idx[idx_k[size]]=1;
//			h[1]=h[size--];
			heap_swap(1,size--);
			down(1);
		}else if(s=="D"){
			cin>>k;
			int idx=k_idx[k];
			heap_swap(idx,size--);
			down(idx);
			up(idx);
		}else if(s=="C"){
			cin>>k>>x;
			int idx=k_idx[k];
			h[idx]=x;
			down(idx);
			up(idx);
		}
	}
	return 0;
} 

哈希表

知识点

key: 一般为整数或者字符串类型,
value:可以是任何类型

存储结构:

  1. 开放寻址法
  2. 拉链法

冲突:两个不同的key,被hash函数映射到了同一个位置

常用操作

在算法题中一般只有插入和查找两个操作

模板

开放寻址法:

开数组一般开到题目范围的2到3倍,冲突会比较少

//开放寻址法的的这个取余的N取素数,效率最好 
const int N=200003,null=0x3f3f3f3f;
int h[N];

//如果x不存在,则返回应该插入的下标,如果存在,则返回已经存在的下标 
int find(int x){
	int k=(x%N+N)%N;
	while(h[k]!=null&&h[k]!=x){
		k++;
		if(k==N)k=0;
	}
	return k;
}

拉链法:

//拉链法,静态链表实现 
int h[N],ne[N],idx;//头指针数组 
int e[N];//存放value,可以是各种类型 

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

字符串哈希(重要)

将字符串看作一个p进制数,q为模,假定不存在冲突
一般取:p=131,p=13331,q=2^64 (经验值)
unsigned long long 刚好是64位,于是用来存放变量自动溢出省去取模运算

作用:可以起到类似前缀和的作用,先求出前缀的哈希值后,可以快速求得区间的哈希值。
可以快速比较字符串的两个子串是否相同
在这里插入图片描述

题目

模拟散列表

题目描述

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

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

思路
代码

拉链法:

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

const int N=100005;
//拉链法,静态链表实现 
int h[N],ne[N],idx;//头指针数组 
int e[N];//存放value,可以是各种类型 

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

int main(){
	int n;
	cin>>n;
	memset(h,-1,sizeof h);
	while(n--){
		string s;
		int x;
		cin>>s>>x;
		if(s=="I")insert(x);
		else if(find(x))cout<<"Yes"<<endl;
		else cout<<"No"<<endl;
	} 
	return 0;
} 

开放寻址法:

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

//开放寻址法的的这个取余的N取素数,效率最好 
const int N=200003,null=0x3f3f3f3f;
int h[N];

//如果x不存在,则返回应该插入的下标,如果存在,则返回已经存在的下标 
int find(int x){
	int k=(x%N+N)%N;
	while(h[k]!=null&&h[k]!=x){
		k++;
		if(k==N)k=0;
	}
	return k;
}

int main(){
	int n;
	cin>>n;
	memset(h,0x3f,sizeof h);
	while(n--){
		string s;
		int x;
		cin>>s>>x;
		int k=find(x);
		if(s=="I"){
			h[k]=x;
		}
		else {
			if(h[k]==x)cout<<"Yes"<<endl;
			else cout<<"No"<<endl;
		} 
	} 
	return 0;
} 

例题:字符串哈希

题目描述

给定一个长度为 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;
const int N=100005,p=13331;
typedef unsigned long long ULL;

//h存放hash值,pr存放p的次幂
//str从1开始 
ULL h[N];
ULL pr[N];
char str[N];

//获取区间上子串的hash值 
ULL get(int l,int r){
	return h[r]-h[l-1]*pr[r-l+1];
}
int main(){
    int n,m;
    cin>>n>>m;
    scanf("%s",str+1);
    pr[0]=1;
    for(int i=1;i<=n;++i){
        pr[i]=pr[i-1]*p;
        h[i]=h[i-1]*p+str[i];
    }
    
    while(m--){
    	int l,r,x,y;cin>>l>>r>>x>>y;
    	if(get(l,r)==get(x,y))cout<<"Yes"<<endl;
    	else cout<<"No"<<endl;
	}
    return 0;
}

C++ STL

知识点

参考:https://www.acwing.com/blog/content/1846/

重点:C++对系统分配内存时,时间消耗只与分配次数有关,与分配大小无关
eg: 1000次new int 是new int[1000] 的一千倍
所以要尽量减少分配内存的操作

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

/*
size()和empty()全部都有
clear(): 只有queue、priotiry_queue没有 
top(): 只有 priority_queue 和stack() 
push_back(): 只有vector 和deque 


iterator 迭代器(看成指针): vector<int>::iterator i =v.begin(); i++  *i 
vector 变长数组,倍增分配内存  push_back() pop_back() front() back() begin()/end() [] 支持按照字典序比较
queue 队列	push() pop() front() back() 
priority_queue 默认大根堆  push() pop() top()
stack 栈 	push() pop() top() 
deque 双端队列(效率很低 )  front() back()  push_back()/pop_back()  push_front()/pop_front() begin()/end() []

set,map,multiset,multimap,基于平衡二叉树(红黑树),动态维护有序序列 ,multi表示可以重复
	增删查改时间复杂度都是O(logn) 
	set/multiset 
	insert(x)  find(x)返回迭代器,不存在返回end()  count()	erase():1.如果输入一个数x,删除所有x 2.输入一个迭代器,删除这个迭代器  
	lower_bound(x) 下界 ,返回第一个大于x的值 
	upper_bound(x) 上街,返回第一个小于等于x的值 
	左开右闭区 
	
	map/multimap
	insert()插入一个pair  find() erase()输入的参数是pair或者迭代器 
	[]时间o(logn) 
	lower_bound(x) 下界 ,返回第一个大于x的值 
	upper_bound(x) 上街,返回第一个小于等于x的值 
	左开右闭区 
	
unordered_set,unordered_map,unordered_nultiset,unordered_multimap 基于哈希表实现 
	优点是:增删查改时间复杂度都是O(1) 
	无序,不支持 lower_bound、upper_bound ,   
	
	
bitset 压位
	对于布尔型可以省8倍空间
	bitset<10000> s;
	~,&,| ,^ 按位取反、与、或、异或 
	<< 、>> 对整个bitset移位操作
	== !=
	[]
	count() 返回1的个数
	any()返回是否有1
	none() 返回是否全0 
	set() 把所有位置为1
	set(k,v)
	reset() 把所有位置为0
	flip()等价于~
	flip(k) 第k位取反 

*/ 


int main(){
	vector<int> a;
	vector<int> a(10);
    vector<int> a(4,3); //初始化4个3
	
	pair<int,string> p; 
	p.first=1;
	p.second="egg"; 
	p=make_pair(10,"apple");
	p={1,"grape"};
	
	queue<int> q; 
	q=queue<int>();//代替clear() 
	
	priority_queue<int> heap;//默认大根堆 
	priority_queue<int,vector<int>,greater<int>> heap; //小根堆 
	 
	
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wow_awsl_qwq

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

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

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

打赏作者

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

抵扣说明:

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

余额充值