目录
2.12.2 lower_bound/upper_bound(二分)
2.12.15 priority_queue(优先队列/堆)
写在前面的话
此篇涵盖:经典题目458道左右+杂题数量不定,每天都会更新一些题目和详细的题解,另外还有一些自己平时遇到的难题,给自己打个笔记,供以后复习使用,同时也希望对大家有所帮助,感谢支持~。
level1——基础题目——100多道
level2——提高题目——200多道
level3——Top1题目——100多道
level4——遇到的难题——数量不定
levle5——Top2题目——300多道
快读快写的模板
数据比较大时,可以加快读入写入的速度。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
void read(int &x)//快读
{
x=0;
int w=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') w=-1;
c=getchar();
}
while(c>='0'&&c<='9')
{
x=x*10+c-'0';
c=getchar();
}
x*=w;
return ;
}
void print(int x)//快写
{
if(x<0)
{
putchar('-');
x=-x;
}
if(x>=10) print(x/10);
putchar(x%10+'0');
return ;
}
int main()
{
int x;
read(x);
print(x);
}
Level1
1.基础算法篇
1.1快速排序
给定你一个长度为 n 的整数数列。
请你使用快速排序对这个数列按照从小到大进行排序。
并将排好序的数列按顺序输出。
输入格式
输入共两行,第一行包含整数 n。
第二行包含 n 个整数(所有整数均在 1∼1e9),表示整个数列。
输出格式
输出共一行,包含 n 个整数,表示排好序的数列。
数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5
快速排序是基于分治的思想对区间进行处理。
思路
快排是基于分治的思想对区间进行处理,每次都会选择一个比较的值x,x可以是首元素、中间元素、尾元素,一般多为选择中间的元素作为排序的关键字元素,把两个指针分别置于左右两个端点,利用两个指针的暂停的条件,使得所有小于等于x的分在x的左边,大于等于x的在x的右边,当两个指针相遇的时候,这一趟结束,然后分别进行递归处理划分的子区间,直到结束。
时间复杂度
快排最坏的时间复杂度为O(nlongn),因为每次都是将区间一分为二,假设一共x次才将区间划分好,n/2^x=1,x=log2n,一共n趟,那么为n*logn.
快排的性质
快速排序是不稳定的一种排序方法,判断一个排序算法是否稳定,取决于两个相同的元素在排好序前后的相对位置是否一样,比如x1=x2,在排序之前的相对位置为x1,x2,在排好序之后相对位置依然不发生改变,那么这个排序算法就是稳定的,否则是不稳定的。
快速排序的做法是非常优美的做法,利用了两个指针的相向而行和规定的限制条件,就可以将序列排好序。
流程
- 确定分界点:x=q[l]或者q[r]或者q[l+r>>1]。
- 调整区间:
使得小于等于x在x左边;大于等于x在x右边。
- 递归处理左右端点。
模拟:
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int q[N];
void quick_sort(int q[],int l,int r){
if(l>=r) return ;
int x=q[l+r>>1],i=l-1,j=r+1;
while(i<j){
do i++;while(q[i]<x);
do j--;while(q[j]>x);
if(i<j) swap(q[i],q[j]);
}
quick_sort(q,l,j);
quick_sort(q,j+1,r);
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++) cin>>q[i];
quick_sort(q,0,n-1);
for(int i=0;i<n;i++) cout<<q[i]<<" ";
return 0;
}
1.2归并排序
给定你一个长度为 n 的整数数列。
请你使用归并排序对这个数列按照从小到大进行排序。
并将排好序的数列按顺序输出。
输入格式
输入共两行,第一行包含整数 nn。
第二行包含 n 个整数(所有整数均在 1∼109),表示整个数列。
输出格式
输出共一行,包含 n 个整数,表示排好序的数列。
数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5
归并排序主要是先将区间划分成两个区间,然后再递归两个区间,开一个辅助数组tmp[]对两个区间的数值进行处理,本质也是双指针算法。
思路:
- 确定分界点mid=l+r>>1;
- 递归排序l,r;
- 将两个有序的数组合二为一;
模拟:
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int q[N],tmp[N];
void merge_sort(int q[],int l,int r){
if(l>=r) return ;
int mid=l+r>>1;
merge_sort(q,l,mid);
merge_sort(q,mid+1,r);
int k=0,i=l,j=mid+1;
while(i<=mid&&j<=r)
if(q[i]<q[j]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
while(i<=mid ) tmp[k++] = q[i++];
while(j<=r) tmp[k++] = q[j++];
for(i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
}
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&q[i]);
merge_sort(q,0,n-1);
for(int i=0;i<n;i++) cout<<q[i]<<" ";
return 0;
}
1.2.1求逆序对的数量
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i个和第 j个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。
输入格式
第一行包含整数 n,表示数列的长度。
第二行包含 n 个整数,表示整个数列。
输出格式
输出一个整数,表示逆序对的个数。
数据范围
1≤n≤100000,
数列中的元素的取值范围 [1,109]。
输入样例:
6
2 3 4 5 6 1
输出样例:
5
假设归并排序在排好序的同时可以将区间中的逆序对求出来。
思路:
- 划分区间;[l,r]->[l,mid],[mid+1,r]
- 递归处理[l,mid]和[mid+1,r];
- 归并,将左右两个有序序列合并成为一个序列;
区间中逆序对的分布:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e6+10;
int n;
int q[N],tmp[N];
LL merge_sort(int l,int r)
{
if(l>=r) return 0;
int mid=l+r>>1;
LL res=merge_sort(l,mid)+merge_sort(mid+1,r);
int k=0,i=l,j=mid+1;
while(i<=mid&&j<=r)
{
if(q[i]<=q[j]) tmp[k++]=q[i++];
else
{
tmp[k++]=q[j++];
res+=mid-i+1;
}
}
while(i<=mid) tmp[k++]=q[i++];
while(j<=r) tmp[k++]=q[j++];
for(int i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
return res;
}
int main()
{
cin>>n;
for(int i=0;i<n;i++) cin>>q[i];
cout<<merge_sort(0,n-1)<<endl;
return 0;
}
1.4二分
1.4.1数的范围
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回 -1 -1。
输入格式
第一行包含整数 n和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1。
数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
整数二分模板
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N=100100;
int q[N],x;
int bsearch_1(int l,int r)
{
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<=x) l=mid;
else r=mid-1;
}
return l;
}
int bsearch_2(int l,int r)
{
while(l<r)
{
int mid=l+r>>1;
if(q[mid]>=x) r=mid;
else l=mid+1;
}
return l;
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=0;i<n;i++) cin>>q[i];
while(m--){
cin>>x;
if(q[bsearch_1(0,n-1)]==x&&q[bsearch_2(0,n-1)]==x)
{
cout<<bsearch_2(0,n-1)<<" "<<bsearch_1(0,n-1)<<endl;
}
else
cout<<"-1 -1"<<endl;
}
return 0;
}
1.4.2数的三次方根
给定一个浮点数 n,求它的三次方根。
输入格式
共一行,包含一个浮点数 n。
输出格式
共一行,包含一个浮点数,表示问题的解。
注意,结果保留 6 位小数。
数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000
浮点数二分模板
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
AC代码
#include<bits/stdc++.h>
using namespace std;
int main()
{
double n;
cin>>n;
double l=-1000,r=1000;
while(r-l>1e-8){
double mid=(l+r)/2;
if(mid*mid*mid>=n) r=mid;
else l=mid;
}
printf("%.6lf",r);
return 0;
}
1.5高精度算法
性质:数组或者容器从低位往高位依次存储大整数,方便进位。
1.5.1高精度加法
给定两个正整数(不含前导 0),计算它们的和。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的和。
数据范围
1≤整数长度≤100000
输入样例:
12
23
输出样例:
35
思路:
模拟人工加法。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
using namespace std;
vector<int> sum(vector<int> &A,vector<int> &B)
{
vector<int> C;
int k=0;
for(int i=0;i<max(A.size(),B.size());i++)
{
if(i<A.size()) k+=A[i];
if(i<B.size()) k+=B[i];
C.push_back(k%10);
k/=10;
}
if(k) C.push_back(1);
return C;
}
int main()
{
string a,b;
vector<int> A,B;
cin>>a>>b;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
vector<int> C=sum(A,B);
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
return 0;
}
1.5.2高精度减法
给定两个正整数(不含前导 0),计算它们的差,计算结果可能为负数。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的差。
数据范围
1≤整数长度≤105
输入样例:
32
11
输出样例:
21
思路:
模拟人工减法。
#include <bits/stdc++.h>
using namespace std;
vector<int> A,B;
bool cmp(vector<int> &A,vector<int> &B){
if(A.size()!=B.size()) return A.size()>B.size();
else{
for(int i=A.size()-1;i>=0;i--){
if(A[i]!=B[i]) return A[i]>B[i];
}
}
return 1;
}
vector<int> sub(vector<int> &A,vector<int> &B){
int k=0;//表示上一位在这一位借走的位数
vector<int> C;
for(int i=0;i<A.size();i++){
int t=A[i]-k;
if(i<B.size()) t-=B[i];
if(t<0) t+=10,k=1;
else k=0;
C.push_back(t%10);
}
while(C.size()>1&&C.back()==0) C.pop_back();
return C;
}
int main(){
string a,b;
cin>>a>>b;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
vector<int> C;
if(cmp(A,B)) C=sub(A,B); //当A>=B时,答案为0或正值
else C=sub(B,A),cout<<"-"; //当A<B时,答案为负值
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
return 0;
}
1.5.3高精度乘法
给定两个非负整数(不含前导 0)A 和 B,请你计算 A×B 的值。
输入格式
共两行,第一行包含整数 A,第二行包含整数 B。
输出格式
共一行,包含 A×B 的值。
数据范围
1≤A的长度≤100000,
0≤B≤10000
输入样例:
2
3
输出样例:
6
高精度x低精度
//高精度x低精度
#include<bits/stdc++.h>
#include<vector>
using namespace std;
vector<int> mul(vector<int> &A,int b)
{
vector<int> C;
int t=0;
for(int i=0;i<A.size();i++)
{
t+=A[i]*b;
C.push_back(t%10);
t/=10;
}
while(t)
{
C.push_back(t%10);
t/=10;
}
while(C.size()>1&&C.back()==0) C.pop_back();
return C;
}
int main()
{
string a;
int b;
cin>>a>>b;
vector<int> A;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
auto C=mul(A,b);
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
return 0;
}
高精度x高精度
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 1e5+10;
int A[N],B[N],C[N];
int la,lb,lc;
void mul(int A[],int B[],int C[])
{
for(int i=0;i<la;i++)
for(int j=0;j<lb;j++)
{
C[i+j]+=A[i]*B[j];
C[i+j+1]+=C[i+j]/10;
C[i+j]%=10;
}
while(lc&&C[lc]==0) lc--;
}
int main()
{
string a,b;
cin>>a>>b;
la=a.size();
lb=b.size();
lc=la+lb+10;
for(int i=a.size()-1;i>=0;i--) A[la-i-1]=a[i]-'0';
for(int i=b.size()-1;i>=0;i--) B[lb-i-1]=b[i]-'0';
mul(A,B,C);
for(int i=lc;i>=0;i--) cout<<C[i];
return 0;
}
1.5.4高精度除法
给定两个非负整数(不含前导 0)A,B,请你计算 A/B的商和余数。
输入格式
共两行,第一行包含整数 A,第二行包含整数 B。
输出格式
共两行,第一行输出所求的商,第二行输出所求余数。
数据范围
1≤A的长度≤100000,
1≤B≤10000,
B 一定不为 00
输入样例:
7
2
输出样例:
3
1
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> div(vector<int> &A,int B,int &r)
{
vector<int> C;
for(int i=0;i<A.size();i++)
{
r=r*10+A[i];
C.push_back(r/B);
r%=B;
}
reverse(C.begin(),C.end());
while(C.size()>1&&C.back()==0) C.pop_back();
return C;
}
int main()
{
string a;
int B,r=0;
cin>>a>>B;
vector<int> A;
for(int i=0;i<a.size();i++) A.push_back(a[i]-'0');
auto C=div(A,B,r);
for(int i=C.size()-1;i>=0;i--) cout<<C[i];
cout<<endl<<r;// 输出余数
return 0;
}
1.5.5高精度阶乘
问题描述
输入一个正整数n,输出n!的值。
其中n!=1*2*3*…*n。
算法描述
n!可能很大,而计算机能表示的整数范围有限,需要使用高精度计算的方法。使用一个数组A来表示一个大整数a,A[0]表示a的个位,A[1]表示a的十位,依次类推。
将a乘以一个整数k变为将数组A的每一个元素都乘以k,请注意处理相应的进位。
首先将a设为1,然后乘2,乘3,当乘到n时,即得到了n!的值。
输入格式
输入包含一个正整数n,n<=1000。
输出格式
输出n!的准确值。
样例输入
10
样例输出
3628800
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int n;
int a[N];
int main()
{
scanf("%d",&n);
a[1]=1;
int t=0;
for(int i=2;i<=n;i++)
{
for(int j=1;j<=10000;j++)
{
int p=a[j]*i+t;
a[j]=p%10;
t=p/10;
}
}
n=10000;
while(a[n]==0) n--;
for(int i=n;i>=1;i--) cout<<a[i];
return 0;
}
1.6前缀和与差分
1.6.1一维前缀和
输入一个长度为 n 的整数序列。
接下来再输入 m个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l个数到第 r 个数的和。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式
共 m 行,每行输出一个询问的结果。
数据范围
1≤l≤r≤n,
1≤n,m≤100000,
−1000≤数列中元素的值≤1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10
思路:
如果s[i]是a[i]的前缀和数组,那么就有s[i]=s[i-1]+a[i];
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],s[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
s[i]=s[i-1]+a[i];
}
while(m--)
{
int l,r;
cin>>l>>r;
cout<<s[r]-s[l-1]<<endl;
}
return 0;
}
1.6.2二维前缀和
输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数 n,m,q。
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。
输出格式
共 q 行,每行输出一个询问的结果。
数据范围
1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21
思路:
如果s[i][j]是a[i][j]的前缀和数组,那么我们求(x1,y1)到(x2,y2)就有:
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int a[N][N],s[N][N];
int main()
{
int n,m,q;
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];
}
}
while(q--)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
cout<<s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]<<endl;
}
return 0;
}
1.6.3一维差分
输入一个长度为 n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r]之间的每个数加上 c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数序列。
接下来 m 行,每行包含三个整数 l,r,c表示一个操作。
输出格式
共一行,包含 n个整数,表示最终序列。
数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2
思路:
我们要改变a[]数组中[l,r]区间上的数,那么我们就可以想到一种方法改变a[]数组,我们可以构造a[]数组的差分数组b[]数组。
首先,第一个问题,什么是差分数组?
差分数组的意思是某个数组是差分数组的前缀和数组,则本题中a[]数组就是b[]数组的前缀和数组。
第二,为什么差分数组能够改变a[]数组中任意[l,r]区间里面的数值?
因为a[]是b[]数组的前缀和如果b[l]+1,那么a[]数组中a[l]之后的所有的数都会+1,因为我们要改变的是a数组[l,r]区间的值,这时候r之后的值也会改变,只需要b[r+1]-1这样就可以只改变[l,r]区间的值.
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],b[N];
int n,m;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
b[i]=a[i]-a[i-1];//构造差分数组
}
while(m--)
{
int l,r,c;
scanf("%d%d%d",&l,&r,&c);
b[l]+=c;
b[r+1]-=c;
}
for(int i=1;i<=n;i++)
{
a[i]=a[i-1]+b[i];//构造b数组的前缀和维a数组
cout<<a[i]<<" ";
}
return 0;
}
1.6.4二维差分
输入一个 n行 m列的整数矩阵,再输入 q个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1)和 (x2,y2)表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上 cc。
请你将进行完所有操作后的矩阵输出。
输入格式
第一行包含整数 n,m,q。
接下来 n行,每行包含 m个整数,表示整数矩阵。
接下来 q行,每行包含 5个整数 x1,y1,x2,y2,c,表示一个操作。
输出格式
共 n行,每行 m个整数,表示所有操作进行完毕后的最终矩阵。
数据范围
1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例:
2 3 4 1
4 3 4 1
2 2 2 2
思路:
基于一维差分的思想,我们来考虑二维数组的差分。
我们如果让a[][]数组(x1,y1)和(x2,y2)区间的每个数的值都加上c,我们构造b[][]数组的前缀和是a[][]数组。
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int a[N][N],b[N][N];
int main()
{
int n,m,q;
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
b[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];//构造a数组的差数组
}
}
while(q--)
{
int x1,y1,x2,y2,c;
cin>>x1>>y1>>x2>>y2>>c;
b[x1][y1]+=c;
b[x1][y2+1]-=c;
b[x2+1][y1]-=c;
b[x2+1][y2+1]+=c;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
a[i][j]=b[i][j]+a[i-1][j]+a[i][j-1]-a[i-1][j-1];//构造b数组的前缀和
cout<<a[i][j]<<" ";
}
cout<<endl;
}
return 0;
}
1.7双指针算法
基本思路:找到某种性质,对循环进行优化。
1.7.0输出单词
给定一行句子,输出其中的单词
样例输入
i am student
样例输出
i
am
student
#include<bits/stdc++.h>
#include<string>
using namespace std;
int main()
{
char str[1000];
gets(str);
int n=strlen(str);
for(int i=0;i<n;i++)
{
int j=i;
while(j<n&&str[j]!=' ') j++; //指针j扫描到空格
for(int k=i;k<j;k++) cout<<str[k];
cout<<endl;
i=j; //指针i等于空格后的第一个字符
}
return 0;
}
1.7.1最长不重复子序列
给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数 n。
第二行包含 n 个整数(均在 0∼105 范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
数据范围
1≤n≤1e5
输入样例:
5
1 2 2 3 5
输出样例:
3
思路:
我们用count(x)记录x是否重复出现过,我们先用i指针向后探测,j指针先不动,直到出现过重复的,再将j指针向后移动,count(x)--来移动j指针。
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int q[N], s[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
int res = 0;
for (int i = 0, j = 0; i < n; i ++ )
{
s[q[i]] ++ ;
while (j < i && s[q[i]] > 1) s[q[j ++ ]] -- ;
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
1.7.2数组元素的目标和
给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0开始。
请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
数据保证有唯一解。
输入格式
第一行包含三个整数 n,m,x,分别表示 A 的长度,B 的长度以及目标值 x。
第二行包含 n个整数,表示数组 A。
第三行包含 m 个整数,表示数组 B。
输出格式
共一行,包含两个整数 i 和 j。
数据范围
数组长度不超过105。
同一数组内元素各不相同。
1≤数组元素≤109
输入样例:
4 5 6
1 2 4 7
3 4 6 8 9
输出样例:
1 1
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],b[N];
int main()
{
int n,m,x;
cin>>n>>m>>x;
for(int i=0;i<n;i++) cin>>a[i];
for(int i=0;i<m;i++) cin>>b[i];
int l=0,r=m-1;
while(l<n&&r>=0)
{
if(a[l]+b[r]<x) l++;
else if(a[l]+b[r]>x) r--;
else
{
cout<<l<<" "<<r;
return 0;
}
}
return 0;
}
1.7.3判断子序列
给定一个长度为 n 的整数序列 a1,a2,…,an 以及一个长度为 m 的整数序列 b1,b2,…,bm。
请你判断 a 序列是否为 b 序列的子序列。
子序列指序列的一部分项按原有次序排列而得的序列,例如序列 {a1,a3,a5}是序列 {a1,a2,a3,a4,a5} 的一个子序列。
输入格式
第一行包含两个整数 n,m。
第二行包含 n 个整数,表示 a1,a2,…,an。
第三行包含 m 个整数,表示 b1,b2,…,bm。
输出格式
如果 a 序列是 b 序列的子序列,输出一行 Yes。
否则,输出 No。
数据范围
1≤n≤m≤105,
−1e9≤ai,bi≤1e9
输入样例:
3 5
1 3 5
1 2 3 4 5
输出样例:
Yes
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],b[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=0;i<n;i++) cin>>a[i];
for(int i=0;i<m;i++) cin>>b[i];
int ans=0;
int i=0,j=0;
while(i<n&&j<m)
{
if(a[i]==b[j]) ans++,i++;
j++;
}
if(ans==n) cout<<"Yes";
else cout<<"No";
return 0;
}
1.8位运算
&:按位与,1&0=0,0&1=0,0&0=0,1&1=1,只有都为1时才为1.
|:按位或,1|1=1,1|0=1,0|1=1,0|0=0,只有都为0时才为0.
^:按位异或,1^1=0,1^0=1,0^a=a,相同为0,不同为非0的那个数.
>>:右移,a>>x,表示a除以2^x;
<<:左移,a<<x,表示a乘2^x;
~:把0变成1,把1变成0;
-x=~x+1;
(1)lowbit(x)
将十进制数的二进制表示的最低位1取出来。
int lowbit(int x)
{
return x&-x;
}
如x的二进制表示时100,-x在计算机中为~x+1,则~x=011,~x+1=111,那么就有
(100)&(111)=(100),这样就可以把最低位上面的1取出来。
(2)把n对应二进制表示中第k位取出来(注意有第0位)
int get(int n,int k)
{
return n>>k&1;
}
(3)输出所有小于k的十进制
for(int i=0;i<1<<k;i++)
cout<<i;
1.8.1二进制中1的个数
给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。
输入格式
第一行包含整数 n。
第二行包含 n 个整数,表示整个数列。
输出格式
共一行,包含 n 个整数,其中的第 i 个数表示数列中的第 i 个数的二进制表示中 1 的个数。
数据范围
1≤n≤100000,
0≤数列中元素的值≤1e9
输入样例:
5
1 2 3 4 5
输出样例:
1 1 2 1 2
#include<iostream>
using namespace std;
int f(int n)
{
int ans=0;
while(n>0)
{
if(n%2==1) ans++;
n/=2;
}
return ans;
}
int main()
{
int t;
cin>>t;
while(t--)
{
int n;
cin>>n;
cout<<f(n)<<" ";
}
return 0;
}
1.9离散化
概念:在一些问题中,我们只关心n个数字之间的相对大小关系,而不关心他们具体是什么,因此我们可以将这n个数映射成1~n的整数,从而降低规模,通常的实现方法是对所有的数字进行排序,然后再重新遍历一遍所有的数字,通过二分查找法来找到他们的"排名",然后用排名代替数字。
如我们将9999 1 100 1000进行离散化:
(1)离散化方法1--map哈希映射离散化后的值(考虑相对大小),查询离散化后值的时间复杂度O(1)
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
#include<unordered_map>
using namespace std;
unordered_map<int,int> mp;
vector<int> v,h;
int n;
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
{
int x;
scanf("%d",&x);
v.push_back(x);
h.push_back(x);
}
sort(h.begin(),h.end());
h.erase(unique(h.begin(),h.end()),h.end());
for(int i=0;i<h.size();i++)
mp[h[i]]=i+1;
int x;
scanf("%d",&x);
printf("%d",mp[x]);
return 0;
}
(2)离散化方法2--二分查找离散化的值(考虑相对大小),查询离散化后的值的时间复杂度O(logn)
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
#include<unordered_map>
using namespace std;
unordered_map<int,int> mp;
vector<int> v,h;
int n;
int find(int x)
{
int l=0,r=h.size()-1;
while(l<r)
{
int mid=l+r>>1;
if(h[mid]>=x) r=mid;
else l=mid+1;
}
return l+1;
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
{
int x;
scanf("%d",&x);
v.push_back(x);
h.push_back(x);
}
sort(h.begin(),h.end());
h.erase(unique(h.begin(),h.end()),h.end());
int x;
scanf("%d",&x);
printf("%d",find(x));
return 0;
}
(3) 离散化方法3--不需要保序的离散化--unordered_map
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<unordered_map>
using namespace std;
unordered_map<int,int> h;
int idx;
int x;
int get(int x)
{
if(h.count(x)==0) h[x]=++idx;
return h[x];
}
int main()
{
scanf("%d",&x);
printf("%d",get(x));
return 0;
}
1.9.1区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x上的数加 c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r]之间的所有数的和。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含两个整数 x 和 c。
再接下来 m 行,每行包含两个整数 l和 r。
输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。
数据范围
−109≤x≤109,
1≤n,m≤105,
−109≤l≤r≤109,
−10000≤c≤10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N];
vector<int> alls;
vector<PII> add, query;
int find(int x)
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
vector<int>::iterator unique(vector<int> &a)
{
int j = 0;
for (int i = 0; i < a.size(); i ++ )
if (!i || a[i] != a[i - 1])
a[j ++ ] = a[i];
// a[0] ~ a[j - 1] 所有a中不重复的数
return a.begin() + j;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
{
int x, c;
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}
for (int i = 0; i < m; i ++ )
{
int l, r;
cin >> l >> r;
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
// 去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls), alls.end());
// 处理插入
for (auto item : add)
{
int x = find(item.first);
a[x] += item.second;
}
// 预处理前缀和
for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];
// 处理询问
for (auto item : query)
{
int l = find(item.first), r = find(item.second);
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
1.10区间合并
给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。
输入格式
第一行包含整数 nn。
接下来 nn 行,每行包含两个整数 l 和 r。
输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。
数据范围
1≤n≤100000,
−109≤li≤ri≤109
输入样例:
5
1 2
2 4
5 6
7 8
7 9
输出样例:
3
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
int n;
void merge(vector<PII> &interval)
{
vector<PII> ans;
sort(interval.begin(), interval.end()); //! pair排序 优先左端点, 再以右端点排序
int st = -1e9-10, ed = -1e9-10; //! 只要比 -1e9 小就可以
for(auto item:interval)
{
if(ed<item.first) //! 第一段区间一定是 ed< item.first
{
if(st!=-1e9-10) ans.push_back({st,ed}); //! 第一次在这里初始化
st = item.first, ed = item.second;//! 第一段区间从这里开始
}
else ed = max(ed, item.second);
}//todo 这个循环结束之后还会剩下一个区间
if(st!=-1e9-10) ans.push_back({st,ed}); //! 如果不是空的 那我们就加上一段
interval = ans;
}
int main(void)
{
ios::sync_with_stdio(false);
cin >> n;
vector<PII> interval;
while(n--)
{
int l, r;
cin >> l >> r;
interval.push_back({l, r});
}
merge(interval);
cout << interval.size() << endl;
return 0;
}
2.数据结构篇
用数组模拟链表。
2.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
我们用-1表示空指针。
实现一些基本的操作:
(1)初始化
头节点指向-1表示空节点,idx=0表示从0好节点进行编号。
void init()//链表的初始化
{
head=-1;//头节点指向空节点
idx=0;
}
(2)向头节点后面插入一个新节点
(3)向第k个插入的点后面添加一个点同(2)
void add(int k,int x)//向第k个插入的数后面插入一个数
{
e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}
因为是从0号节点进行编号的,所以第k个插入的点其实是第k-1个点add(k-1,x);
(4)删除头节点
void remove()//删除头节点
{
head=ne[head];
}
(5)删除第k个插入的点
void de(int k)//删除第k个插入的数
{
ne[k]=ne[ne[k]];
}
remove(k-1);
AC代码
#include<bits/stdc++.h>
#include<string>
using namespace std;
const int N=1e6+10;
int head,e[N],ne[N],idx;
void init()//链表的初始化
{
head=-1;
idx=0;
}
void add_head(int x)//向头节点之后插入一个数
{
e[idx]=x,ne[idx]=head,head=idx++;
}
void add(int k,int x)//向第k个插入的数后面插入一个数
{
e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}
void de(int k)//删除第k个插入的数
{
ne[k]=ne[ne[k]];
}
void remove()//删除头节点
{
head=ne[head];
}
int main()
{
int t;
scanf("%d",&t);
init();
while(t--){
string op;
int k,x;
cin>>op;
if(op=="H"){
scanf("%d",&x);
add_head(x);
}
else if(op=="D"){
scanf("%d",&k);
if(k==0) remove();
de(k-1);
}
else{
scanf("%d%d",&k,&x);
add(k-1,x);
}
}
for(int i=head;i!=-1;i=ne[i])
cout<<e[i]<<" ";
return 0;
}
2.2双链表
实现一个双链表,双链表初始为空,支持 55 种操作:
在最左侧插入一个数;
在最右侧插入一个数;
将第 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[],r[],分别指向前驱和后继。
模板:
// 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];
}
#include<bits/stdc++.h>
#include<string>
#include<algorithm>
using namespace std;
const int N=1e6+10;
int l[N],r[N],e[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 remove(int k)
{
r[l[k]]=r[k];
l[r[k]]=l[k];
}
int main()
{
init();
int t;
cin>>t;
while(t--)
{
string op;
cin>>op;
int k,x;
if(op=="R")
{
cin>>x;
add(l[1],x);
}
else if(op=="L")
{
cin>>x;
add(0,x);
}
else if(op=="D")
{
cin>>k;
remove(k+1);
}
else if(op=="IL")
{
cin>>k>>x;
add(l[k+1],x);
}
else
{
cin>>k>>x;
add(k+1,x);
}
}
for(int i=r[0];i!=1;i=r[i])
cout<<e[i]<<" ";
return 0;
}
2.3栈
2.3.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≤1e9
所有操作保证合法。
输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO
栈:后进先出的数据结构。
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[ ++ tt] = x;
// 从栈顶弹出一个数
tt -- ;
// 栈顶的值
stk[tt];
// 判断栈是否为空
if (tt > 0)
{
}
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int stk[N],tt=0;
int main()
{
int t;
cin>>t;
while(t--)
{
string op;
cin>>op;
if(op=="push")
{
int x;
cin>>x;
stk[++tt]=x;
}
else if(op=="pop")
{
tt--;
}
else if(op=="empty")
{
if(tt>0)
cout<<"NO"<<endl;
else
cout<<"YES"<<endl;
}
else if(op=="query")
{
cout<<stk[tt]<<endl;
}
}
return 0;
}
2.3.2表达式求值
给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。
注意:
数据保证给定的表达式合法。
题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。
题目保证表达式中所有数字均为正整数。
题目保证表达式在中间计算过程以及结果中,均不超过 2^31-1。
题目中的整除是指向 0取整,也就是说对于大于 0的结果向下取整,例如 5/3=1,对于小于 0的结果向上取整,例如 5/(1−4)=−1。
C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。
输入格式
共一行,为给定表达式。
输出格式
共一行,为表达式的结果。
数据范围
表达式的长度不超过 1e5。
输入样例:
(2+2)*(1+1)
输出样例:
8
我们按照优先级和入栈的顺序对表达式进行处理即可。
#include<bits/stdc++.h>
#include<iostream>
#include<string>
#include<stack>
#include<unordered_map>
using namespace std;
stack<int> num;
stack<char> op;
unordered_map<char,int> h{
{'+',1},{'-',1},{'*',2},{'/',2}};//规定优先级
void eval()
{
int a=num.top();
num.pop();
int b=num.top();
num.pop();
char p=op.top();
op.pop();
int r=0;
if(p=='+') r=b+a;
else if(p=='-') r=b-a;
else if(p=='*') r=b*a;
else if(p=='/') r=b/a;
num.push(r);
}
int main()
{
string s;
cin>>s;
for(int i=0;i<s.size();i++)
{
if(isdigit(s[i]))
{
int x=0,j=i;
while(j<s.size()&&isdigit(s[j]))
{
x=x*10+s[j]-'0';
j++;
}
num.push(x);
i=j-1;
}
else if(s[i]=='(') op.push(s[i]);
else if(s[i]==')')
{
while(op.top()!='(')
eval();
op.pop();
}
else
{
while(op.size()&&h[op.top()]>=h[s[i]])
eval();
op.push(s[i]);
}
}
while(op.size()) eval();
cout<<num.top()<<endl;
return 0;
}
2.4队列
实现一个队列,队列初始为空,支持四种操作:
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≤1e9,
所有操作保证合法。
输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
队列:先进先出。
普通队列
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
// 向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++ ;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh <= tt)
{
}
循环队列
// 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)
{
}
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int q[N],hh=0,tt=-1;
int main()
{
ios::sync_with_stdio(false);
int T;
cin>>T;
while(T--){
string op;
cin>>op;
if(op=="push"){
int x;
cin>>x;
q[++tt]=x;
}
else if(op=="pop"){
hh++;
}
else if(op=="empty"){
if(hh<=tt) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
else cout<<q[hh]<<endl;
}
return 0;
}
2.5单调栈
给定一个长度为 N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式
第一行包含整数 N,表示数列长度。
第二行包含 N个整数,表示整数数列。
输出格式
共一行,包含 N个整数,其中第 i 个数表示第 i个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围
1≤N≤1e5
1≤数列中元素≤1e9
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2
常见模型:给定一个序列,求序列中每个数左边或者右边第一个比他小或者比他大的数。
暴力做法:
for(int i=1;i<=n;i++)
{
int ans=0;
for(int j=i;j>=1;j--)
{
if(a[i]>a[j])
{
cout<<a[j]<<" ";
ans++;
break;
}
}
if(ans==0) cout<<"-1"<<" ";
}
暴力做法的时间复杂度约为O(n2),我们可以考虑用单调栈去维护减低时间复杂度。
单调栈做法:
我们把a[i]左边的数用一个栈存储起来:
如果在栈中,有x<y并且a[x]>=a[y],那么a[x]一定不会作为答案输出,因为a[y]在a[x]的右边,且离a[i]最近,那么我们就可以把a[x]从栈中删除,最后栈中的元素一定是单调的。
如图所示,i=7时,栈中元素分布:
模板:
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
AC代码:
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
scanf("%d", &x);
while (tt && stk[tt] >= x) tt -- ;//如果栈顶元素大于当前待入栈元素,则出栈
if (!tt) printf("-1 ");//如果栈空,则没有比该元素小的值。
else printf("%d ", stk[tt]);//栈顶元素就是左侧第一个比它小的元素。
stk[ ++ tt] = x;
}
return 0;
}
2.6单调队列
2.6.1滑动窗口
给定一个大小为 n≤1e6的数组。
有一个大小为 k的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 kk 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [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
性质:队列里面的元素值是单调的,递增或者递减。
思想:
例如:求滑动窗口的最大值。
用单调队列储存当前窗口内单调递减的元素的下标,并且队头是窗口内的最大值,队尾是窗口内的尾元素。也就是说,队列从队头到队尾对应窗口内从最大值到窗口的尾元素的子序列下标。
1.队头出队:当队头元素从滑动窗口划出时,队头元素出队,hh++。
2.队尾出队:当新的元素进入滑动窗口时,要把新元素从队尾插入,分两种情况:
(1).直接插入:如果新元素小于队尾元素,那么直接从队尾插入(q[++tt]=i),因为他可能在前面的最大值滑出窗口后成为最大值。
(2).先删后插:如果新元素大于等于队尾元素,那就先删除队尾元素(因为队尾不可能成为滑动窗口的最大值),删除队尾tt--,循环删除,直到队列为空或遇到一个大于新元素的值,再插入。
求最小值的思路相同。
AC代码
#include<iostream>
using namespace std;
const int N = 1e6+10;
int a[N],q[N];
int n,k;
int main()
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>a[i];
//求滑动窗口里面的最小值。
int hh=0,tt=-1;
for(int i=1;i<=n;i++)
{
if(hh<=tt&&q[hh]<i-k+1) hh++; //如果队头元素值表示序列的下表不在滑动窗口的范围内,队头出队。
while(hh<=tt&&a[i]<=a[q[tt]]) tt--; //如果插入的元素小于队尾元素,队尾出队,直到不小于为止。
q[++tt]=i; //下表入队
if(i>k-1) cout<<a[q[hh]]<<" "; //如果在滑动窗口的范围,输出最小值即可。
}
puts("");
//求滑动窗口里面的最大值
hh=0,tt=-1;
for(int i=1;i<=n;i++)
{
if(hh<=tt&&q[hh]<i-k+1) hh++;
while(hh<=tt&&a[i]>=a[q[tt]]) tt--;
q[++tt]=i;
if(i>k-1) cout<<a[q[hh]]<<" ";
}
return 0;
}
2.7KMP
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P在字符串 S中多次作为子串出现。
求出模式串 P在字符串 S中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0开始计数),整数之间用空格隔开。
数据范围
1≤N≤1e5
1≤M≤1e6
输入样例:
3
aba
5
ababa
输出样例:
0 2
1.串的普通算法BF
BF算法图示过程(返回匹配成功的位置)
思想:
从主串的第pos个字符开始匹配和模式串中第一个字符串开始比较。
(1)如果相等:继续比后续字符,i++,j++;
(2)如果不相等,从主串的下一个字符和模式串 的第一个字符相比较。
任何求主串的下一个字符的位置?
方法一:设置一个变量k,在主串未开始时,领k=i+1(主串的下一个位置),每当匹配失败,另i=j,即可。
int bf(char s[],char t[],int pos)
{
int i=pos,j=1;//从主串的第pos个字符,和模式串第一个字符比较
while(i<=s.length&&j<=t.length)
{
int k=i+1; //让k等于i的下一个位置
if(s[i]==t[j]) //匹配成功,继续比较下一个位置
{
++i;
++j;
}
else //匹配失败
{
i=k;
j=1;
}
}
if(j>T.length) return i-T.length;//如果j大于模式串的长度,说明匹配成功
else return 0; //匹配失败
}
方法二:找出每次失败i和j的关系。
则下一个位置是i-j+2.
int BF(char s[],char t[],int pos)
{
int i=pos,j=1;
while(i<=s.length&&j<t.length)
{
if(s[i]==s[j])
{
++i;
++j;
}
else
{
i=i-j+2;
j=1;
}
}
if(j>t.length) return i-t.length;
else return 0;
}
2.KMP算法
特点:在匹配过程中,不需要回溯主串的指针i,时间复杂度为O(m+n)
思路:
则我们可知next数组的含义,next[i]表示:以i结尾的后缀和从1开始模式串的前缀相等,且相等最大 。
假设我们已知next数组,则模式匹配如下:
思想
主串的第pos个字符和模式串的第一个字符串进行比较
(1).相等:继续比较后继字符 i++,j++。
(2).不相等:主串的位置不变和模式串的第next[j]字符比较,j=next[j]。
下面展示一个代码:
int KMP(char s[],char t[],int pos)
{
int i=pos,j=1;
while(i<=s.length&&j<=t.length)
{
if(j==0||s[i]==t[j]) //j==0表示当前比较的是模式串的首字符且不匹配,应从主串的后一个位置继续匹配;s[i]==t[j]表示匹配成功,继续匹配。
{
++i;
++j;
}
else j=next[j];
}
if(j>t.length) return i-t.length;
else return 0;
}
求KMP的next指针的值
(1)如果t[j]==t[next[j]],则next[j+1]=next[j]+1.
(2)如果t[j]!=t[next[j]],判断t[j]和t[next[...next[j]...]],重复 过程(1),直到相等,退到0时,表示不存在,next[j+1]=1.
换句话说,要求next[j],需要判断t[j-1]和t[next[j-1]].
void get_next(char t[],int next[])
{
int j=1,k=0;
next[1]=0;
while(j<t.length)
{
if(k==0||t[j]==t[k])//k为0,或者找到时,next[j+1]=k。
{
++j;
++k;
next[j]=k;
}
else k=next[k];
}
}
KMP的nextval值
思想:
当s[i]和t[j]比较后,发现两者不相等时,但t[j]和t[k]相等,那就意味着s[i]和t[k]不需要进行额外的比较,因此j的位置的nextval值修改为k位置的nextval值,当s[i]和t[j]比较后,发现两者不相等,发现t[j]和t[k]也不相等,因此j位置的nextval值仍是k,即nextval[j]=next[j].
已知next[j],应如下修改nextval值
k=next[j];
if(t[j]==t[k]) nextval[j]=next[k];
else nextval[j]=next[j];
例如:求aaaab的nextval值。
如果t[j]==t[next[j]],nextval[j]=nextval[next[j]]
否则nextval[j]=next[j].
void get_nextval(chat t[],int next[],int nextval[])
{
int j=2,k=0;
get_next(t,next);
nextval[1]=0;
while(j<=t.length())
{
k=next[j];
if(t[j]==t[k]) nextval[j]=nextval[j];
else nextval[j]=next[j];
}
}
匹配过程和next的匹配过程类似。
AC代码
#include <iostream>
using namespace std;
const int N = 100100, M = 1000010;
int n, m;
int ne[N];
char s[M], p[N];
void get_next()
{
for (int i = 2, j = 0; i <= n; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];//没有推到0并且不相等的话,j指针一直回退
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
}
void kmp()
{
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];
}
}
}
int main()
{
cin >> n >> p + 1 >> m >> s + 1;
get_next();
kmp();
return 0;
}
2.8Trie
2.8.1Trie字符串统计
维护一个字符串集合,支持两种操作:
I x 向集合中插入一个字符串 x;
Q x 询问一个字符串在集合中出现了多少次。
共有 N个操作,所有输入的字符串总长度不超过 1e5,字符串仅包含小写英文字母。
输入格式
第一行包含整数 N,表示操作数。
接下来 N行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。
输出格式
对于每个询问指令 Q x,都要输出一个整数作为结果,表示 xx 在集合中出现的次数。
每个结果占一行。
数据范围
1≤N≤2∗1e4
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
一.Trie树的原理
1.Trie树的作用
快速地查询某个字符串在集合中出现的次数,高效地存储和查找字符串,时间复杂度可以达到O(n)。
2.实现思路
类似于树的形式,将字符串存储起来,如果存在以某个字符结尾的字符串,我们就进行标记次数,方便查找字符串出现的次数。
我们把小写字母或者大写字母映射成0-25进行创建Trie树。
3.各个变量代表的意思
儿子数组son[p][j]:存储从节点p沿着j这条边走的子节点。边为26个小写的字母(a-z)对应的映射值0-25,每个节点最多可以有26个分支。
例如,son[0][2]=1,son[1][2]=0.
计数数组cnt[p]:存储以p结尾字符串出现的次数。
节点编号idx:来给节点进行编号。
二.建Trie树
1.过程
(1)空的Trie树只有一个节点,节点编号为0.
(2)从根开始进行插入,枚举字符串的每个字符,如果有儿子,p 指针走到儿子,如果没有儿子,先创建儿子,p指针再走向儿子。
(3).在单词的结尾记录插入的次数。
2.图解过程
3.代码展示
void insert(char str[])
{
int p=0;//从根开始遍历
for(int i=0;str[i];i++)//沿着字符串一直走
{
int j=str[i]-'a';//映射成分支
if(!son[p][j]) son[p][j]=++idx;//如果没有这个节点,创建节点
p=son[p][j];//令p走向该节点
}
cnt[p]++;//记录次字符串出现的次数
}
三.查询Trie
1.过程
(1).从根开始查询,对字符串进行扫描。
(2).有字符串str[i],则走到下一个节点,走到字符串尾,返回插入的次数。
(3).没有字符串str[i],返回0.
2.代码展示
int query(char str[])
{
int p=0;//从根开始
for(int i=0;str[i];i++)
{
int j=str[i]-'a';
if(!son[p][j]) return 0;//不存在节点,返回0
p=son[p][j];
}
return cnt[p];//返回字符串的次数
}
AC代码
2.8.2最大异或对
在给定的 N个整数 A1,A2……AN中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N个整数 A1~AN。
输出格式
输出一个整数表示答案。
数据范围
1≤N≤1e5,
0≤Ai<2^31,
输入样例:
3
1 2 3
输出样例:
3
1.思路
我们首先考虑遍历枚举的方法,然后通过发现某些性质去优化它。
int res=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
res=max(res,a[i]^a[j]);
cout<<res<<endl;
显然暴力的方法为O(n2),会超时。
我们发现异或(^)的性质为二进制表示中,两个数某一位进行异或, 相同为0,不同为1,如果二进制100,我们首先考虑011,因为只有不同的位时,得到的值才能最大,我们可以用trie树从高位往低位存储,如果找某一个数的最大值时,我们应该首先考虑它对于二进制某一位不同的值是否存在,如果存在,我们沿着这个分支走到 下一个节点,如果不存在,只能走和他相同的分支。
说明
用Trie存储单词,由26个字母构成的Trie树,是一颗26叉树,26个字母构成分支,深度为最长单词的长度。
用Trie存储整数,由整数的十进制位构成的Trie,是一颗10叉树,0-9个数字构成分支,深度为10层。
用Trie存储整数,由整数的二进制位构成的Trie,是一颗二叉树,0和1构成分支,深度为31层。
2. 图解
int res=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
res=max(res,a[i]^a[j]);
cout<<res<<endl;
AC代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e6+10,M=30000000;
int n,a[N],son[M][2],idx;
void insert(int x)//和字典树一样的思路
{
int p=0;
for(int i=30;i>=0;i--)//从二进制的最高位开始建树
{
int j=x>>i&1;//取出该位置的二进制表示的数
if(!son[p][j]) son[p][j]=++idx;
p=son[p][j];
}
}
int query(int x)
{
int res=0,p=0;
for(int i=30;i>=0;i--)
{
int j=x>>i&1;
if(son[p][!j])//如果存在某个节点和x该位置的二进制数不相同的话,说明异或结果为1,加上这一个二进制位对应十进制的数值,让p走到下一个节点
{
res+=1<<i;
p=son[p][!j];
}
else p=son[p][j];//否则只能走相等的分支,说明异或结果为0,即res+=0<<i,因为0<<i的结果为0,所有可以省略,p走到下一个节点
}
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<<endl;
return 0;
}
2.9并查集
(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合并集合
一共有 n个数,编号是 1∼n1∼n,最开始每个数各自在一个集合中。
现在要进行 mm 个操作,操作共有两种:
M a b,将编号为 aa 和 bb 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
Q a b,询问编号为 aa 和 bb 的两个数是否在同一个集合中;
输入格式
第一行输入整数 nn 和 mm。
接下来 mm 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。
输出格式
对于每个询问指令 Q a b,都要输出一个结果,如果 aa 和 bb 在同一集合内,则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤n,m≤1051≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
一.基本原理
每个集合用一个树来表示,树根的编号就是整个集合的编号,每个节点储存他的父节点,p[x]表示x的父节点。
1.支持的操作
(1).将两个集合合并。
(2).询问两个集合是否在同一集合当中。
2.问题
(1).如何判断树根?if(p[x]==x)
(2).如何求x的集合编号?while(p[x]!=x) x=p[x];
(3).如何合并两个集合?px是x的集合编号,py是y的集合编号,px=y。
因为在查询的过程中,如果不进行压缩处理的话,需要的时间复杂度是很高的,所以我们可以进行压缩处理,我们在寻找祖宗的时候,我们可以让每个节点都指向祖宗节点,这样的话,我们就降低了树的高度,降低了时间复杂度。
合并其实也有一个优化,称为按秩合并,让小集合的根指向大集合的根 ,因为这样的话层数会缩小一些。
3.图解
(1).大集合接到小集合
此时树的高度为4
(2).小集合接到大集合
此时高度为3。
vector<int>siz(N,1);
void refind(int a,int b)
{
a=find(a),b=find(b);//让a,b分别指向自己的祖宗节点
if(a==b) return ;//如果a和b在同一集合中
if(siz[a]>siz[b]) swap(a,b);
p[a]=b;
siz[b]+=siz[a];
}
find(x):返回x的祖宗节点;
p[x]:表示x的父亲;
AC代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e6+10;
int p[N];
int n,m;
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);//如果没有走到祖宗节点,一直往上找祖宗节点,再让经过的节点都指向祖宗节点。
return p[x];//找到了祖宗节点,返回祖宗节点
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;//开始时,每个节点都是一个独立的集合,让自己的祖宗指向自己
while(m--)
{
string op;
int a,b;
cin>>op>>a>>b;
if(op=="M")
{
if(find(a)!=find(b)) p[find(a)]=find(b);//如果a的集合编号不等于b的集合编号,合并两个集合
}
else
{
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
2.9.2连通块中点的数量
给定一个包含 nn 个点(编号为 1∼n1∼n)的无向图,初始时图中没有边。
现在要进行 mm 个操作,操作共有三种:
C a b,在点 aa 和点 bb 之间连一条边,aa 和 bb 可能相等;
Q1 a b,询问点 aa 和点 bb 是否在同一个连通块中,aa 和 bb 可能相等;
Q2 a,询问点 aa 所在连通块中点的数量;
输入格式
第一行输入整数 nn 和 mm。
接下来 mm 行,每行包含一个操作指令,指令为 C a b,Q1 a b 或 Q2 a 中的一种。
输出格式
对于每个询问指令 Q1 a b,如果 aa 和 bb 在同一个连通块中,则输出 Yes,否则输出 No。
对于每个询问指令 Q2 a,输出一个整数表示点 aa 所在连通块中点的数量
每个结果占一行。
数据范围
1≤n,m≤1051≤n,m≤105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
思路:
维护一个size[]数组,size[x]:表示以x为根节点连通块的数量。我们只需要保证根节点的size有意义即可,如果x不是根节点,我们可以通过find(x),找到x的祖宗节点,也就是根节点;
如果合并两个节点,找到根节点a,b,让size[a]+=size[b]即可。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e6+10;
int p[N],s[N];
int n,m;
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);//如果x不是祖宗,让x父亲节点更新成祖宗节点
return p[x];//返回x的祖宗节点
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
p[i]=i;//每个节点都是一个单独的集合,让自己的祖宗指向自己
s[i]=1;//每个集合初始化为1
}
while(m--)
{
string op;
int a,b;
cin>>op;
if(op=="C")
{
cin>>a>>b;
if(find(a)==find(b)) continue;//如果a和b已经在一个集合中
s[find(b)]+=s[find(a)];//把a集合中的连通块的数量加到b的祖宗上
p[find(a)]=find(b);//合并两个集合
}
else if(op=="Q1")
{
cin>>a>>b;
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
else
{
cin>>a;
cout<<s[find(a)]<<endl;//输出a集合中连通块的数量
}
}
return 0;
}
2.9.3食物链
动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。
A吃 B,B吃 C,C吃 A。
现有 N个动物,以 1∼N 编号。
每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y,表示 XX 和 YY 是同类。
第二种说法是 2 X Y,表示 XX 吃 YY。
此人对 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
思路: 不管两个集合是否属于同类,还是x吃y的捕食关系,我们都把他放进一个集合中,这样我们就可以知道题目两两之间的关系。
如何确定之间的关系?记录一下每个点和根节点之间的关系。
如果我们知道每个点和根节点之间的关系的话,我们就可以知道任意两个点之间的关系。
由于只有3类关系,我们用每个点到根节点的距离来表示和根节点的关系,如果某个点到根节点的距离是1的话,表示它可以吃根节点,如果某个点到根节点的距离是2的话,表示它可以被根节点吃,如果到根节点的距离是3的话,说明他和根节点是同类,然后3个一个循环。
d%3==1:吃根节点;
d%3==2:被根节点吃;
d%3==0:和根节点是同类;
我们维护某个点到根节点的距离,把距离分成上面3类即可。
find(x)维护d[x]的操作,d[x]:x到父节点的距离。
d[i]的正确理解,应是第 i 个节点到其父节点距离,而不是像有些同学所讲的,到根节点的距离!
//使得路径上的点直接指向根节点
int find(int x)
{
if(p[x]!=x)//如果x不是树根
{
int root=find(p[x]);
//该点到根节点的距离 = 该点到父节点的距离 + 父节点到根节点的距离
d[x]+=d[p[x]];
//使该点的父节点直接指向根节点
p[x]=root;
}
return p[x];
}
AC代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=50010;
int n,m;
int p[N],d[N];
int find(int x)
{
if(p[x]!=x)
{
int t=find(p[x]);//t先存储旧的祖宗节点
d[x]+=d[p[x]];//更新节点长度即x到父节点之间的距离加上父节点到根节点之间的距离
p[x]=t;//x的父亲指向祖宗节点
}
return p[x];
}
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++;//如果不符合x,y的取值范围,是假话
else
{
int px=find(x),py=find(y);
if(t==1)
{
if(px==py&&(d[x]-d[y])%3!=0) res++;//如果x和y在同一集合中,但是发现他们之间不属于同类,则是假话
else if(px!=py)//如果不属于一个集合,我们合并两个集合
{
p[p[x]]=py;
d[px]=d[y]-d[x];//更新两个集合
}
}
else
{
if(px==py&&(d[x]-d[y]-1)%3!=0) res++;
else if(px!=py)
{
p[px]=py;
d[px]=d[y]+1-d[x];
}
}
}
}
printf("%d",res);
return 0;
}
2.9.4连通图
题目描述
给你一个无向图,还有这个图上顶点与顶点之间的边,你能判断这个图连通吗?也就是能否从任意一个点开始可以直接或者间接访问到图上的任何一个顶点。
输入
首先输入一个整数t,表示有t组例子。
每组例子包括两部分;
第一部分(占一行): 一个整数n和m 表示图的顶点的个数和边的个数。
第二部分 有m行 ,每行两个整数s和t,表示顶点s和t之间有一条边(顶点的标号为1到n,其中1<=s,t<=n,1<=n<=100)。
输出
对应每组例子,如果此图为联通图,输出yes,否则输出no。每组结果占一行。
样例输入 Copy
2
4 4
1 2
1 3
1 4
2 4
4 3
1 2
2 4
1 4
样例输出 Copy
yes
no
维护每条边数的并查集。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int p[N],size[N];
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main()
{
int t;
cin>>t;
while(t--)
{
memset(p,0,sizeof(p));
memset(size,0,sizeof(size));
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i,size[i]=1;
while(m--)
{
int a,b;
cin>>a>>b;
if(find(a)==find(b)) continue;
size[find(b)]+=size[find(a)];
p[find(a)]=find(b);
}
int ans=0;
for(int i=1;i<=n;i++)
if(size[find(i)]==n) ans++; //每个点可以到所有的边
if(ans==n) cout<<"yes"<<endl; //所有的点都满足
else cout<<"no"<<endl;
}
return 0;
}
2.10堆
// 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堆排序
输入一个长度为 n的整数数列,从小到大输出前 m小的数。
输入格式
第一行包含整数 n和 m。
第二行包含 n个整数,表示整数数列。
输出格式
共一行,包含 m个整数,表示整数数列中前 m小的数。
数据范围
1≤m≤n≤1e5,
1≤数列中元素≤1e9
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
一、堆的基本概念
堆:是一个完全二叉树。
堆分成两类,小根堆和大根堆。
小根堆:父节点小于等于左右孩子节点;
大根堆:父节点大于等于左右孩子节点。
STL里面的堆又称为优先队列;
如何手写一个堆?
本篇文章以小根堆为例,实现堆的一些基本的操作。
我们用一维数组来维护一个堆,规定数组的下标从1开始,每个下标的左右儿子分别为2*x,2*x+1;
我们先讲述堆中两个最基本的操作down(x),up(x)两个操作。
down(x),如果我们修改堆某个节点或者删除某个节点 ,我们就需要用down和up来维护我们堆中的关系,我们以小根堆为例,如果父节点变大,那么他就要往下沉,因为我们小根堆满足父节点小于等于左右儿子,同理,up恰好相反,如果父节点变小,它就要和自己的父节点比较,直到满足小根堆的定义为止。
二、堆的基本操作
那么我们就可以用down和up操作完成堆中最基本的操作:
1.插入一个数
我们插入一个数一般是插入到堆中最后一个数的后面再进行up操作。
heap[++size]=x,up(size);
2.求集合当中的最小值
因为是小根堆,我们堆顶元素是最小值。
heap[1];
3.删除最小值
我们需要删除堆顶元素,都是如果直接删除堆顶元素的话,会很麻烦,我们可以用最后一个元素来覆盖堆顶元素,如何进行down(1)操作。
heap[1]=heap[size];size--;down(1);
4.删除任意一个值
我们类似于删除堆顶元素的操作,我们先用最后一个元素的值覆盖删除元素的值,因为我们不知道覆盖后的元素是变大还是变小了,所有我们需要判断是执行up还是down。
int t=heap[k];
heap[k]=heap[size];
size--;
if(heap[k]>t) down(k);
else up(k);
当然我们可以简化:
heap[k]=heap[size];
size--;
down(k);
up(k);
5.修改任意一个元素
heap[k]=x;
down(k);
up(k);
AC代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int h[N],siz;
int n,m;
void down(int u)
{
int t=u;//t存储3个节点中的最小值,开始时假设最小值为父节点
if(2*u<=siz&&h[2*u]<h[t]) t=2*u;//和左儿子比较
if(2*u+1<=siz&&h[2*u+1]<h[t]) t=2*u+1;//和右儿子比较
if(t!=u)
{
swap(h[t],h[u]);
down(t);
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>h[i];
siz=n;
for(int i=n/2;i;i--) down(i);
while(m--)
{
cout<<h[1]<<" ";
h[1]=h[siz];
siz--;
down(1);
}
return 0;
}
2.10.2模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
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≤1e5
−1e9≤x≤1e9
数据保证合法。
输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6
思路:
我们需要维护第i个插入的数,则需要再开两个数组维护信息;
AC代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e6+10;
int n,h[N],ph[N],hp[N],siz;
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<=siz&&h[2*u]<h[t]) t=2*u;
if(2*u+1<=siz&&h[2*u+1]<h[t]) t=2*u+1;
if(u!=t)
{
heap_swap(t,u);
down(t);
}
}
void up(int u)
{
if(u/2&&h[u/2]>h[u])
{
heap_swap(u/2,u);
up(u/2);
}
}
int main()
{
scanf("%d",&n);
int m=0;
while(n--)
{
string op;
cin>>op;
if(op=="I")
{
int x;
scanf("%d",&x);
m++;
h[++siz]=x;
ph[m]=siz;
hp[siz]=m;
up(siz);
}
else if(op=="PM") printf("%d\n",h[1]);
else if(op=="DM")
{
heap_swap(1,siz);
siz--;
down(1);
}
else if(op=="D")
{
int k;
scanf("%d",&k);
k=ph[k];
heap_swap(k,siz);
siz--;
down(k);
up(k);
}
else
{
int k,x;
scanf("%d%d",&k,&x);
k=ph[k];
h[k]=x;
down(k);
up(k);
}
}
return 0;
}
2.11哈希表
(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;
}
1.什么是哈希表?
哈希表就是当范围很大时,我们可以通过哈希表将范围缩小,并快速找出一些数,如数组的下标范围是1~1000000000,但是其中的数很少,我们可以将其映射为1~100000,并快速找出,如原本数组下标是500000,我们可以映射成50,40....
2.哈希表产生的冲突
我们可以在映射的过程中,把两个数映射成为一个数,这个就是哈希表的冲突。
如何解决冲突?
有两种办法:开放寻址法和链地址法
(1)开放寻址法
我们可以先将h[]中每个位置上的值初始化成一个很大的数,如何通过除留余数法来找到每个数映射后的地址,如果该位置上有数,那么就继续向下一个位置探测,如果探测到最后一个位置,从第0个位置再进行探测。
查找一个数也是类似的,如果这个数待探测的位置上有数,那么就向下一个位置探测,如果最终探测的位置上面的数为很大的数,那么查找失败,哈希表中没有该数。
(2)拉链法
拉链法不同于开放地址法的是,把每个位置看成一个单链表,如果要某个数通过除留余数法算出来的数位置上有数,不用向后探测,只需要用头插法插入到该位置上的单链表上,查找也是如此。
2.11.1模拟散列表
维护一个集合,支持如下几种操作:
I x,插入一个数 x;
Q x,询问数 x是否在集合中出现过;
现在要进行 N次操作,对于每个询问操作输出对应的结果。
输入格式
第一行包含整数 N,表示操作数量。
接下来 N行,每行包含一个操作指令,操作指令为 I x,Q x 中的一种。
输出格式
对于每个询问指令 Q x,输出一个询问结果,如果 xx 在集合中出现过,则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤N≤1e5
−1e9≤x≤1e9
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
开放寻址法
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e5+3;
const int null=0x3f3f3f3f;
int h[N];
int n;
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;
}
int main()
{
cin>>n;
memset(h,0x3f,sizeof h);
while(n--)
{
string op;
int x;
cin>>op>>x;
if(op=="I") h[find(x)]=x;
else
{
if(h[find(x)]==null) puts("No");
else puts("Yes");
}
}
return 0;
}
链地址法
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e5 + 3; // 取大于1e5的第一个质数,取质数冲突的概率最小 可以百度
//* 开一个槽 h
int h[N], e[N], ne[N], idx; //邻接表
void insert(int x) {
// c++中如果是负数 那他取模也是负的 所以 加N 再 %N 就一定是一个正数
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool find(int x) {
//用上面同样的 Hash函数 讲x映射到 从 0-1e5 之间的数
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 n;
int main() {
cin >> n;
memset(h, -1, sizeof h); //将槽先清空 空指针一般用 -1 来表示
while (n--) {
string op;
int x;
cin >> op >> x;
if (op == "I") {
insert(x);
} else {
if (find(x)) {
puts("Yes");
} else {
puts("No");
}
}
}
return 0;
}
2.11.2字符串哈希表
给定一个长度为 n的字符串,再给定 m个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1]和 [l2,r2]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数 n和 m,表示字符串长度和询问次数。
第二行包含一个长度为 n的字符串,字符串中只包含大小写英文字母和数字。
接下来 m行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。
注意,字符串的位置从 1开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤n,m≤1e5
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
字符串前缀哈希法。
str="ABCADEFGKLM"
预处理出所有字符串的前缀的哈希
h[0]=0
h[1]="A"的哈希值
h[2]="AB"的哈希值
h[3]="ABC"的哈希值
1.如何定义某个前缀的哈希?
把字符串看成P进制的数。
如"ABCD"可以看成P进制的1234
转化成十进制的数就是(1*p^3+2*p^2+3*p^1+4*p^0)%Q;
由于结果很大,我们模上2^64次方,可以直接用unsigned long long 来存储,unsigned long long 相当于2^64,溢出的部分就相当于取模。
注:一般不能映射成0,比如A->0,则AA->00,这样就十分容易产生冲突。
前面的数字哈希会产生冲突,但是这里如果P取131或者13331的话,在99.99%的情况下不会产生冲突,则不需要进行处理冲突。
2.好处就是可以快速的求[l,r]子串的哈希值,判断两个子串是否相等。
前缀和公式 h[i+1]=h[i]×P+s[i] i∈[0,n−1] h为前缀和数组,s为字符串数组;
区间和公式 h[l,r]=h[r]−h[l−1]×P^(r−l+1);
#include <iostream>
#include <algorithm>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, 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", &n, &m);
scanf("%s", str + 1);
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}
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;
}
2.12 STL
2.12.1 sort
1.头文件:<algorithm>
2.采用的是快速排序算法,可以保证很好的平均性能。
3.时间复杂度:O(nlogn)
4.对数字排序
(1)从小到大
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+n+1);
(2)从大到小
bool cmp(int a,int b)
{
return a>b;
}
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+n+1,cmp);
5.对字母排序
(1)从小到大
char a[7]="abcedfg";
sort(a,a+7);//从小到大
(2)从大到小
char a[7]="abcedfg";
sort(a,a+7,greater<char>());//从大到小
2.12.2 lower_bound/upper_bound(二分)
- 原理:二分
- 数组:a[1~n];
- lower_bound(a+1,a+n+1,x):从数组1~n查找第一个大于等于x的数,返回该数的地址,不存在的话返回n+1,然后减去起始地址a,得到下标。
for(int i=1;i<=n;i++) cin>>a[i];
int x;
cin>>x;
cout<<lower_boumd(1,n,x)-a<<endl;
//如果想要查找降序数组
cout<<lower_bound(a+1,a+n+1,x,greater<int>())-a<<endl;
- upper_bound(a+1,a+n+1,x):从数组1~n查找第一个大于x的数,返回该数的地址,不存在的话返回n+1,然后减去起始地址a,得到下标。
for(int i=1;i<=n;i++) cin>>a[i];
int x;
cin>>x;
cout<<upper_boumd(1,n,x)-a<<endl;
//如果想要查找降序数组
cout<<upper_bound(a+1,a+n+1,x,greater<int>())-a<<endl;
2.12.3 vector
- 原理:变长数组倍增的思想。
- 一些基本操作
vector<int> v;
v.resverse(30);//调整空间大小
v.size();//返回大小
v.push_back(x);//尾部插入一个数x
v.