1. 程式人生 > >算法導論23章思考題(轉載)

算法導論23章思考題(轉載)

選擇 operator nim 三角形 pre fine 圖的最小生成樹 transform 最小生成樹

23-1次優最小生成樹

技術分享

a.

最小生成樹唯一性證明:

已知當前構造的邊集A是最小生成樹的子集。令無向圖G的一個切割是,顯然該切割是尊重A的。已知跨越該切割的輕量級邊對於A是安全的,又因為該無向圖G的每條邊的權值都不相同,所以對於當前A而言,安全邊有且只有一條,即對於每個狀態下的A,構造最小生成樹的方式是唯一的。所以最小生成樹是唯一的。

次優最小生成樹不唯一性證明:

技術分享

如上圖:{(C, D), (A, D), (A, B)} 和 {(C, D), (A, C), (B, D)} 是兩個次優最小生成樹,權值和都是8。

b.

如果最小生成樹T刪去一條邊,就必然要添加另一條邊,否則不能形成一個連通塊

②如果最小生成樹T和次小生成樹有兩條邊不同,即T‘ = T - {(u1, v1)} + {(x1, y1)} - {(u2, v2)} + {(x2, y2)},則可以構造出一棵和最小生成樹只有一條邊不同的生成樹T‘‘ = T - {(u1, v1)} + {(x1, y1)},使得w(T) < w(T‘‘) < w(T‘)。這和T‘是次小生成樹矛盾,所以次小生成樹和最小生成樹只有一條邊不同。

③由①②可知,圖G包含邊(u, v)屬於T和邊(x, y)不屬於T,使得T - {(u, v)} + {(x, y)}是G的一棵次小生成樹。

c. 假設當前已經構造的最小生成樹的子集為A,維護max_edge數組,max_edge[i][j]表示max(i, j)。按照Prim算法,新添加進了一條邊(u, v),就可以利用維護的信息計算出A中任意一個點k到新添加的點v的max(k, v)。G[i][j]表示邊(i, j)的長度。

計算公式為:max(k, v) = max(max(k, u), G[u][v])

d. 算法:假設當前求出的最小生成樹為T,枚舉所有不屬於T的邊(u, v),向T中添加(u, v)。

因為會形成環,所以要刪掉一條邊。因為我們希望得到的生成樹權值最小,所以要 刪掉環中權值最大的邊,也就是max_edge(u, v),然後就會得到新的生成樹T‘。在得到的所有T‘中,權值和最小的就是次小生成樹。

代碼如下(POJ1679):

技術分享
#include <algorithm>
#include <iostream>
#include <cstdlib>
#include 
<cstring> #include <cstdio> using namespace std; const int MAX_N = 500; const int INF = 0x3f3f3f3f; // used[i][j] = 1表示最小生成樹中包含(i, j)這條邊。 int used[MAX_N][MAX_N]; // 題目中輸入的圖 int G[MAX_N][MAX_N]; // max_edge[i][j]表示在最小生成樹中i到j的唯一簡單路徑中權值最大的邊長。 int max_edge[MAX_N][MAX_N]; // 標記i是否使用過 int vis[MAX_N]; // mincost[i]表示i到已構造的最小生成樹子集的最短距離。 int mincost[MAX_N]; // (pre[i], i)為i到已構造的最小生成樹子集的最短邊。 int pre[MAX_N]; // n為頂點數,m為邊數。 int n, m; // ans1為最小生成樹的權值和,ans2為次小生成樹的權值和。 int ans1, ans2; // 初始化 void init() { memset(vis, 0, sizeof(vis)); memset(used, 0, sizeof(used)); memset(mincost, INF, sizeof(mincost)); memset(max_edge, 0, sizeof(max_edge)); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) G[i][j] = (i == j) ? 0 : INF; } // 求最小生成樹的權值和 int MST() { mincost[1] = 0; pre[1] = 1; int ret = 0; for (int cnt = 1; cnt <= n; cnt++) { int minval = INF, k; for (int i = 1; i <= n; i++) { if (!vis[i] && minval > mincost[i]) minval = mincost[k = i]; } // 提前結束循環,說明不存在最小生成樹。 if (minval == INF) return -1; // 標記(pre[k], k)這條邊已經使用過。 used[k][pre[k]] = used[pre[k]][k] = 1; vis[k] = 1; ret += minval; for (int i = 1; i <= n; i++) { // 如果i在已構造的子集中,就利用維護的max_edge信息求出max_edge[i][k]。 if (vis[i]) max_edge[i][k] = max_edge[k][i] = max(max_edge[i][pre[k]], G[pre[k]][k]); else if (G[k][i] != INF && mincost[i] > G[k][i]) { mincost[i] = G[k][i]; pre[i] = k; } } } return ret; } // 求次小生成樹的權值和 int second_MST() { int ret = INF; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { // 如果i, j之間有未使用過的邊,就添加(i, j),但這個時候會形成環, // 所以要刪除環中最長的一條邊,即max_edge[i][j]。 if (i != j && G[i][j] != INF && !used[i][j]) ret = min(ret, ans1 + G[i][j] - max_edge[i][j]); } return ret; } int main() { //freopen("t1.txt", "r", stdin); int T; scanf("%d", &T); while (T--) { scanf("%d%d", &n, &m); init(); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); G[u][v] = G[v][u] = w; } ans1 = MST(); ans2 = second_MST(); // 如果次小生成樹等於最小生成樹,說明最小生成樹不唯一。 if (ans1 == ans2) printf("Not Unique!\n"); else printf("%d\n", ans1); } return 0; }
View Code

23-2 稀疏圖的最小生成樹

分析:

我們將這個算法分成四個過程來分析,分析完了,也就明白了題目是什麽意思,以及orig什麽的到底是啥。我們所用的圖是本章的那個圖,如下,按字母順序用數字1.2.3...編號:

技術分享

第一個過程:算法1~3行

對每個頂點初始化並查集和訪問標誌域;

第二個過程:算法4~9行

1、檢查每一個頂點的訪問標誌域mark,若已被設置則結束,不掃描,繼續下一個頂點,否則轉2;

2、掃描該頂點的鄰接鏈表(按照鄰接點從到小的順序掃描),找到與其鄰接的最小權值邊(設為(u,v))後,轉3;

3、將u,v兩端點合並到同一個集合;將邊(u,v)直接加到MST中,和算法些許不同,見稍後解釋;將兩個端點的訪問標誌都置上,意味著頂點v的鄰接鏈表之後不會被掃描了,結束後轉1.。

關於第3步和算法不同的原因:此時沒有必要對這樣的邊也設置orig屬性,因為它們並不和收縮後的圖的邊對應。該過程結束後,T中呈現如下景象:

技術分享

其中,紅色的頂點是其所在樹的樹根。可以看見,現在的最小生成樹的雛形已經出現,它是一個森林。之後的過程才會對這個森林進行收縮,因此這些邊不需要設置orig屬性。

第三個過程:算法第10行

這個過程就是找出T森林中的各棵樹的樹根,根據之前的並查集來查找。由第三個過程可以得到樹根分別為2,9,7,它們將會是收縮圖G‘中的頂點,在此,我們將這三個頂點重新編號為1,2,3,便於後面的prim算法的運行。

圖我就不畫了。

第四個過程:算法11~22行

這個過程的目的是獲得收縮圖G‘的邊,也就是上述幾個根的聯系,它們通過各自樹中節點的最小權值邊聯系。

1、掃描原圖中的每一條邊(x,y),沒有邊了,轉5;否則,找到它們所屬的樹的根,分別為u,v,轉2;

2、若u和v相同,意味著它們同屬於一棵樹,轉1;否則,轉3;

3、若邊(u,v)不存在於E‘,說明這兩棵樹還沒有建立聯系,那麽自然加入該邊,設置orig記錄邊(u,v)和它實際所引用的原圖的邊(x,y),權值也記錄下來;若存在,則轉4;

4、找到這個orig,將w(x,y)和orig中記錄的權值比較,若較小,則更改orig的引用邊以及權值,因為這兩棵樹之間出現了更小的聯系代價;否則,不變。轉1;

5、根據orig建立G‘的鄰接表,結束。

經過第三和第四個過程,算法進展如下:

1、T中各棵樹已經收縮,每棵樹收縮成一個頂點,由根代表,整個森林成為一棵樹,即G‘;

2、G‘的各頂點由原圖中除加入T中的各邊之外的最小權值邊聯系著,orig記錄了這些聯系。

到了這個過程結束,可以得到下面的圖,左圖是G‘,邊上數字表示該邊的權;右圖是加入orig的記錄的圖,圈中數字是T中各棵樹的樹根,紅色頂點是它們在圖G‘中的新編號,邊上的(x,y)表示這些樹是通過原圖的某邊聯系的,數字就是該邊的權。

技術分享技術分享

預處理過程到這裏就結束了,得到G‘和orig,之後采用prim算法求G‘的MST,然後將它的邊全部加入T中,加入之前要根據orig換成原圖的邊加入,最後得到原圖的最小生成樹T。

該算法的C++實現代碼如下,註釋詳細,小題解答見後面:

技術分享
#include<iostream>
#include<algorithm>
#include<fstream>
#include<vector>
#include<queue>
#include<map>
#include"FibonacciHeap.h"

#define NOPARENT 0
#define MAX    0x7fffffff

using namespace std;
enum color{ WHITE, GRAY, BLACK };

struct edgeNode
{//邊節點
    size_t adjvertex;//該邊的關聯的頂點
    size_t weight;//邊權重
    edgeNode *nextEdge;//下一條邊
    edgeNode(size_t adj, size_t w) :adjvertex(adj), weight(w), nextEdge(nullptr){}
};

struct findRoot:public binary_function<vector<size_t>,size_t,size_t>
{//函數對象類,用於查詢並查集
    size_t operator()(const vector<size_t> &UFS, size_t v)const
    {
        while (v != UFS[v]) v = UFS[v];
        return v;
    }
};

struct edge
{//邊,和edgeNode有別
    size_t u, v;
    size_t weight;
    edge(size_t u_, size_t v_, size_t w) :u(u_), v(v_), weight(w){}
};

struct edgeRef
{//在preMST和MST23_2過程用到
    size_t u, v;//
    size_t x, y;//及其引用邊
    size_t weight;
    size_t u_map, v_map;//u,v的新編號
    edgeRef(size_t u_, size_t v_, size_t x_, size_t y_, 
        size_t w,size_t u_m = 0,size_t v_m = 0) :u(u_), v(v_), x(x_), y(y_), 
        weight(w),u_map(u_m),v_map(v_m){}
};

class AGraph
{//無向圖
private:
    vector<edgeNode*> graph;
    size_t nodenum;
    void transformGraph(vector<edge>&);
    void preMST(AGraph*, AGraph*, vector<edgeRef>&);
public:
    AGraph(size_t n = 0){editGraph(n); }
    void editGraph(size_t n)
    {
        nodenum = n;
        graph.resize(n + 1);
    }
    size_t size()const { return nodenum; }
    void initGraph();//初始化無向圖
    edgeNode* search(size_t, size_t);//查找邊
    void add1Edge(size_t, size_t, size_t);//有向圖中添加邊
    void add2Edges(size_t, size_t, size_t);//無向圖中添加邊
    size_t prim(AGraph*,size_t);
    void mst23_2(AGraph *mst);
    void print();
    void destroy();
    ~AGraph(){ destroy(); }
};

void AGraph::initGraph()
{
    size_t start, end;
    size_t w;
    ifstream infile("F:\\mst.txt");
    while (infile >> start >> end >> w)
        add1Edge(start, end, w);
}

void AGraph::transformGraph(vector<edge> &E)
{
    for (size_t i = 1; i != graph.size(); ++i)
    {//改造edgeNode,變成edge
        edgeNode *curr = graph[i];
        while (curr != nullptr)
        {
            if (i < curr->adjvertex)
            {//頂點u,v之間的邊只存儲一條,(u,v),且u < v。
                edge e(i, curr->adjvertex, curr->weight);
                E.push_back(e);
            }
            curr = curr->nextEdge;
        }
    }
}

edgeNode* AGraph::search(size_t start, size_t end)
{
    edgeNode *curr = graph[start];
    while (curr != nullptr && curr->adjvertex != end)
        curr = curr->nextEdge;
    return curr;
}

void AGraph::add1Edge(size_t start, size_t end, size_t weight)
{
    edgeNode *curr = search(start, end);
    if (curr == nullptr)
    {
        edgeNode *p = new edgeNode(end, weight);
        p->nextEdge = graph[start];
        graph[start] = p;
    }
}

inline void AGraph::add2Edges(size_t start, size_t end, size_t weight)
{
    add1Edge(start, end, weight);
    add1Edge(end, start, weight);
}

size_t AGraph::prim(AGraph *mst, size_t u)
{//普利姆算法求最小生成樹,采用斐波那契堆。返回最小權值和;mst存儲最小生成樹,時間O(E+VlgV)
    vector<size_t> parent(nodenum + 1);
    //存儲每個頂點在斐波那契堆中的對應節點的地址,這樣便於修改距離等
    vector<fibonacci_heap_node<size_t, size_t>*> V(nodenum + 1);
    fibonacci_heap<size_t, size_t> Q;//斐波那契堆,鍵為距離,值為頂點標號
    for (size_t i = 1; i <= nodenum; ++i)
    {
        parent[i] = i;
        if (i == u) V[i] = Q.insert(0, i);//向堆中插入元素,並且將節點句柄存入數組
        else V[i] = Q.insert(MAX, i);
    }
    size_t sum = 0;
    while (!Q.empty())
    {
        pair<size_t, size_t> min = Q.extractMin();
        V[min.second] = nullptr;//置空,標誌著該節點已刪除
        sum += min.first;
        for (edgeNode *curr = graph[min.second]; curr; curr = curr->nextEdge)
        {//以其為中介,更新各點到MST的距離
            if (V[curr->adjvertex] != nullptr && curr->weight < V[curr->adjvertex]->key)
            {
                Q.decreaseKey(V[curr->adjvertex], curr->weight);
                parent[curr->adjvertex] = min.second;
            }
        }//將該邊加入MST
        if (min.second != u) mst->add2Edges(parent[min.second], min.second, min.first);
    }
    return sum;
}

void AGraph::preMST(AGraph *T, AGraph *G, vector<edgeRef> &orig)
{//稀疏圖求MST預處理,T存儲mst,G存儲收縮後的圖,orig存儲收縮後的圖的邊,以及它所引用的原圖的邊
    //和該邊權值,註意該過程結束後mst並未完全求出。
    vector<color> mark(nodenum + 1);//訪問標誌
    vector<size_t> ufs(nodenum + 1);//並查集
    for (size_t i = 1; i <= nodenum; ++i)
    {
        mark[i] = WHITE;
        ufs[i] = i;
    }
    //-------------------------------------------------------
    for (size_t i = 1; i != graph.size(); ++i)
    {//一次掃描每個頂點
        if (mark[i] == WHITE)
        {//若未訪問,
            edgeNode *curr = graph[i];
            size_t u = 0, w = MAX;
            while (curr != nullptr)
            {//則一次訪問其鄰接表,
                if (curr->weight < w)
                {//找到最短的邊
                    u = curr->adjvertex;
                    w = curr->weight;
                }
                curr = curr->nextEdge;
            }
            T->add2Edges(i, u, w);//將其加入到T中成為mst的一條邊
            ufs[i] = u;//並設置並查集
            mark[i] = mark[u] = BLACK;//且標為訪問
        }
    }//該過程結束後,T是森林,存儲了一些mst的邊,森林中樹的根則在ufs中可以查到
    //-------------------------------------------------------------------------
    map<size_t, size_t> V_of_G;//記錄圖G的頂點,即T中森林中各樹的樹根,鍵為樹根編號,值為其在收縮後的圖的編號
    size_t num_of_V = 0;
    for (size_t i = 1; i != ufs.size(); ++i)
    {//掃描ufs
        size_t p = findRoot()(ufs, i);//找尋各頂點的根,
        map<size_t, size_t>::iterator it = V_of_G.find(p);
        if (it == V_of_G.end())//若沒有記錄則加入,並一次編號為1,2,3...便於之後的處理,故用map存儲
            V_of_G.insert(pair<size_t, size_t>(p, ++num_of_V));
    }
    //------------------------------------------------------------------------------
    vector<edge> E;
    transformGraph(E);//該函數在原圖的鄰接表中抽取所有的邊
    for (size_t i = 0; i != E.size(); ++i)
    {//依次訪問這些邊
        size_t u_root = findRoot()(ufs, E[i].u), v_root = findRoot()(ufs, E[i].v),j;//找到改變兩頂點的根
        if (u_root == v_root) continue;//若相等,說明該邊已存在於mst中,則不處理,繼續掃描下一條邊
        for (j = 0; j != orig.size(); ++j)//否則查詢是否以存入orig
            if ((orig[j].u == u_root && orig[j].v == v_root)
                || (orig[j].u == v_root && orig[j].v == u_root)) break;
        if (j == orig.size())
        {//若沒有,則添加,其中(u_root,v_root),是G中的邊,其引用的是E[i]這條邊
            edgeRef er(u_root, v_root, E[i].u, E[i].v, E[i].weight);
            orig.push_back(er);
        }
        else if (E[i].weight < orig[j].weight)
        {//若存在,且新邊比之前的引用邊的權值更小,則更改引用邊信息
            orig[j].x = E[i].u;
            orig[j].y = E[i].v;
            orig[j].weight = E[i].weight;
        }
    }//該過程結束後,orig記錄了T中森林之間的聯系,以及該聯系引用的權值最小的邊
    //------------------------------------------------------------------------
    G->editGraph(num_of_V);//根據頂點數目重新編輯收縮圖G的大小
    for (size_t i = 0; i != orig.size(); ++i)
    {//根據orig,構造出圖G的鄰接表,此時用樹根的相應編號構造圖G,便於後續處理
        map<size_t, size_t>::iterator it1 = V_of_G.find(orig[i].u), it2 = V_of_G.find(orig[i].v);
        orig[i].u_map = it1->second; orig[i].v_map = it2->second;//記下orig中u和v的編號
        G->add2Edges(it1->second, it2->second, orig[i].weight);
    }
}

void AGraph::mst23_2(AGraph *T)
{//稀疏圖求mst
    AGraph G;
    vector<edgeRef> orig;
    preMST(T, &G, orig);//調用預處理過程以求得MST雛形,存儲於T中;收縮後的圖G,以及G中的引用邊orig
    AGraph mst_G(G.size());
    G.prim(&mst_G,1);//對圖G用普利姆算法求出MST
    for (size_t i = 1; i != mst_G.graph.size(); ++i)
    {//依次掃描G的MST的每個頂點
        edgeNode *curr = mst_G.graph[i];
        while (curr != nullptr)
        {//若該頂點有鄰接表
            size_t j;
            //由於圖G的頂點是經過編號的,為1,2,3...,因而要找出它在原圖中的頂點標號
            for (j = 0; j != orig.size(); ++j)
                if (i == orig[j].u_map && curr->adjvertex == orig[j].v_map)
                    //找到後,在T中加入該邊的的引用邊————T中森林是用該引用邊聯系起來的
                    //根據引用邊的求取過程,可以知道每條引用邊是聯系這兩棵樹的最小權值邊
                    T->add2Edges(orig[j].x, orig[j].y, orig[j].weight);
            curr = curr->nextEdge;
        }
    }
}//結束後即構造出稀疏圖的MST

inline void AGraph::print()
{
    for (size_t i = 1; i != graph.size(); ++i)
    {
        edgeNode *curr = graph[i];
        cout << i;
        if (curr == nullptr) cout << " --> null";
        else
            while (curr != nullptr)
            {
                cout << " --<" << curr->weight << ">--> " << curr->adjvertex;
                curr = curr->nextEdge;
            }
        cout << endl;
    }
}

void AGraph::destroy()
{
    for (size_t i = 1; i != graph.size(); ++i)
    {
        edgeNode *curr = graph[i], *pre;
        while (curr != nullptr)
        {
            pre = curr;
            curr = curr->nextEdge;
            delete pre;
        }
        graph[i] = curr;
    }
}

const size_t nodenum = 9;

size_t main()
{
    AGraph graph(nodenum), mst(nodenum);
    graph.initGraph();
    graph.print();
    cout << endl;
    graph.mst23_2(&mst);
    mst.print();
    getchar();
    return 0;
}
View Code

23-3 瓶頸生成樹

解:(a) 使用替換法,假設瓶頸樹T‘的最大邊長為W,而某棵MST的最大邊>W,則將MST從最大邊切斷,變成兩個部分,選擇T‘中連接兩部分的邊,則得到更小的樹。

(b) 運行DFS(或BFS),計算過程中忽略所有大於b的邊。

(c) 二分+收縮法:將邊按大小分成兩半,在較小的那一半上進行BFS,如果得到的是幾個連通分量,那麽將每個連通分量收縮成一個點;如果得到的一棵樹,那麽再將邊減半。

#23-4 其他MST算法

解:參考這裏http://mypathtothe4.blogspot.com/2013/04/alternative-minimum-spanning-trees-234.html

(a) 正確,考慮算法運行的任意階段,對於任意割,如果有多個邊跨越割,那麽先刪除的永遠是最大的那條邊,即輕量邊會一直留下;另,每次刪除的邊一定是某個環裏面的最大邊。Algorithm Design by Kleinberg 定理4.21 有詳細證明。

(b) 錯誤。比如一個三角形,第一次選擇了最長的那條邊。

(c) 正確。

算法導論23章思考題(轉載)