1. 程式人生 > >最小樹形圖【有向圖的最小生成樹】

最小樹形圖【有向圖的最小生成樹】

  做了POJ的一道題,總是WA,不知道為什麼,後來去看了,才知道,原來有向圖的最小生成樹與無向圖不一樣,它得是從某個點出發能遍歷到其他所有點的才行,以此為條件,我們學習到了最小樹形圖。

  與很多其他人的講解不一樣吧,我把我看了部落格後自己的思路分享出來,關於什麼是最小樹形圖。

  我們知道的既然要建立最小樹形圖,就要理解什麼是最小樹形圖,(概念好難懂啊,還是自己寫的清楚明白),我們從某一點出發(或者是固定點),能通過它跑完所有點的最小花費,就是最小樹形圖了。

  那麼,怎麼去搭建最小樹形圖?會有人看到關於最小弧這樣的講法,誒!確實是這樣的,但是你們理解什麼是最小弧嗎?我們想構建一顆最小生成樹的時候,就是找到這樣的最優解的邊逐條放進去的(Kruskal演算法思想),但是這也是一樣的,我們找到所有非根節點的最小入邊

,先把這樣的所有入邊給加進來,那麼得到的一幅圖,可能還真是不完全,要是遇到了個,豈不是有趣,或者呢,壓根就走不完!不就GG?所以,就這樣就被我們想出了兩個需要判斷的條件了。

  把所有的最小入邊先加起來,我們得到了一個花裡胡哨的圖,可能它就是多個環的集合,也許恰好是答案,這都是不確定的,若是恰好是已經沒有環了,那麼這就是答案了;反之,就是有環,那麼,我們得到的邊,就不一定是所有的點構在一起的圖(也許會成為森林這樣的情況),那麼,把環搜尋起來吧,我們把一個環搜尋成一個點,然後對於它(新點——即所謂的縮點)的入邊,我們建立新邊的時候,需要考慮到我們得刪除原來在這幅圖裡的改點的入邊(就是我們已經存入

了這個原節點的入邊了,但是,它卻構成了環,說明不是我想要的解),所以,新邊的權值就是原權值減去終點節點的最小入邊。然後,節點數就會變少了,我們就可以繼續在這樣子優化下去了。直到上面說到的沒有再構成環的時候,就說明是解了。


然後呢,我這掛一道Ice_cream’s world II HDU--2121的解題報告,帶上完整的註釋說明:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <limits>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <map>
#define lowbit(x) ( x&(-x) )
#define pi 3.141592653589793
#define e 2.718281828459045
#define INF 0x3f3f3f3f
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int maxN = 1005;
int N, M;
ll sum;
struct Eddge    //存邊
{
    int u, v;
    ll val;
    Eddge(int a=0, int b=0, ll c=0):u(a), v(b), val(c) {}
}edge[maxN*maxN];
int pre[maxN], id[maxN], vis[maxN], pos;
ll in[maxN];    //最小入邊權,pre[]為其前面的點(該邊的起點)
ll Dir_MST(int root, int V, int E)  //root是此時的根節點,我們最初的時候將0(萬能節點作為根節點進入),V是點的個數(包括之後要收縮之後點的剩餘個數),E是邊的條數(不會改變)
{
    ll ans = 0;
    while(true)     //如果還是可行的話
    {
        for(int i=0; i<V; i++) in[i] = INF; //給予每個點進行初始化
        /* (1)、最短弧集合E0 */
        for(int i=1; i<=E; i++)     //通過這麼多條單向邊,確定的是每個點的指向邊的最小權值
        {
            int u = edge[i].u, v = edge[i].v;
            if(edge[i].val < in[v] && u!=v)     //頂點v有更小的入邊,記錄下來    更新操作,u!=v是為了確保縮點之後,我們的環將會變成點的形式
            {
                pre[v] = u;     //節點u指向v
                in[v] = edge[i].val;    //最小入邊
                if(u == root) pos = i;  //這個點就是實際的起點
            }
        }
        /* (2)、檢查E0 */
        for(int i=0; i<V; i++)     //判斷是否存在最小樹形圖
        {
            if(i == root) continue;     //是根節點,不管
            if(in[i] == INF) return -1;     //除了根節點以外,有點沒有入邊,則根本無法抵達它,說明是獨立的點,一定不能構成樹形圖
        }
        /* (3)、收縮圖中的有向環 */
        int cnt = 0;    //接下來要去求環,用以記錄環的個數  找環開始!
        memset(id, -1, sizeof(id));
        memset(vis, -1, sizeof(vis));
        in[root] = 0;
        for(int i=0; i<V; i++)     //標記每個環
        {
            ans += in[i];   //加入每個點的入邊(既然是最小入邊,所以肯定符合最小樹形圖的思想)
            int v = i;  //v一開始先從第i個節點進去
            while(vis[v] != i && id[v] == -1 && v != root)  //退出的條件有“形成了一個環,即vis迴歸”、“到了一個環,此時就不要管了,因為那邊已經建好環了”、“到了根節點,就是條鏈,不用管了”
            {
                vis[v] = i;
                v = pre[v];
            }
            if(v != root && id[v] == -1)    //如果v是root就說明是返回到了根節點,是條鏈,沒環;又或者,它已經是進入了對應環的編號了,不需要再跑一趟了
            {
                for(int u=pre[v]; u!=v; u=pre[u])   //跑這一圈的環
                {
                    id[u] = cnt;    //標記點u是第幾個環
                }
                id[v] = cnt++;  //如果再遇到,就是下個點了
            }
        }
        if(cnt == 0) return ans;    //無環的情況,就說明已經取到了最優解,直接返回,或者說是環已經收縮到沒有環的情況了
        for(int i=0; i<V; i++) if(id[i] == -1) id[i] = cnt++;   //這些點是環外的點,是鏈上的點,單獨再給他們賦值
        for(int i=1; i<=E; i++)     //準備開始建立新圖  縮點,重新標記
        {
            int u = edge[i].u, v = edge[i].v;
            edge[i].u = id[u];  edge[i].v = id[v];  //建立新圖,以新的點進入
            if(id[u] != id[v]) edge[i].val -= in[v];    //為了不改變原來的式子,使得展開後還是原來的式子
        }
        V = cnt;    //之後的點的數目
        root = id[root];    //新的根節點的序號,因為id[]的改變,所以根節點的序號也改變了
    }
    return ans;
}
int main()
{
    while(scanf("%d%d", &N, &M)!=EOF)
    {
        sum = 0;
        for(int i=1; i<=M; i++)
        {
            scanf("%d%d%lld", &edge[i].u, &edge[i].v, &edge[i].val);
            edge[i].u++;   edge[i].v++;   //把‘0’號節點空出來,用以做萬能節點,留作之後用
            sum += edge[i].val;
        }
        sum++;  //一定要把sum給擴大,這就意味著,除去萬能節點以外的點鎖構成的圖的權值和得在(sum-1)之內(包含)
        for(int i=M+1; i<=M+N; i++)     //這就是萬能節點了,就是從0這號萬能節點有通往所有其他節點的路,而我們最後的最小樹形圖就是從這個萬能節點出發所能到達的整幅圖
        {
            edge[i] = Eddge(0, i-M, sum);   //對於所有的N個其他節點都要建有向邊
        }       //此時N+1為總的節點數目,M+N為總的邊數
        ll ans = Dir_MST(0, N + 1, M+N);    //ans代表以超級節點0為根的最小樹形圖的總權值
        if(ans == -1 || ans - sum >= sum) printf("impossible\n");   //從萬能節點的出度只能是1,所以最後的和必須是小於sum的,而萬能節點的出度就由“ans - sum >= sum”保證
        else printf("%lld %d\n", ans - sum, pos - M - 1);   //pos-M得到的是1~N的情況,所以“-1”的目的就在於這裡
        printf("\n");
    }
    return 0;
}