我理解的状态压缩dp问题是这样的:
给出你n个物品,你要在这n个物品中选出一些物品(或一个都不选),那么对于每个物品你都有两种选择(选或者不选),这样,一共有2^n种情况。而n<=20通常(2^n<=10^6+x),然后我们就枚举 1 - 2^n,每个数分别对应一个二进制数,这个二进制数又可表示我们取物品的一种状态。
如:总共有5个物品,则最多有32种取物品的情况,13 对应5位的二进制数01101,就表示取物品2,3,5。对于每个5位的二进制数我们都用5次位运算判断它的各个位是1还是0。本来每个状态要用5个空间存,现在可以用一个数字来表示。
简而言之就是,通过枚举数字来枚举选择的情况。
涉及到的一些位运算
i & (1 << j) 表示 i 这种状态时,取了第 j 个物品
i | (1 << j) 得到在 i 这种状态下,再取第 j 个物品后的状态
i & ~(1 << j) 得到在 i 这种状态下,去掉第 j 个物品的状态
( i & (i << 1)) || ( i & (i >> 1)) 表示 i 状态下,取了两个相邻的元素
i & j 表示相邻的两行分别取 i 状态和 j 状态时,在某一列取了两个元素
j != (i & j) j 状态中包含 i 状态时不取的元素
下面是一些状态压缩dp入门题
题目:HDU1074 http://acm.hdu.edu.cn/showproblem.php?pid=1074
题意:小明做功课,给出每项功课的名字,最后期限和完成所花时间,小明开始做一个功课之后他就会把它做完,如果完成功课的时间比最后期限晚就会扣分,扣的分数=完成该功课的时间 - 该功课的最后期限。求小明扣分最少时完成功课的顺序,多种方案时输出字典序小的。
思路:小明已经完成的功课的集合是我们要压缩的状态,已完成功课的集合为done,dp[done]记录完成它们最少扣的分数,然后在done中加入一个未完成的功课 i ,比较更新dp[done + i]
代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#define MOD 1000000007
#define INF 0x7fffffff
using namespace std;
typedef long long ll;
int d[20], c[20], dp[1 << 15], add[1 << 15];
char sub[20][105];
void showpath(int x)
{
if(x == 0) return;
showpath(x & ~(1 << add[x]));
printf("%s\n", sub[add[x]]);
}
int main()
{
#ifdef LOCAL
freopen("dpdata.txt", "r", stdin);
#endif
int t, n;
scanf("%d", &t);
while(t--)
{
scanf("%d", &n);
for(int i = 0; i < n; i++)
{
scanf(" %s %d%d", sub[i], &d[i], &c[i]);
}
for(int i = 0; i <= (1 << n); i++)
{
dp[i] = INF;
}
dp[0] = 0;
for(int done = 0; done <= (1 << n) - 1; done++)
{ //枚举已经完成的作业的集合
int time = 0; //当前完成功课总用时
for(int i = 0; i < n; i++)
{
if(done & (1 << i)) time += c[i];
}
//printf("done=%d time=%d\n", done, time);
for(int i = 0; i < n; i++)
{
if(!(done & (1 << i)) && dp[done] != INF)
{
int cur = 0;
if(time + c[i] > d[i]) cur = time + c[i] - d[i]; //算出当前状态下完成第 i 个作业要扣的分数
//printf("i=%d cur=%d\n", i, cur);
if(dp[done] + cur < dp[done | (1 << i)]) //比较更新
{
dp[done | (1 << i)] = dp[done] + cur;
add[done | (1 << i)] = i; //记录完成作业集合为done + i时,最后完成的作业是i
//printf("%d-%d\n", done | (1 << i), dp[done | (1 << i)]);
}
}
}
}
printf("%d\n", dp[(1 << n) - 1]);
showpath((1 << n) - 1);
}
return 0;
}
题目:HDU 1065 http://acm.hdu.edu.cn/showproblem.php?pid=1565
题意:在一个n*n的棋盘里,每格有一个非负数,取出若干个数,满足每两个个数所在格子没有公共边,求所取的数和的最大值。
代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#define MOD 1000000007
#define INF 0x7fffffff
using namespace std;
typedef long long ll;
int check[1 << 20], grid[25][25], pre[1 << 20], cur[1 << 20], can[1 << 20];
int main()
{
#ifdef LOCAL
freopen("dpdata.txt", "r", stdin);
#endif
//printf("cnt=%d\n", cnt);
int n;
while(scanf("%d", &n) != EOF)
{
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
scanf("%d", &grid[i][j]);
}
}
memset(check, 0, sizeof(check));
int cnt = 0;
for(int i = 0; i <= (1 << n) - 1; i++)
{ //先得到只考虑一行时,符合条件的状态,装入can[],减少枚举状态数
int flag = 1;
for(int j = 1; j < n; j++)
{
if((i & (1 << (j - 1))) && ( i & (1 << j)))
{ //同一行中第j格与第j- 1格都取了,不符合
flag = 0; break;
}
}
if(flag)
{
//printf("add i=%d\n", i);
check[i] = 1;
can[cnt++] = i;
}
}
memset(pre, 0, sizeof(pre));
memset(cur, 0, sizeof(cur));
for(int lay = 0; lay < n; lay++)
{
for(int i = 0; i < cnt; i++)
{ //枚举第lay行的状态,计算这样取这行取了多少格
//if(lay && pre[i] == 0) continue;
int add = 0;
for(int k = 0; k < n; k++)
{
if(can[i] & (1 << k))
{
add += grid[lay][k];
}
}
for(int j = 0; j < cnt; j++)
{ //枚举lay-1行的状态
if(!(can[i] & can[j])) //没有取上下相邻的格
{
cur[can[i]] = max(cur[can[i]], pre[can[j]] + add);
}
}
}
for(int i = 0; i < cnt; i++)
{
pre[can[i]] = cur[can[i]];
cur[can[i]] = 0;
}
}
int ans = 0;
for(int i = 0; i < cnt; i++)
{
if(ans < pre[can[i]]) ans = pre[can[i]];
}
printf("%d\n", ans);
}
return 0;
}
题目:POJ 3254 http://poj.org/problem?id=3254
题意:m * n地,在上面种草,标记为1的格子才能种,任意两个种草的格子没有公共边,问有多少种种草的方案(可以一棵草也不种)。
思路:dp[ f ][ i ] 表示只考虑了前 f 行,在第 f 行种草状态为 i 的方案数
代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#define MOD 1000000000
#define INF 0x7fffffff
using namespace std;
typedef long long ll;
int a[15], dp[15][1 << 13];
int main()
{
#ifdef LOCAL
freopen("dpdata.txt", "r", stdin);
#endif
int n, m, aa;
while(scanf("%d%d", &n, &m) != EOF)
{
memset(a, 0, sizeof(a));
for(int i = 0; i < n; i++)
{
for(int j = 0; j < m; j++)
{
scanf("%d", &aa);
if(aa) a[i] |= (1 << j);
}
}
memset(dp, 0, sizeof(dp));
for(int i = 0; i < (1 << m); i++)
{
if(i != (i & a[0])) continue;
if((i & (i << 1)) || (i & (i >> 1))) continue;
dp[0][i] = 1;
}
for(int f = 1; f < n; f++)
{
for(int i = 0; i < (1 << m); i++)
{
if(i != (i & a[f])) continue; //i状态在不该种草的地方种草了
if((i & (i << 1)) || (i & (i >> 1))) continue; //左右相邻的格子种草了
for(int j = 0; j < (1 << m); j++)
{
if(j != (j & a[f - 1])) continue;
if(i & j) continue; //上下相邻的格子种草了
dp[f][i] += dp[f - 1][j];
dp[f][i] %= MOD;
}
}
}
int ans = 0;
for(int i = 0; i < (1 << m); i++)
{
ans += dp[n - 1][i];
ans %= MOD;
}
printf("%d\n", ans);
}
return 0;
}
题目:POJ 1185 http://poj.org/problem?id=1185
题意:n*m的地方,标记为P的地方可以安排炮兵,炮兵攻击的范围为“十字”,向上下左右各攻击两格(看图),安排炮兵是每两个炮兵不能彼此攻击到,问最多能安排多少个炮兵。
思路:一开始我没用三维数组,枚举f,f-1,f-2行的状态,转移的时候实际上只考虑了 f 和 f - 1 不冲突,f 和 f - 2 不冲突, f - 1 和 f - 2 不冲突,但不能保证 f - 1 和 f - 2 - 1 不冲突,所以是错的。枚举没一行的状态时都要考虑上一行的状态和上上一行的状态,上一行的状态又是和上上一行的上一行有关的,所以dp数组需要增加一个维度,dp[ f ][ i ][ j ] 表示第 f 行取 i 状态同时 f - 1 行取 j 状态时能得到的最大值。
代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#define MOD 1000000007
#define INF 0x7fffffff
using namespace std;
typedef long long ll;
int dp[105][100][100], add[100], can[100], a[105], cnt;
void init(int m)
{ //先找出只有一行时可以取的状态,减少枚举量,同时算出各种取法取了多少个位置
memset(can, 0, sizeof(can));
memset(add, 0, sizeof(add));
cnt = 0;
for(int i = 0; i < (1 << m); i++)
{
if((i & (i << 1)) || (i & (i << 2)) ||
(i & (i >> 1)) || (i & (i >> 2))) continue;
can[cnt] = i;
for(int j = 0; j < m; j++)
if(i & (1 << j)) add[cnt]++;
cnt++;
}
memset(dp, 0, sizeof(dp));
}
int main()
{
#ifdef LOCAL
freopen("dpdata.txt", "r", stdin);
#endif
int n, m;
char aa;
while(scanf("%d%d", &n, &m) != EOF)
{
memset(a, 0, sizeof(a));
for(int i = 0; i < n; i++)
{
for(int j = 0; j < m; j++)
{
scanf(" %c", &aa);
if(aa == 'P') a[i] |= (1 << j);
}
}
init(m);
//printf("cnt=%d\n", cnt);
int ans = 0;
for(int i = 0; i < cnt; i++)
{
if(can[i] != (can[i] & a[0])) continue;
dp[0][i][0] = add[i];
ans = max(add[i], ans);
}
for(int f = 1; f < n; f++)
{ //枚举层
for(int i = 0; i < cnt; i++)
{ //枚举当前层的状态
if(can[i] != (can[i] & a[f])) continue; //当前状态取了不该取的位置
for(int j = 0; j < cnt; j++)
{
if(can[j] != (can[j] & a[f - 1])) continue;
if(can[j] & can[i]) continue; //当前层与上一次层冲突
if(f == 1)
{
dp[1][i][j] = max(dp[1][i][j], dp[0][j][0] + add[i]);
ans = max(dp[1][i][j], ans);
continue;
}
for(int k = 0; k < cnt; k++)
{
if(can[k] != (can[k] & a[f - 2])) continue;
if(can[k] & can[j]) continue;
if(can[k] & can[i]) continue;
dp[f][i][j] = max(dp[f][i][j], dp[f - 1][j][k] + add[i]);
//printf("dp[%d][%d][%d]=%d\n", f, i, j, dp[f][i][j]);
ans = max(dp[f][i][j], ans);
}
}
}
}
printf("%d\n", ans);
}
return 0;
}
题目:HDU 4539 http://acm.hdu.edu.cn/showproblem.php?pid=4539
题意:和上一题差不多,也要三维数组,攻击范围不同
代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#define MOD 1000000007
#define INF 0x7fffffff
using namespace std;
typedef long long ll;
int dp[105][200][200], can[200], add[200], cnt, a[105];
void init(int m)
{
memset(can, 0, sizeof(can));
memset(add, 0, sizeof(add));
cnt = 0;
for(int i = 0; i < (1 << m); i++)
{
if((i & (i << 2)) || (i & (i >> 2))) continue;
can[cnt] = i;
for(int j = 0; j < m; j++)
{
if(i & (1 << j)) add[cnt]++;
}
cnt++;
}
memset(dp, 0, sizeof(dp));
}
int main()
{
#ifdef LOCAL
freopen("dpdata.txt", "r", stdin);
#endif
int n, m, aa;
while(scanf("%d%d", &n, &m) != EOF)
{
memset(a, 0, sizeof(a));
for(int i = 0; i < n; i++)
{
for(int j = 0; j < m; j++)
{
scanf("%d", &aa);
if(aa) a[i] |= (1 << j);
}
}
init(m);
int ans = 0;
for(int i = 0; i < cnt; i++)
{
if(can[i] != (can[i] & a[0])) continue;
dp[0][i][0] = add[i];
ans = max(add[i], ans);
}
for(int f = 1; f < n; f++)
{
for(int i = 0; i < cnt; i++)
{
if(can[i] != (can[i] & a[f])) continue;
for(int j = 0; j < cnt; j++)
{
if(can[j] != (can[j] & a[f - 1])) continue;
if(can[i] & (can[j] >> 1)) continue;
if(can[i] & (can[j] << 1)) continue;
if(f == 1)
{
dp[1][i][j] = max(dp[1][i][j], dp[0][j][0] + add[i]);
ans = max(dp[1][i][j], ans);
continue;
}
for(int k = 0; k < cnt; k++)
{
if(can[k] != (can[k] & a[f - 2])) continue;
if(can[k] & (can[j] >> 1)) continue;
if(can[k] & (can[j] << 1)) continue;
if(can[k] & can[i]) continue;
dp[f][i][j] = max(dp[f][i][j], dp[f - 1][j][k] + add[i]);
ans = max(dp[f][i][j], ans);
}
}
}
}
printf("%d\n", ans);
}
return 0;
}
题意:给出0-n个点间两两到达的距离,求从0出发,1-n中每个点至少经过一次,然后回到0点的所需走的最短路径
思路:读完题目容易想到旅行商问题(TSP),同样将已经到达过的点的集合作为状态,但要注意的是,点是可以重复到达的。dp[s][v]记录的是走过点集合s最后停在v的最短路径长度。最主要是要想好循环的次序。我的程序第一层循环是枚举集合s,第二层循环枚举走过集合s后再走到的最后一个点u(可以在集合s中也可以不在集合s中),第三层循环枚举在集合s中,连接最后一个u的点v。建议结合样例,把dp数组更新的过程打出来感受一下。
代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#define MOD 1000000007
#define INF 0x7fffffff
using namespace std;
typedef long long ll;
int d[15][15], dp[1 << 12][15];
int main()
{
#ifdef LOCAL
freopen("dpdata.txt", "r", stdin);
#endif
int n;
while(scanf("%d", &n) != EOF)
{
if(!n) break;
memset(d, 0, sizeof(d));
for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= n; j++)
{
scanf("%d", &d[i][j]);
}
}
n++;
for(int s = 0; s < (1 << n); s++)
{
for(int v = 0; v < n; v++)
{
dp[s][v] = INF;
}
}
dp[0][0] = 0;
for(int s = 0; s < (1 << n); s++)
{ //枚举已经走过的点的集合
//printf("s=%d\n", s);
for(int u = 0; u < n; u++)
{ //从集合s中的某点v走到u
for(int v = 0; v < n; v++)
{
if(dp[s][v] != INF)
{
{
dp[s | (1 << u)][u] = min(dp[s | (1 << u)][u], dp[s][v] + d[v][u]);
//printf("dp %d %d %d\n", s | (1 << u), u, dp[s | (1 << u)][u]);
}
}
}
}
}
int ans = INF;
for(int i = 0; i < n; i++)
{
ans = min(ans, dp[(1 << n) - 1][i] + d[i][0]);
}
printf("%d\n", ans);
}
return 0;
}
题目:POJ 2411 http://poj.org/problem?id=2411
题意:给出一个h*w的长方形,你用许多个1*2的小长方形铺满它,小长方形不能重叠,问有多少钟铺法。
思路:这题枚举两行的状态后判断可不可行的方法我没想到,是看了网上题解才明白的。我们一行行地枚举铺砖的状态,每铺新的一行,上一行都要铺满。
首先,第一行如果有空缺,那么它在还没铺满前必定是由小长方形横向铺成的,容易想到要先找出这些状态。
然后,枚举铺新一行的状态,再枚举上一行铺成的状态,如果上一行的状态有空缺,那么空缺的位置肯定是由一个竖着放的小长方形铺的,则新一行的这个位置一定不能为空,所以两行中每一列至少有一格被铺了。
我们可以通过上一行的状态确定新行状态的什么位置铺了竖着的小长方形(即上一行状态的空缺位置),则当前状态剩下的被覆盖位置一定是被横着的小长方形铺的,这又用回了第一行找出来的状态。
代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#define MOD 1000000007
#define INF 0x7fffffff
using namespace std;
typedef long long ll;
ll dp[15][1 << 12];
int check[1 << 12];
void init(int m)
{ //只由小长方形横着铺成的一行的状态
memset(check, 0, sizeof(check));
memset(dp, 0, sizeof(dp));
for(int i = 0; i < (1 << m); i++)
{
int x = i, cur = 0;
while(x)
{
if(cur)
{
if(x & 1) cur = 0;
else break;
}
else if(x & 1) cur = 1;
x >>= 1;
}
if(!cur)
{
check[i] = 1; dp[0][i] = 1;
}
}
}
int main()
{
#ifdef LOCAL
freopen("dpdata.txt", "r", stdin);
#endif
int n, m;
while(scanf("%d%d", &n, &m) != EOF && (n + m))
{
init(m);
for(int f = 1; f < n; f++)
{
for(int i = 0; i < (1 << m); i++)
{
for(int j = 0; j < (1 << m); j++)
{
if((i | j) != (1 << m) - 1) continue; //有一列一格也没,则铺完f行,f-1行的空缺还会在,后面也不可能铺满
if(!check[i & j]) continue; // i & j 排除了被竖着小长方形铺的格子
dp[f][i] += dp[f - 1][j];
}
}
}
printf("%I64d\n", dp[n - 1][(1 << m) - 1]);
}
return 0;
}