图论之最短路径(C++) -- 拉帮结派搞关系

图论之最短路径

  • 图论之最短路径(C++) --拉帮结派搞关系
    • 带权图的边
    • Dijkstra算法 -- 就近优先,趋短避长
    • Bellman-Ford算法 -- 全体一起,相互利用

图论之最短路径(C++) --拉帮结派搞关系

  这时一个能让人悟出人生道理的算法,为什么这么说?0?,让我们试想一下,有这么一群相互认识的人,这群人都想吃烤串,但是只有其中一个人会烤串,而且这个人一次只能为一个人烤串,先为谁烤,只取决于谁和他关系好(大佬总是这么会对金钱烦恼的,所以不考虑金钱),这时这群人中的另外两个人,小明和这个烤串大佬是兄弟关系,小红和这个大佬只见过一面或者压根不认识,但和小明是同学关系,这时对于这两人来说,小明想快点吃到烤串肯定是直接取找烤串大佬来得快,但是小红直接找烤串大佬可能会被无视,或者等到最后才有得吃,较快的方式当然是通过托小明的关系取找烤串大佬,能更快地吃到烤串,至此,就悟出了一个人生道理,如果没有兄弟会烤串,可以尝试从同学下手ˋ▽ˊ ;所以最短路径就是以一点为中心,找点这个中心点到图中别的点的各个最短距离,这个过程可以借助中间点到达,如果经过中间点的距离要小于直接到达的距离~~
  最短路径的基本玩法有两个,一个是Dijkstra算法,另一个是Bellman Ford算法;

  • Dijkstra算法:从直接与源点相连的顶点开始,不断找到离得最近的顶点,找到后更新经过该顶点可以到达源点的顶点距离,如果已有路径到达,进行路径长度比较,取较小的路径,以此类推,获取到所有顶点后就得到了各顶点到源点的最短距离;
  • Bellman Ford算法:图中两个顶点的路径最多经过vNum-1条边,所以可以最多vNum-2次的松弛操作,所谓松弛操作也就是使两个顶点间的连接边增加一条,也就是多经过一个中间节点,从矩阵上来看就是其连接矩阵(行和列分别列出各个顶点,如果有相连的边则在对应顶点的垂直边和平行边的交点位置填上边的权值)的多次乘积,只是连接矩阵探究的是两点间是否连通,连通的路径有多少条,这里用的是同一个思路,探究的是经过更多的边,也就是松弛操作是否能使路径更短;
  • 带权图的边类:边可能有权值,设置一个边类,即可设定是否有向,也可以存储权值;
    • 边的遍历是在图中有用到自定义的图的边的遍历器,其实可以在图中加一个方法返回边来简化,通过设置一个遍历器,优化了空间复杂度,相关图和遍历器的设置在图遍历传送门;

带权图的边

// 有权图的边类
template

class Edge{
private:
    int vA, vB;    // 边的两个顶点
    Weight w;  // 边的权值

public:
    // 构造函数
    Edge(int vA, int vB, Weight w){
        this->vA = vA;
        this->vB = vB;
        this->w = w;
    }
    // 空的构造函数, 所有的成员变量都取默认值
    Edge(){}

    ~Edge(){}

    int getVA(){ return vA;} // 返回第一个顶点

    int getVB(){ return vB;} // 返回第二个顶点

    Weight getWeight(){ return w;}    // 返回权值

    // 给定一个顶点, 返回另一个顶点
    int linkTo(int x){
        assert( x == vA || x == vB );

        return x == vA ? vB : vA;
    }

    // 输出边的信息
    friend ostream& operator<<(ostream &os, const Edge &e){
        os << e.vA << "-" << e.vB << ": " << e.w;
        return os;
    }

    // 比较符号重载
    // 边的大小比较, 是对边的权值的大小比较
    bool operator<(Edge& e){
        return w < e.getWeight();
    }
    bool operator<=(Edge& e){
        return w <= e.getWeight();
    }
    bool operator>(Edge& e){
        return w > e.getWeight();
    }
    bool operator>=(Edge& e){
        return w >= e.getWeight();
    }
    bool operator==(Edge& e){
        return w == e.getWeight();
    }

};

Dijkstra算法 – 就近优先,趋短避长

  Dijkstra算法用一个数组记录源点到各个顶点的距离,一个距离记录该顶点是否已找到到源点最短路径,一开始初始化为空或者无穷大,然后从直接与源点相连的顶点开始,更新这些顶点到源点的距离,最短边连接的顶点就是第一个找到离源点最近的顶点,然后查看该顶点所连接顶点的边,对比下是否可能通过这个顶点使之到源点的距离更近,是则更新距离,不是则不进行操作,更新好距离后查看所有未找到到源点最短距离的顶点当前到源点最短距离的顶点,以此类推,整体来说就是从局部到整体~~

// Dijkstra算法
template
class Dijkstra {

private:

    // 记录到源点距离的数组
    Weight* shortestWeight;
    // 记录顶点是否已访问
    bool* mark;
    // 记录到达顶点的边,从源点到达各顶点的最短路径有确定的边,把这个边记录下来可以用于还原路径
    vector*> pathEdgeRecord;
    int source;
    int size;

	// 获取当前到源点最近的顶点,因为用的是Weight()作为初始化,而不是无穷大(可取最大值),所以判断中会多一部判断是否存在权值
    int getMinDisV() {
        int minV = -1;
        Weight minW = Weight();
        for(int i=0; i(s,s,Weight());

        while(1) {
            typename Graph::EdgeIterator adj(g,tail);
            // 遍历当前找到离源点最近顶点的边进行可能的以其为中间点缩短连接顶点到源点路径的操作
            for(Edge* e = adj.begin(); !adj.end(); e = adj.next()) {
                int to = e->linkTo(tail);
                // 还没找到到源点最短距离的顶点才需进行路径更新
                if(!mark[to]) {
                    // 如果未记录边或途经这个中间点的距离会更近则更新
                    if(pathEdgeRecord[to] == NULL || shortestWeight[tail] + e->getWeight() < shortestWeight[to]) {
                        shortestWeight[to] = shortestWeight[tail] + e->getWeight();
                        pathEdgeRecord[to] = e;
                    }
                }
            }

            tail = getMinDisV();
            mark[tail] = true;
            if(tail == -1)
                break;
        }
    }

    ~Dijkstra() {
        delete[] shortestWeight;
        delete[] mark;
        delete pathEdgeRecord[source];
    }

    // 获取指定顶点到源点的最小权值
    Weight getShortestPathWeight(int v) {

        return shortestWeight[v];
    }

    // 指定顶点到源点是否连通,用上面获取权值是否为0来判断也可以
    bool hasPath(int v) {

        return mark[v];
    }

    // 打印指定顶点到源点的路径
    void showPath(int v) {
        int from = v;
        vector link;
        link.clear();
        link.push_back(v);

        // 从指定顶点开始获取边来寻迹到源点,记录下所有经过的元素
        while(1) {
            if(from == source)
                break;
            if(pathEdgeRecord[from] != NULL) {
                int to;
                to = pathEdgeRecord[from]->linkTo(from);
                link.push_back(to);
                from = to;
            }
        }
        // 打印路径,从源点到指定顶点需要反过来打印
        for(vector::reverse_iterator it = link.rbegin(); it != link.rend(); it++) {

            cout << *it << " -> ";
        }
    }

};

Bellman-Ford算法 – 全体一起,相互利用

  Bellman-Ford算法上来就考虑所有顶点到源点的距离,然后进行尽可能多次的松弛操作,也就是依次经过1,2,…,n-2个中间顶点,每多经过一个顶点就是多一次松弛操作,每一次松弛时,保留原权值还是更新权值取决于路径是否更短,所以基本操作思路与Dijkstra算法是相似的,只是Bellman-Ford算法从全局(所有顶点)来找~~

// Bellman-Ford算法
template
class BellmanFord {

private:
    // 图
    Graph &g;
    // 记录到源点距离的数组
    Weight* shortestWeight;
    // 记录顶点是否已访问
    bool* mark;
    // 记录到达顶点的边,从源点到达各顶点的最短路径有确定的边,把这个边记录下来可以用于还原路径
    vector*> pathEdgeRecord;
    int source;
    // 这是判断是否有负权环,不是是否有负权边,负权边很好求,遍历边看下权值是否为负即可
    bool hasNegativeCycle;

    // 这个方法在找到最短路径后调用,这时遍历各个顶点,然后进行一次松弛操作,
    // 如果还会出现路径变短,说明有负权环
    bool findNegativeCycle() {
        for(int i=0; i < g.getVNum(); i++) {
            typename Graph::EdgeIterator adj(g,i);
            for(Edge* e = adj.begin(); !adj.end(); adj.next()){
                // 这里无需判断VB是否有到源点的路径,因为如果没有,那么没有边,则无从谈负权环
                if(pathEdgeRecord[e->getVA()] && shortestWeight[e->getVA()] +
                                                 e->getWeight() < shortestWeight[e->getVB()]) {
                    return true;
                }
            }
        }

        return false;
    }

public:
    // 构造函数
    BellmanFord(Graph &graph, int s):g(graph) {
        source = s;
        shortestWeight = new Weight[g.getVNum()];
        // 初始化,所有节点到源点s的最短路径所存的边为NULL
        for(int i=0; i < g.getVNum(); i++) {
            pathEdgeRecord.push_back(NULL);
        }

        // 源点到源点的距离为0,源点记录到自己的边为空,这个边是new出来的,在析构函数中要释放空间,
        // 这里为源点放置一条距离为0的边节省了代码,不用将与源点直接相连的边先放入相应的定点中,
        // 直接在下面的循环中进行路径压缩操作就行了
        shortestWeight[s] = Weight();
        pathEdgeRecord[s] = new Edge(s,s,Weight());

        // 循环,进行最多n-2次距离缩短,基础是两个顶点直接相连,所以进行n-1次路径寻找操作
        for(int go=1; go < g.getVNum(); go++) {
            // 每一次距离缩短时,尝试以每一个顶点作为中间点进行缩短
            for(int i=0; i < g.getVNum(); i++) {
                // 获取当前顶点边的遍历器
                typename Graph::EdgeIterator adj(g,i);
                for(Edge* e = adj.begin(); !adj.end(); adj.next()) {
                    // 如果当前顶点有路径到源点,则判断当前边的另一顶点是否有路径到源点,
                    // 如果没有则更新这个顶点的路径,如果有,但路径距离要大于经过当前顶点过来,
                    // 则更新这个顶点的路径
                    if(pathEdgeRecord[e->getVA()] && (!pathEdgeRecord[e->getVB()] ||
                                                      shortestWeight[e->getVA()] + e->getWeight() < shortestWeight[e->getVB()])) {
                        pathEdgeRecord[e->getVB()] = e;
                        shortestWeight[e->getVB()] = shortestWeight[e->getVA()] + e->getWeight();
                    }
                }
            }
        }

        // 获取是否有负权环
        hasNegativeCycle = findNegativeCycle();
    }

    ~BellmanFord() {

        delete[] shortestWeight;
        delete pathEdgeRecord[source];
    }

    // 获取源点是否有到顶点的路径
    bool hasPath(int to) {

        return shortestWeight[to] != Weight();
    }

    // 获取顶点到源点的权值
    Weight getShortestPathWeight(int to) {

        return shortestWeight[to];
    }

    // 是否有负权环
    bool ifNegativeCircle() {

        return hasNegativeCycle;
    }

    // 打印指定顶点到源点的路径
    void showPath(int v) {
        int from = v;
        vector link;
        link.clear();
        link.push_back(v);

        // 从指定顶点开始获取边来寻迹到源点,记录下所有经过的元素
        while(1) {
            if(from == source)
                break;
            if(pathEdgeRecord[from] != NULL) {
                int to;
                to = pathEdgeRecord[from]->linkTo(from);
                link.push_back(to);
                from = to;
            }
        }
        // 打印路径,从源点到指定顶点需要反过来打印
        for(vector::reverse_iterator it = link.rbegin(); it != link.rend(); it++) {

            cout << *it << " -> ";
        }
    }

};

例行总结:
  两个算法看下来,很明显的Bellman-Ford算法更费劲,更费劲自然体现出来的就是时间复杂度的上升,Dijkstra算法的时间复杂度是O(ElogV),而Bellman-Ford算法的时间复杂度则达到了O(VE),logV和V的增长明显不是一个数量级的,所以从时间复杂度上来看Dijkstra算法完胜,这就显得Bellman-Ford算法好像没有存在的意义,然而当图中有负权边时,Bellman-Ford算法是唯一选择;Dijkstra算法之所以不能处理负权边是因为该算法基于一个前提是,权值与边的数量是正相关的,也就是经过的边递增,权值必定递增,路径也就加长,但是当有负权边时,这个正相关被打破了,如一开始找到了一个离源点最近的顶点A,这时按Dijkstra算法规则,这个顶点到原点的距离就必定是最短的了,但是如果这时有另一个顶点B与源点相连(距离大于A到源点的距离),这时如果顶点B有直接到顶点A的路径,且权值为负,如果这时源点到顶点B,再从顶点B到顶点A的路径权值小于源点直接到顶点A的路径权值也不会进行更新了,所以Dijkstra算法不能处理负权边,而Bellman-Ford算法因为一开始就是对所有顶点一起进行最短路径更新,每一次的松弛操作都会考量是否能多绕一个顶点使得整体路径权值更短,所以处理负权边得心应手,不仅如此Bellman-Ford算法还可以判断出图中是否存在负权环,有负权环最短路径还需要用算法取算吗?环里越转越短,标准答案负无穷~~

你可能感兴趣的:(数据结构与算法游乐场(C++),数据结构,算法,java,图论)