首先说明一下,此博文来自我在CSDN上看到的一篇哈密顿回路(有向图中)的位运算算法,出自GDTZX大神之手,(侵删),虽然刚从校园毕业,但脑子已经完全僵住了,花了许久才看懂了这个算法。

哈密顿回路,具体到本题之中即从某一个点开始经过所有的点一次后再回到该点的不同路径数。对于这个不同需要注意两点:

  1. 如果我们将路径经过的点按顺序写下,比如当n=3时,若存在123和231。此时,我们认为这两条路径是同一条哈密顿回路。而123和213则是不同的哈密顿回路。

  2. 若两个点之间有多条边,经过不同的边的路径仍然看作同一条哈密顿回路。不同哈密顿回路只和经过的点有关。因此对于多条边的情况我们可以将其合并为一条边来考虑。

对于哈密顿回路,一个简单的想法就是枚举所有可能的路径,判定这个路径是否存在。即时间复杂度为O(n!)。而题目给定的数据范围为:n <= 12,所以最大可能的枚举次数为12! = 479,001,600。

极限的数据不到5亿,所以我们可以考虑使用暴力来枚举所有的哈密顿回路。直接采用DFS枚举我们的每一步,最后判定是否走回到起点。

伪代码如下:

DFS(int nowVertex, bool visitedVertex, int path, int length)visitedVertex[ nowVertex ] = True;If (all Vertex is visited) ThenCount = Count + 1Else For (u is next vertex of nowVertex)If (not visitedVertex[ u ]) Thenpath[ length ] = uDFS(u, visitedVertex, path, length + 1)End IfEnd For    End IfvisitedVertex[ nowVertex ] = False

由于哈密顿回路始终会经过每一个点,所以我们只以1为起点就一定可以找出所有的哈密顿回路。

那么这样是否能够解决这道题目呢?我只能说不一定能够解决。

虽然伪代码相同,但是根据实现的方法会有不同的运行效率,在某些实现方法下就能够通过所有的数据点,在某些实现方法下就会超过时限。

这里我们介绍一种利用位运算的实现,能够使得整个程序的效率提高很多倍。

首先来看看代码:

const int MAXN = 14;int edge[ MAXN ];
int p[1 << MAXN];
int cnt;void dfs(int nowVertex, int unused) {if (!unused) {cnt += (edge[nowVertex] & 1);return ;}int rest = unused & edge[ nowVertex ];while (rest) {int tp = rest & (-rest);dfs(p[ tp ], unused - tp);rest -= tp;}return ;
}int main()
{int n, m;scanf("%d %d", &n, &m);for (int i = 0; i < n; ++i)p[ 1 << i ] = i + 1;while (m--) {int u, v;scanf("%d %d", &u, &v);edge[u] |= 1 << (v - 1);}dfs(1, (1 << n) - 2);printf("%d\n", cnt);return 0;
}

我们一个一个来解释每一个变量的含义:

edge[i]表示点i的next节点情况,我们用二进制表示edge[i],比如当edge[i]=01011时:

+---+---+---+---+---+
| 5 | 4 | 3 | 2 | 1 | 右起第j位
+---+---+---+---+---+
| 0 | 1 | 0 | 1 | 1 | 二进制位的值
+---+---+---+---+---+

从右起第j位,若为1,则表示存在i到j的边;若为0,则表示不存在i到j的边。所以edge[i]=01011就表示节点i可以到达节点1,2,4。

p[i]是为了方便查找点的编号。在edge[i]中若存在01000,我们可以根据01000=8, 而p[8]=4来快速的通过二进制位来定位节点编号。

所以有初始化:

for (int i = 0; i < n; ++i)p[ 1 << i ] = i + 1;

而通过节点编号来找到二进制位,则可以直接使用1 << (i - 1)实现。

我们在读入数据时,通过位运算边可以记录下所有点之间的连边情况:

while (m--) {int u, v;scanf("%d %d", &u, &v);edge[u] |= 1 << (v - 1);    // 记录u有后继节点v
}

unused该二进制数表示我们尚未访问的节点集合,同样的右起第i位表示节点i,比如unused = 01100

+---+---+---+---+---+
| 5 | 4 | 3 | 2 | 1 | 右起第i位
+---+---+---+---+---+
| 0 | 1 | 1 | 0 | 0 | 二进制位的值
+---+---+---+---+---+

表示我们现在深度优先搜索已经经过了节点1,2,5,而节点3,4还尚未经过。

由于我们是以节点1作为起点,初始化的unused也就要去掉最右边的1,所以代码为dfs(1, (1 << n) - 2)

接下来我们逐行解释dfs函数:

if (!unused) {cnt += (edge[nowVertex] & 1);return ;
}

当我们所有的节点都经过一次之后,unused中一定不存在1,因此有!unused = true。但是此时并不一定找到了哈密顿回路,我们还需要判断当前节点是否能回到起点,也就是节点1。若nowVertex可以到达节点1,则edge[nowVertex]最右边1位一定是1,那么就一定有edge[nowVertex] & 1 = 1

int rest = unused & edge[ nowVertex ];

rest表示当前节点还可以走到的未访问节点。由于&运算的性质,只有当unusededge[ nowVertex ]对应二进制位同时为1时,rest对应的二进制位才会为1。其含义就是该节点尚未访问,且节点nowVertex可以到达该节点。

while (rest) {int tp = rest & (-rest);dfs(p[ tp ], unused - tp);rest -= tp;
}

该循环的作用是枚举每一个可以到达的点。

int tp = rest & (-rest);

这里利用了一个性质,即p & -p等于取出p的最右边的一个1。举个例子p=10110:

   +---+---+---+---+---+
p  | 1 | 0 | 1 | 1 | 0 | +---+---+---+---+---+
-p | 0 | 1 | 0 | 1 | 0 | +---+---+---+---+---+
&  | 0 | 0 | 0 | 1 | 0 | +---+---+---+---+---+

我们不断的利用这个性质从rest里面取出可以使用的二进制位,进行dfs(p[ tp ], unused - tp);。同时再枚举完一个节点后,将其从rest中删除,即rest -= tp;

最后我们再使用printf("%d\n", cnt);来输出我们找到的方案数。

在上面DFS的基础上,我们还可以进一步优化。

递归的过程中,unused很有可能会出现重复的情况,比如说从1->3->2->4和从1->2->3->4,对于dfs(4, unused)来说,此时的unused值都是相同的。因此我们可以采用记忆化搜索的方式进一步降低复杂度。

定义数组f[n][unused]表示当前节点为n,且节点访问情况为unused时的方案数量。

那么有:

f[n][unused] = sigma(f[p][unused + (1 << (n - 1))] | (unused & (1 << (n - 1)) == 0) and (p != n) and (edge[p] & (1 << (n - 1)) != 0))

这对应的是原dfs函数中下面这段代码的逆过程。

while (rest) {int tp = rest & (-rest);dfs(p[ tp ], unused - tp);rest -= tp;
}

三个条件分别为:

  • (unused & (1 << (n - 1)) == 0)表示当前状态中右起第n位为0
  • (p != n)表示前驱结点不等于n
  • (edge[p] & (1 << (n - 1)) != 0)表示节点p能够到达节点n

在计算f[n][unused]的过程中,假设unused的二进制表示中有i个1,则我们需要事先计算出所有i+1个1的状态才能够保证unused + (1 << (p - 1))是正确的结果。

因此我们在枚举过程中,需要按照状态中1的个数来枚举。

其伪代码:

For numberOfOnes = n-1 .. 0For (the number of ones of unused equals numberOfOnes)For nowVertex = 1 .. nFor prevVertex = 1 .. nIf (unused & (1 << (nowVertex - 1)) == 0) and (prevVertex != nowVertex) and (edge[ prevVertex ] & (1 << (nowVertex - 1)) != 0) Thenf[ nowVertex ][ unused ] = f[ nowVertex ][ unused ] + f[ prevVertex ][unused + (1 << (nowVertex - 1))]End IfEnd ForEnd ForEnd For
End For

对于f[n][ unused ]数组,其初始条件为f[1][(1 << n) - 2] = 1

最后需要将所有的f[n][0]中能够到达节点1的节点n累加起来,即可得到所有的方案数。该算法的理论时间复杂度为O(2^n*n^2)

结果分析

该题目一共只有7名选手通过,大部分提交过该题的选手也都有一定的部分分值。

本题直接采用搜索就能够通过,拉开差距的主要原因是对于伪代码实现方式的不同,而导致通过的测试点数量不同。

另外还有一个易错点是走完所有节点之后一定要判断是否可以回到起点。

哈密顿回路的非暴力解法(转自CSDN大神GDTZX)相关推荐

  1. 用爬虫来爬取csdn大神的文章的url

    上一篇文章中爬虫来获取廖老师python教程的url并加入搜索url功能,这一篇文章会爬取csdn大神july的所有博客的url,并实现搜索功能就像下面这样 请输入你要查询的知识点:Machine M ...

  2. CSDN大神多,在这里驻扎一下,沾沾神气

    百度文库对名字的限制真多,本来系统就叫android4.0 ,带个点就传不上去略感坑爹了,我说百度何必跟个名字过不去.还是CSDN好,大神各处是,咱也来这里混混.沾沾神气!嘿嘿!

  3. CCAI 2017 | 德国DFKI科技总监Hans Uszkoreit:如何用机器学习和知识图谱来实现商业智能化? 原2017.07.25AI科技大本营 文/CSDN大琦 7 月22 - 2

    CCAI 2017 | 德国DFKI科技总监Hans Uszkoreit:如何用机器学习和知识图谱来实现商业智能化? 原2017.07.25AI科技大本营 文/CSDN大琦 7 月22 - 23 日, ...

  4. CSDN博客大神汇总

    http://blog.csdn.net/llp1992/article/details/43768111 本篇文章主要纪录我在学习过程中,发现的非常给力的CSDNBlog和个人Blog,这些Blog ...

  5. 2019年CSDN排名前10名大神

    截止到2019年4月20日,CSDN前十的大神: 排名第1:https://blog.csdn.net/stpeace 排名第2:https://blog.csdn.net/yuanmeng001 排 ...

  6. Android 开发--CSDN博客大神汇总

    CSDN鸿洋:http://blog.csdn.net/lmj623565791 爱哥:http://blog.csdn.net/aigestudio 老罗:http://blog.csdn.net/ ...

  7. CSDN排名靠前的大神:

    CSDN排名靠前的大神: 排名第1:https://blog.csdn.net/stpeace/article/details/50811595 排名第2:https://blog.csdn.net/ ...

  8. CSDN大揭晓:很好奇,35岁以上的软件测试员都干嘛去了?

    岁月是把杀猪刀 悄咪咪地架在我们的脖子上 我们这些别人眼中的"老司机" 一直行走在IT行业的测试老鸟 已经/正在/即将踏上35岁的尴尬年龄 面临前有强敌,后有追兵的复杂境况 ▼ 所 ...

  9. July大神---SVM讲解

    支持向量机通俗导论(理解SVM的三层境界) 作者:July :致谢:pluskid.白石.JerryLead. 出处:结构之法算法之道blog. 前言 动笔写这个支持向量机(support vecto ...

最新文章

  1. 归并排序及C语言实现
  2. python 移动文件
  3. 前端工程师面试题汇总
  4. 酷睿i7cpu适合的linux,CPU性能篇 - Core i7-4770K Linux之旅:有喜有忧_Linux新闻_Linux公社-Linux系统门户网站...
  5. 我的Go+语言初体验——Go+语言构建神经网络实战手写数字识别
  6. hdu5358 ( First One )数学+思维
  7. kafka jar包_Windows环境下Flink消费Kafka实现热词统计
  8. 单片机c语言仿真,单片机c语言教程:C51表达式语句及仿真器
  9. 当“雷布斯”遇到“乔布斯”——小米美国专利状况分析
  10. 运维面试官喜欢问的问题
  11. wince 百度地图懒人包_百度导航车载wince版下载
  12. 常用无损压缩算法原理简析
  13. 面向对象下 abstract关键字(宋红康JAVASE)
  14. 最完整的SSM框架搭建流程
  15. windows2008 没有本地用户和组
  16. 在数据库中一个汉字到底占几个字节
  17. ORACLE如何学习
  18. vue展示信息卡片_vue 个人中心会员卡片组件
  19. Pktgen-DPDK使用说明
  20. Oracle 创建数据库并为新建数据库创建用户及赋予权限

热门文章

  1. Python scapy抓包程序
  2. 线性规划简单理解(LP问题)
  3. 矩阵快速幂以及其优化【华东交大课程】
  4. 建模示范视频EA022/社区团购系统试看片段-视频+图片版
  5. web前端培训多长时间
  6. 《算法笔记》4.1小节——算法初步-排序 问题 F: 小白鼠排队
  7. apache优化速度--嗖嗖嗖
  8. 推荐一个非常好的P2P应用
  9. SSI--什么是SHTML?
  10. Cocos2dx富文本 RichText的使用