状压DP学习笔记(持续更新ing)

本文是关于状态压缩动态规划(状压DP)的学习笔记,包括定义、前置知识和位运算介绍,通过多个题目(互不侵犯、炮兵阵地、Fish、奖励关、Survival)的解析,详细阐述状压DP的思路和应用,强调了理解和应用位运算的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:

最近学了这个神奇的东东,真的好难啊[仙女叹气]~

感觉自己没大理解,就写笔记强迫自己学会吧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里主要用到的几个技巧见下图:

状压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[i1][s][kcnt(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[n1][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 NM的初始状态,规定了哪些位置可以放炮兵。要求每个炮兵前后左右格不能有他人。求最多可以放多少兵。

数据范围: 1 ≤ N ≤ 100 1 \leq N \leq 100 1N100, 1 ≤ M ≤ 10 1≤M≤10 1M10.

思路

因为每一行的情况与前两行有关,就要定义三个状态,又因为当前状态可以有前一行转移而来,所以只用定义当前状态前一行的状态,前前行的状态可以由前一行表示,所以定义:

d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k],表示第 i − 1 i-1 i1行状态为 j j j,第 i i i状态为 k k k,

然后再循环枚举 d p [ i − 1 ] [ h ] [ j ] dp[i - 1][h][j] dp[i1][h][j](表示第 i − 2 i-2 i2行状态为 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 i1行状态为 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] 1p[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 1K100 , 1 ≤ n ≤ 15 1 \leq n \leq 15 1n15 1 ≤ K ≤ 100 1≤K≤100 1K100, 1 ≤ n ≤ 15 1≤n≤15 1n15.

思路

首先看题,概率 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 n1个小 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状态

  • 思考全面,状态转移方程

  • 顺应思路,明确最终结果

状态定义翻来覆去也就那几个,重点和难点在于转移方程的转移和正确性以及对位运算的灵活运用……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值