第三节 状压 dp (草稿版)

总述

状压 dp,一般用于解决 \(n\) 较小时需要转移的动态规划问题。其特点是记录所有位置的状态,将其压缩成二进制形式,然后进行转移。

有时,状压 dp 可以预先处理状态,然后再进行操作。

[蓝桥杯 2020 国 C] 补给

考虑记状态 \(dp_{s, j}\) 表示各个点访问状态为 \(s\),到达 \(j\) 时,所经过的最短距离。

对于一个状态,枚举经过的上一个点。如果这两个点之间的距离在 \(D\) 以内则转移,否则什么都不做。

最终终点可以在任意位置,需要 Floyd 跑个最短路回 \(1\)。因为中间可以中转,故最开始处理下边长和 Floyd 即可。

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++){
            double ds = sqrt((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]));
            dis[i][j] = (ds <= k ? ds : INF);
    }
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
    memset(dp, 0x7f, sizeof dp); dp[1][1] = 0;
    for (int s = 2; s < (1 << n); s++){
        for (int i = 1; i <= n; i++){
            if (!(s & (1 << i - 1))) continue;
            int p = s ^ (1 << i - 1);
            for (int j = 1; j <= n; j++){
                if (!(p & (1 << j - 1))) continue;
                dp[s][i] = min(dp[s][i], dp[p][j] + dis[j][i]);
            }
        }
    }
    for (int i = 1; i <= n; i++)
        ans = min(ans, dp[(1 << n) - 1][i] + dis[i][1]);
    printf("%.2lf\n", ans);

[蓝桥杯 2021 省 AB2] 国际象棋

这题傻到没法预处理。直接做即可,因为行数极小,枚举这一行上一行和上上行,无脑动态规划就过了。

毫无思维水平。

dp[0][0][0][0] = 1;
    for (int i = 1; i <= m; i++){
        for (int x = 0; x < (1 << n); x++){
            for (int y = 0; y < (1 << n); y++){
                if (x & (y << 2)) continue;
                if (x & (y >> 2)) continue;
                for (int z = 0; z < (1 << n); z++){
                    if (x & (z << 1)) continue;
                    if (x & (z >> 1)) continue;
                    if (y & (z << 2)) continue;
                    if (y & (z >> 2)) continue;
                    int tw = count(x);
                    int w = tw + count(y) + count(z);
                    for (int j = w; j <= k; j++){
                        dp[i][j][x][y] = (dp[i][j][x][y] + dp[i-1][j-tw][y][z]) % MOD;
                    }
                }
            }
        }
    }
    for (int x = 0; x < (1 << n); x++)
        for (int y = 0; y < (1 << n); y++)
            ans = (ans + dp[m][k][x][y]) % MOD;

[GDOI2014] 拯救莫莉斯

计算得到 \(m \le 7\),容易想到状态压缩。

考虑对于每一行,记 \(f\) 表示答案,\(g\) 表示最小油库数量。

不想讲了看代码吧。

int main(){
    memset(dp, 0x3f, sizeof dp);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> c[i][j];
    for (int i = 1; i <= n; i++)
        for (int x = 0; x < (1 << m); x++)
            for (int p = 1; p <= m; p++)
                if (x & (1 << p - 1)) sum[i][x] += c[i][p];
    for (int x = 0; x < (1 << m); x++){
        dp[1][x][0] = sum[1][x];
        f[1][x][0] = count(x);
    }
    for (int i = 2; i <= n; i++)
        for (int x = 0; x < (1 << m); x++)
            for (int y = 0; y < (1 << m); y++)
                for (int z = 0; z < (1 << m); z++)
                    if (((x | y | z | (y << 1) | (y >> 1)) & ((1 << m) - 1)) == ((1 << m) - 1)){
                        int fp = dp[i - 1][y][z] + sum[i][x], ff = f[i - 1][y][z] + count(x);
                        if (dp[i][x][y] > fp) { dp[i][x][y] = fp; f[i][x][y] = ff; }
                        else if (dp[i][x][y] == fp && f[i][x][y] > ff){ f[i][x][y] = ff; }
                    }
    for (int x = 0; x < (1 << m); x++)
        for (int y = 0; y < (1 << m); y++){
            if (((x | y | (x << 1) | (x >> 1)) & ((1 << m) - 1)) == ((1 << m) - 1)){
                if (ans1 > dp[n][x][y]){ ans1 = dp[n][x][y]; ans2 = f[n][x][y]; }
                else if (ans1 == dp[n][x][y] && ans2 > f[n][x][y]) ans2 = f[n][x][y];
            }
        }
    cout << ans2 << " " << ans1 << endl;
    return 0;
}

[NOIP 2016 提高组] 愤怒的小鸟

特殊的预处理一下。

#include <bits/stdc++.h>
using namespace std;

typedef long double ld;
const ld eps = 1e-8;
int T, n, m;
int p[20][20], r[1 << 20], dp[1 << 20];
ld x[20], y[20];

void cal(ld &x, ld &y, ld a1, ld b1, ld c1, ld a2, ld b2, ld c2){
    y = (a1 * c2 - a2 * c1) / (a1 * b2 - a2 * b1);
    x = (c1 - b1 * y) / a1;
    return ;
}

bool equ(ld a, ld b) { return fabs(a - b) < eps; }

int main(){
    for (int i = 0; i < (1 << 18); i++){
        int res = 1;
        while (res <= 18 && i & (1 << res - 1)) res++;
        r[i] = res;
    }
    scanf("%d", &T);
    while (T--){
        scanf("%d%d", &n, &m); memset(dp, 0x3f, sizeof dp); dp[0] = 0;
        for (int i = 1; i <= n; i++){
            scanf("%Lf%Lf", &x[i], &y[i]);
        }
        for (int i = 1; i <= n; i++){
            for (int j = 1; j <= n; j++){
                p[i][j] = 0;
                if (equ(x[i], x[j])) continue;
                ld a, b;
                cal(a, b, x[i] * x[i], x[i], y[i], x[j] * x[j], x[j], y[j]);
                if (a > -eps) continue;
                for (int k = 1; k <= n; k++){
                    if (equ(a * x[k] * x[k] + b * x[k], y[k]))
                        p[i][j] |= (1 << k - 1);
                }
            }
        }
        for (int i = 0; i < (1 << n); i++){
            int pi = r[i];
            dp[i | (1 << pi - 1)] = min(dp[i | (1 << pi - 1)], dp[i] + 1);
            for(int k = 1; k <= n; k++) dp[i | p[pi][k]] = min(dp[i | p[pi][k]], dp[i] + 1);
        }
        cout << dp[(1 << n) - 1] << endl;
    }
    return 0;
}

[CQOI2018] 解锁屏幕

跟上一道题有点像,既然上一道题没有好好讲那这道题就稍微好好讲一下。

考虑状态压缩,时间复杂度应该是 \(O(n^22^n)\)。

说白了不就是把上一道题的二次函数换成了一次函数么。考虑预处理在两个点之间移动一定会经过哪些点。

转移时,枚举状态和起点终点就完事了。

#include <bits/stdc++.h>
using namespace std;

typedef long double ld;
const int MOD = 1e8 + 7;
const ld eps = 1e-8;

struct node {
    int x, y;
    friend bool operator <(const node &a, const node &b){
        if (a.x != b.x) return a.x < b.x;
        return a.y < b.y;
    }
} a[20];

int n, ans;
int dp[1 << 19][20], dis[20][20];

bool equal(ld a, ld b){ return fabs(a - b) < eps; }
ld slope(node a, node b){
    if (a.x == b.x) return 1e9; 
    return 1.0 * (a.y - b.y) / (a.x - b.x);
}

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i].x >> a[i].y;
    sort(a+1, a+n+1);
    for (int i = 1; i <= n; i++)
        for (int j = i + 1; j <= n; j++)
            for (int k = i + 1; k < j; k++)
                if (equal(slope(a[i], a[k]), slope(a[k], a[j]))){
                    dis[i][j] |= (1 << k - 1);
                    dis[j][i] |= (1 << k - 1);
                }
    for (int i = 1; i <= n; i++)
        dp[1 << i - 1][i] = 1;
    for (int x = 0; x < (1 << n); x++)
        for (int i = 1; i <= n; i++){
            if (!(x & (1 << i - 1))) continue;
            for (int j = 1; j <= n; j++){
                if (x & (1 << j - 1)) continue;
                if ((x & dis[i][j]) != dis[i][j]) continue;
                dp[x | (1 << j - 1)][j] = (dp[x | (1 << j - 1)][j] + dp[x][i]) % MOD;
            }
        }
    for (int x = 0; x < (1 << n); x++){
        if (__builtin_popcount(x) < 4) continue;
        for (int i = 1; i <= n; i++){
            if (!(x & (1 << i - 1))) continue;
            ans = (ans + dp[x][i]) % MOD;
        }
    }
    cout << ans << endl;
    return 0;
}

Mondriaan's Dream / 蒙德里安的梦想

忘了是哪次考试原题,考场上觉得眼熟来着,见了一万遍但是都没写。

显然这个状态应该只需要记录两行。

考虑先行枚举匹配的状态以减小时间复杂度。

然后就做完了。真没啥好讲的。

#include <bits/stdc++.h>
using namespace std;

int n, m;
long long dp[12][1 << 11];
vector<int> g[1 << 11];
bool flag[1 << 11];

bool check(int x){
    int ret = 0;
    for (int i = 0; i < m; i++){
        if (x & (1 << i)){
            if (ret & 1) return false;
            ret = 0;
        }
        else ret ^= 1;
    }
    return ret ^ 1;
}

int main(){
    while (cin >> n >> m){
        if (!n && !m) return 0;
        for (int x = 0; x < (1 << m); x++){
            flag[x] = check(x);
            g[x].clear();
        }
        for (int x = 0; x < (1 << m); x++){
            for (int y = 0; y < (1 << m); y++){
                if (x & y) continue;
                if (!flag[x | y]) continue;
                g[x].push_back(y);
            }
        }
        memset(dp, 0, sizeof dp); dp[0][0] = 1;
        for (int i = 1; i <= n; i++){
            for (int x = 0; x < (1 << m); x++){
                for (int y : g[x]){
                    dp[i][x] += dp[i - 1][y];
                }
            }
        }
        cout << dp[n][0] << endl;
    }
}

中国象棋 - 摆上马

可以说是目前最恶心的一道题,因为唯一涉及到了别马腿的问题。

考虑对于这一行 \(x\),上一行 \(y\),再上一行 \(z\),何种状态是合法的。

在实际处理中肯定保证了上面两行合法。这一行对于上一行能踩到的格子是一块马的边缘向其对应方向踩的位置,同时不能被上一行的那几个玩意踩到。

而对于上上行,则不能被上上行的玩意踩到,且如果上面没东西就能踩到左右两格。

综上所述,\((x, y, z)\) 合法时,单向判断 \((x, y, z)\) 和 \((z, y, x)\) 即可。有点小小的麻烦,写出来看看。

#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;
int n, m, ans;
int dp[105][1 << 6][1 << 6];

bool check(int x, int y){
    int t = (~(((x >> 1) & x) >> 1)) & y;
    if ((x >> 2) & t) return false;
    t = (~(((x << 1) & x) << 1)) & y;
    if ((x << 2) & t) return false;
    return true;
}

bool check(int x, int y, int z){
    int t = (~((x & y) >> 1)) & z;
    if ((x >> 1) & t) return false;
    t = (~((x & y) << 1)) & z;
    if ((x << 1) & t) return false;
    return true;
}

bool judge(int x, int y){ return check(x, y) && check(y, x); }
bool judge(int x, int y, int z){ return check(x, y, z) && check(z, y, x); }

int main(){
    cin >> n >> m;
    for (int x = 0; x < (1 << m); x++)
        dp[1][x][0] = 1;
    for (int x = 0; x < (1 << m); x++)
        for (int y = 0; y < (1 << m); y++)
            if (judge(x, y)) dp[2][x][y] = 1;
    for (int i = 3; i <= n; i++){
        for (int x = 0; x < (1 << m); x++){
            for (int y = 0; y < (1 << m); y++){
                if (!judge(x, y)) continue;
                for (int z = 0; z < (1 << m); z++){
                    if (!judge(y, z)) continue;
                    if (!judge(x, y, z)) continue;
                    dp[i][x][y] = (dp[i][x][y] + dp[i-1][y][z]) % MOD;
                }
            }
        }
    }
    for (int x = 0; x < (1 << m); x++)
        for (int y = 0; y < (1 << m); y++)
            ans = (ans + dp[n][x][y]) % MOD;
    cout << ans << endl;
    return 0;
}

[PA 2014] Pakowanie

刚在想做什么题,打开练着玩的题单的这道题,一看我去这不状态压缩么。

那好,直接来!

显然小小贪心一下,一定要先填最大的包。然后简单记状态表示现在塞这么多用了几个包。

容易想到枚举下一个包装的状态,但是显然要爆炸。

考虑到实质上可以一个一个元素枚举,只需要记录包的剩余质量即可。

显然加在状态上会影响时间复杂度,能不能加在值上?显然是可以的,因为剩余空间越大越好。

很有启发性的一道好题!

#include <bits/stdc++.h>
using namespace std;

struct node {
    int x, t;
    friend bool operator <(const node &a, const node &b){
        if (a.x != b.x) return a.x < b.x;
        return a.t > b.t;
    }
} dp[1 << 24];

int n, m, a[25], b[105];

int main(){
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 1; i <= m; i++)
        cin >> b[i];
    sort(b+1, b+m+1, greater<int>());
    memset(dp, 0x3f, sizeof dp); dp[0] = {0, 0};
    for (int x = 0; x < (1 << n); x++){
        if (dp[x].x > m) continue;
        for (int i = 1; i <= n; i++){
            if (x & (1 << i - 1)) continue;
            if (dp[x].t >= a[i])
                dp[x | (1 << i - 1)] = min(dp[x | (1 << i - 1)], {dp[x].x, dp[x].t - a[i]});
            else if (b[dp[x].x + 1] - a[i] >= 0)
                dp[x | (1 << i - 1)] = min(dp[x | (1 << i - 1)], {dp[x].x + 1, b[dp[x].x + 1] - a[i]});
        }
    }
    if (dp[(1 << n) - 1].x > m) cout << "NIE" << endl;
    else cout << dp[(1 << n) - 1].x << endl;
    return 0;
}
posted @ 2026-09-14 16:51  _lhr  阅读(12)  评论(0)    收藏  举报