图算法: 哈密尔顿路径问题(上)

哈密尔顿路径问题

1859年,爱尔兰数学家哈密尔顿(Hamilton) 提出了一个周游世界的游戏

在正十二面体上依次标记伦敦、巴黎、莫斯科等世界著名大城市, 正十二面体的棱表示连接这些城市的路线.
试问能否在图中做一次旅行, 从顶点到顶点, 沿着边行走, 经过每个城市一次之后再回到出发点.

哈密尔顿回路

从一个点出发,沿着边行走, 经过每个顶点一次, 之后在回到出发点. 这样走的一条路径叫哈密尔顿回路

如下图, 第一张图存在哈密尔顿路径, 因为可以访问每个顶点一次并回到出发点0. 第二张图则不存在哈密尔顿路径

哈密尔顿回路求解

一个哈密尔顿回路需要满足两个要求

  • 每个顶点都要访问一次
  • 能够回到初始顶点

对于这个问题, 可以使用深度优先搜索为框架来解决

首先, 从顶点0出发访问其相邻顶点 (第一个相邻顶点为0)

由于是深度优先搜索, 接着从1开始访问其相邻顶点2


从2访问相邻顶点, 现在要访问0, 但0已经被访问过了
因此执行到这里满足了哈密尔顿回路的第一个条件: 从起始点回到起始点.
但并没有满足哈密尔顿回路第二个条件: 所有顶点访问一次, 这里顶点3还没有访问, 因此执行到这求得的哈密尔顿回路不成立.

当这种条件出现时, 需要回溯, 同时将此时访问的顶点标记为没有访问过.

这样做的原因是: 此时搜索的路径是一条死路, 需要回退并重新开始搜索.
而回退的方式就是利用深度优先搜索的回溯特性, 重新搜索则需要将之前搜索过程中标记的访问的状态置为空.

回溯到1后, 1还有相邻顶点3没有访问, 因此会访问顶点3

image.png

从1来到3后, 发生了和从1到2一样的问题: 3的所有相邻节点都访问过了, 并且回到了起始顶点, 满足了哈密尔顿回路第一个条件, 但还存在顶点2没有访问过, 因此哈密尔顿回路条件还是不成立.

这时从3回溯到1, 同时将顶点3标记为未访问

回溯到1后, 我们发现1的顶点已经全部访问完毕了, 同时哈密尔顿回路的任一条件都没有满足.

因此从1回溯到0, 并将1标记为未访问. 此时问题回到了原点.

这时候上一次从0 -> 1 的深度优先搜索执行完成, 这时候开始访问顶点0的另一个邻边2

继续执行深度优先搜索, 2首先访问相邻顶点0, 但没满足哈密尔顿回路第二个条件, 于是访问相邻顶点1


继续执行深度优先搜索, 1首先访问顶点0, 但没满足哈密尔顿回路第二个条件, 于是访问顶点2, 同样也没满足, 最终访问顶点3


来到顶点3后, 从顶点3开始执行深度优先搜索, 访问相邻顶点0, 这时候发现0访问过了, 满足了哈密尔顿回路第一个条件, 同时图中所有顶点也有访问过了, 满足了哈密尔顿回路第二个条件, 因此这条路径满足哈密尔顿回路

总结一下这个过程:

  1. 从某个顶点v出发, 深度优先搜索访问所有相邻顶点
  2. 如果相邻顶点w访问过, 但图中的其他顶点还存在没有访问的顶点, 则进行回溯, 并标记v为未访问
  3. 回溯到v重复这个过程, 直到满足相邻顶点访问过且其为初始顶点,且图中所有顶点都访问过, 算法执行结果

算法复杂度分析

TODO

回溯与剪枝

上面哈密尔顿回路求解的算法, 并不需要遍历所有顶点组成的路径的全排列, 在两个顶点之间没有边的情况下, 那么不会形成一条路径, 也就是剪枝.

对于上图中的图结构, 虽然理论上是全排列生成的。但实际上{2, 3} 之间并没有路径,因此也就不相连。所以在深度优先搜索的过程中,肯定不会去查找包含 2 - 3 这两个顶点的路。

实现

下面是求解哈密尔顿回路的基本实现: 使用C++11, 基于深度优先搜索. 图的存储使用了邻接表, 其中表替换成了标准库的stl::set

编译: g++ -std=c++11 hamilton.cc -o hamilton

#include <iostream>
#include <vector>
#include <set>
#include <exception>
#include <memory>

// graph data structure use map
class GraphMap {
public:
    GraphMap(int vertexCount, std::vector<std::vector<int>> edges) throw();

    ~GraphMap() = default;

    std::vector<int> adjacency(int v) const;

    inline int V() const { return v_; };

private:
    int v_;
    std::vector<std::set<int>> g_;
};

GraphMap::GraphMap(int vertexCount, std::vector<std::vector<int>> edges) throw() {
    v_ = vertexCount;
    g_ = std::vector<std::set<int>>(vertexCount, std::set<int>());

    for (auto it = edges.begin(); it != edges.end(); it++) {
        auto from = (*it)[0];
        if (from < 0 && from >= vertexCount) {
            throw std::logic_error("invalid vertex");
        }

        int to = (*it)[1];
        if (to < 0 && to >= vertexCount) {
            throw std::logic_error("invalid vertex");
        }


        std::set<int> &set = g_[from];

        // parallel edge
        if (g_[from].find(to) != g_[from].end()) {
            throw std::logic_error("not support parallel edge");
        }

        // self-loop edge
        if (to == from) {
            throw std::logic_error("not support self-loop edge");
        }

        g_[from].insert(to);
        g_[to].insert(from);
    }
}

std::vector<int> GraphMap::adjacency(int v) const {
    std::vector<int> adj;
    std::set<int> gset = g_[v];
    for (auto it = gset.begin(); it != gset.end(); it++) {
        adj.push_back(*it);
    }

    return std::move(adj);
}

class HamiltonLoop {
public:
    explicit HamiltonLoop(std::shared_ptr<GraphMap> g);

    ~HamiltonLoop() = default;

    bool operator()(int s);

private:

    bool dfs(int s, int v);

    bool all_visited();

    std::shared_ptr<GraphMap> g_;

    std::vector<bool> visited_;
};

HamiltonLoop::HamiltonLoop(std::shared_ptr<GraphMap> g) {
    g_ = g;
    visited_ = std::vector<bool>(g_->V(), false);
}

bool HamiltonLoop::operator()(int s) {
    return dfs(s, s);
}

bool HamiltonLoop::dfs(int s, int v) {
    visited_[v] = true;

    std::vector<int> adj = g_->adjacency(v);
    for (int w :  adj) {
        if (!visited_[w]) {
            if (dfs(s, w)) {
                return true;
            }

        } else if (w == s && all_visited()) {
            return true;
        }
    }

    visited_[v] = false;
    return false;
}

bool HamiltonLoop::all_visited() {
    for (bool v : visited_) {
        if (!v) {
            return false;
        }
    }

    return true;
}


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