前言
只会暴力朱刘,据说还有优化,但没仔细去看。
据说这个算法没啥用,除了板子题几乎派不上用场。。。
树形图与最小树形图
树形图是针对有向图的一个概念,可以类比自无向图的生成树。
一张\(n\)个点的以\(rt\)为根的树形图,就是要保留图中的\(n-1\)条边,形成一棵以\(rt\)为根的外向树,使得从根节点能到达所有点。
至于最小树形图,顾名思义就是一张图中边权和最小的树形图。
有一种题型是要求不定根的最小树形图,此时可以建一个虚拟根向所有点连一条大小为\(INF\)的边,显然只要有解就最多只会选中一次大小为\(INF\)的边。那么只要求出以虚拟根为根的最小树形图,把答案减去\(INF\)即为不定根最小树形图的边权和,而虚拟根连向的那个点就可以看作一个最优的的根。
所以说,这类问题的关键还是求出给定根\(rt\)的最小树形图。
与最小生成树的差异
这一部分主要是探讨无向图中的最小生成树算法为什么不能直接套用到有向图的最小树形图上。
考虑最小生成树贪心正确性的证明,核心在于边权小的边如果不选,则加在求出的最小生成树上将会形成一个环,而环上保留这条边后删去原先的任意一条边,都能得到更优的答案。因此边权小的边必然能选就选。
反观最小树形图,因为有向,加上一条边之后形成的环中并不能随意删去其中的一条边,硬要删去则会产生一系列连锁反应,也因此最小生成树的算法的正确性依据在此将不复存在。
但是啊,最小生成树的贪心思想还是能够借鉴于我们的最小树形图中的。
一个极其贪心的想法
这个算法名叫“朱刘算法”。
考虑我们对于根节点之外的每个点,因为最终的树形图中必然会有恰好一条边连向它,那么我们不妨假设它是边权最小的边。
如果说此时恰好能形成一张树形图,那么就皆大欢喜了。
然而实际情况中肯定没有这么幸运,可能会存在环。而由于每个点的入度恰好为\(1\),所以这其实是由根节点所在的一棵正常树和其余的若干棵基环树组成的。
对于一个环,因为它是由所有最短的边构成的,根据贪心的思想,我们肯定只会从环上删去恰好一条边。
假设删去了点\(x\)前面的边(设其边权为\(v_x\)),那么我们之后肯定需要选择另一条连向\(x\)的边(设其边权为\(val\))替代它,而这条边对于答案的更新相当于是\(val-v_x\),与环上的其他点没有任何关系。
所以说,对于一个环,只要我们给答案加上这个环的边权和,然后给连向环的所有边都减去相应权值,实际上就可以把这个环缩成一个点,而转化后的问题依旧是要求最小树形图,直接重复先前的操作即可。
由于缩了一个环至少会减少一个点,因此复杂度上界是\(O(nm)\)的。
具体实现中我们想要找出一个环,由于这是基环树,直接暴力上跳直至出现重复点即可。
【洛谷4716】【模板】最小树形图
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100
#define M 10000
#define INF 1e9
using namespace std;
int n,m,rt,f[N+5],v[N+5],id[N+5],vis[N+5];struct edge {int x,y,v;}e[M+5];
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
Tp I void writeln(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
int main()
{
RI i;for(read(n,m,rt),i=1;i<=m;++i) read(e[i].x,e[i].y,e[i].v);
RI x,ct,ans=0;W(true)//重复执行操作
{
for(i=1;i<=n;++i) v[i]=INF,f[i]=id[i]=vis[i]=0;
for(i=1;i<=m;++i) e[i].x^e[i].y&&v[e[i].y]>e[i].v&&(f[e[i].y]=e[i].x,v[e[i].y]=e[i].v);//求出每个点最小的入边
for(ct=0,i=1;i<=n;++i) if(i^rt)
{
if(!f[i]) return puts("-1"),0;if(ans+=v[i],vis[i]) continue;//没有入边无解;已经访问过可直接跳过
x=i;W(!vis[x]&&x^rt) vis[x]=i,x=f[x];if(x^rt&&vis[x]==i) {++ct;W(!id[x]) id[x]=ct,x=f[x];}//暴力上跳,给环编号
}
if(!ct) break;for(i=1;i<=n;++i) !id[i]&&(id[i]=++ct);//没环可以直接结束,否则给不在环上的点编号
for(n=ct,rt=id[rt],i=1;i<=m;++i) e[i].v-=v[e[i].y],e[i].x=id[e[i].x],e[i].y=id[e[i].y];//转化边权,给环缩点
}return printf("%d\n",ans),0;
}