算法提高课 -- 5

状态压缩哟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;
}

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值