状态压缩 DP 学习笔记【入门篇】

前言

状态压缩 DP,简称状压 DP

之前一直觉得状压特别难,学了一下,发现基本形态挺简单的。

在学习之前,你需要掌握:

  1. 简单 DP(如线性 DP,背包)
  2. 基本二进制运算:& 运算、| 运算、 运算、左右移运算符。

什么是状压 DP

状态压缩,顾名思义,就是对当前的状态压缩。

怎么压缩呢?答案是二进制。

比如一个简单的例子:我有五个小球,依次编号 05

我想表达『选 1 号与 3 号球』,怎么压缩?

很显然,对应二进制是 (01010)2,转换成十进制就是 10

这个就是状压 DP。

也就是说,状态的转移(二进制转移),可以变成整数的加减(十进制加减)。

二进制运算

既然都有二进制,二进制的简单技巧肯定少不了。

二进制数 x,第 n 位是否为 k

此处 k 取值 10

x 右移 (n1) 位,即 x>>(n1)

此时,x 的末位就是第 n 位,拉出个位即可:x>>(n1) & 1

所以判断语句即为:if ( (x >> (n-1) & 1) == k)

* 在打代码时,需要注意括号。位运算优先级较为复杂,保险起见可以打括号,但必须保证可读性。

二进制数 x,修改第 n 位为 1

利用或运算求解。

将第 n 位修改成 1,相当于 x | 10000,应该有 (n1)0

所以修改语句即为:x | (1 << (n-1))

二进制数 x 最低位的 1 改成 0

比如说,将 (100101)2 修改为 (100100)2 ,将 (110100)2 修改为 (110000)2

方法一:利用 lowbit() 修改:x - (x&-x)。其中的 lowbit() 在树状数组中有使用,不理解没关系。

方法二:

对于一个数 x(x1) 总是等于:将末尾 0 变成 1,最低位 1 变成 0,前面不变。

所以,x & (x-1) 就是答案。

你可能听不懂,那么举个例子。

x=(110 100)2,则 (x1)=(110 001)2

容易发现,前三位进行 & 运算,不变;后三位进行 & 运算,必为 0

这下懂了吧!

状压 DP 思路

一般地,状态都用一个二维数组 dp[][] 表示。

dpi,j 的第一维 i,通常是二进制数,表示哪些物品被选过。

第二维 j 则比较多变,需要根据题目转换。

这里还需要知道一个东西:i取值范围

假如一共有 n+1 个点,编号 0n,则 i=[0,20+21++2n]

后半段的 20+21++2n 是等比数列,简单普及一下求解方法。

S=20+21++2n

2S=21+22++2n+2n+1

两式相减得:2SS=2n+120

简化得:S=2n+11

大家肯定还是不理解,我们来看一道经典例题。

经典例题 - TSP 问题

题意

前置知识:Floyd / dijkstra 算法。

旅行商问题(Traveling salesman problem,即 TSP),是组合优化中的 NP 问题。

至今还没有多项式解法,仅有指数级做法。

具体问题如下:

有一张图(一般为无向图),保证所有点均连通。

有一个旅行商在 0 号点,要求他从 0 号点出发,访问过所有的城市并回到原点。

给定每对城市之间的距离,求出最短路。

如果用暴力,时间复杂度过高。

所以使用状压 DP 实现。


思路

首先,看图的稠密性决定使用 floyd 还是 dijkstra

反正,用最短路算法,求出任意两点的最短距离。

dpi,j 表示行走过的点的状态为 i(对应二进制数),最后一个点是 j 号点。

我们可以枚举 k=[0,maxn]

接下来再枚举 ij,表示几号点。

我们可以大致写出有关 ij 的状态转移方程:

如果 k 的第 j 位为 0,说明可以尝试转移:

dp[ k|(1<<n) ][ j ]=min{dp[ k|(1<<n) ][ j ]dp[ k ][ i ]+ej,i

啊这个方程写崩溃了。

最后的答案即为:dp[maxn][0]

对了,不要忘记初始化 dpi,j=dp0,0=0


代码

给出代码。

大致讲一下这份代码的读入格式。

第一行 n 表示有 (n+1) 个点,编号 0n

接下来一个 (n+1)×(n+1) 的矩阵,矩阵的第 ij 列表示 dis[ij]

道路是单向的。

这里由于图是稠密图,所以使用 floyd

#include <iostream>
#include <cstdio>
#include <cstring>
#define INF 0x3f3f3f3f
#define N 20
using namespace std;
int n, e[N][N], dp[1<<N][N];
void Input()
{
	scanf("%d", &n);
	for (int i = 0; i <= n; i++)
		for (int j = 0; j <= n; j++)
			scanf("%d", &e[i][j]);	
}
void floyd()
{
	for (int k = 0; k <= n; k++)
		for (int i = 0; i <= n; i++)
			for (int j = 0; j <= n; j++)
				e[i][j] = min(e[i][j], e[i][k] + e[k][j]);
}
void DP()
{
	memset(dp, INF, sizeof(dp));
	dp[0][0] = 0;
	int maxn = (1 << n+1) - 1;
	for (int k = 0; k <= maxn; k++)
		for (int i = 0; i <= n; i++)
				for (int j = 0; j <= n; j++)
					if ( ((k >> j) & 1) == 0) //判断第 j 位是否为 0。
						//把第 j 位改成 1。
						dp[ k|(1<<j) ][j] = min(dp[ k|(1<<j) ][j], dp[k][i] + e[j][i]);
	printf("%d", dp[maxn][0]);
}
int main()
{
	Input();
	floyd();
	DP();
}

进一步思考

通过这道题目,我们发现:状压 DP 的时间复杂度一般是 O(2nn2)

由此可得,状压 DP 的适用范围不会很广,n20 左右。

如果 n 再大一点,不说时间问题,空间都爆炸啦(空间至少需要 2n,本题空间 2nn+n2)!

考虑到这个后,你就可以一眼知道,一道 DP 题有没有使用状压 DP 的可能性。

后记

貌似没有实战例题,以后有时间再补例题吧。

状压 DP 真的不难,希望大家努力学会!

此外,推荐两道状压入门题:link1 & link2

还有大神整理的题单:link

首发:2022-06-12 17:34:29

posted @   liangbowen  阅读(130)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示