您的位置:首页 > 其它

朱刘算法 , 以UVa11183为例

2015-10-12 22:36 246 查看
本文参考借鉴了其他文章 , 但原作者以无从考证了 , 表示感谢

我对朱刘算法的认识不够深入 , 但希望我的文章能给初学者一些帮助 , 因为很多网友的文章淡化了初学者不理解的细节 , 希望这篇文章能够帮你初步理解朱刘算法

背景

  该问题是由朱永津与刘振宏在上个世纪60年代解决的,值得一提的是,这2个人现在仍然健在,更另人敬佩的是,朱永津几乎可以说是一位盲人。解决最小树型图的算法后来被称作朱刘算法,也是当之无愧的。

题目及其介绍

本题是刘汝佳和陈峰的训练指南上的一道beginner习题 , 一道朱刘算法的模版训练题 , 题意: 有从0-n-1的n个点 , m条有向带权边 , 输出以0为根节点的最小树形图(如果去掉方向就是一个最小生成树 ,但有一个限制:除了根节点的其余点入度为1 , 好好想想这是个什么玩意) , 或者不存在树形图输出:"Possums!"

关于算法:

朱刘算法的思想:首先值得说明的是 , 朱刘算法与你可能学过的最小生成树的算法不同 , 它是一个逐步调整完善的过程 , 并且我们在进行算法之前就确定根节点 ,如果没有明确要求根节点是什么 , 我们可以模拟一个根节点(自行脑补)。

现在我们来明确一个要达到的目标也是算法最终的状态:如果一个图中每个点(除根节点)都仅有一条边指向自己,并且图中没有"有向环" , 这就是一个树形图 , 又如果每条边都尽量的小(先不想关于生成的树形图是否是最小树形图的证明 ),那么就是我们要求的最小树形图

现在我们来看看是怎么做到的,如下:

考虑到每一个点都要有一条尽可能小的边指向自己(即是下文中的“每个点的入边”) , 最好的情况就是我们枚举每一个点(除了根节点)并找到最小的一条指向这个点的边 , 这些边不构成有向环,形成了一个所求的最小树形图 , 但实际很可能会出现环 , 这些环(除了根节点所在的联通块,其余的都是环,这样准确一些) 是独立的, 为什么是独立的呢 , 因为仅有n-1条边(不枚举根节点的入边) , 只有是一棵树才能联通。注意: 换句话说 , 如果图联通了
, 就一定是树形图 , 于是我们尝试去换一些边 , 使图联通 , 在换的过程中我们总是选择较小的边 , 那么得到的就是最小树形图(有点像kruscal的选边规则) , 想到我刚刚说的“逐步调整”了吗?调整就是换边 , 明确这一点。

现在我们的初始状态已经枚举了每一个点最小的入边 , 并且记录了每个点,入边的权值大小和父亲编号

如何换边才是算法的精妙之处 , 可能你会想到去枚举一些其它的边把有向环拆掉 , 但有可能会出现新的环...这不是一个好思路 , 朱刘算法就不直接去换边 , 它也不去拆掉环 , 而是在不增加边的情况下让图联通 , 记得吗 , 联通就一定是树形图 (这里又强调一遍 , 重要事情说两遍)  ,既然是让所有环联通 , 那么我们应该以每一个环为单位来尝试使它们彼此联通 , 所以我们想到了缩点(学过强联通分量吗) , 通俗一点 , 就是用一个新的点代替原来图的一个环(或者你叫它强联通分量)
, 并且修改跟这个环里的点有关的边的权值 , 为什么要修改权值呢?因为我们是换边 , 不是增加边 , 当我们每更换一个点的入边的时候我们就要去掉原来那个入边  ,于是我们把这个点所有可能的入边全部减小原来我们枚举的那个边的权值 , 这样每增加一条入边无形中就删去了原来那条边(此处想通再继续)。 当我们把所有的环都缩点并且修改权值之后 , 相当于就重新建图了(看代码就懂了)

主算法的过程就是:  找最小入边->判断有没有环(没有环就退出 , 算法成功)->缩点,改权值 , 如此往复 , 见代码

先看看代码 , 然后谈几个你可能会有的疑问 :

1. 时间复杂度 , while(true) 会让人有怀疑 , 但是每一次要么没有环 , 退出 , 要么就缩点 , 而缩点后整个图的节点数至少减少1 , 所以至多循环进行n次 , 而每一次的时间复杂度为O(m) , 所以整个的复杂度为O(nm)

2. 图根本没有树形图 , 虽然每一次建图 , 整个图不见得联通 , 但是每个点(除了根节点)肯定有一条入边 , 没有入边 与 无法形成树形图是等价的 , 并且这一次判断在第一次循环中就可以判定 , 之所以写在大循环中仅仅是为了方便

3. inw[] 是每个点最小入边的权值 , v[] 防止同一个环被多次访问中出现 , idx[] 是重新建图的各个点所在点的编号

下面是UVa11183的代码 , 建议自己看完实现

//
//  main.cpp
//  UVa11183
//
//  Created by Fuxey on 15/10/12.
//  Copyright © 2015年 corn.crimsonresearch. All rights reserved.
//

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <set>
#include <map>
#include <string>
#include <algorithm>
#include <vector>
#include <deque>

using namespace std;
const int maxn = 1e3+10;
const int INF = 1<<29;
struct edge
{
int u , v , w;
edge(int u = 0,int v =0 ,int w = 0):u(u),v(v),w(w){}
};
int n , m, ans ;
vector<edge> g;

// about below
int id[maxn] , inw[maxn] , v[maxn] , pre[maxn];
bool zhuLiuAlg(int root)
{
ans = 0;
while(true)
{

// 以下是初始化

for(int i=0;i<n;i++) inw[i] = INF , id[i] = -1 , v[i] = -1 , pre[i] = -1;
//以下是找最下入边
for(int i=0;i<g.size();i++) if(g[i].w < inw[g[i].v] && g[i].v!=g[i].u)
inw[g[i].v] = g[i].w , pre[g[i].v] = g[i].u;
pre[root] = root; inw[root] = 0;
//判断是否可能 , 并且累加答案 , 因为我们在后边修改了权值 , 所以这些边的值是可以直接加到答案里的
for(int i=0;i<n;i++) { if(inw[i]==INF) return false; ans+=inw[i]; }
//找圈 , 并且缩点 , 给缩成的点进行编号 , 顺便判断是否还有环
int idx = 0;
for(int i=0;i<n;i++) if(v[i]==-1)
{
int t = i;
while(v[t]==-1) v[t] = i , t = pre[t];
if(v[t]!=i || t==root) continue;
id[t] = idx++;
for(int j=pre[t];j!=t;j=pre[j]) id[j] = idx-1;  // we should know that the circle may not end in ith point
}

if(idx==0) return true;  // 没有环了

for(int i=0;i<n;i++) if(id[i]==-1)  id[i] = idx++;
// 重新建图
for(int i=0;i<g.size();i++)
{
g[i].w -= inw[g[i].v];
g[i].u = id[g[i].u];
g[i].v = id[g[i].v];
}
n = idx;
root = id[root];
}
}

int main(int argc, const char * argv[]) {

int t;
cin>>t;

for(int Case=1;Case<=t;Case++)
{
cin>>n>>m;
g.clear();

for(int i=1;i<=m;i++) { int a , b , c;cin>>a>>b>>c; g.push_back(edge(a , b , c)); }

cout<<"Case #"<<Case<<": ";
if(zhuLiuAlg(0)) cout<<ans<<endl;
else cout<<"Possums!\n";
}

return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: