数据结构合集
并查集
普通并查集
先看一个问题:
P1551 亲戚
规定:
思路
我们令
接下来开始维护“合并”操作。对于一条“
int find(int x){//寻找 x 所在树的根节点
if(f[x]==x){
return f[x];
}
else{
return find(f[x]);
}
}
int merge(int x,int y){//将 y 所在的集合(树)合并到 x 所在的集合(树)
int fx=find(x),fy=find(y);
f[fy]=fx;
}
那么“查询”
bool query(int x,int y){//查询 x 与 y 是否在一个集合
if(find(x)==find(y)){
return 1;
}
return 1;
}
并查集的路径压缩优化
为什么要路径压缩?
考虑并查集的这种情况:
这样的话,每次查询操作的时间复杂度就会退化为线性。
那么怎么进行路径压缩?
我们每次查询的时候直接把查询一路上的所有点的
我们对上图进行路径压缩:
代码如下:
int find(int x){
if(f[x]!=x){
f[x]=find(f[x]);//路径压缩,即将每个访问路径上的点的父亲都直接设为这棵树的根节点
}
return f[x];
}
优化了整整
此外,普通并查集还被用于 kruskal 最小生成树的算法中。
带权并查集
我们在每个点与父亲之间的连边上定义一个权值,并在路径压缩时做维护,就能够解决更多的问题。
例题1 食物链
我们令边权为
然后我们发现在
树状数组
普通树状数组
这个玩意大概长成这个样子:
(这里用了百度的图片)
其实它就是一个特殊的前缀和数组。
单点修改
仔细观察红色框内与灰色框的关系:
于是我们可以发现以下规律:
那么我们找出
那么问题来了,怎么获取最低位的
这时候就要引入
先假设该数最低位的
知道了以上知识以后,便可以写出修改函数:
void add(int x,ll y){//在位置x的数加上y
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=y;
}
}
那么上面那个公式可以这么写:
那么在跑代码的过程中,科技数据结构内部发生了啥?这里用
可以看到,我们要想单点修改
区间查询
利用前缀和思想,我们可以知道求
那么把问题拆开来看,如何求
我们可以先将
那么我们接下来可以将
不断重复以上操作,直到
代码如下:
ll search(int x,int y){//查询x到y的和
int sum1=0,sum2=0;
for(int i=x-1;i;i-=lowbit(i)){
sum1+=c[i];
}
for(int i=y;i;i-=lowbit(i)){
sum2+=c[i];
}
return sum2-sum1;
}
我们还是来看看树状数组内部发生的事情,这里拿查询区间
(上图的答案计算写反了,应该是
可以看到每一步中,都把
那么普通树状数组的模板就打好了,代码:
#include <bits/stdc++.h>
#define ll long long
#define lowbit(x) ((x)&(-x))
using namespace std;
int n,m;
ll a[500001],c[500001];
void add(int x,ll k){
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=k;
}
}
ll search(int x,int y){
int sum1=0,sum2=0;
for(int i=x-1;i;i-=lowbit(i)){
sum1+=c[i];
}
for(int i=y;i;i-=lowbit(i)){
sum2+=c[i];
}
return sum2-sum1;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
add(i,a[i]);
}
for(int i=1;i<=m;i++){
int op;
scanf("%d",&op);
if(op==1){
int x;
ll k;
scanf("%d%lld",&x,&k);
add(x,k);
}
else{
int x,y;
scanf("%d%d",&x,&y);
printf("%lld\n",search(x,y));
}
}
return 0;
}
树状数组求逆序对
我们倒着扫一遍要求逆序对的序列
为什么要这么做?
考虑逆序对的定义:
这个树状数组
我们在扫数的时候,就是在统计
#include<bits/stdc++.h>=
#define int long long
using namespace std;
int n,a[500001],b[500001],c[500001],ans;
bool cmp(int x,int y){
if(a[x]==a[y]) return x<y;
return a[x]<a[y];
}
void update(int x,int y){
for(int i=x;i<=n;i+=(i&(-i))){
c[i]+=y;
}
}
int sum(int x){
int ans=0;
for(int i=x;i;i-=(i&(-i))){
ans+=c[i];
}
return ans;
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
b[i]=i;
}
sort(b+1,b+n+1,cmp);
for(int i=n;i>=1;i--){
ans+=sum(b[i]);
update(b[i],1);
}
cout<<ans;
return 0;
}
树状数组的区间修改、单点查询
如果扫一遍区间去维护,那么单次操作时间
容易想到对原序列作差分,此时区间
那么原序列
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+10;
int n,c[maxn],m;
int lb(int x){
return x&(-x);
}
void update(int x,int v){
for(int i=x;i<=n;i+=lb(i)){
c[i]+=v;
}
}
int query(int x){
int sum=0;
for(int i=x;i;i-=lb(i)){
sum+=c[i];
}
return sum;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
cin>>x;
update(i,x);
update(i+1,-x);
}
for(int i=1;i<=m;i++){
int op;
cin>>op;
if(op==1){
int x,y,k;
cin>>x>>y>>k;
update(x,k);
update(y+1,-k);
}
if(op==2){
int x;
cin>>x;
cout<<query(x)<<endl;
}
}
return 0;
}
树状数组的区间修改、区间查询
如果直接暴力更新或查询,那么单次操作复杂度将会是
所以还是考虑维护差分数组。
我们现在要求的是一个前缀和,即
我们根据差分数组
于是我们就有
发现
根据乘法分配律,有
然后我们开两个树状数组分别维护
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+10;
int n,m,c1[maxn],c2[maxn];
int lb(int x){
return x&(-x);
}
void update(int x,int v){
for(int i=x;i<=n;i+=lb(i)){
c1[i]+=v;
c2[i]+=x*v;
}
}
int query(int x){
int ans=0;
for(int i=x;i;i-=lb(i)){
ans+=(x+1)*c1[i]-c2[i];
}
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
cin>>x;
update(i,x);
update(i+1,-x);
}
for(int i=1;i<=m;i++){
int op;
cin>>op;
if(op==1){
int x,y,k;
cin>>x>>y>>k;
update(x,k);
update(y+1,-k);
}
if(op==2){
int x,y;
cin>>x>>y;
cout<<query(y)-query(x-1)<<endl;
}
}
return 0;
}
二维树状数组
前置芝士:二维前缀和,二维差分
考虑二维差分数组
于是我们集中注意力,发现每个
括号乘开,得:
于是开四个树状数组,分别维护
#include<bits/stdc++.h>
using namespace std;
const int maxn=2048+10;
int n,m;
char op;
int c1[maxn][maxn],c2[maxn][maxn],c3[maxn][maxn],c4[maxn][maxn];
void update(int x,int y,int puck){
int k=puck;
for(int i=x;i<=n;i+=(i&(-i))){
for(int j=y;j<=m;j+=(j&(-j))){
c1[i][j]+=k;
c2[i][j]+=k*x;
c3[i][j]+=k*y;
c4[i][j]+=k*x*y;
}
}
}
int sum(int x,int y){
int ans=0;
for(int i=x;i;i-=(i&(-i))){
for(int j=y;j;j-=(j&(-j))){
ans+=(x+1)*(y+1)*c1[i][j]-(x+1)*c3[i][j]-(y+1)*c2[i][j]+c4[i][j];
}
}
return ans;
}
int main(){
getchar();getchar();
scanf("%d%d",&n,&m);
while(cin>>op){
if(op=='L'){
int a,b,c,d,k;
scanf("%d%d%d%d%d",&a,&b,&c,&d,&k);
update(a,b,k);
update(a,d+1,-k),update(c+1,b,-k),update(c+1,d+1,k);
}
else{
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
int sum1=sum(c,d),sum2=sum(c,b-1),sum3=sum(a-1,d),sum4=sum(a-1,b-1);
printf("%d\n",sum1-sum2-sum3+sum4);
}
getchar();
}
return 0;
}
线段树
线段树,它是树上的每个节点都用来表示一个区间的一颗树。
对于一颗线段树,其根结点为
线段树的性质
-
对于一个序列长度是
的序列构造线段树,则这颗线段树有 个节点,高度为 。 -
对于一颗线段树上的非叶子节点,都有两个儿子(换句话说就是要么没有儿子要么有两个儿子)。
证一下第一条性质:
知周所众,线段树一共只有
普通线段树
构造
我们都知道,树是递归构造的,线段树也是一样。
所以我们需要写一个函数来构造线段树。
这里使用结点表示法。
递归边界为
void build(int now,int l,int r){
a[now].l=l;
a[now].r=r;
a[now].v=sum[r]-sum[l-1];
if(l!=r){
build(now*2,l,(l+r)/2);
build(now*2+1,(l+r)/2+1,r);
}
}
我们通过一张图来解释线段树对于
单点查询
单点查询实际上就是定位到线段树的叶子结点。
我们现在假设我们需要定位到
int search(int u,int L,int R,int p){
if(L==R){
return a[u].v;
}
else{
int Mid=(L+R)>>1;
if(Mid>=p) return scarch(u<<1,L,Mid,p);
else return scarch((u<<1)|1,M+1,R,p);
}
}
单点修改
进行单点修改,首先也需要定位到这个结点。然后修改完成后,我们需要一路往上更新,这样才能保证线段树的正确性。
int pushup(int u){
a[u].v=a[u<<1].v+a[(u<<1)|1].v;
}
int search(int u,int L,int R,int p,int x){
if(L==R){
a[u].v=x;
}
else{
int Mid=(L+R)>>1;
if(Mid>=p) return scarch(u<<1,L,Mid,p.x);
else return scarch((u<<1)|1,M+1,R,p,x);
}
pushup(u);
}
由于线段树共
区间查询
假设查询区间为
-
当前区间被目标区间完全包含。此时直接返回当前区间的值即可。
-
当前区间与目标区间无交集。此时返回
。 -
当前区间没有被目标区间包含且有交。此时递归处理左子树与右子树的和。
举个例子:在以
ll query(int u,int L,int R){
if(a[u].tag) pushdown(u);
if(inrange(L,R,a[u].l,a[u].r)){
return a[u].v;
}
else if(!outofrange(L,R,a[u].l,a[u].r)){
return search(ls(u),L,R)+search(rs(u),L,R);
}
else return 0ll;
}
区间修改
需要进行区间修改的时候,我们需要引入一个新东西:懒标记。
对于一个区间
这个复杂度甚至比暴力还高。所以我们引入了懒标记。
懒标记的主要原理是区间修改操作时先对这个区间打上标记,暂时不进行更新,若之后需要用到该节点的信息时再调用
单打标记的复杂度为一个常数。
void pushdown(int u){
int L=a[u].l,R=a[u].r,M=L+R>>1,k=a[u].tag,g=1;
if(L==R) return ;
//if(g==1) printf("%d %d %d %d\n",L,R,M,k);
a[u].tag=0;
a[ls(u)].tag+=k;
a[rs(u)].tag+=k;
a[ls(u)].v+=k*(M-L+1);
a[rs(u)].v+=k*(R-M);
}
void update(int u,int L,int R,ll k){
if(a[u].tag) pushdown(u);
if(inrange(L,R,a[u].l,a[u].r)){
a[u].tag+=k;
a[u].v+=(a[u].r-a[u].l+1)*k;
pushdown(u);
}
else if(!outofrange(L,R,a[u].l,a[u].r)){
update(ls(u),L,R,k);
update(rs(u),L,R,k);
pushup(u);
}
}
单调栈
单调栈解决的问题
很喜欢扶苏的一句话:单调栈的本质是求前缀的后缀最值。
可以理解为在这个数左/右边离他最近的、且比它大/小的值的位置。(在
这样说有点抽象,看例题。
例题1 [USACO09MAR] Look Up S
借着这个例题说一下单调栈的基本过程。
既然是单调栈,必定要开一个栈。
既然是求右边的比它大的值,我们就从左到右扫。对于每个数,我们扫到这个数的时候需要用这个数更新一些数的答案。
如何更新?
我们不断弹出栈顶,直到栈顶值大于当前值,然后把当前值压入栈中。弹出的元素的答案就是当前的这个元素。不难发现,这个栈中的元素始终单调递减。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n,ans[maxn],a[maxn];
stack<int> s;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
while(s.size()&&a[s.top()]<a[i]){
ans[s.top()]=i;
s.pop();
}
s.push(i);
}
for(int i=1;i<=n;i++){
cout<<ans[i]<<endl;
}
return 0;
}
有了单调栈这个工具以后,我们尝试解决一些很新的问题。
例题2 发射站
注意到发出的能量只被两边最近的且比它高的发射站接收,一眼单调栈可以解决。
现在的问题就是如何统计答案。
其实也很简单,我们求出了左右两边最近且比它大的值的位置的时候,直接把它的能量累加到这两个发射站的答案上去就可以了。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int n,k1[maxn],k2[maxn],a[maxn],b[maxn],sum[maxn],ans=-1;
stack<int> s;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i];
}
for(int i=1;i<=n;i++){
while(s.size()&&a[s.top()]<a[i]){
k1[s.top()]=i;
s.pop();
}
s.push(i);
}
for(int i=n;i>=1;i--){
while(s.size()&&a[s.top()]<a[i]){
k2[s.top()]=i;
s.pop();
}
s.push(i);
}
for(int i=1;i<=n;i++){
sum[k1[i]]+=b[i];
sum[k2[i]]+=b[i];
}
for(int i=1;i<=n;i++){
ans=max(ans,sum[i]);
}
cout<<ans;
return 0;
}
例题3 乘积
不难发现每一个数作为最小值的时候,如果要想贡献最大,那就必须要找到它左右边最近的比它小的数(记为
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+10;
int n,a[maxn],v[maxn],l[maxn],r[maxn],sum[maxn],ans;
stack<int> s1,s2;
int query(int l,int r){
return sum[r]-sum[l-1];
}
signed main(){
// freopen("big.in","r",stdin);
// freopen("big.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
for(int i=1;i<=n;i++){
while(s1.size()&&a[s1.top()]>a[i]){
r[s1.top()]=i;
s1.pop();
}
s1.push(i);
}
for(int i=n;i>=1;i--){
while(s2.size()&&a[s2.top()]>a[i]){
l[s2.top()]=i;
s2.pop();
}
s2.push(i);
}
for(int i=1;i<=n;i++){
if(l[i]==0){
l[i]=1;
}
else{
l[i]++;
}
if(r[i]==0){
r[i]=n;
}
else{
r[i]--;
}
}
for(int i=1;i<=n;i++){
// cout<<l[i]<<" "<<r[i]<<endl;
ans=max(ans,query(l[i],r[i])*a[i]);
}
cout<<ans;
return 0;
}
单调队列
解决的问题
它可以在
但是存在感似乎很低:如果不卡,st 表和 线段树基本足矣。(哪个没木出题人会卡这种东西)
又不如更灵活的双指针。
所以就不写了。
st 表
解决的问题
st 表常用与解决可重复贡献问题。
可重复贡献问题就是对于单个数,将其多次算入答案中不会对答案产生影响。
例如区间
建立与查询
st 表建立在倍增的思想之上。这里我们拿区间最小值来举例。
我们令
那么我们很显然就会有
这个操作其实就是在找最大的能够将这个区间全部覆盖的两个区间,将它们取
查询同理,可以找到两个区间
code
#include<bits/stdc++.h>
using namespace std;
int n,m,a[100001],st[100001][31];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
st[i][0]=a[i];
}
for(int i=1;(1<<i)<=n;i++){
for(int j=1;j<=n;j++){
if(j+(1<<i)-1<=n){
st[j][i]=max(st[j][i-1],st[j+(1<<(i-1))][i-1]);
}
}
}
for(int i=1;i<=m;i++){
int l,r,k;
scanf("%d%d",&l,&r);
k=log2(r-l+1);
printf("%d\n",max(st[l][k],st[r-(1<<k)+1][k]));
}
return 0;
}
例题 1 Iva & Pav
solution
首先我们注意到
然后考虑快速维护
code
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,m,x[maxn],st[maxn][20];
int query(int l,int r){
int k=log2(r-l+1);
return (st[l][k]&st[r-(1<<k)+1][k]);
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>x[i];
st[i][0]=x[i];
}
for(int i=1;(1<<i)<=n;i++){
for(int j=1;j<=n;j++){
if(j+(1<<i)-1<=n){
st[j][i]=(st[j][i-1]&st[j+(1<<(i-1))][i-1]);
}
}
}
cin>>m;
while(m--){
int l,k;
cin>>l>>k;
if(x[l]<k){
cout<<-1<<" ";
continue;
}
int L=l,R=n+1;
while(L<R){
int mid=L+R>>1;
if(query(l,mid)<k){
R=mid;
}
else{
L=mid+1;
}
}
cout<<L-1<<" ";
}
cout<<endl;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int t;
cin>>t;
while(t--) solve();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】