数据结构
数据结构
单链表
1.单链表
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 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
//要点
//head为链表头的下标
//要使得终点指的是-1
#include<iostream>
using namespace std;
const int N=1e5+10;
int e[N],ne[N];
int idx,m,head;
void init(){
head=-1;
idx=0;
}
//向链表头插入一个数 x
void H(int x){
e[idx]=x;
ne[idx]=head;
head=idx;
idx++;
}
//在第k个插入的数后面插入一个数 x(此操作中 k 均大于 0)
void I(int k,int x){
e[idx]=x;
ne[idx]=ne[k];
ne[k]=idx;
idx++;
}
//删除下标为k的数的后面的数
void D(int k){
ne[k]=ne[ne[k]];
}
int main(){
int x,y;
init();
cin>>m;
while(m--){
string op;
cin>>op;
if(op=="H"){
cin>>x;
H(x);
}
//`D k`,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)
else if(op=="D"){
cin>>x;
if(!x) head=ne[head];
D(x-1);
}
//`I k x`,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)
else {
cin>>x>>y;
I(x-1,y);
}
}
//遍历单链表
for(int i=head;i!=-1;i=ne[i])
cout<<e[i]<<" ";
cout<<endl;
return 0;
}
双链表
2.双链表
实现一个双链表,双链表初始为空,支持 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
//每个点有两个指针,一个指向前,一个指向后
//l[N]存左,r[N]存右
//第一种解法思路
#include<iostream>
using namespace std;
const int N=1e5+10;
int e[N],idx,l[N],r[N];
//0表示左端点,1表示右端点,且0和1表示的是端点的值
void init(){
idx=2;
r[0]=1;
l[1]=0;
}
//`L x`,表示在链表的最左端插入数 x
void L(int x){
e[idx]=x;
//两边伸展
l[idx]=0; r[idx]=r[0];
//先右后左
l[r[0]]=idx;
r[0]=idx;
idx++;
}
//`R x`,表示在链表的最右端插入数 x。
void R(int x){
e[idx]=x;
//两边伸展
l[idx]=l[1];
r[idx]=1;
//先左后右
r[l[1]]=idx;
l[1]=idx;
idx++;
}
//`D k`,表示将第 k 个插入的数删除。
//[将下标为k的结点删除]
void D(int k){
l[r[k]]=l[k];
r[l[k]]=r[k];
}
//`IR k x`,表示在第 k 个插入的数右侧插入一个数
//[函数表示的k是下标为k]
void IR(int k,int x){
e[idx]=x;
r[idx]=r[k]; l[idx]=k; //两边伸展
//先右后左
l[r[k]]=idx;
r[k]=idx;
idx++;
}
int main(){
int m;
int k,x;
init();
cin>>m;
while(m--){
string op;
cin>>op;
//`L x`,表示在链表的最左端插入数 x。
if(op=="L") {
int x;
cin>>x;
L(x);
}
//`R x`,表示在链表的最右端插入数 x
else if(op=="R"){
int x;
cin>>x;
R(x);
}
//`D k`,表示将第 k 个插入的数删除
else if(op=="D")
{ int k;
cin>>k;
D(k+1);
}
//`IR k x`,表示在第 k 个插入的数右侧插入一个数
else if(op=="IR"){
int k,x;
cin>>k>>x;
IR(k+1,x);
}
//`IR k x`,表示在第 k 个插入的数左侧插入一个数
else{
int k,x;
cin>>k>>x;
IR(l[k+1],x);;
}
}
for(int i=r[0];i!=1;i=r[i]){
cout<<e[i]<<" ";
}
return 0;
}
//第二种解法思路
#include<iostream>
using namespace std;
const int N=1e5+10;
int e[N],idx,l[N],r[N];
//0表示左端点,1表示右端点,且0和1表示的是端点的值
void init(){
idx=2;
r[0]=1;
l[1]=0;
}
//[将下标为k的结点删除]
void D(int k){
l[r[k]]=l[k];
r[l[k]]=r[k];
}
//`IR k x`,表示在第 k 个插入的数右侧插入一个数
//[函数表示的k是下标为k]
void IR(int k,int x){
e[idx]=x;
r[idx]=r[k]; l[idx]=k; //两边伸展
//先右后左
l[r[k]]=idx;
r[k]=idx;
idx++;
}
int main(){
int m;
int k,x;
init();
cin>>m;
while(m--){
string op;
cin>>op;
//`L x`,表示在链表的最左端插入数 x。
if(op=="L") {
int x;
cin>>x;
IR(0,x);
}
//`R x`,表示在链表的最右端插入数 x
else if(op=="R"){
int x;
cin>>x;
IR(l[1],x);
}
//`D k`,表示将第 k 个插入的数删除
else if(op=="D")
{ int k;
cin>>k;
D(k+1);
}
//`IR k x`,表示在第 k 个插入的数右侧插入一个数
else if(op=="IR"){
int k,x;
cin>>k>>x;
IR(k+1,x);
}
//`IR k x`,表示在第 k 个插入的数左侧插入一个数
else{
int k,x;
cin>>k>>x;
IR(l[k+1],x);;
}
}
for(int i=r[0];i!=1;i=r[i]){
cout<<e[i]<<" ";
}
return 0;
}
栈
3.模拟栈
实现一个栈,栈初始为空,支持四种操作:
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
//tt下标从1开始存东西
//tt就是本身,每次先给tt加一下再存东西
//query--查询栈顶元素一定是保证不空
#include<iostream>
using namespace std;
const int N=1e5+10;
int stk[N],tt;
int main(){
int m;
cin>>m;
while(m--){
string op;int x;
cin>>op;
//1. `push x` – 向栈顶插入一个数 x;
//2. `pop` – 从栈顶弹出一个数;
//3. `empty` – 判断栈是否为空;
//4. `query` – 查询栈顶元素。
if(op=="push"){
cin>>x;
stk[++tt]=x;
}else if(op=="query"){
if(tt>0) cout<<stk[tt]<<endl;
}else if(op=="pop"){
tt--;
}else if(op=="empty"){
if(!tt) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
}
return 0;
}
4.表达式求值
给定一个表达式,其中运算符仅包含 +,-,*,/
(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。
注意:
- 数据保证给定的表达式合法。
- 题目保证符号
-
只作为减号出现,不会作为负号出现,例如,-1+2
,(2+2)*(-(1+1)+2)
之类表达式均不会出现。 - 题目保证表达式中所有数字均为正整数。
- 题目保证表达式在中间计算过程以及结果中,均不超过 231−1。
- 题目中的整除是指向 0 取整,也就是说对于大于 0 的结果向下取整,例如 5/3=1,对于小于 0 的结果向上取整,例如 5/(1−4)=−1。
- C++和Java中的整除默认是向零取整;Python中的整除
//
默认向下取整,因此Python的eval()
函数中的整除也是向下取整,在本题中不能直接使用。
输入格式
共一行,为给定表达式。
输出格式
共一行,为表达式的结果。
数据范围
表达式的长度不超过 105105。
输入样例:
(2+2)*(1+1)
输出样例:
8
//栈顶的优先级大于等于要进入栈的就出栈运算
//栈顶的优先级小于要进栈的就加进来进栈的
#include<iostream>
#include<stack>
#include<string>
#include<unordered_map>
using namespace std;
stack<int> num;
stack<char> op;
unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};
void eval(){
int b=num.top(); num.pop();
int a=num.top(); num.pop();
int x=0;
if(op.top()=='+') x=a+b;
if(op.top()=='-') x=a-b;
if(op.top()=='*') x=a*b;
if(op.top()=='/') x=a/b;
op.pop();
num.push(x);
}
int main(){
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(c=='(')
op.push(c);
else if(c==')'){
while(op.top()!='(') eval();
op.pop();//出左括号
}
else {
//算的是栈里面的
while(op.size()&&pr[op.top()]>=pr[str[i]]) eval();
//然后新进栈的放后面
op.push(str[i]);
}
}
while(op.size()) eval();
cout<<num.top()<<endl;
return 0;
}
队列
5.模拟队列
实现一个队列,队列初始为空,支持四种操作:
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
//1. `push x` – 向队尾插入一个数 x;
//2. `pop` – 从队头弹出一个数;
//3. `empty` – 判断队列是否为空;
//4. `query` – 查询队头元素。
//队头指向第一个,队尾指向下一个
#include<iostream>
using namespace std;
const int N=1e5+10;
int q[N],hh,tt;
int main(){
int m,x;
cin>>m;
string op;
while(m--){
cin>>op;
if(op=="push")
{ cin>>x;
q[tt++]=x;
}
else if(op=="pop"){
hh++;
}
else if(op=="empty"){
if(tt>hh) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
else if(op=="query"){
cout<<q[hh]<<endl;
}
}
return 0;
}
单调栈
6.单调栈
给定一个长度为 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<iostream>
using namespace std;
const int N=1e5+10;
int stk[N],top;
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++){
int x;
cin>>x;
//比较
while(top&&stk[top]>=x) top--;
if(!top) cout<<-1<<" ";
else cout<<stk[top]<<" ";
stk[++top]=x;
}
return 0;
}
单调队列
7.滑动窗口
给定一个大小为 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
样例执行过程
1 3 -1 -3 5 3 6 7
k=3
i=0 队列中 1进队
i=1 1<3 3进队
i=2 -1比1、3都小,队尾出队 3出队 1出队 -1进队 i+1>=3 输出-1
i=3 -1的下标是2 满足3-3+1=1小于2 -1比-3大 -1出队 -3进队 输出队头-3
i=4 此时队列中只有一个元素-3 且下标为3 满足4-3+1=2不大于3 5比-3大 5进队 输出队头-3
i=5 此时队列中有两个元素 -3 5 下标分别为3、4 满足5-3+1=3 不大于3 5>3 将5出队 3进队 输出队头-3
i=6 此时队列中有两个元素 -3 3 下标分别为3、5 满足6-3+1=4>3 队头后移 此时队列中有一个元素3 3小于6 6进队 输出队头3
i=7 此时队列中有两个元素 3 6 下标分别为5、6 满足7-3+1=5 不大于5 6<7 将7入队,输出队头3
程序结束
//单调队列
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int a[N],q[N],hh,tt=-1;
int main(){
int n,k;
cin>>n>>k;
for(int i=0;i<n;i++) scanf("%d",&a[i]);
//求最小值
for(int i=0;i<n;i++){
//1.判定是否在窗口内,若不在窗口内,hh++即窗口后移一位
if(hh<=tt&&i-k+1>q[hh]) hh++;
//2.判断当遍历的(即窗口外的)更小时,将窗口内的出队列(队尾出队)
while(hh<=tt&&a[i]<=a[q[tt]]) tt--;
//3.再将窗口外的入队
q[++tt]=i;
//4.从第一个完整的窗口开始,进行输出
if(i+1>=k) printf("%d ",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[i]>=a[q[tt]]) tt--;
q[++tt]=i;
if(i+1>=k) printf("%d ",a[q[hh]]);
}
return 0;
}
KMP
8.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
题目分析
next[i]数组表示的含义是以i为终点的后缀和从1开始的前缀两者相等的最大值
next[i]=j
两边往中间延申,达到长度最长即为至少往后移动的距离,求next的数组就是求前缀和后缀相等时的最大值
含义是p[1,j]=p[i-j+1,i]
p | a | b | a | b | a |
---|---|---|---|---|---|
下标 | 1 | 2 | 3 | 4 | 5 |
next[] | 0 | 0 | 1 | 2 | 3 |
求next数组的执行过程
ne[1]=0;j=0//就从头比较
i=2 p[2]==p[1]? 不等于-->ne[2]=0;
i=3 p[3]==p[1] 等于-->j=1 ne[3]=1;
i=4 p[4]==p[2]? 等于-->j=2 ne[4]=2;
i=5 p[5]!=p[3]? 等于-->j=3 ne[5]=3;
#include<iostream>
using namespace std;
const int N=100010,M=1000010;
int n,m;
char p[N],s[M];
int ne[N];
int main(){
cin>>n>>p+1>>m>>s+1;
//求next的过程
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[]赋值
ne[i]=j;
}
//kmp匹配过程
for(int i=1,j=0;i<=m;i++){
//j是有对应的p串元素,失配则回溯
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
9.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 = 1e5+10;
//Trie树:用来高效地存储和查找字符串集合的数据结构。
//son[p][u] p是指某一层,u是指该层真正的结点0~25 表示a~z
//idx是类似一个指针,确定整个树的结点序号,方便给cnt[idx]赋值
int son[N][26],cnt[N];//son存的是整颗树的结点,cnt存的是以当前结点结尾的字符串有多少个
int idx;//下标是0
char str[N];
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];
}
int main(){
int n;
cin>>n;
while(n--){
char op[2];
scanf("%s%s",op,str);
if(op[0]=='I') insert(str);
else printf("%d\n",query(str));
}
return 0;
}
10.最大异或对
在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N 个整数 A1~AN。
输出格式
输出一个整数表示答案。
数据范围
1≤N≤105,
0≤Ai<231
输入样例:
3
1 2 3
输出样例:
3
//求第k位
//n>>k&1
#include<iostream>
#include<algorithm>
using namespace std;
//N是输入数的个数,每个输入的数可拆解31位,最大存储结点为N*31,有31位但都可以分为0或1
const int N=1e5+10,M=31*N;
int n;
int a[N];
//M是总结点
int son[M][2],idx;
//31位是题目给的
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 search(int x){
int p=0,res=0;
for(int i=30;i>=0;i--){
int u=x>>i&1;
//相反位存在则取相反位,不存在就源路径
if(son[p][!u]){
//取相反位,例如u是0,!u是1
p=son[p][!u];
res=res*2+1;//此处是最高位,因为它不停的*2累加,看似是最低位,其实累加操作完成后是最高位,下面同理
}
else{
p=son[p][u];
res=res*2+0;
}
}
return res;
}
int main(){
cin>>n;
for(int i=0;i<n;i++) {
scanf("%d",&a[i]);
insert(a[i]);
}
int res=0;
for(int i=0;i<n;i++)
res=max(res,search(a[i]));
cout<<res<<endl;
return 0;
}
并查集
11.合并集合
一共有 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
并查集
1.将两个集合合并
2.询问两个元素是否在一个集合当中
基本原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。
问题1:如何判断树根:if(p[x]==x)
问题2:如何求x的集合编号:while(p[x]!=x) x=p[x];
问题3:合并两个集合:px是x的集合编号,py是y的集合编号。p[x]=y
并查集的优化:路径压缩。
树太高的话遍历时间太长,都指向根节点可解决这个问题,实现路径压缩。
#include<iostream>
using namespace std;
const int N=1e5+10;
int p[N];
int n,m;//n个数、m种操作
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);//先往上找,找到顶,代代相传往下落
return p[x];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) p[i]=i;
while(m--){
char op[2];
int a,b;
scanf("%s%d%d",op,&a,&b);
if(op[0]=='M') p[find(a)]=find(b);
else {
if(p[find(a)]==p[find(b)])
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
}
return 0;
}
12. 连通块中点的数量
给定一个包含 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<iostream>
using namespace std;
const int N=1e5+10;
int n,m;
int p[N],se[N];
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main(){
cin>>n>>m;
int a,b;
for(int i=1;i<=n;i++)
{ p[i]=i;
se[i]=1;
}
while(m--){
char op[5];
scanf("%s",op);
//合并
if(op[0]=='C')
{
scanf("%d%d",&a,&b);
if(find(a)==find(b)) continue;
se[find(a)]+=se[find(b)];
p[find(b)]=find(a);
}
//查询
else if(op[1]=='1')
{
scanf("%d%d",&a,&b);
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
//查看大小
else
{
scanf("%d",&a);
printf("%d\n",se[find(a)]);
}
}
return 0;
}
13.食物链
动物王国中有三类动物 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
//核心代码理解,主要是看给d[i]赋值的过程
int find(int x)
{
if(p[x]!=x)
{
int tmp=find(p[x]);
d[x]+ = d[p[x]];
p[x] = tmp;
}
return p[x];
}
//d[i]的正确理解,应是第i个节点到其父节点距离
//find()函数进行路径压缩,当查询某个节点i时,如果i的父节点不为根节点的话,就会进行递归调用,将i节点沿途路径上所有节点均指向父节点,此时的d[i]存放的是i到父节点,也就是根节点的距离
//只有3种动物且构成单方向环形
//有n个动物
//根是0 1吃根 2吃 1
//1 2 3 1 2 3
//0->1->2->3->4>5->0
//模三余0 与根是同类
//模三余1 吃根
//模三余0 与根是同类
#include<iostream>
using namespace std;
const int N=5e4+10;
int n,m;
int p[N],d[N];
int find(int x){
if(p[x]!=x){
int t=find(p[x]);
d[x]=d[x]+d[p[x]];
p[x]=t;
}
return p[x];
}
//主函数更新集合,d[i]指的是与父节点的关系
//find函数一个是查找根节点,另一个是路径压缩,全部指向根节点,且求出距离长度
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) p[i]=i;
int res=0;
while(m--){
int t,x,y;
scanf("%d%d%d",&t,&x,&y);
if(x>n||y>n) res++;
else{
int px=find(x),py=find(y);
if(t==1){
//在一个集合里面
if(px==py&& (d[x]-d[y])%3) res++;
//说明不在一个集合里面,不在一个集合的话不是判断,而是加入,当真话的弄
else if(px!=py){
//py是老大
p[px]=py;
// (?+d[x]-d[y])%3=0又或者(d[y]-d[x]-?)%3=0
d[px]= d[y]-d[x];
}
}else{
if(px==py&& (d[x]-d[y]-1)%3) res++;
//不在一个集合,姑且认为是真话,直接加进来
else if(px!=py){
p[px]=py;
//x吃y (x+1-y)%3=0
// (d[x]+?-1-d[y])%3=0
d[px]=d[y]-d[x]+1;
}
}
}
}
cout<<res<<endl;
}
堆
14.堆排序
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式
第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。
数据范围
1≤m≤n≤105,
1≤数列中元素≤109
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
//小根堆
#include<iostream>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],se;
void down(int u)
{ //t类似一个指针
int t=u;
if(2*u<=se&&h[u*2]<h[t]) t=u*2;
if(2*u+1<=se&&h[u*2+1]<h[t]) t=u*2+1;
if(t!=u)
{ swap(h[t],h[u]);
down(t);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
se=n;
for(int i= n/2;i;i--) down(i);
while(m--){
printf("%d ",h[1]);
h[1]=h[se];
se --;
down(1);
}
return 0;
}
15.模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
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
//1. `I x`,插入一个数 x;
//2. `PM`,输出当前集合中的最小值;
//3. `DM`,删除当前集合中的最小值(数据保证此时的最小值唯一);
//4. `D k`,删除第 k 个插入的数;
//5. `C k x`,修改第 k 个插入的数,将其变为 x;
//现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。
//小根堆
//需要两个额外数组,ph[]和hp[]
//ph存的是第k个插入的点的堆里面的下标
//hp存的是堆里面的点下标对应的插入次序
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int h[N],ph[N],hp[N],se;
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<=se&&h[u*2]<h[t]) t=u*2;
if(u*2+1<=se&&h[u*2+1]<h[t]) t=u*2+1;
if(t!=u){
heap_swap(u,t);
down(t);
}
}
void up(int u){
// u/2是父节点
while(u/2&&h[u/2]>h[u]){
heap_swap(u/2,u);
u/=2;
}
}
int main(){
int n,m=0;
cin>>n;
while(n--){
char op[5];
scanf("%s",op);
//strcmp函数等于返回0
if(!strcmp(op,"I")){
int x;
scanf("%d",&x);
m++;
h[++se]=x;
ph[m]=se;hp[se]=m;
up(se);
}
else if(!strcmp(op,"PM"))
printf("%d\n",h[1]);
else if(!strcmp(op,"DM")){
heap_swap(1,se);
se--;
down(1);
}
else if(!strcmp(op,"D")){
int k;
scanf("%d",&k);
k=ph[k];
heap_swap(k,se);
se--;
down(k),up(k);
}
else if(!strcmp(op,"C")){
int k,x;
scanf("%d%d",&k,&x);
k=ph[k];
h[k]=x;
down(k),up(k);
}
}
return 0;
}
哈希表
16.模拟散列表
维护一个集合,支持如下几种操作:
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<iostream>
#include<cstring>
using namespace std;
const int N=1e5+3;
int h[N],e[N],ne[N],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;
}
void insert(int x){
//转换成数学意义的余数
//最后模N是为了防止正数无缘无故多加一遍
int k=(x%N+N)%N;
e[idx]=x;
ne[idx]=h[k];//h[k]指的是链子
h[k]=idx;
idx++;
}
int main(){
int n;
scanf("%d",&n);
//将h数组全部初始化为-1
memset(h,-1,sizeof h);
while(n--){
char op[2];
int x;
scanf("%s%d",op,&x);
if(op[0]=='I'){
insert(x);
}else{
if(find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
17.字符串哈希
给定一个长度为 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<iostream>
using namespace std;
typedef unsigned long long ULL;
const int N=1e5+10,P=131;
int n,m;
char str[N];
ULL h[N],p[N];
ULL get(int l,int r){
return h[r]-h[l-1]*p[r-l+1];
}
int main(){
scanf("%d%d%s",&n,&m,str+1);
p[0]=1;
for(int i=1;i<=n;i++){
p[i]=p[i-1]*P; //p[i]就是p的i次方
h[i]=h[i-1]*P+str[i];//求值
}
while(m--){
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if(get(l1,r1)==get(l2,r2))
puts("Yes");
else
puts("No");
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端