状压DP解决哈密顿回路问题(C/C++实现)

2023-11-27 19:50

本文主要是介绍状压DP解决哈密顿回路问题(C/C++实现),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 一、序言
      • 1. 什么是Hamilton图?
      • 2. 题目描述
    • 二、为什么可以使用**状压DP**?
    • 三、解题思路
      • 1.判断是否存在哈密顿路径(状压DP)
      • 2.判断是否有哈密顿回路
    • 四、后记

一、序言

1. 什么是Hamilton图?

哈密顿图:图G的一个回路,若它通过图的每一个节点一次,且仅一次,就是哈密顿回路.存在哈密顿回路的图就是哈密顿图.

2. 题目描述

在这里插入图片描述
刚看到这个哈密顿回路的题时,第一感觉就是可以采用回溯法并通过 深度优先搜索(DFS) 解决,笔者初次就是使用这个方法并结合状态压缩AC了这道题。但是因为要使用递归,“翻译”成MIPS汇编代码(原题是汇编语言测试题)的时候需要使用大量堆栈保存返回地址、函数参数、还有在调用过程中使用过的并希望保存的寄存器(笔者曾因为堆栈处理不当陷入了十分抓狂的境地)。

后来听翔宇哥哥说这个题可以采用 状压DP 的方法解决(xyggyyds!),于是笔者便开始面向百度和CSDN,最后终于用非递归的方法解决了这道题,于是便写了本篇文章分享一下心得。


二、为什么可以使用状压DP

首先,本题满足DP算法的三个条件——

  • 重复子问题
  • 最优子结构
  • 无后效性

在此就不具体展开,有兴趣的同学可以参考CSDN上这篇博客。如果暂时不想了解也可以跳过,相信不会影响对本题算法的理解。

其次,“状压”顾名思义就是“状态压缩”,也就是 “将一个阶段或集合的状态压缩到一个二进制整数,其中 01 分别表示两种不同的状态,从而达到节省储存空间和提高查找效率的作用。以本题为例,我们把图中每个顶点的状态压缩到二进制的位,用 1 表示该点被访问过,0 表示该点没有被访问过。于是,我们就可以用一个二进制正数表示 所有被访问过的点构成的集合S(以下简称“点集”)。例如,当S=39时,二进制表示为100111,说明此时编号为0、1、2、6的点在集合中。

既然状态压缩使用二进制数,那么必然和位运算脱不了干系。以下是常用的状态压缩操作和相应的位运算表示——

操作位运算表示
取出整数n在二进制表示下的第k位(n>>k)&1
取出整数n在二进制表示下的第0~k-1位(后k位)n&((1<<k)-1)
取出整数n在二进制表示下的第k位取反n^(1<<k)
取出整数n在二进制表示下的第k位赋值为1n|(1<<k)
取出整数n在二进制表示下的第k位赋值为0n&(~(1<<k))

三、解题思路

为判断图中的是否存在 哈密顿回路,我们可以先判断是否存在 哈密顿路径( 即沿着边访问每个顶点,每个顶点恰好访问一次 ),然后再判断 最后访问的点最初访问的点 之间是否有通路即可。为方便判断,我们选取 0号点 为最初访问的点。

1.判断是否存在哈密顿路径(状压DP)

解决本题我们需要两个数组——

#define N 8
int edge[N][N];
int dp[N][N];

其中,edge 是储存无向图的邻接矩阵,edge[u][v] = 1 表示点v和点u之间有通路;dp 表示状态,以 dp[S][v] 为例,S 表示点集,即此时已经被访问的所有点的集合v 表示该点集中最后一个被访问的点的序号

例如,dp[39][2]表示 点集“100111”中最后被访问的点的序号是2。访问顺序可以是 “0->6->1->2”,也可以是 “0->1->6->2”。也就是说,dp[39][2] 之前的状态可以是 dp[35][6](点集S为”100011“, 最后访问的点是6号点),也可以是 dp[35][1](点集S为”100011“, 最后访问的点是1号点)

dp[S][v]的值只能是0和1。1 表示此时S中的点可以形成 哈密顿路径,而且该哈密顿路径的终点为 v0 表示此时S中的点无法形成哈密顿路径。

因为我们默认最早访问的点为0号点,所以我们首先规定

dp[1][0] = 1;    //表示0号点第一个被访问

为找出状态转移方程,我们可以先从小问题入手分析:

  • Q:图中现在有4个点(标号为0,1,2,3),我们怎么判断存在哈密顿路径呢?

  • A:只需要求得 dp[0b1111][1] = 1 或者 dp[0b1111][2] = 1 或者 dp[ob1111][3] = 1 即可。

  • Q:那我们怎么求 dp[0b1111][1] 的值呢???(其他同理)

  • A:此时我们转移到上一个状态(点集S = ”1101“)。只需要0号点,2号点和3号点可以形成哈密顿路,并且其中至少有一个点(除了0号点)与1号点之间存在通路,即 dp[0b1101][2] = 1 并且 edge[2][1] = 1 或者 dp[0b1101][3] = 1 并且 edge[3][1] = 1 ,那么 dp[0b1111][1] 的值就是1,否则为0。

  • Q:那我们怎么求 dp[0b1101][2] 的值呢???(其他同理)

  • A:我们再次转移到上一个状态(点集S = ”1001“)。只需要0号点与3号点之间有哈密顿路径,并且3号点和2号点之间有通路(因为0号点是初始点,所以没有必要判断0号点和2号点之间是否有通路),即 dp[0b1001][3] = 1 并且 edge[3][2] = 1, 那么 dp[ob1101][2] 的值就是1,否则是0。

  • Q:我们怎么求 dp[0b1001][3] 的值呢???

  • A:呃……好像结束了,只需要判断 edge[0][3] 就行了。

经过以上的Q&A,你大概可以发现,每一个状态的计算都依赖他的上一个状态(即子过程)的值,状态转移方程可以写作

D P [ S ] [ v ] = ⋃ i = 1 n ( D P [ S _ p r e v ] [ u i ] & r o a d [ u i ] [ v ] ) \mathsf{DP[S][v]=\bigcup_{i=1}^n \ (DP[S\_prev][u_i]} \ \ \&\ \ \mathsf{road[u_i][v])} DP[S][v]=i=1n (DP[S_prev][ui]  &  road[ui][v])
相信你已经对状态转移的具体过程有了大致的了解,那么我们结合下面的代码来进一步分析

    for(S = 1; S < (1 << n); S++){for(v = 0; v < n; v++){if((S >> v) & 1){int S_prev = S ^ (1 << v);for(u = 0; u < n; u++){if((S_prev >> u) & 1) dp[S][v] | = (dp[S_prev][u] & edge[u][v]);}}}}

在最外层的循环 for(S = 1; S < (1 << n); S++) 中,我们让 子集S1 (0b00000001)1<<8 (0b1111111) (假设n=8)进行遍历。因为表示子集状态的值S是从小到大变化的,所以可以保证当计算当前状态时,其子状态一定被计算过。

在第二层循环 for(v = 0; v < n; v++) 中,我们遍历所有的点,通过 (S >> v) & 1 来判断序号为 v 的点是否在点集 S 中,如果结果为1,我们便找出计算对象 dp[S][v],并找出上一状态对应的点集 S_prev = S ^ (1 << v) ,然后进入第三层循环。

在第三层循环 for(u = 0; u < n; u++) 中,我们继续遍历所有点,找出可以作为上一状态终点的所有点的序号 u,进一步可以找出 dp[S_prev][u] 的值(因为 S_prev < S,因此之前必然计算过 dp[S_prev][u] 的值)。

最后我们让 dp[S][v] 或上 dp[S_prev][u] & edge[u][v] 即可。因为只要存在一个 u 使得 dp[S_prev][u] & edge[u][v] = 1,就说明 0号点到v号点之间有哈密顿路径(0号点到u号点之间存在哈密顿路径,u号点和v号点之间有通路)。

2.判断是否有哈密顿回路

经过上面的计算,我们已经得出点集"11111111"(假设点的个数n = 8)中存在的所有哈密顿路径,现在只要存在某个哈密顿路径的终点与0号点(起点)之间有通路即可。该过程仅需一个循环便可解决

    for(int i = 0; i < n; i++){if(dp[(1<<n)-1][i] & edge[i][0]) {cout << "1" << endl;return 0;}}cout << "0" << endl;return 0;

思路比较简单,此处不再赘述。


四、后记

本题的解题方法也可以用于解决最短哈密顿回路问题,有兴趣的同学可以尝试一下。另外,因为笔者之前也没有学过算法,能力也十分有限,如果本篇题解中有纰漏甚至错误之处,还望各位dl不吝赐教!

这篇关于状压DP解决哈密顿回路问题(C/C++实现)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/428381

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

mybatis和mybatis-plus设置值为null不起作用问题及解决

《mybatis和mybatis-plus设置值为null不起作用问题及解决》Mybatis-Plus的FieldStrategy主要用于控制新增、更新和查询时对空值的处理策略,通过配置不同的策略类型... 目录MyBATis-plusFieldStrategy作用FieldStrategy类型每种策略的作

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

linux下多个硬盘划分到同一挂载点问题

《linux下多个硬盘划分到同一挂载点问题》在Linux系统中,将多个硬盘划分到同一挂载点需要通过逻辑卷管理(LVM)来实现,首先,需要将物理存储设备(如硬盘分区)创建为物理卷,然后,将这些物理卷组成... 目录linux下多个硬盘划分到同一挂载点需要明确的几个概念硬盘插上默认的是非lvm总结Linux下多