算法基础 第二章 数据结构目录
静态单链表
知识点
指针型链表需要调用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开始,以小根堆为例
- 下滑操作down(x)
- 上滑操作up(x)
所有的功能都是通过down和up的组合来实现 - 插入=插到末尾+up()
heap[++size]=x;
up(size);
- 删除=尾节点放到顶再down()
heap[1]=heap[size];
size--;
down(1);
- 删除任意一个数
heap[k]=heap[size];
size--;
//如果大了就要down,如果小了就要up();
down(k);up(k);//只会执行其中一个
- 修改任意一个数
heap[k]=x;
down(k);up(x);//只会执行其中一个
建堆方式
- 从n/2到1依次down(),复杂度为O(n)
- 从1开始,插入,复杂度为O(nlogn)
模板
- 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);
}
}
- up
void up(int u){
while(u>1&&h[u]<h[u/2]){
swap(h[u],h[u/2]);
u/=2;
}
}
- 初始化建堆
想要保证数据已经顺序存放在数组了
for(int i=n/2;i>0;--i)down(i);
- 带映射的交换
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:可以是任何类型
存储结构:
- 开放寻址法
- 拉链法
冲突:两个不同的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;
}