数量规模不超过20时 要存储每个状态取或不取的状态时可以用状态压缩。
所谓状态压缩,就是用二进制数表示集合 用1代表有该元素,0代表没有该元素
{100101}=37 表示集合中有第1,3,6个元素。 用状压dp存储状态消耗的空间是2^n
例题1:
有一个n*m的矩阵 其中有的格子可以选,有的不能选, 且不能选中相邻的格子
问: 最多能选定多少个格子?
例如下面的格子, 可选的标为1,不可选的标为0
选择的方案是 共3个可选位置
解决方案: 自上而下逐行的选择格子,每行的格子选择限定条件:1 这行原本的限制可选位置、2 这行选的格子不能相邻、3 这行的格子不能和上一行的格子相邻
这一行的选择情况可以用二进制表示,设这行选了now , 这行的限定条件是 lim , 上一行选了 pre
对于条件1: lim | now == lim
条件2: now&(now>>1) ==0
条件3: now&pre==0
完整代码
1 const int MAX_N = 20; 2 const int MAX_M = 20; 3 int state[MAX_N + 1]; 4 int dp[MAX_N + 1][1 << MAX_M]; 5 6 bool not_intersect(int now, int prev) { 7 return (now & prev) == 0; 8 } 9 10 bool fit(int now, int flag) { 11 return (now | flag) == flag; 12 } 13 bool ok(int x) { 14 // 行内自己是否相交 15 return (x & (x / 2)) == 0; 16 } 17 int count(int now) { 18 int s = 0; // 统计 now 的二进制形式中有多少个 1 19 while (now) { 20 s += (now & 1); // 判断 now 二进制的最后一位是否为 1,如果是则累加 21 now >>= 1; // now 右移一位 22 } 23 return s; 24 } 25 26 int main() { 27 int n, m; 28 cin >> n >> m; 29 // 初始化所有数组 30 for (int i = 1; i <= n; ++i) { 31 for (int j = 0; j < m; ++j) { 32 int flag; 33 cin >> flag; 34 state[i] |= (1 << j) * flag; // 将 (i,j) 格子的状态放入 state[i] 中,state[i] 表示第 i 行的可选格子组成的集合 35 } 36 } 37 for (int i = 1; i <= n; ++i) { 38 for (int j = 0; j < (1 << m); ++j) { // 枚举当前行的状态 39 if (!ok(j) || !fit(j, state[i])) { // 如果当前行状态不合法则不执行后面的枚举 40 continue; 41 } 42 int cnt = count(j); // 统计当前行一共选了多少个格子 43 for (int k = 0; k < (1 << m); ++k) { 44 if (ok(k) && fit(k, state[i - 1]) && not_intersect(j, k)) { // 判断前一行是否合法和当前行和前一行的方案是否冲突 45 dp[i][j] = max(dp[i][j], dp[i - 1][k] + cnt); // 更新当前行、当前状态的最优解 46 } 47 } 48 } 49 } 50 int ans = 0; // 保存最终答案 51 for (int i = 0; i < (1 << m); ++i) { 52 ans = max(ans, dp[n][i]); // 枚举所有状态,更新最大值 53 } 54 cout << ans << endl; 55 return 0; 56 }
统计二进制数中1的数目
int count(int x) { int num=0; while(x) { num+=x&1; x>>=1; } return num; }
例子2: 消除回文串
给定一个字符串s 可以从中消除回文串, 回文串不必连续,问最少多少次可以消除整个串
状态d[ i ] 表示消除字符串i 需要的最少步数。
状态转移: if( i串不是回文串) d[ i ] = d [ j ] + d [ j^i]; else d[ i ] = 1; // j 是i串的子集 , j^i 是 j的补集
for (int t = 1; t < (1 << n); t++) { // 枚举当前状态 if(IsPalindrome(t) ) dp[t]=1 ; // 判断当前状态是否是回文,如果是回文则步骤数为 1 else { for(int i = t; i; i = (i - 1) & t) { // 枚举 t 的所有子集 dp[t] = min(dp[t], dp[i] + dp[t ^ i]); // 更新当前状态的解的最小值 } } } printf("%d ", dp[(1 << n) - 1]); // 输出最终答案
//枚举子集 for(int i = t ; i ; i = (i - 1) & t )