集合类状压dp:AcWing 91. 最短Hamilton路径

本文介绍了位运算的基本知识,包括与、或、非、异或及移位运算,并详细阐述了如何在二进制状态压缩中使用位运算。通过一个具体的动态规划问题——求解最短Hamilton路径,展示了状态压缩DP的思想,即用整数表示状态,以减少空间和时间复杂度。文章提供了相应的代码实现,展示了如何通过位运算进行状态转移和计算。

前置位运算知识

and, &:1 & 1 = 1,0 & 1 = 0,0 & 0 = 0;(联想电路串联
or, l:1 | 1 = 1,0 | 1 = 1,0 | 0 = 0;(联想电路并联
not, ~:not 1 = 0,not 0 = 1;
异或 xor(写代码的时候用 “^” 表示):1 xor 1 = 0,0 xor 1 = 1,0 xor 0 = 0;(俗称 不进位加法相同得 0,相异得 1

m 位二进制数 中,为方便起见,通常称 最低位为第 0 从右到左 依此类推,最高位 为 第 m-1 位。

移位运算

左移:在 二进制表示 下,把数字 同时向左移动低位以 0 填充高位越界后舍弃

1 << n = 2^nn << 1 = 2n左移几位就向末尾补几个 0,比如 11,左移 1 位 <<1 变成 110,左移 2 位 <<2 变成 1100,以此类推)

算术右移:在 二进制补码表示 下,把数字 同时向右移动高位以符号位填充低位越界后舍弃

n >> 1 = n / 2.0右移几位删除几个末尾元素,比如 1110101,右移 1 位 >>1 变成 111010,右移 2 位 >>2 变成 11101,以此类推)

算术右移 等于 除以 2 向下取整(-3) >> 1 = -13 >> 1 = 1

二进制状态压缩

二进制状态压缩,是指将一个 长度为 m bool 数组 用一个 m 位二进制整数 表示并存储的方法。

利用下列 位运算操作 可以实现 bool 数组对应下标元素 的存取。(注意下面的“第 k 位”是指 下标从 0 开始计数

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

这种方法运算简便,并且 节省了程序运行的时间和空间

m 不太大 时,可以 直接使用一个整数类型存储

m 较大 时,可以使用 若干个整数类型( int 数组),也可以直接利用 C++ STL 为我们提供的 bitset 实现。

状态压缩dp

线性 DP 中,我们提到,动态规划的过程是随着“阶段”的增长,在 每个状态维度上不断扩展

在任意时刻,已经求出最优解的状态 与 尚未求出最优解的状态 在各维度上的分界点组成了 DP扩展的“轮廓”。

对于某些问题,我们需要 在动态规划的“状态”中记录一个集合,保存这个 “轮廓的详细信息,以便进行 状态转移

集合大小 不超过 N ,集合中 每个元素都是小于 K 的自然数,则我们可以把这个集合看作一个 N K 进制数,以一个 [0, K^N - 1] 之间的十进制整数的形式 作为 DP 状态的一维(核心要义)。

这种 把集合转化为整数 记录在 DP 状态中的一类算法,被称为:状态压缩动态规划 算法。

接下来的例题 “AcWing 91. 最短Hamilton路径” 向我们展示了简单的 状态压缩DP 思想。

例题:AcWing 91. 最短Hamilton路径

在这里插入图片描述
在这里插入图片描述

题意:

给定一张 n (n ≤ 20) 个点的 带权无向图,点从 0~n-1 标号,求起点 0 到终点 n - 1最短Hamilton路径

Hamilton路径的定义:从 0n-1 不重不漏地经过每个点恰好一次

思路:

如果用纯暴力的话,时间复杂度为O(n*n!)n<=20,这样肯定会超时,我们考虑用状态压缩dp求解。

本题核心即为状压dp的思想,用一个整数来表示一个状态

f[i, j]状态表示:

我们发现每个点遍历的顺序我们是不关心的,只注意两方面:

  • ① 哪些点被遍历过(且每个点有且只被遍历一次
  • ② 目前停在了哪一个点上

两方面能唯一确定当前搜索的状态是什么

集合:所有从 0 走到 j,中间所有点是 i (走过的所有点存于整数 i 当中)的所有路径(每个点只能走一次)

i 即表示一个压缩的状态,要看作一个二进制数,这个二进制数中每一位分别表示 当前这个点是否走过

举个例子,如果 i = (1110011),表示第0、1、4、5、6个点都已经走过了

属性:最小值min
在这里插入图片描述

f[i, j]状态计算:

集合划分:以走过的 倒数第 2 个点是哪一个 进行划分,显然 倒数第 2 个点 有 n 种情况:0、1、2、...、n-1
在这里插入图片描述

如果倒数第 2 个点是 k 的话,即从起点 0 根据某一可能的路线先走到 k, 最后一步从 k 走到 j

我们来分析一下:首先最后一步是已知的,即为 k -> j 对应的权值,若要使得整个路径最短,我们 只需使得 0 -> k 这段路径最短即可,

0 -> j 走过的所有点 用 i 表示,那么 从 0 -> k 走过的所有点 即在 i 表示的状态基础上除去 j 点(f[i - {j}, k]

粗略表示一下:从 0 -> j 的最短路径为 f[i - {j}, k] + weight[k, j]

f[i, j] 的计算:对所有 kk = 0, 1, ..., n-1)的情况取最小值即可。

时间复杂度:

第一维表示哪些点被遍历过,共有 2 种情况: 用 or 不用,共 20 个点,共 2^20 种情况

第二维表示目前停在哪个点,共 20 种情况

总状态数量为两维相乘: 2^20 * 20 = 2e7

状态数量(2e7) × 状态转移(20)约等于 4e8,本题时限为 5s ,计算量最多 5e8,所以是合法的。

细节:

我们应当外层循环路径 i ,内层循环终点 j ,这样能保证 dp 状态是按照拓扑序来计算的,状态转移需要的状态必须被提前计算出来才行。

代码:

#include<bits/stdc++.h>

using namespace std;
const int N = 20;
int dp[1<<N][N];
int w[N][N];
int n;

int main()
{
    cin>>n;
    for(int i=0; i<n; ++i)
        for(int j=0; j<n; ++j)
        cin>>w[i][j];
    
    memset(dp, 0x3f, sizeof dp); //正无穷
    dp[1][0] = 0; //初始时,位于 0 号点,相当于从 0 走到 0,走过的点只有 0 这一个点,即第 0 位是 1,其余为 0
    for(int i=0; i<1<<n; ++i) //枚举每种状态
    {
        for(int j=0; j<n; ++j) //枚举目前停留的点
        {
            if(i>>j&1) //因为是从 0 走到 j,那么 i 这个状态肯定要包含 j,这样才有意义
            {
                for(int k=0; k<n; ++k) //枚举是从哪个点转移而来
                {
                    if((i & (~(1 << j)))>>k&1) //如果是从 k 点转移而来,那么当前枚举的 i 除去 j 点之后一定要包含 k 点(参考本篇开头的公式),当然也可以写成:(i-(1<<j))>>k&1
                    {
                        dp[i][j] = min(dp[i][j], dp[i & (~(1 << j))][k] + w[k][j]);
                    }
                }
            }
        }
    }
    cout<<dp[(1<<n)-1][n-1]<<endl; //走完了所有点,最终落脚与 n-1 号点,(1<<n)-1 表示为 一个各个位都是 1 的 n 位二进制数
    
    return 0;
}
### Hamilton 路径算法与图论分析 Hamilton 路径问题在图论中是一个经典问题,其目标是在一个加权图中找到一条从起点到终点的路径,该路径恰好经过每个顶点一次,并且路径的总权重小。该问题属于 NP-Hard 问题,因此对于大规模图,通常需要使用动态规划等优化策略来求解。 #### 动态规划方法 Hamilton 路径可以通过动态规划来高效求解。缩的核心思想是使用二进制数来表示已经访问过的顶点集合,从而减少态存储的空间和计算复杂度。 定义态 `dp[i][j]` 表示当前处于顶点 `j`,并且已经访问过的顶点集合由二进制数 `i` 表示时的路径长度。初始态为 `dp[1][0] = 0`,表示从起点 `0` 出发,仅访问了自己。 态转移方程为: ```python dp[i][j] = min(dp[i ^ (1 << j)][k] + weight[k][j]) for all k in i if k != j ``` 其中: - `i` 是一个二进制数,表示已访问的顶点集合。 - `j` 是当前所在的顶点。 - `weight[k][j]` 是顶点 `k` 到顶点 `j` 的边权值。 - `i ^ (1 << j)` 表示从集合 `i` 中移除顶点 `j`。 终答案是 `min(dp[(1 << n) - 1][j] + weight[j][n-1])`,其中 `n` 是图中顶点的数量,`j` 遍历所有可能的中间顶点。 #### 图论中的应用 在图论中, Hamilton 路径问题与旅行商问题(TSP)密切相关,但不同之处在于 TSP 要求路径形成一个回路,而 Hamilton 路径只需要从起点到终点。该问题在实际应用中广泛存在,例如电路设计、物流路径规划等领域。 #### 示例代码 以下是一个实现 Hamilton 路径的 Python 示例代码: ```python n = int(input()) weight = [list(map(int, input().split())) for _ in range(n)] # 初始化动态规划dp = [[float('inf')] * n for _ in range(1 << n)] dp[1][0] = 0 # 起点为顶点0 # 态转移 for i in range(1 << n): for j in range(n): if (i >> j) & 1: for k in range(n): if (i ^ (1 << j)) >> k & 1: dp[i][j] = min(dp[i][j], dp[i ^ (1 << j)][k] + weight[k][j]) # 终结果 print(dp[(1 << n) - 1][n-1]) ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值