题解 P4716 【【模板】最小树形图】
1 DAG的最小树形图
一个树形图中,除了r点其他点都只有一个入边
那么对于一个DAG,只要我们对于每一个点选出最小的入边,那么这一定是个树形图(显然,易证
那么算法就非常的简单
for(int i=1;i<=n;i++,des=r){
for(int j=1;j<=n;j++)
if(e[i][j]<e[i][des]&&i!=j)
des=j;
ans+=e[i][des];
}
2 环的最小树形图
这个问题十分傻,显然,从r开始绕一圈少一条边就行了
3 真正的最小树形图
这个东西叫做朱刘算法
好像伟大的图论专家塔扬先生有一种更好的方法,不过太烦了,没必要研究这种黑科技
我们看到,我们用DAG的算法把边分为了两种,红边就是要选的边,蓝边是因为比红边长而被抛弃的边,然后黄色的是个红边组成的环
如果我们一开始用DAG的贪心算出来的图就是一个一个DAG,那么皆大欢喜,这个环没有影响,直接输出即可
(这是一个逆时针的环)
1处理环
那么有环呢?
一旦有个环,那么这个环在选出来的图上肯定不会被其他点连接
那我们肯定要把其中一条边换成环外边
那么如果有环,我们就把贪心算出来的那个所谓的“树形图”上的环缩成一个点,环外边指向这个所称的点
为了方便统计,由于我们确信环外边的长度
用那张图解释一下:
左上角的那个点,指向它的那条边,边权设为自己的长度,减去左上角的点到左下角的点的边的长度
有什么好处呢?
(引用一下别人的别人的别人的题解的图,这张图实在太好用了,放这里做参考)
这样,我们以后再选到这条边,就直接把它加到ans里面,就得到了这个环和连接这个环的边的所需长度了
为什么呢?易证,易得,显然如此,
假设环内去除的边权为
2 找环
说一遍,我不想TARJAN
fa[u]:
tp[u]:相当于并查集路径压缩的数组,代表
lp[u]:代表u是哪个环上的。如果lp=0,那么代表这个点目前还不是环上的
遍历每个节点
如果最后走到了根,代表目前这个点还不是环上点
如果最后走到了前驱是自己的点
-
lp[v]不是0,代表它已经是另一个环的节点,一个节点不可能同时处于两个环,不用设置新环
-
lp[v]是0,代表这是发现的一个新环,给环上的每个点的id都标记上
for(int u=1,v=1;u<=n;u++,v=u){ while(v!=root&&tp[v]!=u&&!lp[v]) tp[v]=u,v=fa[v]; if(v!=root&&!lp[v]){ lp[v]=++tot; for(int k=fa[v];k!=v;k=fa[k]) lp[k]=tot; } }
3 循环结束时间
总体的算法流程:对于每一次,求出所谓的“带环最小树形图”,然后把环缩点,修改边权
何时结束?
我们知道,一旦他是一个没有环的树形图,那么就是一个树形图了(雾
总的来说,一旦没有环,那么所有点都能到,那么就OK了,直接退出循环
撒花HORRAY
算法流程
对于每一次循环:
- 贪心找出所谓的“最小带环树形图”,(就是上面的万能图的红边)
顺便记录一下自己是从哪里来的(入边的起点) - 把所有选出的边加到ans里面
- 找环记录环,统计数量
- 如果没环,代表完成了,退出循环
- 把所有不是环上的点全部设置为自己是一个独立环(大小为1的新环)
- 重新设置边权&缩点
- 完成缩点,重新设置n和root,然后初始化
#include<bits/stdc++.h>
using namespace std;
const int N=109,M=10009;
struct edge{int u,v,w;}e[M]; //用边表存储
int n,m,root,mn[N],fa[N],tp[N],lp[N],tot,ans;
int zl(){
while(1){
for(int i=1;i<=n;i++) mn[i]=1e9,fa[i]=tp[i]=lp[i]=0;
for(int i=1,u,v,w;i<=m;i++) //Step 1
if(e[i].u!=e[i].v&&(w=e[i].w)<mn[v=e[i].v])
mn[v]=w,fa[v]=e[i].u;
mn[root]=0;
for(int u=1;u<=n;u++){ans+=mn[u];if(mn[u]==1e9)return -1;} //Step 2
for(int u=1,v=1;u<=n;u++,v=u){ //Step 3
while(v!=root&&tp[v]!=u&&!lp[v]) tp[v]=u,v=fa[v];
if(v!=root&&!lp[v]){
lp[v]=++tot;
for(int k=fa[v];k!=v;k=fa[k]) lp[k]=tot;
}
}
if(!tot) return ans; //Step 4
for(int i=1;i<=n;i++) if(!lp[i]) lp[i]=++tot; //Step 5
for(int i=1;i<=m;i++) //Step 6
e[i].w-=mn[e[i].v],e[i].u=lp[e[i].u],e[i].v=lp[e[i].v];
n=tot, root=lp[root], tot=0; //Step 7
}
}
int main(){
scanf("%d%d%d",&n,&m,&root);
for(int i=1,u,v,w;i<=m;i++)
scanf("%d%d%d",&u,&v,&w),e[i]=(edge){u,v,w};
printf("%d",zl());
return 0;
}
最后说一下个人的想法:
最小树形图在联赛中并不常见,但是zl算法却有一个很重要的思想:见到有向有环图,可以想到缩点,然后利用DAG的特性去做题,因为许多算法在 DAG 上可以有更优/更方便的解。