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)
板中之板,代码里面有——
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,x)\) 投放 \(z\) 类型救济粮
-
路径 \((1,y)\) 投放 \(z\) 类型救济粮
-
路径 \((1,\text{lca}(x,y))\) 撤回 \(z\) 类型救济粮
-
路径 \((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;
}
后记
难度不大,就是有点杂……
完结撒花!