Graph-一般算法

和图相关的算法有:最小生成子树,最短路径,拓扑排序。

这里仅介绍最小生成树和最短路径,拓扑排序暂时省略。

最小生成子树(minimum spanning tree)

最小生成子树:构造一颗树,这棵树连接所有点,这些边权重纸盒最小。

比如一个图是这样:

msp1

它的最小生成树是这样:

msp2

构造最小生成树有两种方法:Prim算法和kruskal算法。

Prim算法:

  1. 输入:一个加权连通图,其中顶点集合为V,边集合为E;
  2. 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
  3. 重复下列操作,直到Vnew = V:
    1. 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
    2. 将v加入集合Vnew中,将<u, v>边加入集合Enew中;
  4. 输出:使用集合Vnew和Enew来描述所得到的最小生成树。

实现:


class GraphUtil {
public:
    class MinimumSpanningTree {
    public:
        static int Prim(const Graph &g, int start, std::vector<std::pair<int, int>> &paths);
        static void test(Graph &g); // test function
        
        struct PrimHelperEdge {
            int adjVertex = -1;   // the vertex that makes minimum weight with this node
            int cost2Added = 0; // minimum weight from this to the vertexes in added set. if this belongs to added set, cost2Added = 0;
        };
    protected:
        static int Prim_nextMinEdge(std::vector<PrimHelperEdge> &closest);
    };
    
    static const int MAX_INT;   // = std::numeric_limits<int>::max() / 2;

};

// .cpp 

const int GraphUtil::MAX_INT = std::numeric_limits<int>::max() / 2;

int GraphUtil::MinimumSpanningTree::Prim(const Graph &g, int start, std::vector<std::pair<int, int>> &paths) {
    const int N = g.GetNumberOfNodes();
    if (N <= 1) {
        return 0;
    }

    vector<PrimHelperEdge> closestEdges(N);

    int max_int = MAX_INT;
    for (int i = 0; i < N; i++) {
        closestEdges[i].adjVertex = start;
        int w = g.GetWeightForEdge(start, i);
        if (w) {
            closestEdges[i].cost2Added = w;
        } else {
            closestEdges[i].cost2Added = max_int;
        }
    }

    closestEdges[start].cost2Added = 0;

    int totWeight = 0;

    for (int i = 0; i < N - 1; i++) {
        int k = Prim_nextMinEdge(closestEdges);

        int x = closestEdges[k].adjVertex;
        paths.emplace_back(x, k);
        totWeight += closestEdges[k].cost2Added;

        closestEdges[k].cost2Added = 0;     // add k to the added set


        auto it = g.CreateSpIteratorForNode(k); // edges connected with k
        while (it->HasNext()) {         // update u, that u connects with k and u doesn't belong to added set
            auto e = it->Next();
            int y = e.y;
            if (e.weight > 0 && e.weight < closestEdges[y].cost2Added) {
                closestEdges[y].cost2Added = e.weight;
                closestEdges[y].adjVertex = k;
            }
        }
    }

    return totWeight;
}

int GraphUtil::MinimumSpanningTree::Prim_nextMinEdge(vector<GraphUtil::MinimumSpanningTree::PrimHelperEdge> &closest) {
    int mn = MAX_INT;
    int k = -1;
    for (int i = 0; i < closest.size(); i++) {
        if (closest[i].cost2Added > 0 && closest[i].cost2Added < mn) {
            mn = closest[i].cost2Added;
            k = i;
        }
    }
    return k;
}

void GraphUtil::MinimumSpanningTree::test(Graph &g) {
    vector<pair<int, int>> paths;
    int tot = MinimumSpanningTree::Prim(g, 1, paths);

    auto print = [](int sum, vector<pair<int, int>> &p) {
        cout << "min wieght: " << sum << endl;
        for (auto &e: p) {
            cout << "(" << e.first << ", " << e.second << ") ";
        }
        cout << endl;
    };
    print(tot, paths);

    paths.clear();
    tot = MinimumSpanningTree::Kruskal_ShortestPath(g, paths);
    print(tot, paths);
}

其中paths[i] 为一个pair<int, int>,paths中的一个元素p表示, p.first到p.second这条边是在最小生成子树中。

时间复杂度为O(n2)

kruskal算法

  1. 先选取权重最小的边;
  2. 选取一条权重最小的边(没有被选过的),如果这条边不会构成一个环路,则把这条边加入已经选好的集合中;否则舍弃这条边。选取下一条权重最小的边。
  3. 直至所有的点都被连通且无环路。

实现:


class MinimumSpanningTree {
public:
    static int Prim(const Graph &g, int start, std::vector<std::pair<int, int>> &paths);

    

    // helper function to determine if two nodes belong to same subgraph
    static void MergeUnion(std::vector<int> &unionVec, int root, int root2);

    // help function
     static int FindRoot(const std::vector<int> &unionVec, int x);
    
    struct KruskalCmp {
        bool operator()(const Graph::Edge &e1, const Graph::Edge &e2) {
            return e1.weight >= e2.weight;
        }
    };
};
    
// .cpp
int GraphUtil::MinimumSpanningTree::Kruskal_ShortestPath(const Graph &g, std::vector<std::pair<int, int>> &paths) {

    const int N = g.GetNumberOfNodes();     // construct heap
    if (N <= 1) {
        return 0;
    }

    priority_queue<Graph::Edge, vector<Graph::Edge>, KruskalCmp> q;
    for (int i = 0; i < N; i++) {
        auto p = g.CreateSpIteratorForNode(i);
        while (p->HasNext()) {
            auto e = p->Next();
            if (e.x < e.y) {
                q.push(e);
            }
        }
    }

    vector<int> unionVec(N);
    for (int i = 0; i < N; i++) {
        unionVec[i] = -1;
    }

    int tot = 0;
    while (!q.empty()) {
        auto e = q.top();
        q.pop();
        auto r1 = FindRoot(unionVec, e.x);
        auto r2 = FindRoot(unionVec, e.y);
        if (r1 != r2) {     // won't make a cycle
            tot += e.weight;
            MergeUnion(unionVec, r1, r2);
            paths.emplace_back(e.x, e.y);
        }
    }
    return tot;
}

int GraphUtil::MinimumSpanningTree::FindRoot(const std::vector<int> &unionVec, int x) {
    while (unionVec[x] >= 0) {
        x = unionVec[x];
    }
    return x;
}

void GraphUtil::MinimumSpanningTree::MergeUnion(std::vector<int> &unionVec, int root, int root2) {
    unionVec[root2] = root;
}

时间复杂度为O(eloge). loge来自从优先队列中选取一条边。

注意:里面的MergeUnionFindRoot可以用来判断一个两个数是否属于一个集合,以及合并两个集合。

最短路径

最短路径也有两种算法:Dijkstra算法和Floyd算法。

Dijkstra算法

  1. 初始集合V2,仅包括起始点
  2. 从V中选取距离集合V2最近的点(距离V2最近的点:意思是距离V2中某一点最近)
  3. 直至选到终点
class GraphUtil {
public:
    class ShortestPath {
    public:
        static void test(const Graph &g, int start, int end);

        static int Dijkstra(const Graph &g, int start, int end, std::vector<int> &paths);

    protected:
        static int DjNextMin(const std::vector<std::pair<int, int>> &d, const std::vector<int> &found);

        static void BuildDjPath(const std::vector<int> &d, int start, int end, std::vector<int> &paths);
    };
};

// .cpp


int GraphUtil::ShortestPath::Dijkstra(const Graph &g, int start, int end, vector<int> &paths) {
    const int N = g.GetNumberOfNodes();
    if (N <= 1) {
        return 0;
    }

    // first is distance, second is the vertex that connects to this node in dj graph
    pair<int, int> initValue(MAX_INT, -1); 

    vector<pair<int, int>> d(N, initValue);  // all initialized to a big integer
    vector<int> found(N, 0);
    vector<int> djTmpPath(N, -1);

    found[start] = 1;

    {
        auto it = g.CreateSpIteratorForNode(start);
        while (it->HasNext()) {
            auto e = it->Next();
            d[e.y].first = e.weight;
            d[e.y].second = start;
        }
    }

    for (int i = 0; i < N; i++) {
        int k = DjNextMin(d, found);
        if (k == -1) {  // no nodes anymore
            break;
        }

        found[k] = 1;
        djTmpPath[k] = d[k].second;

        auto it2 = g.CreateSpIteratorForNode(k);
        while (it2->HasNext()) {        // update it's adjacent nodes
            auto e = it2->Next();
            if (d[e.y].first > (d[k].first + e.weight)) {
                d[e.y].first = d[k].first + e.weight;   // start -> e.y will pass node k. It's weight is d[k].first + e.weight
                d[e.y].second = k;
            }
        }
    }
    if (d[end].first != MAX_INT) {
        BuildDjPath(djTmpPath, start, end, paths);  // prevent infinte loop
    }
    return d[end].first;
}

// find the next vertex that has min length to the found set
int GraphUtil::ShortestPath::DjNextMin(const std::vector<std::pair<int, int>> &d, const std::vector<int> &found) {
    int min_value = MAX_INT;
    int k = -1;
    for (int i = 0; i < d.size(); i++) {
        if (!found[i] && d[i].first < min_value) {
            min_value = d[i].first;
            k = i;
        }
    }
    return k;
}

void GraphUtil::ShortestPath::BuildDjPath(const std::vector<int> &d, int start, int end,
                                          std::vector<int> &paths) {
    paths.clear();
    int i = end;
    while (i != -1) {
        paths.push_back(i);
        i = d[i];
    }
    std::reverse(paths.begin(), paths.end());
}

void GraphUtil::ShortestPath::test(const Graph &g, int start, int end) {
    vector<int> paths;
    cout << "dj shortest path: " << Dijkstra(g, start, end, paths) << endl;
    for (auto i: paths) {
        cout << i << " ";
    }
    cout << endl;
    vector<vector<int>> pathMat, mat;
    Floyd_ShortestPath(g, mat, pathMat);
    paths.clear();
    cout << "floyd shorted path: " << mat[start][end] << endl;
    Floyd_GetPath(pathMat, start, end, paths);
    for (auto i: paths) {
        cout << i << " ";
    }
}

Floyd算法

算法的核心思想是:

点i到j的最短距离,要么经过k,要么不经过k。

mat[i][j]表示i到k的最短距离: mat[i][j] = min {mat[i][k] + mat[k][j]}, 其中顶点i属于V。

class ShortestPath {
public:
    using FloyMat = std::vector<std::vector<int>>;

    static void Floyd_ShortestPath(const Graph &g, GraphUtil::ShortestPath::FloyMat &mat,
                                   GraphUtil::ShortestPath::FloyMat &path);

    static void Floyd_GetPath(const FloyMat &pathMat, int start, int end, std::vector<int> &paths);


};

// .cpp


void GraphUtil::ShortestPath::Floyd_ShortestPath(const Graph &g, GraphUtil::ShortestPath::FloyMat &mat,
                                                 GraphUtil::ShortestPath::FloyMat &path) {
    if (g.GetNumberOfNodes() < 1) {
        return;
    }

    const int N = g.GetNumberOfNodes();
    mat.resize(N, vector<int>(N, MAX_INT));
    path.resize(N, vector<int>(N, -1)); // path[i][j] = k, means i -> j will pass k

    for (int i = 0; i < N; i++) {
        auto it = g.CreateSpIteratorForNode(i);
        while (it->HasNext()) {
            auto e = it->Next();
            mat[i][e.y] = e.weight;
            path[i][e.y] = e.y;
        }
    }

    for (int j = 0; j < N; j++) {
        for (int k = 0; k < N; k++) {
            for (int i = 0; i < N; i++) {
                if ((mat[j][i] + mat[i][k]) < mat[j][k]) {
                    mat[j][k] = mat[j][i] + mat[i][k];
                    path[j][k] = i;
                }
            }
        }
    }
}

void GraphUtil::ShortestPath::Floyd_GetPath(const GraphUtil::ShortestPath::FloyMat &pathMat, int start, int end,
                                            vector<int> &paths) {
    paths.clear();
    int i = end;
    while (i != -1) {
        paths.push_back(i);
        if (i == pathMat[start][i]) {
            break;
        }
        i = pathMat[start][i];
    }
    paths.push_back(start);
    std::reverse(paths.begin(), paths.end());
}

应该注意以上这些算法中路径的保存。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 参见贪心算法——最短路径Dijkstra算法参见动态规划 目录 0.最短路径问题0.1 最短路径问题描述 0.1....
    王侦阅读 4,766评论 1 9
  • 课程介绍 先修课:概率统计,程序设计实习,集合论与图论 后续课:算法分析与设计,编译原理,操作系统,数据库概论,人...
    ShellyWhen阅读 2,252评论 0 3
  • 目录 1.贪心算法步骤 2.两个关键要素 3.两种背包问题3.1 0-1背包问题(适用于动态规划,不满足贪心选择性...
    王侦阅读 4,900评论 2 3
  • 人生的磨难是很多的,所以我们不可对于每一件轻微的伤害都过于敏感。在生活磨难面前,精神上的坚强和无动于衷是我们抵抗罪...
    0skywalker0阅读 313评论 0 0