zoukankan      html  css  js  c++  java
  • 算法导论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!
    ");
            else
                printf("%d
    ", 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) 正确。

  • 相关阅读:
    lnmp环境搭建
    Git常用命令
    博客园写随笔环境搭建
    Win常用软件
    Docker环境搭建
    ESP-8266 RTOS 环境搭建
    查看Linux信息
    博客园markdown语法
    Java后台技术(TDDL)
    Java后台技术(Dubbo入门)
  • 原文地址:https://www.cnblogs.com/mhpp/p/7597676.html
Copyright © 2011-2022 走看看