状压 DP 学习笔记

前言

2023.8.30 开始停课集训。

开始补 \(CSP-S\) 的知识点,先打算来学状压 \(DP\)

定义

状压 \(DP\) 的全称是状态压缩动态规划,也是动态规划中的一种。但是其与普通 \(DP\) 不同的是它将某种状态(一般为二进制 \(01\) 串,\(1\) 表示选,\(0\) 表示不选。也有其它进制)作为了 \(dp\) 数组的下标,这样更方便两种不同状态之间的转移。因为状态是用二进制表示的,所以状压 \(DP\) 的时间复杂度一般为 \(O(n \times 2^{n})\) 或者 \(O(n^{2} \times 2^{n})\)。所以状压 \(DP\) 最大能接受的数据范围是 \(n<=20\)。一般情况下看到数据范围小于 \(20\) 的时候都可以考虑状压。

位运算

状压的实现在于位运算,只有将位运算弄清楚了,才能写好状压。

  • 左移 / 右移

左移和右移的定义是在二进制表示下将一个数向左 / 右移。所以左移 \(x\) 位相当于多添 \(x\)\(0\),右移 \(x\) 位相当于减少 \(x\) 位。

比如 \((11001011)_{2}\) 左移 \(3\) 位后为:\((11001011000)_{2}\)\((11001011)_{2}\) 右移 \(3\) 位后为:\((11001)_{2}\)

可以发现,左移和右移相当于乘/除以 \(2^{x}\)

  • \(&\) / 或 \(|\) / 非 \(!\) / 异或 \(^\)

\(&\):两位都为 \(1\) 结果才为 \(1\)

\(|\):两位至少有一位为 \(1\) 结果才为 \(1\)

\(!\):非 \(1\)\(0\),非 \(0\)\(1\)

异或 \(^\):两位不同,结果才为 \(1\)

位运算常用技巧

  • 询问第 \(i\) 位是否为 \(1\)x & (1 << i - 1)

  • 询问两个数是否在同一个位置上都是 \(1\)x & y

  • 将第 \(i\) 位变成 \(1\),其余位不变:x ^ (1 << i - 1)

  • 这个数的最后一个 \(1\) 以及后面的 \(0\)x & -x

  • 询问这个数一共包含了多少个 \(1\)__builtin_popcount(x)

做法

状压 \(DP\) 很多时候需要初始化两个数组 \(sit\)\(sta\)\(sit_{i}\) 表示 \(i\) 所对应的十进制数是多少,\(sta_{i}\) 表示 \(i\) 中包含多少个 \(1\)。有了这两个数组后 \(dp\) 会变得更容易。而这个过程一般用一个 \(dfs\) 来实现。但这两个数组的定义有一个前提:\(i\) 状态一定合法。

Code

inline void Dfs(int x,int num,int cur)
{
	if(cur >= m)
	{
		sit[++cnt] = x;
		sta[cnt] = num;
		return; 
	}
	Dfs(x,num,cur + 1);
	Dfs(x + (1 << cur),num + 1,cur + 2);
} 

\(dp\) 部分一般会先枚举 \(n\)(也就是每个位置),再枚举状态。如果从状态 \(j\) 到状态 \(k\) 是合法的,那么就转移 \(dp\) 值。判断合不合法可以写在一个 \(Compatible\) 函数里,也可以直接写在循环里。

Code

inline bool Compatible(int j,int x)
{
	if(sit[j] & sit[x]) return false;
	if((sit[j] << 1) & sit[x]) return false;
	if((sit[x] << 1) & sit[j]) return false;
	return true;
}

状压 \(DP\) 还会用到枚举子集。如果直接枚举的活可能会 \(TLE\),但是我们可以对做法进行优化:

for(int j = i & (i - 1);j;j = (j - 1) & x)

这样就不会枚举到一些不合法的序列,时间复杂度从\(O(4^{n})\) 优化到了 \(O(3^{n})\),证明略。

总结

状压 \(DP\) 总体来说还是比较板的,无论是状态还是转移。而且这一类题的特点一般都很明显,一眼就可以看出来是状压,特别是数据范围。但是题型还是很多变的。

例题

【提高组——专题训练】状态压缩DP

posted @ 2023-12-19 11:48  Creeper_l  阅读(1)  评论(0编辑  收藏  举报