1.12 上午-树状数组 & ST 表 & RMQ & LCA

前言

勿让将来,辜负曾经

浅浅中二一下:这是为我所主宰的战场!

正文

知识点

这些知识点都很素,数据结构本身并不难,难的地方在于它们的高阶应用。

若干天以前——云落总结了一个东西,咳咳

树状数组梳理

众所周知,云落是个鸽王,动辄咕一个月啥的。所以——RMQ & LCA 还在制作中……

RMQ 解决静态区间最值问题,LCA 解决求树上两点公共祖先问题

这两个英文缩写构成的算法,或者说表示的一类问题其解决方案本质都是类似的——倍增(别跟我说什么重链剖分求 LCA 哈,不听不听)

在上面这个朴素的倍增做法之下,RMQ 是 \(O(n \log n) - O(1)\) 的,LCA 是 \(O(n) - O(\log n)\) 的,虽然码量小,也好理解,但是时间复杂度总是带个 \(\log\)

大巨一顿乱搞,就整出了 \(O(n) - O(1)\) RMQ 以及 \(O(n) - O(1)\) LCA

前情提要:板子题大杂烩

一题一解

T1 【模板】树状数组 1(P3374) T2 【模板】 树状数组 2(P3368) T3 【模板】 线段树 1(P3372)

T1 链接

T2 链接

T3 链接

板中之板,代码里面有——

T4 【模板】ST 表 && RMQ 问题(P3865)

链接

又是一个板,随便安利一篇我的博客,代码里面也有……

奇了怪了,为什么莫名奇妙地四道题过去了……

T5 【蓝桥杯 2022 省 A】 选数异或(P8773)

链接

首先注意到这是一个静态区间查询问题,其次注意到 \(x\) 是一个给定的定值,最后注意到异或运算的相关性质。直接考虑预处理。

预处理什么?一个经典 trick 叫做维护 \(pre\) 数组,代表例题 HH 的项链。简单的说明一下这道题目的 \(pre\) 数组所表示的含义。\(pre_i\) 表示最大的的 \(j \in [1,i-1]\) 满足 \(a_i \oplus a_j = x\)

然后其实这个东西好做多了,基本上就是 ST 表板子题。具体地,只要 \(\text{ask}(l,r) \ge l\) 就代表有解,否则无解。

这个东西可以用 ST 表或者线段树维护,而云落在 ST 表和线段树之间选择了前缀最大值。

令我没想到的是,时间复杂度瓶颈不在于那个数据结构(无所谓 ST 表还是线段树),居然在于 map 的 离散化……

点击查看代码(前缀最大值)
#include<bits/stdc++.h>
using namespace std;
const int maxn=100050;
int n,m,x,a[maxn],f[maxn];
map<int,int> Hash;
int main(){
    cin>>n>>m>>x;
    for(int i=1;i<=n;i++){
    	cin>>a[i];
    	f[i]=max(f[i-1],Hash[a[i]^x]);
    	Hash[a[i]]=i;
    }
    while(m--){
        int l,r;
        cin>>l>>r;
        if(f[r]>=l)cout<<"yes"<<endl;
        else cout<<"no"<<endl;
    }
    return 0;
}
点击查看代码(ST 表)
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int maxn=1e5+10;
int n,m,x,a[maxn];
map<int,int> mp;
int pre[maxn];
struct Sparse_table{
    int f[maxn][20],lg[maxn];
    void init(){
        lg[0]=-1;
        for(int i=1;i<=n;i++){
            lg[i]=lg[i>>1]+1;
        }
        for(int i=1;i<=n;i++){
            f[i][0]=pre[i];
        }
        for(int j=1;j<=lg[n];j++){
            for(int i=1;i+(1<<j)-1<=n;i++){
                f[i][j]=max(f[i-1][j],f[i+(1<<(j-1))][j-1]);
            }
        }
        return;
    }
    int ask(int l,int r){
        int k=lg[r-l+1];
        return max(f[l][k],f[r-(1<<k)+1][k]);
    }
}ST;
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m>>x;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        pre[i]=mp[a[i]^x];
        mp[a[i]]=i;
    }
    ST.init();
    while(m--){
        int l,r;
        cin>>l>>r;
        if(ST.ask(l,r)>=l){
            cout<<"yes"<<endl;
        }else{
            cout<<"no"<<endl;
        }
    }
    return 0;
}

T6 【模板】ST 表 && RMQ 问题(P3865)

链接

同样的题目再放一遍,自然是希望我们学习 \(O(n)-O(1)\) ST 表咯!

传送门
\(\land\) \(\land\) \(\land\)
\(\text{}\) | \(\text{ }\) | \(\text{ }\) |
你们要的都在这里!

T7 【模板】最近公共祖先(LCA)

链接

倍增什么的都太板了,tarjan 求 LCA 问你们的 LXP 大巨,云落是个蒟蒻,不会!

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int maxn=5e5+5;
int n,m,s;
vector<int> G[maxn];
int fa[maxn][22],dep[maxn];
inline void dfs(int u,int fath){
	fa[u][0]=fath;
	dep[u]=dep[fath]+1;
	for(int v:G[u]){
		if(v==fath){
			continue;
		}
		dfs(v,u);
	}
	return;
}
inline int LCA(int x,int y){
	if(dep[x]<dep[y]){
		swap(x,y);
	}
	for(int i=20;i>=0;i--){
		if(dep[fa[x][i]]>=dep[y]){
			x=fa[x][i];
		}
	}
	if(x==y){
		return x;
	}
	for(int i=20;i>=0;i--){
		if(fa[x][i]!=fa[y][i]){
			x=fa[x][i];
			y=fa[y][i];
		}
	}
	return fa[x][0];
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>s;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(s,0);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			fa[u][i]=fa[fa[u][i-1]][i-1];
		}
	}
	while(m--){
		int x,y;
		cin>>x>>y;
		cout<<LCA(x,y)<<endl;
	}
	return 0;
}

然后再放一个树链剖分求 LCA

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int maxn=5e5+5;
int n,m,s;
vector<int> G[maxn];
int fa[maxn],son[maxn],dep[maxn],sz[maxn];
int dfn[maxn],tim,Top[maxn],rev[maxn];
inline void dfs1(int u,int fath){
	fa[u]=fath;
	dep[u]=dep[fath]+1;
	sz[u]=1;
	int mx=-1;
	for(int v:G[u]){
		if(v==fath){
			continue;
		}
		dfs1(v,u);
		sz[u]+=sz[v];
		if(sz[v]>mx){
			mx=sz[v];
			son[u]=v;
		}
	}
	return;
}
inline void dfs2(int u,int tp){
	dfn[u]=++tim;
	Top[u]=tp;
	rev[tim]=u;
	if(!son[u]){
		return;
	}
	dfs2(son[u],tp);
	for(int v:G[u]){
		if(v==fa[u]||v==son[u]){
			continue;
		}
		dfs2(v,v);
	}
	return;
}
inline int LCA(int x,int y){
	while(Top[x]!=Top[y]){
		if(dep[Top[x]]<dep[Top[y]]){
			swap(x,y);
		}
		x=fa[Top[x]];
	}
	return dep[x]<dep[y]?x:y;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>s;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs1(s,0);
	dfs2(s,s);
	// cout<<"Zyxxxxxxxxxx"<<endl;
	// for(int u=1;u<=n;u++){
	// 	cout<<"Zyx"<<u<<": "<<fa[u]<<" "<<son[u]<<" "<<dep[u]<<" "<<sz[u]<<" "<<dfn[u]<<" "<<Top[u]<<endl;
	// 	cout<<"-----------"<<endl;
	// }
	// cout<<"Zyxxxxxxxxxx"<<endl;
	while(m--){
		int x,y;
		cin>>x>>y;
		cout<<LCA(x,y)<<endl;
	}
	return 0;
}

重点来咯,\(O(n)-O(1)\) LCA 闪亮登场!其实就是一个欧拉序和 ST 表的妙用!

众所周知,LCA 有一些比较优秀的性质(废话)。对于一条路径 \((u,v)\),注意到在路径 \((u,v)\) 上总是不存在一个节点 \(i\) 满足 \(\text{dep}_i < \text{dep}_{\text{lca}(u,v)}\)

而众所又周知,一棵树的欧拉序也有一些比较优秀的性质(废话 \(\times 2\))。比如说,一棵子树的欧拉序总是连续的。所以,当 \(\text{lca}(u,v)\) 这棵子树被欧拉序遍历完毕之前,不会出现子树 \({\text{lca}(u,v)}\) 以外的结点

结合上述两条“优秀”的性质,我们发现,\(\text{lca}(u,v)\)\(u\) 的欧拉序到 \(v\) 的欧拉序这一段区间中深度最小的结点。我们顺利地把一个 LCA 问题转化成了 RMQ 问题,套一个 ST 表就可以秒了!

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int maxn=5e5+5;
int n,m,s;
int head[maxn],tot;
struct Edge{
	int to,nxt;
}e[maxn<<1];
int dep[maxn],dfn[maxn<<1],tim,q[maxn<<1];
struct Sparse_table{
	int lg[maxn<<1],f[maxn<<1][22];
	int Min(int x,int y){
		return dep[x]<dep[y]?x:y;
	}
	void init(){
		lg[0]=-1;
		for(int i=1;i<=tim;i++){
			lg[i]=lg[i>>1]+1;
		}
		for(int i=1;i<=tim;i++){
			f[i][0]=q[i];
		}
		for(int j=1;j<=lg[tim];j++){
			for(int i=1;i+(1<<j)<=tim;i++){
				f[i][j]=Min(f[i][j-1],f[i+(1<<j-1)][j-1]);
			}
		}
		return;
	}
	int ask(int l,int r){
		int p=lg[r-l+1];
		return Min(f[l][p],f[r-(1<<p)+1][p]);
	}
}ST;
inline void add(int u,int v){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}
inline void dfs(int u,int fath){
	dep[u]=dep[fath]+1;
	dfn[u]=++tim;
	q[tim]=u;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fath){
			continue;
		}
		dfs(v,u);
		q[++tim]=u;
	}
	return;
}
inline int LCA(int x,int y){
	if(x==y){
		return x;
	}
	if(dfn[x]>dfn[y]){
		swap(x,y);
	}
	x=dfn[x];
	y=dfn[y];
	return ST.ask(x,y);
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>s;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	dfs(s,0);
	ST.init();
	while(m--){
		int x,y;
		cin>>x>>y;
		cout<<LCA(x,y)<<endl;
	}
	return 0;
}

T8 [USACO15DEC] Max Flow P(P3128)

链接

经典树上差分,太板咯,过!(当然你要是愿意写树链剖分也拦不住哈)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5e4+5;
int n,k;
vector<int> G[maxn];
int dep[maxn],fa[maxn][22];
int d[maxn],ans;
inline void dfs(int u,int fath){
	fa[u][0]=fath;
	dep[u]=dep[fath]+1;
	for(int v:G[u]){
		if(v==fath){
			continue;
		}
		dfs(v,u);
	}
	return;
}
inline int LCA(int x,int y){
	if(dep[x]<dep[y]){
		swap(x,y);
	}
	for(int i=20;i>=0;i--){
		if(dep[fa[x][i]]>=dep[y]){
			x=fa[x][i];
		}
	}
	if(x==y){
		return x;
	}
	for(int i=20;i>=0;i--){
		if(fa[x][i]!=fa[y][i]){
			x=fa[x][i];
			y=fa[y][i];
		}
	}
	return fa[x][0];
}
inline void solve(int u,int fath){
	for(int v:G[u]){
		if(v==fath){
			continue;
		}
		solve(v,u);
		d[u]+=d[v];
	}
	ans=max(ans,d[u]);
	return;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1,0);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			fa[u][i]=fa[fa[u][i-1]][i-1];
		}
	}
	while(k--){
		int x,y;
		cin>>x>>y;
		int lca=LCA(x,y);
		d[x]++;
		d[y]++;
		d[lca]--;
		d[fa[lca][0]]--;
	}
	solve(1,0);
	cout<<ans<<endl;
	return 0;
}

T9 [Vani有约会] 雨天的尾巴 /【模板】线段树合并(P4556)

链接

看在这是一道紫题的面子上,云落就多说两句(明明就是树上差分 + 线段树合并直接做做完了……)

一句话题意:链加,查询单点众数

聪明的你一定能做出如下转化:给路径 \((x,y)\) 上投放种类 \(z\) 的救济粮等价于——

  1. 路径 \((1,x)\) 投放 \(z\) 类型救济粮

  2. 路径 \((1,y)\) 投放 \(z\) 类型救济粮

  3. 路径 \((1,\text{lca}(x,y))\) 撤回 \(z\) 类型救济粮

  4. 路径 \((1,fa_{\text{lca}(x,y)})\) 撤回 \(z\) 类型救济粮

聪明的你一定又能想到,对于每个点都开一个桶数组维护答案,然而这并不优(满脑子都是 \(v \to u\) 合并的过程数组相加的爆炸时间复杂度咯)

然后聪明的你注意到这是一个静态的树形结构,并且桶数组又叫权值数组,你自然想到了权值线段树,进一步地,你想到了线段树合并维护桶数组

以上,你秒掉了这道紫题

点击查看代码
#include<iostream>
#define endl '\n'
#define int long long
using namespace std;
const int maxn=1e5+10;
int n,m;
int head[maxn],tot;
struct Edge{
    int to,nxt;
}e[maxn<<1];
int fa[maxn][20],dep[maxn];
int rt[maxn],cnt;
int ans[maxn];
struct Segment_tree{
	struct node{
		int l,r,sum,col;
	}tr[maxn*50];
	void pushup(int u){
		if(tr[tr[u].l].sum>=tr[tr[u].r].sum){
			tr[u].sum=tr[tr[u].l].sum;
			tr[u].col=tr[tr[u].l].col;
		}else{
			tr[u].sum=tr[tr[u].r].sum;
			tr[u].col=tr[tr[u].r].col;
		}
		return;
	}
	void modify(int &u,int l,int r,int pos,int k){
		if(u==0){
			u=++cnt;
		}
		if(l==r){
			tr[u].sum+=k;
			tr[u].col=pos;
			return;
		}
		int mid=l+r>>1;
		if(pos<=mid){
			modify(tr[u].l,l,mid,pos,k);
		}else{
			modify(tr[u].r,mid+1,r,pos,k);
		}
		pushup(u);
		return;
	}
	int merge(int x,int y,int l,int r){
		if(!x||!y){
			return x+y;
		}
		if(l==r){
			tr[x].sum+=tr[y].sum;
			return x;
		}
		int mid=l+r>>1;
		tr[x].l=merge(tr[x].l,tr[y].l,l,mid);
		tr[x].r=merge(tr[x].r,tr[y].r,mid+1,r);
		pushup(x);
		return x;
	}
}Tr;
inline void add(int u,int v){
    e[++tot].to=v;
    e[tot].nxt=head[u];
    head[u]=tot;
    return;
}
inline void dfs(int u,int fath){
	dep[u]=dep[fath]+1;
	fa[u][0]=fath;
	for(int i=1;i<=18;i++){
		fa[u][i]=fa[fa[u][i-1]][i-1];
	}
	for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
		if(v==fath){
			continue;
		}
		dfs(v,u);
	}
	return;
}
inline int lca(int x,int y){
	if(dep[x]<dep[y]){
		swap(x,y);
	}
	for(int i=18;i>=0;i--){
		if(dep[fa[x][i]]>=dep[y]){
			x=fa[x][i];
		}
	}
	if(x==y){
		return y;
	}
	for(int i=18;i>=0;i--){
		if(fa[x][i]!=fa[y][i]){
			x=fa[x][i];
			y=fa[y][i];
		}
	}
	return fa[x][0];
}

inline void solve(int u,int fath){
	for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
		if(v==fath){
			continue;
		}
		solve(v,u);
		rt[u]=Tr.merge(rt[u],rt[v],1,maxn);
	}
	ans[u]=(Tr.tr[rt[u]].sum?Tr.tr[rt[u]].col:0);
	return;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
        add(u,v);
        add(v,u);
	}
	dfs(1,0);
	while(m--){
		int x,y,z;
		cin>>x>>y>>z;
		Tr.modify(rt[x],1,maxn,z,1);
		Tr.modify(rt[y],1,maxn,z,1);
		Tr.modify(rt[lca(x,y)],1,maxn,z,-1);
		Tr.modify(rt[fa[lca(x,y)][0]],1,maxn,z,-1);
	}
	solve(1,0);
	for(int i=1;i<=n;i++){
		cout<<ans[i]<<endl;
	}
	return 0;
}

后记

难度不大,就是有点杂……

完结撒花!

posted @ 2025-03-03 16:41  sunxuhetai  阅读(13)  评论(1)    收藏  举报