[复习资料]最小树形图

[复习资料]最小树形图

最近在整理自己的模板集,然后就发现了最小树形图这个基本不考的考点,我记得当时学最小树形图的时候都是迷迷糊糊的,跟着题解敲了一遍代码,根本无法理解这个算法,所以后面就直接忘了最小树形图咋写的。。。

现在回顾感觉包括优化都还是挺简单的,不知道当时自己怎么这么蠢。。。


最小树形图问题指的是:给定一个有向图,边有边权,给定根 r ,找到该图的一个子图使得 r 可以到达其他所有点(或者被其他所有点到达),最小化该子图的边权和。很显然,最后得到的子图结构一定是一个外向树(内向树),这里考虑内向树。

朴素的贪心思想,除了根以外每个点都恰好有一条出边,于是我们当然希望每个点都可以选择它所有出边中边权最小的那一条,如果这样选择后没成环最好,如果出现了一个环,那么这个环中只要一个点去改变它选择的出边,其它点就仍然能够保持原来的选择,所以自然最优的选择就是只去更改这个环中一个点的出边,或者说,整个环还需要一个额外的出边,这和一个点的要求相同!于是我们发现,整个环完全可以视作一个单独的点,不过这个点出边的边权需要做一些更改。

没有优化的朱刘算法就是每次找出边,然后缩点,缩点后再找出边,一直这样迭代,复杂度是 O(nm)

考虑优化,找最小值考虑堆,容易发现,一次缩环操作仅仅改变的是这个环内部点的出边,并且环内同一个点的出边边权都是同时加上一个值,然后再将所有出边合并,于是考虑可并堆,整体加不改变堆结构,所以可以实现,比如左偏树,左偏树用懒标记很容易就可以支持整体加。这样缩完点后就可以很快找到新的最小边权的出边。这个可以直接用 dfs 去实现,每次出现环了就地缩环就行。复杂度 O((n+m)logm)

下面是代码:

#include<cmath>
#include<queue>
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
//最小树形图 
//Directed Minimum Spanning Tree
namespace DMST{
	const int maxn=105,maxm=10005;
	struct Val{
		int v,w;
		Val(int v=0,int w=0):
			v(v),w(w){}
		Val operator += (const int o){
			this->w=this->w+o;return *this;
		}
		bool operator < (const Val o)const{
			return w<o.w;
		}
	};
	struct Node{
		int l,r,dis,laz;Val v;
		Node(int l=0,int r=0,int dis=0,int laz=0,Val v=Val()):
			l(l),r(r),dis(dis),laz(laz),v(v){}
	}P[maxm];
	int tot;
	void push(int x,int laz){
		P[x].laz+=laz;
		P[x].v+=laz;
	}
	void pushdown(int x){
		if(!P[x].laz)return;
		push(P[x].l,P[x].laz);
		push(P[x].r,P[x].laz);
		P[x].laz=0;
	}
	void pushup(int x){
		if(P[P[x].l].dis<P[P[x].r].dis)
			std::swap(P[x].l,P[x].r);
		P[x].dis=P[P[x].r].dis+1;
	}
	int newnode(Val v){
		P[++tot]=Node(0,0,1,0,v);
		return tot;
	}
	void merge(int&x,int l,int r){
		if(!l||!r)return x=l|r,void();
		pushdown(l);pushdown(r);
		if(P[r].v<P[l].v)l^=r^=l^=r;
		x=l;merge(P[x].r,P[l].r,r);
		return pushup(x);
	}
	void pop(int&x){
		pushdown(x);
		merge(x,P[x].l,P[x].r);
	}
	int rt[maxn];
	int id[maxn];
	int vis[maxn],cnt;
	int st[maxn],tp;
	int find(int x){
		return id[x]==x?x:id[x]=find(id[x]);
	}
	#define inf 0x3f3f3f3f
	int dfs(int u){
		if(vis[u]&&vis[u]<cnt)return 0;
		if(vis[u]==cnt){
			while(st[tp]!=u){
				id[st[tp]]=u;
				merge(rt[u],rt[u],rt[st[tp]]);
				--tp;
			}
		}
		else vis[u]=cnt,st[++tp]=u;
		if(rt[u]==0)return -inf;
		Val tmp=P[rt[u]].v;
		pop(rt[u]);push(rt[u],-tmp.w);
		return tmp.w+dfs(find(tmp.v));
	}
	#undef inf
	//由于不用存图,这里直接输入了 
	int solve(void){
		int n,m,r;
		scanf("%d%d%d",&n,&m,&r);
		vis[r]=cnt=1;
		for(int i=1;i<=n;++i)
			id[i]=i;
		for(int i=1;i<=m;++i){
			int u,v,w;
			scanf("%d%d%d",&u,&v,&w);
			merge(rt[v],rt[v],newnode(Val(u,w)));
		}
		int ans=0;
		for(int i=1;i<=n;++i){
			if(i!=r&&!vis[i]){
				++cnt;
				int tmp=dfs(i);tp=0;
				if(tmp<0)return -1;//无解 
				ans+=tmp;
			}
		}
		return ans;
	}
}
int main(){
	printf("%d\n",DMST::solve());
	return 0;
}

另外一个值得注意的地方就是最小树形图缩点的过程可以认为是一棵树,类似并查集合并过程建树一样,忽略根的存在一直这样缩点我们就可以得到一棵“最小树形图树”,将树的父边边权设置为缩环的过程中对应边权,那么这样得到的树就有这样一个性质:原图中以 u 为根的最小树形图边权和就是该树中除了 u 到根的路径以外的边的边权和。同样的,如果有多个点是根,“最小森林形图”的边权和也可以这样求。

找不到例题就自己造了道:道路修建

做法就是建树然后看每条边能否被保留。

以下是 std :

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=100005,maxm=200005;
struct Val{
	int u,v,w;
	Val(int u=0,int v=0,int w=0):
		u(u),v(v),w(w){}
	Val operator += (const int o){
		this->w=this->w+o;return *this;
	}
	bool operator < (const Val o)const{
		return w<o.w;
	}
};
struct Node{
	int l,r,dis,laz;Val v;
	Node(int l=0,int r=0,int dis=0,int laz=0,Val v=Val()):
		l(l),r(r),dis(dis),laz(laz),v(v){}
}P[maxm];
int tot;
void push(int x,int laz){
	P[x].laz+=laz;
	P[x].v+=laz;
}
void pushdown(int x){
	if(!P[x].laz)return;
	push(P[x].l,P[x].laz);
	push(P[x].r,P[x].laz);
	P[x].laz=0;
}
void pushup(int x){
	if(P[P[x].l].dis<P[P[x].r].dis)
		std::swap(P[x].l,P[x].r);
	P[x].dis=P[P[x].r].dis+1;
}
int newnode(Val v){
	P[++tot]=Node(0,0,1,0,v);
	return tot;
}
void merge(int&x,int l,int r){
	if(!l||!r)return x=l|r,void();
	pushdown(l);pushdown(r);
	if(P[r].v<P[l].v)l^=r^=l^=r;
	x=l;merge(P[x].r,P[l].r,r);
	return pushup(x);
}
void pop(int&x){
	pushdown(x);
	merge(x,P[x].l,P[x].r);
}
int rt[maxn*2];
int fa[maxn*2];
int vis[maxn*2],cnt;
int st[maxn],tp;
int rn;
int find(int x){
	return fa[x]==x?x:fa[x]=find(fa[x]);
}
int val[maxn*2];
int son[maxn*2],bro[maxn*2],pa[maxn*2];
void ade(int u,int v){
//	printf("ade %d %d %lld\n",u,v,val[v]);
	bro[v]=son[u],son[u]=v;pa[v]=u;
}
#define INF 1000000001
void dfs(int u){
	if(vis[u]&&vis[u]<cnt)return void();
	if(vis[u]==cnt){
		++rn;
		while(st[tp+1]!=u){
			fa[st[tp]]=rn;ade(rn,st[tp]);
			merge(rt[rn],rt[rn],rt[st[tp]]);
			--tp;
		}
		u=rn;
	}
	vis[u]=cnt,st[++tp]=u;
	while(rt[u]&&find(P[rt[u]].v.v)==u)pop(rt[u]);
	if(!rt[u])return val[u]=INF,ade(0,u);
	Val tmp=P[rt[u]].v;val[u]=tmp.w;
	pop(rt[u]);push(rt[u],-tmp.w);
	dfs(find(tmp.v));
	if(find(u)==u)ade(0,u);
	return;
}
int n,k,ans;
const int mod=998244353;
int iac[maxn],fac[maxn];
int getans(int u){
	int sz=(son[u]==0);
	for(int v=son[u];v;v=bro[v]){
		int o=getans(v);
		if(o+k<=n){
			if(val[v]==INF){puts("-1");exit(0);}
			ans+=1ll*fac[n-o]*iac[n-o-k]%mod*val[v]%mod;
			ans-=(ans>=mod?mod:0);
		}
		sz+=o;
	}
	return sz;
}
void solve(void){
	int m;
	scanf("%d%d%d",&n,&m,&k);rn=n;
	fac[0]=fac[1]=iac[0]=iac[1]=1;
	for(int i=2;i<=n;++i)
		iac[i]=1ll*(mod-mod/i)*iac[mod%i]%mod;
	for(int i=2;i<=n;++i)
		fac[i]=1ll*fac[i-1]*i%mod,iac[i]=1ll*iac[i-1]*iac[i]%mod;
	for(int i=1;i<=n*2;++i)fa[i]=i;
	for(int i=1;i<=m;++i){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		merge(rt[u],rt[u],newnode(Val(u,v,w)));
	}
	for(int i=1;i<=n;++i){
		if(!vis[i]){
			++cnt;dfs(i);tp=0;
		}
	}
	getans(0);
	ans=1ll*ans*fac[n-k]%mod*iac[n]%mod;
	printf("%d\n",ans);
}
int main(){
	solve();
	return 0;
}

当然,“最小树形图树”由于保留了缩环过程,所以也是求方案的好方法,具体而言就是从上到下遍历整棵树,轻确定哪些边要保留,如果 u 到它儿子 v 对应的边要保留,这条边是 xy ,那么 v 子树中 x (内向树)或 y (外向树)到 v 链上对应的边就不能保留,这样就能确定最终是哪些边要保留了。

posted @   xiaolilsq  阅读(530)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示