状态压缩哟DP
核心思路是将提米所求的某种状态用2进制来表示(1/0表示正反两种情况)并根据状态转移方程推导出最后答案。
1064.小国王
解题思路:
本题需要考虑的是相邻两行的棋子情况,是否会出现相邻的错误,下面是判断条件的图例解释:
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 12, K = 110;
int n, k;
long long f[N][K][1 << N]; // f[i][j][k]表示前i行已经放好并且第i行的状态用二进制表示为j,此时已经放了k个国王
vector<int> nums;
vector<int> ne[1 << N];
// 判断state中是否存在相邻的棋子这种不符合题意的情况
bool check(int state) {
for (int i = 0; i < n; i++) {
if ((state >> i & 1) && (state >> i + 1 & 1)) return false;
}
return true;
}
// 计算出state这样的状态下有多少小国王棋子
int count(int state) {
int res = 0;
for (int i = 0; i < n; i++) res += state >> i & 1;
return res;
}
int main() {
cin >> n >> k;
// 预处理出所有符合情况的摆放情况
for (int i = 0; i < 1 << n; i++) {
if (check(i)) nums.push_back(i);
}
for (int i = 0; i < nums.size(); i++) {
for (int j = 0; j < nums.size(); j++) {
int a = nums[i], b = nums[j];
// 1.a和b在竖直方向上没有相邻
// 2.a和b在斜角方向上没有相邻
if ((a & b) == 0 && check(a | b)) ne[i].push_back(j);
}
}
f[0][0][0] = 1; // 求方案的初始化一般都是1
for (int i = 1; i <= n + 1; i++) {
for (int j = 0; j <= k; j++) {
for (int u = 0; u < nums.size(); u++) {
for (auto v : ne[u]) {
if (j >= count(nums[u])) f[i][j][u] += f[i - 1][j - count(nums[u])][v];
}
}
}
}
cout << f[n + 1][k][0] << endl;
return 0;
}
327.玉米田
题目解析:
与上体不同的是,少了一种情况:
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 15, mod = 1e8;
int n, m;
int w[N], f[N][1 << N]; // f[i][j] 表示已经摆好了1~i行并且第i行的状态是j
vector<int> state;
vector<int> head[1 << N];
bool check(int state) {
for (int i = 0; i < m; i++) {
if (state >> i & 1 && state >> i + 1 & 1) return false;
}
return true;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
int x;
cin >> x;
w[i] += !x << j; // 这里在田地中将无法种植的地方设置为1
}
}
// 预处理所有符合条件的二进制表示情况
for (int i = 0; i < 1 << m; i++) {
if (check(i)) state.push_back(i);
}
for (int i = 0; i < state.size(); i++) {
for (int j = 0; j < state.size(); j++) {
int a = state[i], b = state[j];
// 注:== 优先级高于 &
if ((a & b) == 0) head[i].push_back(j); // 这里就不需要向上题一样判断斜角的情况了
}
}
// DP状态转移
f[0][0] = 1; // 方案数初始化
for (int i = 1; i <= n + 1; i++) {
for (int j = 0; j < state.size(); j++) {
if (w[i] & state[j]) continue;
for (auto t : head[j]) {
if (w[i - 1] & state[t]) continue;
f[i][j] = (f[i][j] + f[i - 1][t]) % mod; // 由上一行的状态推导至本行状态
}
}
}
cout << f[n + 1][0] << endl;
return 0;
}
292.炮兵阵地
本题的状态表示还是换汤不换药,有一些需要改变的点如下图所示:
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 110, M = 10;
char str;
int n, m;
vector<int> state;
vector<int> head[1 << M];
int cnt[1 << M], w[N], f[2][1 << M][1 << M]; // f[i][j][k] 表示已经摆好了前i行并且第i行的摆放状态为j、第i-1行的拜访状态为k
// 是否存在相邻和隔一个相邻
bool check(int state) {
for (int i = 0; i < m; i++) {
if ((state >> i & 1) && ((state >> i + 1 & 1) || (state >> i + 2 & 1))) return false;
}
return true;
}
// 二进制状态表示为state中有多少个1
int count(int state) {
int res = 0;
for (int i = 0; i < m; i++) res += state >> i & 1;
return res;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
cin >> str;
w[i] += (str == 'H') << j; // 和上题一样,能放是0,不能放是1
}
}
for (int i = 0; i < 1 << m; i++) {
if (check(i)) { // 1.左右不能有连续相邻3个的情况出现
state.push_back(i);
cnt[i] = count(i);
}
}
for (int i = 1; i <= n + 2; i++) {
for (int j = 0; j < state.size(); j++) { // 第i行
for (int k = 0; k < state.size(); k++) { // 第i - 1行
for (int q = 0; q < state.size(); q++) { // 第i - 2行
int a = state[j], b = state[k], c = state[q];
// 2.上下之间不能相邻
if (a & b || a & c || b & c) continue;
// 3.不能在山地上安置炮兵
if (w[i] & a || w[i - 1] & b || w[i - 2] & c) continue;
// 这里由于会出现爆空间的情况,因此使用一个位运算小技巧
// 将行数&1,这样只需要开2的空间即可
f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][k][q] + cnt[a]);
}
}
}
}
cout << f[n + 2 & 1][0][0] << endl;
return 0;
}
524.愤怒的小鸟
题目解析:
本题建议直接看y总的解析,比较难:https://www.acwing.com/video/405/
代码展示(代码中带有注释也比较好理解一些):
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#define x first
#define y second
using namespace std;
typedef pair<double, double> PDD;
const int N = 18;
const double eps = 1e-8;
int T, n, m;
PDD q[N];
// path[i][j] 表示经过i, j两个点的抛物线所能覆盖的点的二进制状态表示
int path[N][N], f[1 << N]; // f[i] 表示覆盖状态为i的情况下需要多少条抛物线
// 浮点数比大小模板
int cmp(double a, double b) {
if (fabs(a - b) < eps) return 0;
if (a > b) return 1;
else if (a < b) return -1;
}
int main() {
cin >> T;
while (T--) {
memset(path, 0, sizeof path);
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;
for (int i = 0; i < n; i++) {
path[i][i] = 1 << i;
for (int j = 0; j < n; j++) {
// 二次函数一般形式:a * x * x + b * x = y
double x1 = q[i].x, y1 = q[i].y;
double x2 = q[j].x, y2 = q[j].y;
if (!cmp(x1, x2)) continue; // 分母不能为0
// 用任意两个点表示出a, b确定一条抛物线
double a = ((y1 / x1) - (y2 / x2)) / (x1 - x2);
double b = y1 / x1 - a * x1;
if (cmp(a, 0) >= 0) continue; // 开口向下,因此a必小于0
int state = 0; // 存储每条抛物线的覆盖点情况
for (int k = 0; k < n; k++) {
double x = q[k].x, y = q[k].y;
if (!cmp(a * x * x + b * x, y)) state += 1 << k;
}
path[i][j] = state;
}
}
memset(f, 0x3f, sizeof f);
f[0] = 0;
for (int i = 0; i < 1 << n; i++) {
int x = 0;
for (int j = 0; j < n; j++) {
if (!(i >> j & 1)) {
x = j; // 存储未覆盖的点
break;
}
}
for (int j = 0; j < n; j++) {
f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);
}
}
cout << f[(1 << n) - 1] << endl;
}
return 0;
}
529.宝藏
这个题贼难,不好理解,直接看y总的讲解吧:AcWing 529. 宝藏(CSP-S辅导课) - AcWing
还要结合一个题解一起看:https://www.cnblogs.com/littlehb/p/15761600.html
代码展示:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 12, M = 1 << 12, INF = 0x3f3f3f3f;
int n, m;
int d[N][N]; // d[i][j] 表示i到j的最短距离
int g[N][M]; // g[i][j] 表示点i到已覆盖点的二进制表示状态j的最短距离
int f[M][N]; // f[i][j] 表示已覆盖点二进制表示为i并且高度为j的生成树所需要付出的代价
int main() {
scanf("%d%d", &n, &m);
// 处理输入情况
memset(d, INF, sizeof d);
for (int i = 0; i < n; i++) d[i][i] = 0;
while (m--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a--, b--;
d[a][b] = d[b][a] = min(d[a][b], c);
}
// 初始化g[][]
memset(g, INF, sizeof g);
for (int i = 0; i < n; i++) {
for (int j = 0; j < 1 << n; j++) {
for (int k = 0; k < n; k++) {
if (j >> k & 1) {
g[i][j] = min(g[i][j], d[i][k]);
}
}
}
}
// DP计算
memset(f, INF, sizeof f);
for (int i = 0; i < n; i++) f[1 << i][0] = 0;
for (int i = 0; i < 1 << n; i++) {
for (int j = i - 1 & i; j; j = j - 1 & i) { // 枚举状态i所有子集的技巧
int r = i ^ j, cost = 0; // r表示j相对于i少了的点的二进制状态表示,cost表示连接这些点需要花费的代价
for (int k = 0; k < n; k++) {
if (j >> k & 1) {
cost += g[k][r];
if (cost >= INF) break; // 防止越界
}
}
if (cost >= INF) continue;
for (int k = 1; k < n; k++) { // k = 0的情况刚刚已经初始化过了
f[i][k] = min(f[i][k], f[r][k - 1] + cost * k);
}
}
}
int res = INF;
for (int i = 0; i < n; i++) res = min(res, f[(1 << n) - 1][i]);
printf("%d\n", res);
return 0;
}