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