LCA及树上倍增
Lca
定义:对于两个点,他们的最近公共祖先
- 是他们的祖先(或自己)
- 距离最近
\(f_{i,j}\)表示从节点i向上跳\(2^j\)步能到达的节点。
- 将较深的点跳到深度相同。
- 两个节点一起跳,直到相同。
如图:3和8的最近公共祖先是1。
树的深度为5,\(\log_2 5=3\)。更深的是8。
\(f_{8,3}=1,f_{8,2}=1,f_{8,1}=5\)。于是8跳到5。
\(f_{5,0}=2\)。2与3齐平。
于是两个一起往上跳,得到公共祖先1。
时间复杂度\(O(\log n)\)
其次,关于\(f\)数组的初始化:
运用dfs序遍历,运用公式\(f_{u,i}=f_{f_{u,i-1},i-1}\)。
时间复杂度\(O(n\log n)\)
#include <algorithm>
#include <cstdio>
using std::swap;
const int MAXN = 100010;
const int LOGN = 17;
struct EDGE { int v; EDGE *n; } edge[MAXN*2], *head[MAXN];
int n, m, p, f[MAXN][LOGN], depth[MAXN];
void AddEdge(const int u, const int v) {
edge[p].n = head[u];
edge[p].v = v;
head[u] = edge + p;
p ++;
}
void dfs(const int u, const int father) {
depth[u] = depth[father] + 1;
f[u][0] = father;
for (int i = 1; i < LOGN; i++)
f[u][i] = f[ f[u][i - 1] ][i - 1];
for (EDGE *e = head[u]; e; e = e->n)
if (e->v != father)
dfs(e->v, u);
}
int lca(int u, int v) {
if (depth[u] > depth[v])
swap(u, v);
for (int i = LOGN - 1; i >= 0; i--)
if (depth[ f[v][i] ] >= depth[u])
v = f[v][i];
for (int i = LOGN - 1; i >= 0; i--) {
int s = f[u][i];
int t = f[v][i];
if (s != t) {
u = s;
v = t;
}
}
if (u == v)
return u;
else
return f[u][0];
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1, u, v; i < n; i++) {
scanf("%d %d", &u, &v);
AddEdge(u, v);
AddEdge(v, u);
}
dfs(1, 1);
while (m--) {
int u, v;
scanf("%d %d", &u, &v);
printf("%d\n", lca(u, v));
}
return 0;
}
LCA的应用
P3128 [USACO15DEC]Max Flow P
LCA加树上(点)差分
如图,有一条路径4到9,那么4,9的公共祖先是2,所以这条路径相当于4->2以及5->9。
在这两条路径上进行差分,所以节点4,9加一,节点1,2减一。
其中1是2的祖先,2是5的祖先。
而最后,我们如何才能计算每个节点对应的值呢?
我们只需从叶子结点向上传递差分值,每个父节点的值相当于他的儿子们差分值的和加上自己的差分值。
代码实现:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define maxn 100001
#define logn 21
#define inf 0x7f7f7f7f
#define Rep(i,a,b) for(int i=(a); i<=(b); ++i)
#define Per(i,a,b) for(int i=(a); i>=(b); --i)
int n,m,f[maxn][logn],depth[maxn];
int head[maxn],ver[maxn<<1],nxt[maxn<<1],tot,u,v;
int c[maxn],dp[maxn],res=-inf;
inline void Addedge(int x,int y) {
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
inline void DFS(int u,int father) {
depth[u]=depth[father]+1;
f[u][0]=father;
Rep(i,1,logn-1)
f[u][i]=f[f[u][i-1]][i-1];
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i];
if(v!=father) DFS(v,u);
}
}
inline void Back_DFS(int u,int father) {
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i];
if(v!=father) {
Back_DFS(v,u);
dp[u]+=dp[v];
}
}
}
inline int Lca(int u,int v) {
if(depth[u]>depth[v]) swap(u,v);
Per(i,logn-1,0)
if(depth[f[v][i]]>=depth[u])
v=f[v][i];
Per(i,logn-1,0) {
int s=f[u][i],t=f[v][i];
if(s!=t) {u=s; v=t;}
}
if(u==v) return u;
else return f[u][0];
}
int main() {
scanf("%d%d",&n,&m);
Rep(i,1,n-1) {
scanf("%d%d",&u,&v);
Addedge(u,v); Addedge(v,u);
}
DFS(1,1);
Rep(i,1,m) {
scanf("%d%d",&u,&v);
int x=Lca(u,v);
c[u]++; c[v]++;
c[x]--; c[f[x][0]]--;
}
Rep(i,1,n) dp[i]=c[i];
Back_DFS(1,1);
Rep(i,1,n) res=max(res,dp[i]);
printf("%d\n",res);
return 0;
}
P6869 [COCI2019-2020#5] Putovanje
树上边的差分:用\(edge_i\)表示与i和i父亲节点连接边的权值
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define maxn 200001
#define logn 23
#define inf 0x7f7f7f7f
#define Rep(i,a,b) for(int i=(a); i<=(b); ++i)
#define Per(i,a,b) for(int i=(a); i>=(b); --i)
int n,m,f[maxn][logn],depth[maxn],edge1[maxn],edge2[maxn];
int head[maxn],ver[maxn<<1],nxt[maxn<<1],tot,u,v,w,x;
int c[maxn],dp[maxn];
ll res;
inline void Addedge(int u,int v,int w,int x) {
ver[++tot]=v;
edge1[tot]=w;
edge2[tot]=x;
nxt[tot]=head[u];
head[u]=tot;
}
inline void DFS(int u,int father) {
depth[u]=depth[father]+1;
f[u][0]=father;
Rep(i,1,logn-1)
f[u][i]=f[f[u][i-1]][i-1];
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i];
if(v!=father) DFS(v,u);
}
}
inline void Back_DFS(int u,int father) {
int e;
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i];
if(v!=father) {
Back_DFS(v,u);
dp[u]+=dp[v];
} else e=i;
}
res=res+min((ll)dp[u]*edge1[e],(ll)edge2[e]);
}
inline int Lca(int u,int v) {
if(depth[u]>depth[v]) swap(u,v);
Per(i,logn-1,0)
if(depth[f[v][i]]>=depth[u])
v=f[v][i];
Per(i,logn-1,0) {
int s=f[u][i],t=f[v][i];
if(s!=t) {u=s; v=t;}
}
if(u==v) return u;
else return f[u][0];
}
int main() {
scanf("%d",&n);
Rep(i,1,n-1) {
scanf("%d%d%d%d",&u,&v,&w,&x);
Addedge(u,v,w,x); Addedge(v,u,w,x);
}
DFS(1,0);
Rep(i,1,n-1) {
int x=Lca(i,i+1);
c[i]++; c[i+1]++; c[x]-=2;
}
Rep(i,1,n) dp[i]=c[i];
Back_DFS(1,1);
printf("%lld\n",res);
return 0;
}
Dark之连锁
我们考虑一个附加边 \((u,v)\),会与主要边中 \(u->v\) 路径所有点形成一个环。
若斩断了 \(u->v\) 上任意一边,那么下一次必须斩断 \((u,v)\)。
那么用树上差分给 \(u->v\) 上所有边权值 \(+1\).
若一边权值为0,则斩断任意附加边即可。
若为1,则斩断唯一边。
若大于1,则无论如何图都会联通。
#include<algorithm>
#include<cstdio>
#include<iostream>
using namespace std;
const int N=2e5+10,logn=18;
int head[N],ver[2*N],nxt[2*N],tot,n,m;
int f[N][logn],depth[N],c[N],ans;
void addedge(int x,int y) {
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void DFS(int u,int father) {
depth[u]=depth[father]+1; f[u][0]=father;
for(int i=1; i<logn; i++) f[u][i]=f[f[u][i-1]][i-1];
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i];
if(v==father) continue;
DFS(v,u);
}
}
int Lca(int u,int v) {
if(depth[u]<depth[v]) swap(u,v);
for(int i=logn-1; i>=0; i--)
if(depth[f[u][i]]>=depth[v]) u=f[u][i];
if(u==v) return u;
for(int i=logn-1; i>=0; i--)
if(f[u][i]!=f[v][i]) {
u=f[u][i]; v=f[v][i];
}
return f[u][0];
}
void solve(int u,int father) {
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i];
if(v==father) continue;
solve(v,u);
c[u]+=c[v];
}
if(u==1) return ;
if(c[u]==1) ans++;
if(c[u]==0) ans+=m;
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1,u,v; i<n; i++) {
scanf("%d%d",&u,&v);
addedge(u,v); addedge(v,u);
}
DFS(1,0);
for(int i=1,u,v; i<=m; i++) {
scanf("%d%d",&u,&v);
int w=Lca(u,v);
c[u]++; c[v]++;
c[w]--; c[w]--;
}
solve(1,0);
printf("%d\n",ans);
return 0;
}
树上倍增
典型例题
树上任意两点之间一定有且只有一条路径。现在要求这条路径上,边权的最大值。
树上倍增,如同LCA,就是在LCA中多开一个数组,用来记录答案。
数组\(g_{i,j}\)表示节点i向上跳\(2^j\)步中最大的值。如同f数组一般更新。
#include<algorithm>
#include<cstdio>
using std::max;
using std::swap;
#define ll long long
#define maxn 100001
#define logn 17
#define inf 0x7f7f7f7f
#define Rep(i,a,b) for(int i=(a); i<=(b); ++i)
#define Per(i,a,b) for(int i=(a); i>=(b); --i)
int n,m,f[maxn][logn],g[maxn][logn],depth[maxn];
int head[maxn],ver[maxn<<1],nxt[maxn<<1],edge[maxn<<1],tot;
int u,v,w;
inline void Addedge(int x,int y,int z) {
ver[++tot]=y;
edge[tot]=z;
nxt[tot]=head[x];
head[x]=tot;
}
inline void DFS(int u,int father,int w) {
depth[u]=depth[father]+1;
f[u][0]=father; g[u][0]=w;
Rep(i,1,logn-1) {
f[u][i]=f[f[u][i-1]][i-1];
g[u][i]=max(g[u][i-1],g[f[u][i-1]][i-1]);
}
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i],w=edge[i];
if(v!=father) DFS(v,u,w);
}
}
inline int Lca(int u,int v) {
int ans=0;
if(depth[u]>depth[v]) swap(u,v);
Per(i,logn-1,0)
if(depth[f[v][i]]>=depth[u])
v=f[v][i],ans=max(ans,g[v][i]);
Per(i,logn-1,0) {
int s=f[u][i],t=f[v][i];
if(s!=t) {
ans=max(ans,g[u][i]);
ans=max(ans,g[v][i]);
u=s; v=t;
}
}
if(u==v) return ans;
else return max(ans,max(g[u][0],g[v][0]));
}
int main() {
scanf("%d",&n);
Rep(i,1,n-1) {
scanf("%d%d%d",&u,&v,&w);
Addedge(u,v,w); Addedge(v,u,w);
}
DFS(1,1,0);
scanf("%d",&m);
Rep(i,1,m) {
scanf("%d%d",&u,&v);
printf("%d\n",Lca(u,v));
}
return 0;
}
次小生成树
见《算法竞赛进阶指南》P385
#include<algorithm>
#include<cstdio>
using std::max;
using std::min;
using std::sort;
using std::swap;
const int MAXN=1e5,MAXM=3*1e5,LOGN=17;
int n,m,fa[MAXN];
int head[MAXN],nxt[MAXM],ver[MAXM],edge[MAXM],depth[MAXN],tot;
bool vis[MAXM];
long long sum,ans=1e18,f[MAXN][LOGN],g[MAXN][LOGN],g2[MAXN][LOGN];
struct node {
int x,y;
long long z;
bool operator < (const node s) const {
return z<s.z;
}
} gra[MAXM];
void Addedge(int x,int y,long long z) {
ver[++tot]=y;
edge[tot]=z;
nxt[tot]=head[x];
head[x]=tot;
}
int Getf(int x) {
if(x==fa[x]) return x;
else return fa[x]=Getf(fa[x]);
}
void DFS(int u,int father,long long w) {
depth[u]=depth[father]+1;
f[u][0]=father;
g[u][0]=w;
g2[u][0]=-1e18;
for(int i=1; i<LOGN; i++) {
f[u][i]=f[f[u][i-1]][i-1];
g[u][i]=max(g[u][i-1],g[f[u][i-1]][i-1]);
if(g[u][i-1]>g[f[u][i-1]][i-1]) {
g2[u][i]=max(g2[u][i-1],g[f[u][i-1]][i-1]);
}
if(g[u][i-1]<g[f[u][i-1]][i-1]) {
g2[u][i]=max(g[u][i-1],g2[f[u][i-1]][i-1]);
}
if(g[u][i-1]==g[f[u][i-1]][i-1]) {
g2[u][i]=max(g2[u][i-1],g2[f[u][i-1]][i-1]);
}
}
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i],w=edge[i];
if(v!=father) DFS(v,u,w);
}
}
void Lca(int u,int v,long long w) {
if(depth[u]>depth[v]) swap(u,v);
for(int i=LOGN-1; i>=0; i--) {
if(depth[f[v][i]]>=depth[u]) {
if(w>g[v][i]) ans=min(ans,sum-g[v][i]+w);
if(w==g[v][i]) ans=min(ans,sum-g2[v][i]+w);
v=f[v][i];
}
}
if(u==v) return ;
for(int i=LOGN-1; i>=0; i--) {
int s=f[u][i],t=f[v][i];
if(s!=t) {
if(w>g[v][i]) ans=min(ans,sum-g[v][i]+w);
if(w==g[v][i]) ans=min(ans,sum-g2[v][i]+w);
if(w>g[u][i]) ans=min(ans,sum-g[u][i]+w);
if(w==g[u][i]) ans=min(ans,sum-g2[u][i]+w);
u=s; v=t;
}
}
if(w>g[v][0]) ans=min(ans,sum-g[v][0]+w);
if(w==g[v][0]) ans=min(ans,sum-g2[v][0]+w);
if(w>g[u][0]) ans=min(ans,sum-g[u][0]+w);
if(w==g[u][0]) ans=min(ans,sum-g2[u][0]+w);
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++) {
scanf("%d%d%lld",&gra[i].x,&gra[i].y,&gra[i].z);
}
sort(gra+1,gra+1+m);
for(int i=1; i<=n; i++) fa[i]=i;
for(int i=1; i<=m; i++) {
if(gra[i].x==gra[i].y) continue;
int fx=Getf(gra[i].x),fy=Getf(gra[i].y);
if(fx==fy) continue;
Addedge(gra[i].x,gra[i].y,gra[i].z);
Addedge(gra[i].y,gra[i].x,gra[i].z);
fa[fx]=fy; vis[i]=1;
sum+=gra[i].z;
}
DFS(1,1,0);
for(int i=1; i<=m; i++) {
if(vis[i]||gra[i].x==gra[i].y) continue;
Lca(gra[i].x,gra[i].y,gra[i].z);
}
printf("%lld\n",ans);
return 0;
}
DFS序
维护子树最大值
DFS序是:12445667752331.
其中,每个元素都出现了两次,第一次是搜索到了加入序列,第二次是回溯到了加入序列。
而这两次之间包含的元素就是它的子树中的元素。
例如:566775 这一段中,所代表的就是子树5中有6,7两号元素。
其中,\(id_0\) 代表它首次出现,\(id_1\) 代表它第二次出现
#include<cstdio>
#include<algorithm>
using std::max;
const int MAXN=100005;
int n,m,p,q,id[MAXN][2];
int ver[MAXN*2],nxt[MAXN*2],head[MAXN],tot;
struct Tree {
int l,r,dat;
}t[MAXN*2*4];
void Addedge(int x,int y) {
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void DFS(int u,int father) {
id[u][0]=++q;
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i];
if(v!=father) DFS(v,u);
}
id[u][1]=++q;
}
void Build(int p,int l,int r) {
t[p].l=l; t[p].r=r;
if(l==r) {t[p].dat=0; return ;}
int mid=(l+r)/2;
Build(p*2,l,mid); Build(p*2+1,mid+1,r);
t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}
void Modify(int p,int x,int v) {
if(t[p].l==t[p].r) {t[p].dat=v; return ;}
int mid=(t[p].l+t[p].r)/2;
if(x<=mid) Modify(p*2,x,v);
else Modify(p*2+1,x,v);
t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}
int Query(int p,int l,int r) {
if(l<=t[p].l&&r>=t[p].r) return t[p].dat;
int mid=(t[p].l+t[p].r)/2;
int val=-(1<<30);
if(l<=mid) val=max(val,Query(p*2,l,r));
if(r>mid) val=max(val,Query(p*2+1,l,r));
return val;
}
int main() {
scanf("%d",&n);
for(int i=1,u,v; i<n; i++) {
scanf("%d%d",&u,&v);
Addedge(u,v);
Addedge(v,u);
}
DFS(1,1); Build(1,1,q);
scanf("%d",&m);
for(int i=1,opt,x,y; i<=m; i++) {
scanf("%d",&opt);
if(opt==1) {
scanf("%d%d",&x,&y);
Modify(1,id[x][0],y);
} else {
scanf("%d",&x);
printf("%d\n",Query(1,id[x][0],id[x][1]));
}
}
return 0;
}
维护链上元素和
对于一个点的修改,可以看作为它的子树都修改,因为它的子树中元素和根节点连接的链都要经过这个点。
对于查询,只要求出单点的值就可以了。
代码:(树状数组)
#include<cstdio>
#include<algorithm>
const int MAXN=100005;
int n,m,p,q,id[MAXN][2];
int ver[MAXN*2],nxt[MAXN*2],head[MAXN],tot;
int c[MAXN*2];
void Addedge(int x,int y) {
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void DFS(int u,int father) {
id[u][0]=++q;
for(int i=head[u]; i; i=nxt[i]) {
int v=ver[i];
if(v!=father) DFS(v,u);
}
id[u][1]=++q;
}
void Modify(int p,int y) {
for(; p<=q; p+=p&(-p))
c[p]+=y;
}
int Query(int p) {
int res=0;
for(; p; p-=p&(-p))
res+=c[p];
return res;
}
int main() {
scanf("%d",&n);
for(int i=1,u,v; i<n; i++) {
scanf("%d%d",&u,&v);
Addedge(u,v);
Addedge(v,u);
}
DFS(1,1);
scanf("%d",&m);
for(int i=1,opt,x,y; i<=m; i++) {
scanf("%d",&opt);
if(opt==1) {
scanf("%d%d",&x,&y);
Modify(id[x][0],y);
Modify(id[x][1]+1,-y);
} else {
scanf("%d",&x);
printf("%d\n",Query(id[x][0]));
}
}
return 0;
}