三、状态压缩DP
3.1. 基本概念
状态压缩动态规划,就是我们俗称的状压DP,是利用计算机二进制的性质来描述状态的一种DP方式很多棋盘问题都运用到了状压,同时,状压也很经常和记忆化搜索连用。一般用状态压缩的DP作为记忆化数组。
3.2. 关键操作
1 << (i - 1) // 左移 i - 1 位.
1 << (i - 1) | state // 加入集合中第 i 个元素.
1 << (i - 1) & state // 判断集合中第 i 个元素是否包含在此子集中.
// 例子
state = 010001, cur = 1 << 2 = 100
state | cur = 010001 | 000100 = 010101 // 将第 i 个元素加入此时的状态.
// 例子
state = 010001, cur = 1 << 4 = 10000
state & cur = 010001 & 010000 = 010000 // state & cur == 0 证明此时状态中不含该元素.
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|
二进制表示 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
是否使用 | 是 | 否 | 否 | 是 | 否 | 否 | 否 | 是 | 是 |
对于上述表格展示的是对于一个长度为9的数组,取 1,4,8,9 四个元素作为子集的时候,状态应该表示为 100100011.
3.3. 经典例题
LeetCode 464 我能赢吗
class Solution {
public:
bool memo_dfs(int maxChoosableInteger, int desiredTotal, vector<int>& memo, int state){
// 之前已经遍历过了,直接返回之前的结果。记忆化搜索中很重要的剪枝步骤.
if(memo[state] != 2){
return memo[state];
}
// 遍历[1, maxchoose],选择当前步加入的数字.
for(int i = 1; i <= maxChoosableInteger; i++){
int add_state = 1 << (i - 1);
// 当前元素已经加入之前的状态中了,不需要继续加入.
if((add_state & state) != 0){
continue;
}
// 此时如果加入当前元素已经满足取胜条件,则return true.
// 或者 下一手中无法满足取胜条件, 此时依然当前手胜利.
if(desiredTotal - i <= 0 || !memo_dfs(maxChoosableInteger, desiredTotal - i, memo, state | add_state)){
memo[state] = 1;
return memo[state];
}
}
// 如果遍历所有状态之后依然没有一个状态可以取胜,返回false.
memo[state] = 0;
return memo[state];
}
bool canIWin(int maxChoosableInteger, int desiredTotal) {
vector<int> dp(1 << `maxChoosableInteger`, 2);
// 小剪枝,所有元素的和一半无法到达目标(求和公式),则返回false.
if((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal){
return false;
}
// 记忆化搜索, 状态压缩数组主要是充当记忆化数组。
int res = memo_dfs(maxChoosableInteger, desiredTotal, dp, 0);
if(res == 1){
return true;
}
return false;
}
};
// 注意:
// 1. 虽然是先后手,但是只在一个状态下面遍历,第一个DFS当成先手选数,第二个DFS当成后手选数。
// 最后返回的结果是第一个DFS的返回结果。
// 2. 取胜条件,如果当前加入第 i 个元素之后达到取胜条件,当前手胜利,返回1,则上一手失败.
// 或者返回的下一手无法找到取胜方式,那么当前手胜利.
LeetCode 526 优美的排列
class Solution {
public:
int memo_dfs(vector<int>& dp, int N, int position_index, int state){
if(position_index == N + 1){
return 1;
}
// 灵魂剪枝, 根据之前计算出的次数直接返回.
// 对于0000 => 0010 => 0011, 0000 => 0001 => 0011
// 上述两个状态从0011 => 1111 的过程是一样的,所以只需要计算一次过程即可.
if(dp[state] != -1){
return dp[state];
}
int res = 0;
for(int i = 1; i <= N; i++){
int add_state = 1 << (i - 1);
// 当前状态中含有第 i 个要素.跳过.
if((add_state & state) != 0){
continue;
}
// 满足当前要求的 position_index 与 i 整除.
if(position_index % i == 0 || i % position_index == 0){
res += memo_dfs(dp, N, position_index + 1, state | add_state);
}
}
// 得到当前状态下到达 0011 => 1111 的可满足方案.存储到dp数组中.
dp[state] = res;
return dp[state];
}
int countArrangement(int N) {
vector<int> dp(1 << N, -1);
return memo_dfs(dp, N, 1, 0);
}
};