状态压缩动态规划算法
状态压缩动态规划是动态规划的一种,它通过使用位运算的方式压缩程序占用的空间,对于可以用来解决一些只有两个状态(是与否)的问题。
多少无益,我们通过下面的一道编程题目来学习这种算法。
题目描述:
小明使用霰弹枪打固定靶,假设有M个目标,小明开了N枪。每一枪都有若干目标被命中。现在小明想知道,自己刚刚理论上最少只需要几枪就能打中全部的目标。
假设有3个目标:[0, 0, 0]
小明开了3枪:[1, 0, 0],[0, 1, 0],[0, 1, 1],其中为1表示命中了对应的目标。
结果输出:小明第一枪加第三枪命中了所有的目标,理论上只要两枪就可以全部命中。
假设有3个目标:[0, 0, 0]
小明开了3枪:[1, 0, 0],[0, 1, 0],[1, 1, 0],其中为1表示命中了对应的目标。
结果输出:小明无法全部命中目标。
分析:
这道题有很多不同解法,可以使用贪心算法,但是为了讲解状压动规,这里不使用贪心算法。
使用动态规划解决问题的第一步就是要确定使用动态规划能否解决问题。题干中有一个关键词,最少几枪。更关键的一点是它没有要求我输出到底是哪几枪(如果要求输出哪几枪可能就要用回溯子集了)。因此大家应该条件反射的尝试使用动态规划。
动态规划最重要的一个步骤就是确定动态数组dp与状态转移矩阵。
确定dp数组的含义
首先确认dp的大小,有的同学可能条件反射的认为dp的大小应该是M,对应M个目标。dp[i]表示命中i个目标需要的最小枪数。但是接下来会发现举步维艰,因为都是命中i个目标,如果打中不一样的靶子,反应的是不同的状态。实际上这道题的dp矩阵的索引的含义并不是直接了当的呈现在我们的面前。
提问:当我有M个目标时,我有多少种命中状态?
答:每个目标都有命中与未命中两种状态,M个目标组合起来应该是2的M次方。
聪明的读者到这里应该恍然大悟一下:原来dp要有2^M个元素,索引反应了目标的命中情况。假设dp[3] = 2,indx = 3的二进制是011b,这里表示命中前两个目标最少需要2枪。
dp数组初始化
明确了dp数组的含义,就需要对dp数组进行初始化了,结合dp数组的含义,我们应该首先明确:
dp[0] = 0:命中0个目标需要开0枪。
假设小明第2枪命中情况为:[1, 0, 1],那么:
dp[101b] = dp[5] = 1:命中第一个与第三个目标只需要一枪。
我们将命中目标用二进制掩码表示,将所有子弹命中的情况对应的dp[indx]置为1即可完成初始化。
这就是这个问题的解决思路,现在我们直接上代码,一些细节用代码来讲述。
完整代码
#include <iostream>
#include <vector>
#include <string>
#include <climits>
using namespace std;int main() {int M, N; // M个目标 开N枪cin >> M >> N; // 输入 M 与 Nvector<int> state; // 装填掩码for (int i = 0; i < N; i++) {int s = 0;for (int j = 0; j < M; j++) {int tmp = 0;cin >> tmp; // 输入第i枪命中情况,1:命中,0:未命中s = (s << 1) + tmp;}state.push_back(s);}// 对state内的所有数据按位取与,如果tmp = 2^M -1,证明N枪后所有目标都命中int tmp = 0;for (int i : state) {tmp |= i;}if (tmp != (1 << M) - 1) { cout << "未命中所有目标" << endl;return 0;}// 初始化dp数组int num = (1 << M) - 1;vector<int> dp(num + 1, INT_MAX);dp[0] = 0;for (int i : state) {dp[i] = 1;}// 状态转移,讲解见后文for (int mask : state) {for (int indx = num; indx >= 0; indx--) {if (dp[indx] != INT_MAX) {int new_indx = indx | mask;if (dp[indx] + 1 < dp[new_indx]) {dp[new_indx] = dp[indx] + 1;}}}}cout << "最少需要" << dp[num] << "枪" << endl;return 0;
}
状态转移分析
假设当前命中状态为x0
,在第k发子弹的射击下,命中状态x1
变为x1 = x0 | state[k];
。
从状态x0到状态x1,射击次数变为:tmp_num = dp[x0] + 1
。
结合dp[x1]
的定义:命中状态为x1
时,需要最少的的射击次数。因此我们需要比较通过x0
转移到x1
的射击次数tmp_num
与dp[x1]
目前的值:
伪代码:
int x1 = x0 | state[k]; // 这里表示一次射击动作
int tmp_num = dp[x0] + 1;
if(tmp_num < dp[x1]) { dp[x1] = tmp_num; // 证明由x0到达x1比原本的方案更快 }
else { ; // 空操作,证明当前到达x1有更快的射击方案 }
小结
上面的讲解就是状态压缩动态规划的一个使用案例与核心逻辑。
之所以选择这种方法有三个关键要素:
1、求最优方案。
2、不要求输出最优方案的具体路径信息。
3、对于最小元素(一个目标),只有是和否(命中/未命中)两种区别。