初识状态压缩DP

状态压缩DP

通过将状态压缩为整数来达到优化转移的目的。 ——OI Wiki

题目状态 ----> 二进制(01串) ----> 每个二进制对应一个数值 ----> 数值代表着DP状态

例题

摸鱼

题目描述(此题并不是状压DP,是用来理解状态压缩的)

蜗蜗一共有 n\((2 ≤ n ≤ 20)\)天假期,在假期的第 i 天摸鱼他会得到 \(a_i\);\((1≤a_i≤100000)\)的快乐值。
如果蜗蜗每天都摸鱼的话,他会有愧疚感,所以蜗蜗制定了这么个计划:对于每一天,蜗蜗都有一
个列表,如果蜗蜗在列表中的每一天都在摸鱼的话,这一天蜗蜗就不能摸鱼。现在请问蜗蜗如何摸
鱼,使得他能获得的快乐值总和最大?请求出快乐值总和最大是多少。

输入

4			// 4天假期
1 2 3 4		// 每天摸鱼的快乐值
0			// 第i天的计划里有0天
1 1			// 第i+1天的计划里有1天,这天是第一天
1 2
2 2 3

输出

8

思路(状态压缩+暴力)

直接暴力做

假设有 n 天假期,则 n 天的摸鱼情况可以用 n 位的二进制表示,所以情况就是从 [0 .... 0] -> [1....1],0 表示没有摸鱼,1 表示这天摸鱼了。

除了假期,每一天的计划也可以用二进制表示,如何判断一个状态 i 是否可行?如果 i 在 j 天摸鱼了,就去看 j 天的计划,如果

\((i\&s[j] = s[j])\)​ 则不行。

代码

#include <bits/stdc++.h>

typedef std::pair<int, int> pii;
#define INF 0x3f3f3f3f
#define MOD 998244353
using i64 = long long;
const int N = 1e5+5;

void solve(){
	int n, ans = 0;
	std::cin >> n;

	std::vector<int> v(n+1), b(n+1), l(n+1), s(n+1);

	for (int i = 1; i <= n; i++) std::cin >> v[i];

	for (int i = 1; i <= n; i++){
		std::cin >> l[i];
		for (int j = 1; j <= l[i]; j++){
			int x;
			std::cin >> x;
			s[i] |= (1 << (x - 1));
		}
	}

	for (int i = 0; i < (1 << n); i++){
		for (int j = 1, k = i; j <= n; j++, k /= 2){
			b[j] = k & 1;
		}
		bool ok = true;
		for (int j = 1; j <= n && ok; j++){
			if (b[j] && l[j] && (i & s[j]) == s[j]){
				ok = false;
			}
		}
		if (!ok) continue;
		int res = 0;
		for (int j = 1; j <= n; j++){
			if (b[j]){
				res += v[j];
			}
		}
		ans = std::max(ans, res);
	}

	std::cout << ans << '\n';
}

signed main()
{
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2);
	int t = 1, i;
	for (i = 0; i < t; i++){
		solve();
	}
	return 0;
}

P10447 最短 Hamilton 路径

https://www.luogu.com.cn/problem/P10447

思路

根据题意,假设一共 n 个城市,则哪些城市去了,哪些城市没去很明显可以用二进制表示。

状态定义:\(f[i][j]\):表示在 i 这个状态下,当前在 j 城市的最小花费

初始化:\(f[1][0] = 0\),解释:第一个参数表示 0 这个城市去了,其他城市都没去,且当前在 0 城市,所以自然还没有产生花费

答案:\(f[(1<<n)-1][n-1]\),解释:所以城市都去了的状态是\((1<<n)-1\),当前在 n-1 这座城市

状态转移:\(f[i][j] = \min(f[i][j], f[1\wedge(1<<j)][k]+a[k][j])\),解释:j 城市去过,并且当前在 j 城市的DP值,一定是从 j 城市没有去过,现在刚好可以到达 j 城市的DP值中转移过来的(当前去的每一个城市都可以一步到达 j 城市)

这个转移办法是:思考目前的DP值是怎么来的

代码

#include <bits/stdc++.h>

typedef std::pair<int, int> pii;
#define INF 0x3f3f3f3f
#define MOD 998244353
using i64 = long long;
const int N = 1e5+5;

void solve(){
	int n; 
	std::cin >> n;

	std::vector a(n, std::vector<int>(n, 0));
	for (int i = 0; i < n; i++){
		for (int j = 0; j < n; j++){
			std::cin >> a[i][j];
		}
	}

	std::vector f(1 << 20, std::vector<i64>(n, INT_MAX));

	f[1][0] = 0;

	for (int i = 1; i < (1 << n); i++){
		for (int j = 0; j < n; j++){
			if (i >> j & 1){
				for (int k = 0; k < n; k++) if ((i^1 << j) >> k & 1){
					f[i][j] = std::min(f[i][j], f[i^1<<j][k]+a[k][j]);
				}
			}
		}
	}
	std::cout << f[(1<<n)-1][n-1];
	
}

signed main()
{
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2);
	int t = 1, i;
	for (i = 0; i < t; i++){
		solve();
	}
	return 0;
}

思路二

除了状态转移不一样之外,其他都是一样的

状态转移:\(f[i+(1<<k)][k] = \min(f[i+(1<<k)][k], f[i][j] + a[j][k])\),解释:如果\(f[i][j]\)这个DP值是有意义的,则它一定可以转移到它没有到过的城市

转移方法:根据已经有的DP值转移出新的DP值

代码二

#include <bits/stdc++.h>

typedef std::pair<int, int> pii;
#define INF 0x3f3f3f3f
#define MOD 998244353
using i64 = long long;
const int N = 1e5+5;

void solve(){
	int n; 
	std::cin >> n;

	std::vector a(n, std::vector<int>(n, 0));
	for (int i = 0; i < n; i++){
		for (int j = 0; j < n; j++){
			std::cin >> a[i][j];
		}
	}

	std::vector f(1 << 20, std::vector<i64>(n, INT_MAX));

	for (int i = 0; i < n; i++){
		f[0][i] = 0;
	}

	f[1][0] = 0;

	for (int i = 1; i < (1 << n); i++){
		for (int j = 0; j < n; j++){
			if (f[i][j] < INT_MAX){
				for (int k = 0; k < n; k++){
					if (!(i & (1 << k))){
						f[i+(1 << k)][k] = std::min(f[i+(1 << k)][k], f[i][j] + a[j][k]);
					}
				}
			}
		}
	}

	std::cout << f[(1<<n)-1][n-1];
	
}

signed main()
{
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2);
	int t = 1, i;
	for (i = 0; i < t; i++){
		solve();
	}
	return 0;
}

消除

题目

桌面上有\(n (2≤n≤20)\)个方块,蜗蜗想把它们都消除掉。每个方块有个权值,第 i 个方块的权值
等于\(a_i(1≤a;≤100000)\)。每一次消除蜗蜗有两种选择:1.选择一个还没有被消除的方块 i,付出
\(a_i\)的代价把它消除;2.选择两个还没有被消除的方块\(i,j(i≠j)\),付出\(a_i \wedge a_j\)​ 的代价把它们消除;
请问蜗蜗最少需要花费多少代价,能把 n 个方块都消除掉?

输入

3
1 4 5

输出

2

思路

用二进制数表示方块的状态,1 表示删除,0 表示没有删除,则所有状态区间是\([0, 2^m-1]\)

然后枚举每一个方块 j 有两种选择:1,单独删 j 这个方块,2,将 j 和其他没有删的方块一起删除,时间复杂度\(O(n^2*2^m)\)

考虑到每一个方块到最后都是要删除的,先删除哪一个是不影响最后的结果的,所以遇到可删除的直接删除即可。

代码

#include <bits/stdc++.h>

typedef std::pair<int, int> pii;
#define INF 0x3f3f3f3f
#define MOD 998244353
using i64 = long long;
const int N = 1e5+5;

void solve(){
	int n;
	std::cin >> n;

	std::vector<int> v(n+1);

	for (int i = 1; i <= n; i++) std::cin >> v[i];

	std::vector<int> f(1 << n, INF);

	f[0] = 0;

	for (int i = 0; i < (1 << n); i++){
		for (int j = 1; j <= n; j++){
			if (!(i & (1 << (j-1)))){
				f[i+(1<<(j-1))] = std::min(f[i+(1<<(j-1))], f[i] + v[j]);
				for (int k = j + 1; k <= n; k++){
					if (!(i & (1<<(k-1))))
						f[i+(1<<(j-1))+(1<<(k-1))] = std::min(f[i+(1<<(j-1))+(1<<(k-1))], f[i]+(v[j]^v[k]));
				}
				break;
			}
		}
	}

	std::cout << f[(1<<n)-1]<<'\n';
}

signed main()
{
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2);
	int t = 1, i;
	for (i = 0; i < t; i++){
		solve();
	}
	return 0;
}

麦当劳

题目

喜欢吃麦当劳的蜗蜗要在学校呆\(n(2≤n≤100000)\)天,如果第i天蜗蜗吃到了麦当劳,他可以获
\(a_i(1≤a_i≤10000\)点快乐值。然而蜗蜗不能吃太多麦当劳,在连续的\(m(2≤m≤8)\)天中,他
最多只能有一半的天数吃麦当劳。请问蜗蜗在这 n 天中最多可以得到多少快乐值?

思路

直接用二进制表示第 i 天吃不吃的状态显然是不行的,因为 n 的范围太大了,但是我们注意到 m 的范围非常小,可以思考一下有没有我们可以利用的东西。我们能想到,对于第 i 天,我们只需要考虑它前 m 天的情况就行了,所以。

状态定义:\(f[i][j]\):第 i 天,前 m 天的状态是 j 的最多快乐值

初始值:\(f[0][0] = 0\)

答案:\(max(\sum_j f[n][j])\)

状态转移见代码即可。

代码

#include <bits/stdc++.h>

typedef std::pair<int, int> pii;
#define INF 0x3f3f3f3f
#define MOD 998244353
using i64 = long long;
const int N = 1e5+5;

void solve(){
	int n, m;
	std::cin >> n >> m;

	std::vector<int> a(n+1), b(1<<m), f(1<<m, -1), v(1 << m, -1);
	for (int i = 1; i <= n; i++) std::cin >> a[i];
	for (int i = 0; i < 1<<m; i++){
		b[i] = 0;
		int cnt = 0;
		for (int j = 1; j <= m; j++){
			if (i & (1<<(j-1))) cnt++;
		}
		if (cnt <= m / 2){
			b[i] = 1;
		}
	}

	f[0] = 0;

	for (int i = 1; i <= n; i++){
		std::fill(v.begin(), v.end(), -1);
		for (int j = 0; j < 1 << m; j++){
			if (f[j] >= 0){
				v[j / 2] = std::max(v[j / 2], f[j]);
				if (b[j / 2 + (1 << (m - 1))]){
					v[j / 2 + (1 << (m - 1))] = std::max(v[j / 2 + (1 << (m - 1))], f[j] + a[i]);
				}
			}
		}
		std::copy(v.begin(), v.end(), f.begin());
	}

	int ans = 0;
	for (int i = 0; i < 1 << m; i++){
		ans = std::max(ans, f[i]);
	}

	std::cout << ans << '\n';


}

signed main()
{
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2);
	int t = 1, i;
	for (i = 0; i < t; i++){
		solve();
	}
	return 0;
}
posted @ 2025-01-03 14:37  califeee  阅读(40)  评论(0)    收藏  举报