• 最小树形图(朱刘算法)--学习笔记


    树形图的定义:一个有向图,满足从某一点u出发可以遍历全部点,并且图中不存在环(恰有点数-1条边),则称该图为一个树形图,其中u是该图的根。对于一个有向图,如果我们可以在其上生成一个包括原图所有点的树形图T,且满足T的所有边权之和最小,那么T就是原图的一个最小树形图。

      从定义上很容易联想到无向图的最小生成树。然而克鲁斯卡尔和prim算法都不能适用于有向图的情况;解决最小树形图问题最早的算法是1965年提出的朱刘算法,时间复杂度O(VE),应用比较广泛。

      根据笔者的理解,朱刘算法基于一种贪心处理之后产生的三种情况的判定:我们在原图G中找出除根外每个点的最小入边,其权值用in[v]表示。那么,我们考虑原先的点和这些入边组成的新图G':如果图中不存在环(包括自环),那么G'一定是所求的最小树形图,这是很直观的。而如果有一些点(除根u)找不到入边,那么这个图一定不存在遍历完全的方案。关键在于第三种情况:如果出现了环,我们如何进行下一步处理?

      假使现在我们遇到了一个由最小入边组成的环c。这个环上有k个点,k条边,由于每个点仅记录了一个入边,这个环与外界不存在由in[v]中的某条边连通的情况。我们现在要做的,就是拆掉这个环上的某一条边in[v],然后选择一条连接外界与点v的边,使得这个环变成一条树上的链。

      朱刘算法对这一步给出的选择策略是:我们把原先的环缩成一个点。对于指向新点(假设指向原来环上的点v)的每条边,我们令它的权值减去in[v];这样,设环上所有边的权为S,in[v] = w,最优选择的那条入边e指向v且e原来的权为cost,则:

      (S - w) + cost = S + (cost - w)

      左边表示直观地拆掉in[v]并加上那条入边e的过程,而右边是原环的值加上“按上述做法预处理过后的边权”。这个式子表明,缩点+预处理后的那张新图再次找到生成的图,对其递归地求in[v]并继续按上述三个情况判断处理,最终得到的最小树形图,与原图的最小树形图的权值和是一样的。这便是朱刘算法的核心。因此我们不断地找环、缩点、处理边权,直到满足没有换或发现不存在最小树形图为止。用循环和递归实现都是可以的。

      对算法复杂度的说明:每次递归都需要跑一遍所有的边来找到in[v],并且最坏情况下每次缩掉一个大小为2的环(自环在选边的时候就筛掉了,见代码),去掉一个点,则最多执行n-1次缩点就一定能得到结果了。按照更熟悉的记号,实际上复杂度就是O(mn)的。

    代码:

    1. #include <iostream>  
    2. #include <cstdio>  
    3. #include <cstring>  
    4. #include <cctype>  
    5. #include <climits>  
    6. #define maxn 110  
    7. #define maxm 10010  
    8. #define inf INT_MAX  
    9. using namespace std;  
    10. int n, m, root;  
    11. long long ans;  
    12. struct E {  
    13.     int u, v, w;  
    14. } edge[maxm];  
    15. int vis[maxn], pre[maxn], in[maxn], c[maxn];  //pre数组记录前驱为找环服务,c表示某点所在的环编号
    16. void zhu_liu() {  
    17.     while (1) {  
    18.         memset(in, 127, sizeof(in));  
    19.         for (int i = 1; i <= m; ++i) {  
    20.             int u = edge[i].u, v = edge[i].v, w = edge[i].w;  
    21.             if (v != u && w < in[v])  
    22.                 in[v] = w, pre[v] = u;  
    23.         }  
    24.         in[root] = 0;  
    25.         for (int i = 1; i <= n; ++i)  
    26.             if (in[i] > 2e9) {  
    27.                 ans = -1; return;  
    28.             }  
    29.         int cnt = 0;  
    30.         memset(vis, 0, sizeof(vis));  
    31.         memset(c, 0, sizeof(c));  
    32.         for (int i = 1; i <= n; ++i) {  
    33.             ans += in[i];  //把每条边的权都预先累积下来,不是环的话一会预处理会直接减成0,不影响结果
    34.             int v = i;  
    35.             while (v != root && vis[v] != i && !c[v]) {  
    36.                 vis[v] = i;  
    37.                 v = pre[v];  
    38.             }  
    39.             if (v != root && !c[v]) {  //是环
    40.                 c[v] = ++cnt;  
    41.                 for (int u = pre[v]; u != v; u = pre[u])  
    42.                     c[u] = cnt;  
    43.             }  
    44.         }  
    45.         if (!cnt) return;  //没有环,找到答案
    46.         for (int i = 1; i <= n; ++i)  //建新图
    47.             if (!c[i]) c[i] = ++cnt;  
    48.         for (int i = 1; i <= m; ++i) {  
    49.             int u = edge[i].u, v = edge[i].v;  
    50.             edge[i].u = c[u], edge[i].v = c[v];  
    51.             if (c[u] != c[v])  
    52.                 edge[i].w -= in[v];  //处理入边权
    53.         }  
    54.         n = cnt, root = c[root];  //转到新图
    55.     }  
    56. }  
    57. int main() {  
    58.     scanf("%d %d %d", &n, &m, &root);  
    59.     int u, v, w;  
    60.     for (int i = 1; i <= m; ++i) {  
    61.         scanf("%d %d %d", &u, &v, &w);  
    62.         edge[i] = (E) {u, v, w};  
    63.     }  
    64.     zhu_liu();  
    65.     printf("%lld", ans);  
    66.     return 0;  
    67. }  

    展开缩点成为原图最小树形图的算法还没有学(我估计用不到),就先写到这里。

  • 相关阅读:
    JavaScript备忘录-逻辑运算符
    CMake 构建项目教程-简介
    C++ 跨语言调用 Java
    Thrift-0.10.0 CenOS 7 编译错误 error: expected ')' before 'PRIu32'
    CentOS 7 安装 MySQL Database
    CentOS 安装 Wine
    FreeBSD 配置
    CentOS 6.5 升级 GCC 4.9.3
    Favorite Setting
    shell编程-1到100的求和与冒泡排序
  • 原文地址:https://www.cnblogs.com/TY02/p/11122886.html
Copyright © 2020-2023  润新知