动态规划最后的坑
状态压缩DP
动态规划的过程是随着“阶段”增长,在每个状态维度上不断扩展的。在任意时刻,已经求出的最优解的状态与尚未求出最优解的状态在各个维度的分界点组成了DP扩展的“轮廓”。
对于某些问题,我们需要在动态规划的“状态”记录一个集合,保存这个“轮廓”的详细信息,以便进行状态转移。若集合大小不超过N,集合中每个数都是小于K的自然数,则我们可以把集合看做一个N为K进制数,
以一个[0, K^N - 1]之间的十进制整数的形式作为DP状态的一维。
这种把集合转化为整数记录在DP状态中的一类算法,被称为状态压缩动态规划算法
在求解最短Hamilton路径时,整个状态空间可以看作n维,每维代表一个节点,只有0(尚未访问)和1(已经访问)两个值。我们可以想象DP的“轮廓”以“访问过的节点数目”为阶段,从(0,0,...,0)扩展到(1,1,...,1)。为了记录
当前状态在每个维度上的坐标是0还是1,我们使用一个N位二进制数,即[0, 2^n-1]之间的十进制整数存储节点的访问情况。另外,为了知道最后经过的节点是哪一个,我们把该节点编号作为附加信息,也保存在DP状态中。因此
该状态压缩DP的状态数组就由大小分别为2^N和N的两个维度构成。
例题1: Mondriaan's Dream(poj2411)
求把N*M(1 <= N,M <= 11)的棋盘分割成若干个1 * 2的长方形,有多少种方案。例如当N=2, M=3时,有3种方案。
输入包含多个测试用例。每个测试用例由两个整数组成:大矩形的高度H和宽度W。输入由H=W=0终止。否则,1<h,w<11。
Sample Input
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
Sample Output
1
0
1
2
3
5
144
51205
首先假设我们要对棋盘进行切割,有横切和纵切这两种思路,按照DP的思想,为了方便,将每一行划分为阶段,由上一行对下一行进行扩展,
这样我们想对于这一行的切割,有一次切出1*2的长方形和先切出1*1两种方式
例如:
那么这样除了一些符合条件的1*2长方形,还存在一些只被切了一半的1*1的长方形,这些是不符合条件的。我们想根据题目是不能出现只有一半的长方形的
所以这些残缺体必然会在下一行被补齐,这样我们就需要知道哪些是未补齐的残缺体,我们使用一个M位的二进制数,对于这个二进制数的M位,当某一位是1时,我们设这表示有一个残缺体需要补齐,当某一位为0时,表示这是一个
横着的1*2的长方形或是已经补齐的竖着的1*2长方形,总之就是其余状态
这样我们设f[i, j]表示第i行的情况的形态为j的二进制数时,前i行切割的方案总数.(j是用十进制表示的M位二进制数)
思考转移情况
对于第i-1行转移到第i行,形态从k转移到j,当且仅当:
1.j与k按位与后全为0.
这保证了每个数字1下方必须是数字0,代表继续补齐了竖着的长方形
2.j和k按位或后的结果的二进制表示中,每一段连续的0必须是偶数个
这些0代表若干个横着的1*2长方形,奇数个0无法分割出这种形态
但是我们对于j和k的比对计算,是已经默认了j和k都已知了,一方面确实我们可以通过枚举j和k,但是不可避免的会有完全不符合题目的j和k因此我们可以在DP前先预处理出[0, 2^m-1]内所有满足“二进制表示下每一段连续的0有
偶数个”的整数,记录在集合S中。
f[i, j] = ∑f[i - 1, k](j&k==0 并且 j|k∈s)
初值:f[0, 0] = 1,其余均为0
目标:f[N, 0]
时间复杂度O(2^M2^MN) = O(4^MN)(当然不是我自己算的,我不会算=-=)
1 #include<iostream>
2 #include<iomanip>
3 #include<cstdio>
4 #include<ctime>
5 #include<cstring>
6 #include<algorithm>
7 #include<cmath>
8 #include<cstdlib>
9 #define ll long long
10 using namespace std;
11 int n, m;
12 ll f[12][1 << 11];
13 bool vis[1 << 11];
14 int main() {
15 while(cin >> n >> m && n) {
16 memset(vis, 0, sizeof(vis));
17 memset(f, 0, sizeof(f));
18 for(int i = 0; i < 1 << m; ++i) {
19 bool flag1 = 0, flag2 = 0;
20 for(int j = 0; j < m; ++j) {
21 if(i >> j & 1) flag1 |= flag2, flag2 = 0;
22 else flag2 ^= 1;//只要0不是连续的,就激活flag1(flag1 = 1),则此时无论后面flag2为多少,最终都不满足
23 }
24 vis[i] = flag1 | flag2 ? 0 : 1;//flag1 | flag2 == 1,取0,否则取1
25 }
26 f[0][0] = 1;
27 for(int i = 1; i <= n; ++i) {
28 for(int j = 0; j < 1 << m; ++j) {
29 f[i][j] = 0;
30 for(int k = 0; k < 1 << m; ++k)
31 if((j & k) == 0 && vis[j | k])
32 f[i][j] += f[i - 1][k];
33 }
34 }
35 cout << f[n][0] << '\n';//最后分割完的大矩形,一定不存在空缺,其M位二进制数表示一定为0
36 }
37 return 0;
38 }
还是很难想的啊=-=
例题2:炮兵阵地
题目描述
司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队。一个N*M的地图由N行M列组成,地图的每一格可能是山地(用“H” 表示),也可能是
平原(用“P”表示),如下图。在每一格平原地形上最多可以布置
一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其
它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。 现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部
队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
输入输出格式
输入格式:
第一行包含两个由空格分割开的正整数,分别表示N和M;
接下来的N行,每一行含有连续的M个字符(‘P’或者‘H’),中间没有空格。按顺序表示地图中每一行的数据。N≤100;M≤10。
输出格式:
仅一行,包含一个整数K,表示最多能摆放的炮兵部队的数量
题意:在矩形中放下尽量多的十字,并且每个十字的中心不能被别的十字覆盖
将行作为阶段
每个位置能否防止炮兵和他上面两行的对应位置相关,所以向第i行扩展时,需要知道i-1行和i-2行的状态。
我们把每一行的情况用一个[0, 2^M-1]的M位二进制数表示,对于第i个二进制数,第p位为1,表示第i行的第p列放了炮兵,第p位为0,表示第i行第p列没
放炮兵(被覆盖或是有山)
我们先预处理出集合S,存储“相邻两个1的距离不小于3”的所有M位二进制数,表示每一行的炮兵距离不小于3
同样为了进行转移:我们还需要知道每一行有多少个1
设count(x)表示M位二进制数x中1的数量
设vaild(i, x)表示M位二进制数x属于集合S,并且x中的每个1对应在地图第i行的位置是平原。
设f[i, j, k]表示第i行压缩后的状态为j,第i-1行压缩后状态为k时,前i行最多能摆放多少炮兵
f[i, j, k] = max{f[i - 1, k, l] + count(j)}(vaild(i, j) && vaild(i - 1, k) && j &k = 0)
初值:f[0, 0, 0] = 0,其余均为负无穷
目标:max(f[n, j, k])(j|k == 0 && vaild(N,j),vail(N-1,k))
最初存图时,我们把平原存为0,山丘存为1
然后在判断某个情况是否符合是,我们将二进制数与原来的a[i]相&,最后若得到的结果不是0,就说明在这个是山的位置出现了炮兵阵地,不合法,直接
continue;
判断每个状态有没有两个炮兵的距离在两格之内
如果把当前状态的二进制数位左移一位,将这个结果与原状态做一次&运算,结果若不是0,就说明在两格之内的距离内,被放置了炮兵,就是非法状态,
同理我们在左移两格相&进行判断
1 #include<bits/stdc++.h>
2 using namespace std;
3 int n, m, ans = 0xcfcfcfcf;
4 int a[1 << 10], sum[1 << 10];
5 int f[4][1 << 10][1 << 10];
6 char ch;
7
8 inline int read() {
9 int x = 0, y = 1;
10 char ch = getchar();
11 while(!isdigit(ch)) {
12 if(ch == '-') y = -1;
13 ch = getchar();
14 }
15 while(isdigit(ch)) {
16 x = (x << 1) + (x << 3) + ch - '0';
17 ch = getchar();
18 }
19 return x * y;
20 }
21
22 int main() {
23 memset(f, 0xcfcfcf, sizeof(f));
24 n = read(), m = read();
25 for(int i = 1; i <= n; ++i)
26 for(int j = 1; j <= m; ++j) {
27 cin >> ch;
28 a[i] <<= 1;
29 a[i] += (ch == 'H') ? 1 : 0;
30 //a[i] <<= 1;//把这个放在这个位置就会wa=-=
31 }
32 for(int i = 0; i < 1 << m; ++i) {
33 int sum1 = 0;
34 for(int j = 0; j < m; ++j)
35 if(i >> j & 1) sum1++;
36 sum[i] = sum1;
37 }
38 f[0][0][0] = 0;
39 for(int i = 1; i <= n; ++i) {
40 for(int j = 0; j < 1 << m; ++j) {
41 if(j & (j << 1) || j & (j << 2) || j & a[i])
42 continue;
43 for(int k = 0; k < 1 << m; ++k) {
44 if(j & k || k & a[i - 1] || k & (k << 1) || k & (k << 2))
45 continue;
46 for(int l = 0; l < 1 << m; ++l) {
47 if(j & l || k & l || l & a[i - 2] || l & (l << 1) || l & (l << 2))
48 continue;
49 f[i%3][j][k] = max(f[i%3][j][k], f[(i - 1)%3][k][l] + sum[j]);
50 }
51 }
52 }
53 }
54 for(int i = 0; i < 1 << m; ++i)
55 for(int j = 0; j < 1 << m; ++j)
56 ans = max(ans, f[n%3][i][j]);
57 cout << ans << '\n';
58 return 0;
59 }
但是炮兵阵地这道题,我的写法并不优美,跑起来非常的慢=-=建议不要看