状态压缩学习笔记

目录

例题一

问题描述

输入格式

输出格式

样例输入

样例输出

例题二

问题描述

输入格式

输出格式

 例题三

问题描述

输入格式

输出格式

提示

例题四

问题描述

输入格式

输出格式

样例输入

样例输出

提示

例题五

问题描述

输入格式

输出格式

样例输入

样例输出


例题一

我们先来一道例题:

                                                       传球游戏

问题描述

n个人在做传球的游戏,编号为1-n。
  游戏规则是这样的:开始时球可以在任意一人手上,他可把球传递给其他人中的任意一位;下一个人可以传递给未接过球的任意一人。
  即球只能经过同一个人一次,而且每次传递过程都有一个代价;不同的人传给不同的人的代价值之间没有联系;
  求当球经过所有n个人后,整个过程的最小总代价是多少。

输入格式

第一行为n,表示共有n个人(16>=n>=2);
  以下为n*n的矩阵,第i+1行、第j列表示球从编号为i的人传递到编号为j的人所花费的代价,特别的有第i+1行、第i列为-1(因为球不能自己传给自己),其他数据均为正整数(<=10000)。

输出格式

一个数,为最小的代价总和。

样例输入

样例输入1:
2
  -1   9794
2724    –1

样例输入2:
4
  -1  4551  3763  4873 
4465    -1  9323  2204 
 705  2102    -1   333 
9264  5206  2596    -1 

样例输出

样例输出1:
2724

样例输出2:
5505

我们以这道题来讲解。n小于等于16,所以dfs搜索肯定会TLE,排除。

我们可以考虑状态压缩DP:

我们先开一个二维数组dp[i][j],i表示第i个人是否接过球,j表示第j个人,dp[i][j]表示代价。

那么以第j个人结束的方案数的值就出来了,状态转移方程如下:

dp[i][j] = min(dp[i][j], dp[i ^ 1 << j][k] + cont[k][j]);

然后再遍历n个人,取他们的最小值。时间复杂度是n方乘上2的n次方

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int INF = 99999999;
const int max2n = 1 << 16 | 5;
const int maxn = 16 + 5;
int dp[max2n][maxn], cont[maxn][maxn], n;
signed main() {
	ios::sync_with_stdio(false);
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			scanf("%d", &cont[i][j]);
		}
	}
	for (int i = 0; i < (1 << n); i++) {
		for (int j = 0; j < n; j++) {
			dp[i][j] = INF;
		}
	}
	for (int i = 0; i < n; i++) {
		dp[1 << i][i] = 0;
	}
	for (int i = 2; i < (1 << n); i++) {
		for (int j = 0; j < n; j++) {
			if (i >> j & 1) {
				for (int k = 0; k < n; k++) {
					if (j != k && (i >> k & 1)) {
						dp[i][j] = min(dp[i][j], dp[i ^ 1 << j][k] + cont[k][j]);
					}
				}
			}
		}
	}
	int ans = INF;
	for (int i = 0; i < n; i++) {
		ans = min(ans, dp[(1 << n) - 1][i]);
	}
	printf("%d", ans);
	return 0;
}

例题二

我们再来一道例题:

                                                                过桥

问题描述

一只队伍在爬山时碰到了雪崩,他们在逃跑时遇到了一座桥,他们要尽快的过桥. 桥已经很旧了, 所以它不能承受太重的东西. 任何时候队伍在桥上的人都不能超过一定的限制.

所以这只队伍过桥时只能分批过,当一组全部过去时,下一组才能接着过.

队伍里每个人过桥都需要特定的时间,当一批队员过桥时时间应该算走得最慢的那一个,每个人也有特定的重量,

我们想知道如何分批过桥能使总时间最少.

输入格式

第一行两个整数: w,n 分别表示 桥能承受的最大重量(100 <= w <= 400) 和 n 队员总数(1 <= n <= 16).
接下来n 行每行两个整数t和w分别表示每个队员过桥所需时间(1 <= t <= 50) 和  该队员的重量(10 <= w <= 100).

输出格式

输出一个数表示最少的过桥时间.

样例输入 1

100 3
24 60
10 40
18 50

样例输出 1

42

样例输入 2

150 5
50 44
40 33
35 36
30 75
11 76

样例输出 2

90

这道题也是状压DP,只不过要枚举子集。先输入w和n然后对dp[i]作初始化。如果这个子集重量没有超过桥的重量限制,就直接处理,将dp[i]=t[i],否则对自己进行拆分。时间复杂度是3的n次方

下面上代码:

#include<bits/stdc++.h>
using namespace std;
const int INF = 99999999;
const int max2n = 1 << 16 | 5;
int dp[max2n], w[max2n], t[max2n], n, W;
signed main() {
	scanf("%d%d", &W, &n);
	for (int i = 0; i < n; i++) {
		scanf("%d%d", &t[1 << i], &w[1 << i]);
		dp[1 << i] = t[1 << i];
	}
	for (int i = 1; i < (1 << n); i++) {
		if (i & i - 1) {
			t[i] = max(t[i & i - 1], t[i & -i]);
			w[i] = w[i & i - 1] + w[i & -i];
			if (w[i] <= W) {
				dp[i] = t[i];
			} else {
				dp[i] = INF;
				for (int j = i - 1 & i; j; j = j - 1 & i) {
					if (w[j] <= W && dp[i] > dp[i ^ j] + t[j]) {
						dp[i] = dp[i ^ j] + t[j];
					}
				}
			}
		}
	}
	printf("%d", dp[(1 << n) - 1]);
	return 0;
}

 例题三

我们再来看一道例题:

                                                            校长的烦恼

问题描述

某中学开设有s门课程,现有教师m个。今天有n个求职者来应聘新教师。
已知每个人工资和能教授的课程。
在职教师不能辞退,校长想知道,最少支付多少工资就能使得每门课都有至少两名教师能教。

输入格式

第一行,三个整数s,m和n
接下来m行,每行若干个整数,描述一名在职教师的情况:第一个整数,表示该教师的工资,接下来的若干整数表示该教师能教的课程;
接下来n行,每行若干个整数,描述一名求职者的情况:第一个整数,表示该求职者要求的工资,接下来的若干整数表示该求职者能教的课程;
注意,每行末尾有空格
 

输出格式

一行,一个整数,表示所求结果。
若无解,输出-1

样例输入 1

2 2 2
10000 1 
20000 2 
30000 1 2 
40000 1 2 

样例输出 1

60000

样例输入 2

5 2 4 
24 2 4 
39 4 1 
9 4 
81 2 1 5 
19 2 3 5 
65 3 1 

样例输出 2

228

提示

1<=s<=8
1<=m<=20
1<=n<=100
1<=每个人的工资<=50000
1<=每个人教的课的编号<=s

这道题我们不能状态压缩老师的情况,我们只能压缩课程的情况。

我们用两个状态表示,第一个状态表示这门课是否有至少一个人上,第二个表示这门课是否有至少两个人上,即可。

#include <bits/stdc++.h>
using namespace std;
int s0, n, m, f[125][1 << 8][1 << 8], c[125], b[125];
int main() {
	scanf("%d %d %d", &s0, &m, &n);
	memset(f, 0x3f3f, sizeof(f));
	int s1 = 0, s2 = 0, cnt = 0;
	for (int k = 1; k <= m; k++) {
		int c;
		scanf("%d", &c);
		cnt += c;
		string s;
		getline(cin, s);
		int num = 0, len = s.length();
		for (int i = 0; i < len ; i++)
			if (s[i] >= '0' && s[i] <= '9') {
				num *= 10, num += s[i] - '0';
			} else if (num) {
				if (!(s2 & (1 << (num - 1)))) {
					if (s1 & ((1 << (num - 1)))) {
						s1 ^= ((1 << (num - 1))), s2 |= ((1 << (num - 1)));
					} else {
						s1 |= ((1 << (num - 1)));
					}
				}
				num = 0;
			}
		if (num) {
			if (!(s2 & (1 << (num - 1)))) {
				if (s1 & ((1 << (num - 1)))) {
					s1 ^= ((1 << (num - 1))), s2 |= ((1 << (num - 1)));
				} else {
					s1 |= ((1 << (num - 1)));
				}
			}
		}
	}
	f[0][s1][s2] = cnt;
	for (int k = 1; k <= n; k++) {
		scanf("%d", &c[k]);
		string s;
		getline(cin, s);
		int num = 0, len = s.length();
		for (int i = 0; i < len ; i++) {
			if (s[i] >= '0' && s[i] <= '9') {
				num *= 10, num += s[i] - '0';
			} else if (num) {
				b[k] |= (1 << (num - 1)), num = 0;
			}
		}
		if (num) {
			b[k] |= (1 << (num - 1));
		}
	}
	for (int k = 0; k < n; k++) {
		for (int i = 0; i < 1 << 8; i++) {
			for (int j = 0; j < 1 << 8; j++) {
				if (f[k][i][j] != 0x3f3f3f3f) {
					f[k + 1][i][j] = min(f[k + 1][i][j], f[k][i][j]);
					int ss1 = i, ss2 = j;
					for (int c = 0; c < s0; c++) {
						if ((!(b[k + 1] & (1 << c))) || (j & (1 << c) )) {
							continue;
						}
						if (i & (1 << c)) {
							ss1 ^= (1 << c), ss2 |= (1 << c);
						} else {
							ss1 |= (1 << c);
						}
					}
					f[k + 1][ss1][ss2] = min(f[k + 1][ss1][ss2], f[k][i][j] + c[k + 1]);
				}
			}
		}
	}
	printf("%d", f[n][0][(1 << s0) - 1]);
	return 0;
}

例题四

                                                 【状压】玉米地

问题描述

Farmer John买了一片土地,可以表示为一片由方块组成的网格,长度为M,宽度为N(1 <= M, N <= 12)。现在FJ要在地里种上一些玉米让他的奶牛们直接在地里食用。FJ知道他的这片土地有些地方很贫瘠,没有办法种玉米;并且奶牛们不喜欢挨在一起吃玉米,所以不能在相邻的两块地上种玉米。请帮助FJ计算一下所有可能的种玉米的方案数。注意,结果输出对于100,000,000的余数,一棵玉米也不中也算是一种方案。

输入格式

第一行 两个整数M和N
接下来是一个M*N的矩阵,1表示该方块可以种玉米,0表示不行

输出格式

一个整数,表示对应的结果

样例输入

2 3
1 1 1
0 1 0

样例输出

9

提示

把每块能中玉米的地编上号:
1 2 3 
0 4 0
只种一块地,方案有(1, 2, 3, 4);种两块地方案有 (13, 14, 34);种三块地 (134), 
一块也不种也算一种方案,总共4+3+1+1=9

这道题是一个十字形的状压DP。

我们可以从上到下去递推,只要知道了第一行的状态,就能推出后面的方案。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e8;
int n, m, ans, c[13], a[13][13], f[15][1 << 15];
int main() {
	ios::sync_with_stdio(false);
	cin >> n >> m;
	int p = (1 << m) - 1;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			cin >> a[i][j];
			if (a[i][j] == 1) {
				c[i] = c[i] | (1 << (j - 1));
			}
		}
	}
	for (int i = 0; i <= p; i++) {
		if ((i | c[1]) == c[1] && ((i >> 1)&i) == 0 && (i & (i << 1)) == 0) {
			f[1][i] = 1;
		}
	}
	for (int i = 2; i <= n; i++) {
		for (int j = 0; j <= p; j++) {
			for (int k = 0; k <= p; k++) {
				if ((j | c[i]) == c[i] && (j & k) == 0 && ((j >> 1)&j) == 0 && (j & (j << 1)) == 0) {
					f[i][j] = (f[i][j] + f[i - 1][k]) % maxn;
				}
			}
		}
	}
	for (int i = 0; i <= p; i++) {
		f[n + 1][0] += f[n][i];
		f[n + 1][0] %= maxn;
	}
	cout << f[n + 1][0];
	return 0;
}

例题五

                                 【NOI2001 Day2 T2】炮兵阵地

问题描述

司令部的将军们打算在  的网格地图上部署他们的炮兵部队。一个N*M的地图由N行M列组成,地图的每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示),如下图。
 



在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);
一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。
  现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

输入格式

第一行包含两个由空格分割开的正整数,分别表示N和M;
接下来的N行,每一行含有连续的M个字符(‘P’或者‘H’),中间没有空格。按顺序表示地图中每一行的数据。
N≤100;M≤10。

输出格式

仅在第一行包含一个整数K,表示最多能摆放的炮兵部队的数量。

样例输入

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

样例输出

6

这道题是例题四的加强版。它是将玉米地的十字扩大了一下,所以我们要判左移一位、右移一位、左移两位、右移两位等等,这时候可以考虑在上一题的基础上用离散化(也可以不用)。

#include<bits/stdc++.h>
using namespace std;
int n, m, cnt, a[105], s[1 << 10], num[1 << 10], f[2][1 << 10][1 << 10];
int main() {
	ios::sync_with_stdio(false);
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j < m; j++) {
			char c;
			cin >> c;
			if (c == 'P') {
				a[i] += 1 << (m - j - 1);
			}
		}
	}
	for (int i = 0; i < (1 << m); i++) {
		if (!(i & i >> 1) && !(i & i >> 2)) {
			s[cnt++] = i;
			for (int j = 0; j < m; j++)
				num[i] += (i >> j & 1);
		}
	}
	f[0][0][0] = 0;
	for (int i = 1; i <= n + 2; i++) {
		for (int j = 0; j < cnt; j++) {
			for (int k = 0; k < cnt; k++) {
				for (int kk = 0; kk < cnt; kk++) {
					if (!(s[j]&s[k]) && !(s[j]&s[kk]) && !(s[k]&s[kk]) && (s[j]&a[i]) == s[j] && (s[k]&a[i - 1]) == s[k]) {
						f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][k][kk] + num[s[j]]);
					}
				}
			}
		}
	}
	cout << f[n + 2 & 1][0][0];
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值