前言:
最近学了这个神奇的东东,真的好难啊[仙女叹气]~
感觉自己没大理解,就写笔记强迫自己学会吧QAQ
定义:
状态压缩动态规划,就是我们俗称的状压 D P DP DP,是利用计算机二进制的性质来描述状态的一种 D P DP DP方式。
自己的小理解:其实就是将状态压缩成二进制下的数( 1 / 0 1/0 1/0)来表示不同情况,进行 D P DP DP……
前置知识:
- 二进制 和 位运算;
- D P DP DP;
- 以及一个聪明的小脑袋瓜;
位运算介绍:
一个神奇的东西……
状压 D P DP DP里主要用到的几个技巧见下图:
引入:
题目 :互不侵犯
题意简述:
在 N × N N×N N×N的棋盘里面放 K K K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到他周围的8个格子。
题目分析:
d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示第 i i i行状态为 j j j,一共放了 k k k个国王时的方案数。
当前方案数仅与上一行有关 ( ( ( 每一行只需要判断与上一行是否攻击 ) ) )
状态转移方程:
d p [ i ] [ j ] [ k ] + = d p [ i − 1 ] [ s ] [ k − c n t ( s ) ] dp[i][j][k] += dp[i - 1][s][k - cnt(s)] dp[i][j][k]+=dp[i−1][s][k−cnt(s)]
s s s为上一行满足条件的状态, c n t cnt cnt是上一行摆放的国王数量。
最终答案:
再次枚举每一个状态:
a n s + = d p [ n − 1 ] [ s ] [ k ] ans += dp[n - 1][s][k] ans+=dp[n−1][s][k];
完整代码:
#include <cstdio>
#include <cmath>
#include <algorithm>
#define ll long long//不开long long见祖宗
using namespace std;
int n,k,cnt[1 << 15];
ll ans,dp[15][1 << 15][100];//状态:1表示该位置放了国王,0没有放
bool flag[1 << 15];
int Count(int x) {
int res = 0;
while(x) {
res += x & 1;
x >>= 1;
}
return res;
}
bool check1(int x) {
for(int i = 0; i + 1 < n; i ++) {
if((x & (1 << i)) && (x & (1 << (i + 1))))
return 0;//如果有相邻的国王,不符条件
}
return 1;
}
bool check2(int x,int y) {
for(int i = 0; i < n; i ++) {
if(x & (1 << i)) {
if(y & (1 << i)) return 0;//上为1
if(i + 1 < n && (y & (1 << (i + 1)))) return 0;//左上为1
if(i - 1 < n && (y & (1 << (i - 1)))) return 0;//右上为1
}
}
return 1;
}
signed main() {
scanf("%d %d",&n,&k);
int m = 1 << n;
for(int i = 0; i < m; i ++) {
flag[i] = check1(i);
cnt[i] = Count(i);
}//预处理,避免重复
for(int i = 0; i < n; i ++) {
for(int j = 0; j < m; j ++) {
if(!flag[j]) continue;
if(!i) dp[i][j][cnt[j]] = 1;
else {
for(int h = Count(j); h <= k; h ++) {//枚举此时的国王总数
for(int a = 0; a < m; a ++) {//枚举上一行的状态
if(!flag[a] || !check2(j,a)) continue;
dp[i][j][h] += dp[i - 1][a][h - cnt[j]];
}
}
}
}
}
for(int i = 0; i < m; i ++) ans += dp[n - 1][i][k];//从0开始,共有n-1行
printf("%lld\n",ans);
return 0;
}
总结:
状压的入门题,思路挺好想的,只是要注意细节。
正文:
题目一 :炮兵阵地
题意简述:给定一个 N ∗ M N * M N∗M的初始状态,规定了哪些位置可以放炮兵。要求每个炮兵前后左右两格不能有他人。求最多可以放多少兵。
数据范围: 1 ≤ N ≤ 100 1 \leq N \leq 100 1≤N≤100, 1 ≤ M ≤ 10 1≤M≤10 1≤M≤10.
思路:
因为每一行的情况与前两行有关,就要定义三个状态,又因为当前状态可以有前一行转移而来,所以只用定义当前状态和前一行的状态,前前行的状态可以由前一行表示,所以定义:
d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k],表示第 i − 1 i-1 i−1行状态为 j j j,第 i i i状态为 k k k,
然后再循环枚举 d p [ i − 1 ] [ h ] [ j ] dp[i - 1][h][j] dp[i−1][h][j](表示第 i − 2 i-2 i−2行状态为 h h h),
这样就可以确定三行的状态
又因为很多状态是不符合题意的,直接用 d p dp dp存储状态太浪费空间。所以可以提前预处理出满足条件的状态,用 m a p [ ] map[] map[]存下来,重新定义 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]为第 i − 1 i-1 i−1行状态为 m a p [ j ] map[j] map[j],第 i i i状态为 m a p [ k ] map[k] map[k] 时的最大值。这样就轻松多啦~
其它讲解都在代码里啦~
完整代码:
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,num,ans,cnt,sum[1050],map[1050],f[105],dp[2][1050][1050];
char s[15];
int c(int x) {//计算x状态中 1的个数
int res = 0;
while(x) {
if(x & 1) res++;
x >>= 1;
}
return res;
}
int main() {
scanf("%d %d",&n,&m);
for(int i = 1; i <= n; i ++) {
scanf("%s",s + 1);
for(int j = 1; j <= m; j ++) f[i] = (f[i] << 1) + (s[j] == 'P');//预处理初始状态
}
num = 1 << m;
for(int i = 0; i < num; i ++) { //预处理满足条件的状态
if((i & (i << 1)) || (i & (i << 2)) || ( i & (i >> 1)) || (i & (i >> 2))) continue;//如果左右有相邻的情况,跳过
map[++cnt] = i;
sum[cnt] = c(i);
//记录满足条件的状态及其 1的个数
if((i & f[1]) == i) dp[1][0][cnt] = sum[cnt];//初始化第1行
//如果当前状态与第一行的状态相符,直接赋值 (上一行没有所以状态为0)
}
for(int i = 1; i <= cnt; i ++) {//初始化第2行
for(int j = 1; j <= cnt; j ++) {
if(!(map[i] & map[j]) && (f[2] & map[j]) == map[j]) dp[0][i][j] += dp[1][0][i] + sum[j];
//如果 i状态和 j状态满足条件(同一位置不存在都有炮兵的情况) 当前行的值就可以加上 上一行的i状态的值 再加上 当前行j状态 1的数量
}
}
for(int i = 3; i <= n; i ++) {//从第三行开始
for(int j = 1; j <= cnt; j ++) {//枚举当前状态
if((f[i] & map[j]) != map[j]) continue;//如果当前情况不满足初始条件,跳过
for(int k = 1; k <= cnt; k ++) {//枚举上一行状态
if(map[j] & map[k]) continue;//如果两行不满足条件(同一位置都有炮兵),跳过
for(int h = 1; h <= cnt; h ++) {//枚举上上行的状态
if(!(map[k] & map[h]) && !(map[j] & map[h])) //判断是否满足情况
dp[i & 1][k][j] = max(dp[i & 1][k][j],dp[(i - 1) & 1][h][k] + sum[j]);//取最大值
}
}
}
}
for(int i = 1; i <= cnt; i ++) {
for(int j = 1; j <= cnt; j ++) {
ans = max(ans,dp[n & 1][i][j]);//枚举所有情况,取最大值
}
}
printf("%d",ans);
return 0;
}
知识点小结:
另外,这里用到了一个状压小技巧。很多时候题目会给定初始状态,就限制了之后的状态枚举。这里就再次用到了二进制小技巧。
这里就以这道题为例:
for(int j = 1; j <= m; j ++)
f[i] = (f[i] << 1) + (s[j] == 'P');
如果当前点可以放炮兵,我们就在 f [ i ] f[i] f[i]的值左移一位的基础上,再加1,最后 f [ i ] f[i] f[i]的值就是当前行的最大值,枚举时的状态不能超过 f [ i ] f[i] f[i].
其实这里就是在模拟二进制的形成,计算出满足题意的最大状态值。
题目二 :Fish
题意简述:有n条鱼,编号从1到n。每对鱼相遇的概率是一样的。如果两条标号为i和j的鱼见面,第一只吃了第二只的概率为 p [ i ] [ j ] p[i][j] p[i][j],则第二只吃掉第一只的概率为 1 − p [ i ] [ j ] 1 - p[i][j] 1−p[i][j]。求每只鱼最后存活在湖里的可能性。
思路:
概率 + 状压 d p dp dp
讲解都在代码里
完整代码:
#include<cstdio>
#include<algorithm>
using namespace std;
int n;
double p[25][25],dp[1 << 20];//dp[i],出现i状态的概率(1:这条鱼活着/0:它被吃啦)
int c(int x) {//计算1的个数
int res = 0;
while(x) {
res += (x & 1);
x >>= 1;
}
return res;
}
int main() {
scanf("%d",&n);
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
scanf("%lf",&p[i][j]);
int num = (1 << n) - 1;
dp[num] = 1;//初始状态,全部鱼都活着的概率为1
for(int i = num - 1; i; i --) {//倒序枚举状态,鱼越吃越少,1的数量也越来越少……这残忍的现实!
int cnt = c(i);//活着的鱼的数量
for(int j = 1; j <= n; j ++) {//枚举这一轮被吃到的鱼的序号
if((i & (1 << (j - 1)))) continue;//如果在当前状态下,j为1(鱼没有被吃了),跳过
for(int k = 1; k <= n; k ++) {//枚举k条鱼吃掉的鱼的编号
if(!(i & (1 << (k - 1)))) continue;//如果在当前状态下,k为0(鱼已经被吃了,k吃不到j),跳过
dp[i] += dp[i | (1 << (j - 1))] * p[k][j] / (1.0 * (cnt + 1) * cnt / 2.0);
//否则,概率为 当前概率 加上 j位存活时的概率 * k条鱼吃掉j条鱼的概率 * 在所有活着的鱼中恰好选到j,k的概率。
}
}
}
for(int i = 0; i < n; i ++) printf("%.6lf ",dp[1 << i]);//只有当前位为1的状态
return 0;
}
题目三 :奖励关
题意简述:共有 K K K轮,有 n n n种物品,每一轮出现每一种物品的概率 1 n \frac{1}{n} n1,物品可选可不选,对于选每一种物品,必须要在前面的轮先选给定的部分物品,每一种物品的价格可正可负。求 k k k轮后按最优方案选择的期望价格。
数据范围: 1 ≤ K ≤ 100 1\leq K \leq 100 1≤K≤100 , 1 ≤ n ≤ 15 1 \leq n \leq 15 1≤n≤15 1 ≤ K ≤ 100 1≤K≤100 1≤K≤100, 1 ≤ n ≤ 15 1≤n≤15 1≤n≤15.
思路:
首先看题,概率 d p dp dp没得跑。
再看数据范围,哦豁!状压 d p dp dp!
这道题不同的是需要倒推,因为正推的话有些情况在转移时选择宝物的概率并不是平均的(有宝物合集的限制),这样就会导致结果出现问题,且最终答案的状态表示十分麻烦(不要问我是怎么知道的!),因此选择倒推。
另外,因为 d p dp dp的转移至于当前局和前一局有关,所以可以用滚动数组优化一下。
详细讲解都在代码里啦~
完整代码:
#include<cstdio>
#include<algorithm>
using namespace std;
int K,n,x,w[20],num[20];
double p,dp[2][1 << 15];
int main() {
scanf("%d %d",&K,&n);
p = 1.0 / n;//选择宝物的平均概率
for(int i = 1; i <= n; i ++) {
scanf("%d",&w[i]);//分值
while(~scanf("%d",&x) && x)
num[i] += 1 << (x - 1);//用二进制数存储 宝物集合
}
int maxn = 1 << n;
for(int i = K; i >= 1; i --) { //游戏轮数
for(int j = 0; j < maxn; j ++) {
dp[i & 1][j] = 0;//初始化(清空上一次循环的值)
for(int k = 1; k <= n; k ++) {//枚举这轮抽到的宝物
if((j & num[k]) == num[k])//如果满足k宝物的 宝物集合要求,
dp[i & 1][j] += max(dp[(i + 1) & 1][j | (1 << (k - 1))] + w[k],dp[(i + 1) & 1][j]);
//满足最优策略,在拿k宝物和不拿之间选最大值
else dp[i & 1][j] += dp[(i + 1) & 1][j];//不满足的话,该状态及为上一次的状态,没有变化
}
dp[i & 1][j] *= p;//最终期望要乘上随机选择一个宝物的概率
}
}
printf("%.6lf",dp[1][0]);
//因为是倒推的,所以最后答案为游戏开始时,未得到任何一个宝物的情况
return 0;
}
题目四 :Survival
题意简述:有 n n n个 B o s s Boss Boss,其中有 n − 1 n-1 n−1个小 b o s s boss boss必须先全部打完才能打第 n n n个大 B O S S BOSS BOSS,打一个小 b o s s boss boss要耗体能 u s e i usei usei,打完后恢复一部分 v a l i vali vali,一开始体能为 100 100 100,在打的过程中也最多为 100 100 100,问能打完全部的 B O S S BOSS BOSS? 能 输出 c l e a r ! ! ! clear!!! clear!!! 否则输出 t r y try try a g a i n again again.
思路:
很板的一道题
d p dp dp的状态定义只有一维
讲解都在代码里……
完整代码:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n, a[25], val[25], dp[1 << 21];
int main() {
while (scanf("%d", &n) != EOF) {
memset(dp, -1, sizeof(dp));
for (int i = 0; i < n - 1; i++) scanf("%d %d", &a[i], &val[i]);
scanf("%d", &a[n - 1]);
dp[0] = 100;//初始生命值为100
for (int i = 1; i < (1 << (n - 1)); i ++) {//枚举状态
dp[i] = -9999;
for (int j = 0; (1 << j) <= i; j ++) {//枚举要打死的小怪
if (i & (1 << j) && dp[i - (1 << j)] >= a[j]) { //如果当前状态下小怪被打死而且打死小怪之前的生命值能够打死他
int k = dp[i - (1 << j)] - a[j] + val[j];
k = min(100, k);//生命值不超过100
dp[i] = max(dp[i], k);//取最大值
}
}
}
if (dp[(1 << (n - 1)) - 1] >= a[n - 1])
printf("clear!!!\n");//如果打完小怪后的生命值可以打死大怪
else printf("try again\n");
}
return 0;
}
题目讲解:
一道一道慢慢来……我太难了[暴风哭泣]
6.愤怒的小鸟
7.Compatible Numbers
8.Arrange the Bulls
9.Kefa and Dishes
10.Corn Fields G
11.Vladik and cards
总结:
其实状压 D P DP DP做到最后都是套路~~
一般分为几个步骤:
-
理解题意,确定 D P DP DP状态
-
思考全面,状态转移方程
-
顺应思路,明确最终结果
状态定义翻来覆去也就那几个,重点和难点在于转移方程的转移和正确性以及对位运算的灵活运用……