Kruskal 重构树

发布时间 2023-08-06 20:22:07作者: SError

\(\text{Kruskal}\) 重构树基于 \(\text{Kruskal}\) 最小生成树算法,通过将边权化为点权实现一些奇妙东西。


构造

\(\text{Kruskal}\) 最小生成树算法类似。

当连边 \((u,v,w)\) 时:

  • 新建节点 \(x\),将其点权设为 \(w\).

  • \(u,v\) 所属集合为 \(S_u,S_v\),连边 \((x,S_u)\)\((x,S_v)\)。(无边权的有向边)

  • \(u,v\) 所属集合变为 \(x\).

重构树的根为最后一个点 \(2n-1\),否则会破坏重构树的形态,也才能遍历整棵树。

边权需要排序,复杂度和 \(\text{Kruskal}\) 一样都是 \(\text{O}(n\log n)\).


性质

  • 重构树中的叶子节点为原树中的节点,其余都代表一条边。

  • 原树中路径 \(u\rightarrow v\) 上的边权最大值为重构树上 \(lca_{u,v}\) 的点权。(假设按照最小生成树的方式加入)

因为边权被排序,显然这颗树是一个大根堆 。最大生成树则相反。


先看一下这个「BZOJ 3732」Network:

一个无向图,多次询问两点之间所有路径中最长边的最小值。

一个性质:最长边最小的路径在图的最小生成树上。

建出重构树,那么答案就是 \(lca_{u,v}\) 的点权。


P4768 [NOI2018] 归程

一个无向连通图,一条边有长度 \(l\) 和海拔 \(a\)

从点 \(v\) 出发,可以先经过一些海拔 \(>p\) 的边,代价为 \(0\),然后再回到 \(1\) 号点,代价为经过的边的边权,试最小化代价。

多组询问和测试数据,强制在线

\(T\le 3\)\(n\le2\times 10^5\)\(m,Q\le4\times 10^5\)\(l\le 10^4\)\(a\le 10^9\).

\(\text{TL}=4\text{s}\).

\(1\)\(v\) 的路径可以分为两个部分,那么这个断点 \(u\) 满足 \(u\rightarrow v\) 的边都大于 \(p\),且 \(1\rightarrow u\) 的最短路最小。

\(v\) 出发能到达点 \(u\),一定存在路径在原图的最大生成树上。

建出的重构树是一个小根堆,求出包含 \(v\) 的子树中根节点深度最小且点权大于 \(p\) 的子树 \(x\),那么 \(v\) 就可以遍历 \(x\) 的子树中的所有点。

深度最小的节点可以倍增求。

子树内的所有点都可能成为断点,即求每个点到 \(1\) 的最短路,然后对子树取 \(\min\) 即可。

假设 \(n,m,Q\) 同阶,时间复杂度 \(\text{O}(Tn\log n)\) .

现在是 \(20:31\),看下 \(1.5\text{h}\) 能不能码完。

什么傻逼细节只能明天调了。

我测 \(\text{tot}\) 没清零!

#include<bits/stdc++.h>
#define N 200010
#define M 400010
#define L 22
#define inf 1<<30
using namespace std;
int read(){
	int x=0,w=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
	while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return x*w;
}
int T,n,m,Q,K,S;
int head[N],ver[M<<1],nxt[M<<1],dist[M<<1],tot;
int dis[N];bool vis[N];
void add(int u,int v,int w){
	nxt[++tot]=head[u];
	ver[tot]=v,dist[tot]=w;
	head[u]=tot;
}
priority_queue< pair<int,int> >q;
void dijkstra(){
	memset(dis,0x3f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	q.push(make_pair(0,1)),dis[1]=0;
	while(!q.empty()){
		int u=q.top().second;q.pop();
		if(vis[u])continue;
		vis[u]=true;
		for(int i=head[u],v;i;i=nxt[i])
			if(dis[v=ver[i]]>dis[u]+dist[i]){
				dis[v]=dis[u]+dist[i];
				q.push(make_pair(-dis[v],v));
			}
	}
}
struct E{
	int u,v,l,a;
	bool operator<(const E &x)const{
		return a>x.a;
	}
}e[M];
int f[N<<1],val[N<<1],node;
int fa[N<<1][L],Minn[N<<1];
vector<int>s[N<<1];
int find(int x){
	return f[x]==x?x:f[x]=find(f[x]);
}
void dfs(int x){
	Minn[x]=(x<=n)?dis[x]:inf;
	for(int i=1;i<L;i++)
		fa[x][i]=fa[fa[x][i-1]][i-1];
	for(int i=0,y;i<s[x].size();i++){
		y=s[x][i],fa[y][0]=x,dfs(y);
		Minn[x]=min(Minn[x],Minn[y]);
	}
}
int main(){
	T=read();
	while(T--){
		memset(head,0,sizeof(head));
		memset(val,0,sizeof(val));
		memset(fa,0,sizeof(fa));
		n=read(),m=read(),node=n,tot=0; 
		for(int i=1,u,v,l,a;i<=m;i++){
			u=read(),v=read(),l=read(),a=read();
			e[i]=(E){u,v,l,a};
			add(u,v,l),add(v,u,l);
		}
		dijkstra(),sort(e+1,e+1+m);
		for(int i=1;i<n<<1;i++)f[i]=i;
		for(int i=1,u,v,a,fu,fv;i<=m;i++){
			u=e[i].u,v=e[i].v,a=e[i].a;
			fu=find(u),fv=find(v);
			if(fu!=fv){
				s[++node].push_back(fu);
				s[node].push_back(fv);
				val[node]=a,f[fu]=f[fv]=node;
			}
		}
		dfs(node);
		Q=read(),K=read(),S=read();
		for(int v,p,x,lst=0;Q;Q--){
			v=(read()+K*lst-1)%n+1,p=(read()+K*lst)%(S+1),x=v;
			for(int i=L-1;i>=0;i--){
				if(fa[x][i]&&val[fa[x][i]]>p)x=fa[x][i];
			}
			printf("%d\n",lst=Minn[x]);
		}
		for(int i=1;i<n<<1;i++)s[i].clear();
	}
	
	return 0;
}

P7834 [ONTAK2010] Peaks 加强版

无向图,带点、边权,问从 \(u\) 出发,只经过权值 \(\le x\) 的边能到达的权值第 \(k\) 大的点的权值,不存在输出 \(-1\).

本题强制在线

\(1\le n\le 10^5\)\(0\le m,q\le 5\times10^5\),权值值域 \(10^9\).

按最小生成树的方式建出重构树可以得到一个森林。

和上一题同理,可以倍增找到深度最小的 \(\le x\) 的边。

任务就是查询子树第 \(k\) 大,这个东西把树搬到 \(\text{dfs}\) 序上用主席树做即可。

#include<bits/stdc++.h>
#define N 100010
#define M 500010
#define L 20
using namespace std;
int read(){
	int x=0,w=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
	while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return x*w;
}
int n,m,q,len,a[N],tp[N];
struct E{
	int u,v,w;
	bool operator<(const E &x)const{
		return w<x.w;
	}
}e[M];
int f[N<<1],val[N<<1],node;
int fa[N<<1][L],st[N<<1],ed[N<<1],cnt;
vector<int>s[N<<1];
int find(int x){
	return f[x]==x?x:f[x]=find(f[x]);
}
void merge(int u,int v,int w){
	int fu=find(u),fv=find(v);
	if(fu==fv)return;
	s[++node].push_back(fu);
	s[node].push_back(fv);
	val[node]=w,f[fu]=f[fv]=node;
}
int rt[N<<2],tot;
struct Tree{
	int ls,rs,cnt;
	#define ls(x) t[x].ls
	#define rs(x) t[x].rs
	#define cnt(x) t[x].cnt
}t[N<<7];
int build(int l,int r){
	int p=++tot;
	if(l==r)return p;
	int mid=(l+r)>>1;
	ls(p)=build(l,mid);
	rs(p)=build(mid+1,r);
	return p;
}
int modify(int pre,int l,int r,int x){
	int p=++tot;
	t[p]=t[pre],cnt(p)++;
	if(l==r)return p;
	int mid=(l+r)>>1;
	if(x<=mid)ls(p)=modify(ls(pre),l,mid,x);
	else rs(p)=modify(rs(pre),mid+1,r,x);
	return p;
}
int query(int p,int q,int l,int r,int k){
	if(cnt(p)-cnt(q)<k)return 0;
	if(l==r)return l;
	int mid=(l+r)>>1,x=cnt(rs(p))-cnt(rs(q));
	if(k<=x)return query(rs(p),rs(q),mid+1,r,k);
	else return query(ls(p),ls(q),l,mid,k-x);
}
void lsh(){
	sort(tp+1,tp+1+n),len=unique(tp+1,tp+1+n)-tp-1;
	for(int i=1;i<=n;i++)
		a[i]=lower_bound(tp+1,tp+1+len,a[i])-tp;
}
void dfs(int x){
	st[x]=++cnt,rt[cnt]=(x<=n)?modify(rt[cnt-1],1,len,a[x]):rt[cnt-1];
	for(int i=1;i<L;i++)
		fa[x][i]=fa[fa[x][i-1]][i-1];
	for(int i=0,y;i<s[x].size();i++)
		y=s[x][i],fa[y][0]=x,dfs(y);
	ed[x]=++cnt,rt[cnt]=rt[cnt-1];
}
int main(){
	n=read(),m=read(),q=read(),node=n;
	for(int i=1;i<=n;i++)
		a[i]=tp[i]=read();
	for(int i=1,u,v,w;i<=m;i++){
		u=read(),v=read(),w=read();
		e[i]=(E){u,v,w};
	}
	sort(e+1,e+1+m);
	for(int i=1;i<=n<<1;i++)f[i]=i;
	for(int i=1,u,v,w;i<=m;i++){
		u=e[i].u,v=e[i].v,w=e[i].w;
		merge(u,v,w);
	}
	lsh(),rt[0]=build(1,len);
	for(int i=1;i<=node;i++)
		if(find(i)==i)dfs(i);
	for(int u,x,k,lst=0,ans;q;q--){
		u=(read()^lst)%n+1,x=(read()^lst),k=(read()^lst)%n+1;
		for(int i=L-1;i>=0;i--)
			if(fa[u][i]&&val[fa[u][i]]<=x)u=fa[u][i];
		ans=query(rt[ed[u]],rt[st[u]-1],1,len,k);
		if(!ans)lst=0,printf("-1\n");
		else printf("%d\n",lst=tp[ans]);
	}
	
	return 0;
}

连森林突然爆炸了很奇怪。可能是我 \(\text{inf}\) 开小了。

顺带过了弱化还是不错的。


Li Hua and Path

一颗 \(n\) 个点的树,统计以下点对 \((u,v)\) 的个数:

  • \(u<v\),且恰好满足以下条件之一:

\(1.\) \(u\)\(u\rightarrow v\)编号最小的点

\(2.\) \(v\)\(u\rightarrow v\)编号最大的点

一共 \(m\) 次修改,每次新增点 \(n+i\),其父亲为 \(x\).

输出所有的 \(m+1\) 个答案。

全面贺题机房摆子哥!

分别定义:

\(\text{A}\) 为满足第一种的路径数。

\(\text{B}\) 为满足第二种的路径数。

\(\text{C}\) 为同时满足两种的路径数。

答案即 \(\text{A}+\text{B}-2\text{C}\).

建出大根和小根的重构树。那么

\(\text{A},\text{B}\) 分别是两颗重构树子树大小总和。

\(\text{C}\):在大根子树中找子节点有多少个是小根树上自己的祖先。

也就是计算一个点到根路径上有多少点在小根树中自己的子树里。

\(\text{dfs}\)\(+\) 树状数组可以解决。

考虑解决动态挂叶子:

\(\text{A}',\text{C'}\):小根树的祖先数,记录一个 \(\text{dep}\).

\(\text{B'}\):显然这部分的贡献是 \(n+i-1\).

那么 \(\Delta_{ans}=(n+i-1)-\text{dep}(n+i)\).

人赢牛的。时间复杂度 \(O(n\log n)\).

\(\text{merge}\) 也一起抄了。

#include<bits/stdc++.h>
#define ll long long
#define N 200010
#define lb(x) x&-x
using namespace std;
int read(){
	int x=0,w=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
	while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return x*w;
}
ll ans;
int n,m,f[N];
vector<int>E[N],mx[N],mn[N];
int siz[N],st[N],ed[N],dep[N<<1],cnt;
void rst(){
	for(int i=1;i<=n;i++)f[i]=i;
}
int find(int x){
	return f[x]==x?x:f[x]=find(f[x]);
}
void merge(int u,int v){
	u=find(u),v=find(v);
	if(u==v)return;
	f[v]=f[u];
}
void dfs1(int u){
	siz[u]=1,st[u]=++cnt;
	for(int v:mx[u])
		dfs1(v),siz[u]+=siz[v];
	ans+=siz[u]-1,ed[u]=cnt;
}
void dfs2(int u){
	siz[u]=1;
	for(int v:mn[u]){
		dep[v]=dep[u]+1;
		dfs2(v),siz[u]+=siz[v];
	}
	ans+=siz[u]-1;
}
int c[N];
void add(int x,int k){
	for(;x<=n;x+=lb(x))c[x]+=k;
}
int qry(int x){
	int ret=0;
	for(;x;x-=lb(x))ret+=c[x];
	return ret;
}
void dfs3(int u){
	ans-=2*(qry(ed[u])-qry(st[u]-1));
	add(st[u],1);
	for(int v:mn[u])dfs3(v);
	add(st[u],-1);
}
int main(){
	n=read(),rst();
	for(int i=1,u,v;i<n;i++){
		u=read(),v=read();
		E[u].push_back(v),E[v].push_back(u);
	}
	for(int u=1;u<=n;u++)
		for(int v:E[u]){
			if(v<u&&find(u)!=find(v))
				mx[u].push_back(find(v)),merge(u,v);
		}
	dfs1(n),rst();
	for(int u=n;u;u--)
		for(int v:E[u]){
			if(v>u&&find(u)!=find(v))
				mn[u].push_back(find(v)),merge(u,v);
		}
	dfs2(1),dfs3(1);
	printf("%lld\n",ans);
	m=read();
	for(int i=1,x;i<=m;i++){
		x=read(),dep[n+i]=dep[x]+1;
		ans+=(n+i-1)-dep[n+i];
		printf("%lld\n",ans);
	}
	
	return 0;
}

P4899 [IOI2018] werewolf 狼人

一个连通无向图,每次从 \(S_i\)\(E_i\),问能否先走不小于 \(L_i\) 的点,再走不超过 \(R_i\) 的点。两段途中需要经过 \(\lbrack L_i,R_i\rbrack\) 中的点。

输出 \(1\)\(0\).

保证输入本身合法。

\(2\le n\le 2\times10^5\)\(m\le 4\times10^5\)\(q\le 2\times 10^5\)

同样比较显然,建两颗重构树,倍增可以爬出来两颗子树。

现在要判断子树是否有交。把它们变成 \(\text{dfs}\) 序:

两个排列 \(A,B\),询问 \(A\)\(\lbrack l_1,r_1\rbrack\)\(B\)\(\lbrack l_2,r_2\rbrack\) 中是否有公共元素。

考虑将 \(A\) 映射入 \(B\):记 \(pos\lbrack i\rbrack\)\(i\)\(A\) 中的出现位置,对 \(pos\lbrack b\lbrack i\rbrack\rbrack\) 建一颗主席树,问 \(\lbrack l_2,r_2\rbrack\) 的版本内有没有 \(\lbrack l_1,r_1\rbrack\) 的元素。

\(\text{dfn}\) 撵爆炸了阳历都过不去。只能回家写了。

原来搞错了要把 \(\text{st}\) 的反函数塞进去。

#include<bits/stdc++.h>
#define ll long long
#define N 200010
#define Lg 20
#define lb(x) x&-x
using namespace std;
int read(){
	int x=0,w=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
	while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return x*w;
}
int n,m,q;
vector<int>E[N];
struct Kruskal{
	int f[N],flag;//true->mx;false->mn;
	int st[N],ed[N],dfn[N],cnt;
	int fa[N][Lg];
	vector<int>s[N];
	void init(bool opt){
		for(int i=1;i<=n;i++)f[i]=i;
		flag=opt;
	}
	int find(int x){
		return f[x]==x?x:f[x]=find(f[x]);
	}
	void merge(int u,int v){
		u=find(u),v=find(v);
		if(u==v)return;
		f[v]=f[u];
	}
	void add(int u,int v){
		if((flag?v<u:v>u)&&find(u)!=find(v))
			s[u].push_back(find(v)),merge(u,v);
	}
	void dfs(int u){
		st[u]=++cnt,dfn[cnt]=u;
		for(int i=1;i<Lg;i++)
			fa[u][i]=fa[fa[u][i-1]][i-1];
		for(int v:s[u])fa[v][0]=u,dfs(v);
		ed[u]=cnt;
	}
	int Subtree(int u,int k){
		for(int i=Lg-1;i>=0;i--){
			if(fa[u][i]&&(flag?fa[u][i]<=k:fa[u][i]>=k))
				u=fa[u][i];
		}
		return u;
	}
}T1,T2;
struct Tree{
	int ls,rs,cnt;
	#define ls(x) tr[x].ls
	#define rs(x) tr[x].rs
	#define cnt(x) tr[x].cnt
}tr[N<<5];
int pos[N],rt[N],tot;
int build(int l,int r){
	int p=++tot;
	if(l==r)return p;
	int mid=(l+r)>>1;
	ls(p)=build(l,mid);
	rs(p)=build(mid+1,r);
	return p;
}
int modify(int pre,int l,int r,int x){
	int p=++tot;
	tr[p]=tr[pre],cnt(p)++;
	if(l==r)return p;
	int mid=(l+r)>>1;
	if(x<=mid)ls(p)=modify(ls(pre),l,mid,x);
	else rs(p)=modify(rs(pre),mid+1,r,x);
	return p;
}
bool query(int p,int q,int l,int r,int L,int R){
	if(L<=l&&r<=R)return cnt(p)^cnt(q);
	int mid=(l+r)>>1;bool ret=false;
	if(L<=mid)ret|=query(ls(p),ls(q),l,mid,L,R);
	if(R>mid)ret|=query(rs(p),rs(q),mid+1,r,L,R);
	return ret;
}
int main(){
	n=read(),m=read(),q=read();
	T1.init(true),T2.init(false);
	for(int i=1,u,v;i<=m;i++){
		u=read()+1,v=read()+1;
		E[u].push_back(v),E[v].push_back(u);
	}
	for(int u=1;u<=n;u++)
		for(int v:E[u])T1.add(u,v);
	for(int u=n;u;u--)
		for(int v:E[u])T2.add(u,v);
	T1.dfs(n),T2.dfs(1);
	for(int i=1;i<=n;i++)pos[T1.dfn[i]]=i;
	rt[0]=build(1,n);
	for(int i=1;i<=n;i++)
		rt[i]=modify(rt[i-1],1,n,pos[T2.dfn[i]]);
	for(int S,E,L,R;q;q--){
		S=read()+1,E=read()+1,L=read()+1,R=read()+1;
		S=T2.Subtree(S,L),E=T1.Subtree(E,R);
		printf("%d\n",query(rt[T2.ed[S]],rt[T2.st[S]-1],1,n,T1.st[E],T1.ed[E]));
	}
	
	return 0;
}