算法导论23章思考题(转载)

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 瓶颈生成树

无向图的最小生成树一定是瓶颈生成树,但瓶颈生成树不一定是最小生成树。(最小瓶颈生成树==最小生成树)

命题:无向图的最小生成树一定是瓶颈生成树。

证明:可以采用反证法予以证明。
假设最小生成树不是瓶颈树,设最小生成树T的最大权边为e,则存在一棵瓶颈树Tb,其所有的边的权值小于w(e)。删除T中的e,形成两棵数T', T'',用Tb中连接T', T''的边连接这两棵树,得到新的生成树,其权值小于T,与T是最小生成树矛盾。[1-2] 

命题:瓶颈生成树不一定是最小生成树。

下面是一个反例:
由红色边组成的生成树是瓶颈树,但并非最小生成树。

解:(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) 正确。

posted @ 2017-09-26 16:59  miaoheping  阅读(2093)  评论(1编辑  收藏  举报