zoukankan      html  css  js  c++  java
  • 图论入门

    图的定义

    一个图含有(n)个点,(m)条边。其中每条边连接两个点。记所有点组成的集合为(V),所有边组成的集合为(E)。如果图上的边有规定方向,即只能从某个点走向另外一个点,那么称整个图是有向图。如果所有的边没有方向的限制,称为无向图。一般的,在储存图的时候,需要指明方向,对于无向图而言,直接当作有两个有向边即可。

    图的储存

    常见的储存方式有两种,一种是邻接矩阵,一种是邻接表。邻接矩阵的储存方式就是对于一条边,如果他是从(u)走向(v)长度为(w),那么就直接记(d[u][v] = w)即以二维数组的方式记录(u)(v)的边的长度。显然如果整个图有(n)个点,那么整个数组的大小就是(n^2)

    在某些情况下,比如点数很多而边数比较少的时候,以邻接矩阵的方式储存整个图会有大量的位置上的值没有意义,即两个点之间并不直接相连的时候,权值是无意义的。那么为了避免大量的空间的浪费,可以使用邻接表的方式储存整张图,邻接表的思想就是记录每个点出发直接相连的点有哪些。在实际的实现中,以C++为例,C++的STL中有一个结构是vector,他的作用是可变长的数组,即可以直接把一个元素插入到这个序列的末尾,那么在最开始可以给所有点开一个vector,假如有某个点是(u->v)那么就向(u)位置上的vector的末尾后面插入一个元素v。代码类似如下:

    vector<int> Edge[N];// N个点,给每个点开一个vector
    //1 通过某个边连向了一个点 2
    Edge[1].push_back(2);// push_back表示将某个元素插入这个vector的末尾
    

    那么,这种储存方式,只能储存对应的点的标号是谁,还不能储存权值,我们不妨把上面vector里的元素类型从单个的int换成一个二元组(pair<int,int>)以{编号,权值}的方式就可以一并存进去了:

    Vector<pii> Edge[N];
    // 1通过一个权值是3的边连向了一个点 2
    Edge[1].push_back({2,3});
    

    最短路问题

    既然我们已经解决了图的储存问题,那么现在可以回到最开始的时候提出的问题,如何求出某些情形下的“最短路”的问题。具体而言,最短路问题可以分成下面两种分别解决。

    单源最短路

    单源最短路问题指的是,固定一个起点,求从这个起点出发,走到图上任何点上的最短的距离是多少。解决这个问题有两种算法,一种是dijkstra,一种是bellman-ford及其优化spfa。

    先来说dijkstra,dijkstra算法的过程是贪心的过程:首先记(dist[i])表示从(1-i)的最短距离是多少,其次记两个点的集合S和T,其中S表示已经算完了(dist[i])的点的集合,T表示不在S集合里的点。为了方便这里记起点的编号是(1),那么显然有(dist[1]=0),其次起点一定一开始就在集合S里了,也就是说一开始S集合就包含一个点(1),T集合里包含点(2-N)

    在最开始的时候,我们只知道(dist[1]=0),且与(1)直接相连的若干个点的编号,和他们之间的相连的边的权值,于是我们就可以推出来这些相连的点的(dist[])的值,显然就是他们相连的边的权值。其次我们可以发现说,在这些直接相连的点中,存在某一个点(u),满足他的(dist[u])小于其他所有点的值,并且这个点(u)(dist[u])一定已经是最小的了,也就是说这个(u)与起点的最短距离已经求好了,(u)可以从T集合中删掉,并加入S集合。之后我们可以如法炮制,第二次拿这个(u)当作第一步时的起点,拿他去更新与他直接相连的点的距离。如此可以抽象一下我们的算法过程:

    (1)找到当前(dist[i])最小的一个点(i),并且(i)不在集合S中。

    (2)拿(i)去更新与他直接相连的(j)(dist[j]),也就是(dist[j] = dist[i] + cost(i,j))其中(cost(i,j))表示(i)(j)的边的权值。

    (3)把(i)从T集合中删掉,并且加入到S集合中。

    我们的这个算法如果要保证正确性,那么每次第一步取出来的点(i)它的(dist[i])必须要保证是最小的,也就是说当前的取出来的这个点(i)其实已经可以放到集合S里去了才可以。那么我们上面最开始介绍这个过程的时候,显然第一步,只有起点的时候是满足的,其次第二步找到的点也是满足这个性质的,那么如果说整个过程都是正确的,其实也就是要归纳证明一下每一步取出来的点都是可以直接放到S集合中去的。那么显然归纳的第一步是正确的,第二步就是假设取出来的前(k)个点都满足他们的(dist[i])一定是最小的,要能推出来下一个取出的(k+1)的从起点走到他的距离也得满足是最小的,那么通过上面的算法过程我们可以知道,假如说第(k+1)次第一步取出来的点是(v)的话,那么显然(dist[v])一定是某个属于前(k)步取出来的某个点更新过来的,就是说有某个点(u)他是前(k)步被拿出来的,并且做了一步(dist[v] = dist[u] + cost(u,v))的。假如说这个(dist[v])并不是最小的,也就是违反了我们的规律的话,那么他一定是经过了某个不属于前(k)步取出来的点(g)算出来的,也就是有某个不属于前(k)步的点(g),他有(dist[v] = dist[u] + cost(u,v) > dist[g] + cost(g,v))也就是说从(u)走到(v)的距离比从(g)走到(v)的距离要严格大,但是根据之前所说的定义,既然(g)不属于前(k)步取出来的点,那么就一定有(dist[u] < dist[g])否则(g)应该属于前(k)个取出来的点之中,很显然不可能有某个(cost(g,v))满足加上(dist[g])之后还能比(dist[u])要小,除非他是负数,只要整个图上权都是正数,那么这个条件就一定是不满足的,也就是产生了矛盾,我们通过上面的算法过程取出来的每一个点,都一定有(dist[v])是最小的。

    那么既然第一步不会选择已经选择过的点,只要每个点都被取出过一次,也就说明所有点的(dist[i])都已经达到了最小,自然整个算法过程也就结束了,下面给个不一定正确的代码说明:

    // dist[i]表示从1走到i的最短距离,假如没有这样的路径,则记为正无穷
    // st[i]表示i是否属于S集合,也就是这个点有没有被第一步拿出来
    void dijkstra()
    {
        dist[1] = 0;
        for(int i = 2;i <= n;++i)	dist[i] = INF;//除了起点1之外的所有点初始的距离都是正无穷
        for(int i = 1;i <= n;++i)
        {
            int t = -1;
            for(int j = 1;j <= n;++j)
                if(!st[j] && t == -1 || dist[t] > dist[j])
                    t = j;
            if(t == -1)	break;//假如说已经找不到这样的点t了就直接退出。
            // 找到最小的dist对应的点t
            for(auto& _ : Edge[t])
            {
                // 遍历从t走出去的所有的点,以及对应的边的权值
                int v = _.first,w = _.second;// v表示对应的点的编号,w表示边的权值
                if(dist[v] > dist[t] + w)
                {
                    dist[v] = dist[t] + w;
                }
            }
            st[t] = 1;// 标记上t,即把t加入到S集合中
        }
        //执行完毕后,dist[i]就表示从1走到i的最短距离
    }
    
    

    关于dijkstra算法的正确性,必须要保证每个点第一次取出来的时候他的(dist[i])是最小的,这个正确性基于说不存在一个点(g)到这个点的距离有负数,因为在正数的时候,必然构成一个三角形两边之和大于第三边,所以就保证了不存在这样的路径更短,但是假如说存在负权边,情况就不是这么简单的了,因为负权边意味着我走负权边的时候距离会减小,这样就不满足三角形的说法了。这个时候dijkstra算法的正确性就被破坏掉了,必须选择其他的算法,常用的是bellman-ford算法,他基于三角不等式以及松弛。具体来说,如果某个从(u->v)权值是(w)的边有(dist[v] > dist[u] + w)那么执行(dist[v] = dist[u] + w)的话可以让(dist[v])变得更小并且显然正确。那么这里就有一个猜想:假如说整个图上的最短路都已经求好了,是否说对于所有的边都有(dist[u] leq dist[v] + w),并且反过来,如果所有边都有(dist[u] leq dist[v] + w)是否就说明所有的(dist[i])都已经求好了呢,也就是问所有的边都满足三角不等式的这个条件是否是任意的(i)对应的(dist[i])都已经达到最小的充要条件。

    现在来简单说明一下:假如说所有的(dist[i])都已经求好了,显然不可能有某个边对应的(dist[v] > dist[u] + w),因为你一定可以把这条边松弛,也就是拿右边的值赋给(dist[v])就能让(dist[v])变得更小了。反过来如果所有的边都已经满足三角不等式了,是否对于任意的(dist[i])就没有变小的机会了呢?这里可以从一个宏观的角度上猜测,既然所有的边都已经满足三角不等式了,也就是说所有的(dist[i])都不存在一个机会能变得更小了,那么也就是说对于所有的(dist[i])都不可能变小了。接下来说明一下他的算法流程并附加的说明后面这个反推的过程:

    首先还是跟dijkstra一样,我们一开始只知道(dist[1]=0),所有其他点都是正无穷。那么我们尝试给图上所有的边进行松弛,也就是遍历一下图上的所有边,去看一下是否有(dist[v] > dist[u] + cost(u,v)),如果有就拿右边的去覆盖左边的。但是我们可以发现说第一步尝试对所有边松弛的时候,除了与起点直接相连的点,其他的点由于(dist[i]=)正无穷,根本就不会产生松弛,也就是说只有与起点直接相连的点会产生松弛,也就是更新他们的(dist[i])

    这里我们可以先抛出一个结论:第一步尝试对所有边进行松弛之后,只有与起点直接相连的点(dist)会变小,也就是从正无穷变成一个正常的数值,也就是说现在求的的(dist[i])是与起点相连,并且只经过一条边的能得到的距离。

    那么继续,在得到了直接相邻的点的(dist)之后,我们再次尝试一下对所有的边进行松弛,这个时候我们就可以尝试更新不是与起点直接相连的点的距离,而是通过了一个直接相连的点连接之后的点的距离了,其实也就是求:与起点直接相连,并且最多只经过两条边得到的最短的距离。

    往后我们可以推出一个结论:假设我们把这个遍历所有边尝试松弛的操作执行(k)轮,那么此时得到的(dist[i])就表示,从起点出发,最多经过(k)条边的前提下,走到(i)的最短的距离是多少。那么最多需要执行多少轮呢?如果一个图有(n)个点,那么起点最多需要走(n-1)条边走到最远的那个点,这个时候整个图构成一个链状,也就是说我们的尝试对所有边松弛的操作最多也只需要执行(n-1)次就可以求出所需的(dist[])了。当然,如果某次过程中发现已经没有任何一条边可以拿来松弛了,算法可以提前结束。

    但是,假如说这个图上存在一个负环,也就是有一个环路,整个环路的权值之和是负数,这种情况极其特殊,在这种情形下,不存在最短路,因为任何路径,只要能走到一个环路上,就可以通过这个环路不停地把(dist[i])缩小,一直转圈使距离变小。那么在这种情况下,不存在一个时刻,图上所有的边都满足三角不等式(因为还可以缩小),假如bellman-ford算法在执行了(n-1)轮之后,仍然没有使所有的边都满足三角形不等式,那么这个时候也就说明存在一个负环。

    以下也许是可以拿来参考的代码:

    int dist[N];
    struct __edge
    {
        int u,v,w;
    }edge[N];
    int n,m,k;
    int BF()
    {
        memset(dist,0x3f,sizeof dist);
        dist[1] = 0;
        while(k--)
        {
            for(int i = 0;i < m;++i)
            {
                int u = edge[i].u,v = edge[i].v,w = edge[i].w;
                dist[v] = min(dist[v],dist[u]+w);
            }
        }
        if(dist[n] > 0x3f3f3f3f/2)  return -1;// 这里表示起点走不到终点,因为终点可能和某个负权边相连导致dist[n]缩小
        // 进而就有dist[n] != 0x3f3f3f3f,于是这里就判断是不是比正无穷的一半大,如果大那么就认为是正无穷
        // 只不过是被负权边松弛挖掉了一点而已。
        return dist[n];
    }
    
  • 相关阅读:
    UVA 254 Towers of Hanoi
    UVA 701 The Archeologists' Dilemma
    UVA 185 Roman Numerals
    UVA 10994 Simple Addition
    UVA 10570 Meeting with Aliens
    UVA 306 Cipher
    UVA 10160 Servicing Stations
    UVA 317 Hexagon
    UVA 10123 No Tipping
    UVA 696 How Many Knights
  • 原文地址:https://www.cnblogs.com/HotPants/p/13996832.html
Copyright © 2011-2022 走看看