狀態壓縮dp

狀態壓縮dp

1. 算法分析

狀壓dp類型:ios

  1. 連通性狀壓dp(棋盤類dp)
  2. 集合類dp

    連通性dp的狀態壓縮表示的是每一個點的位置關係,集合類dp的狀態壓縮表示的是每一個點的是否存在c++

狀壓dp特色:
    處理的棋盤的規模很小,通常n、m規模都在60之內算法

2. 典型例題

2.1 連通性狀壓dp

acwing291蒙德里安的夢想
題意: 求把N * M的棋盤分割成若干個1 * 2的的長方形,有多少種方案。
例如當N=2,M=4時,共有5種方案。當N=2,M=3時,共有3種方案。
以下圖所示:
Image.jpg
1≤N,M≤11
題解: 仔細考慮就能夠知道所有的方案是取決於橫的方塊的方案,一旦橫的方塊肯定後豎的方塊也就肯定了。使用f[i][j]表示前i-1列填完,第i列爲j的狀況,那麼可以合法填充的j和k知足,j&k==0且j|k之間連續的1爲偶數。那麼考慮dp的轉移方程爲:f[i][j]+= f[i-1][k] (k是和j可以匹配的合法方案)
代碼:數組

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

typedef long long LL;

int n, m;
int const N = 1e4 + 10;
LL f[20][N];
int st[N];
vector<int> state[N];

int main()
{
    while (cin >> n >> m && n && m)
    {
        // 初始化
        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 0; i < 1 << n; ++i) state[i].clear();

        // 預處理st
        for (int i = 0; i < 1 << n; i ++ )
        {
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1)
                {
                    if (cnt & 1) st[i] = false;
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) st[i] = false;
        }

        // 預處理state
        for (int i = 0; i < 1 << n; ++i)
        {
            for (int j = 0; j < 1 << n; ++j)
            {
                if ((i & j) == 0 && st[i | j]) state[i].push_back(j);
            }
        }

        // dp轉移
        for (int i = 1; i <= m; ++i)
        {
            for (int j = 0; j < 1 << n; ++j)
            {
                for (int k = 0; k < state[j].size(); ++k)  // 得到全部的合法方案
                {
                    f[i][j] += f[i - 1][state[j][k]];
                }
            }
        }
        printf("%lld\n", f[m][0]);
    }
    
    return 0;
}

acwing1064騎士
題意: 在 n×n 的棋盤上放 k 個國王,國王可攻擊相鄰的 8 個格子,求使它們沒法互相攻擊的方案總數。1≤n≤10,0≤k≤n^2^
題解: 狀壓dp模板題
代碼:優化

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 12, M = 1 << 10, K = 110;

int n, m;
vector<int> state;
int cnt[M];
vector<int> head[M];
LL f[N][K][M];

// 計算每種狀況是否合法
bool check(int state)
{
    for (int i = 0; i < n; i ++ )
        if ((state >> i & 1) && (state >> i + 1 & 1))
            return false;
    return true;
}

// 計算每種狀況內的1
int count(int state)
{
    int res = 0;
    for (int i = 0; i < n; i ++ ) res += state >> i & 1;
    return res;
}

int main()
{
    cin >> n >> m;

    // 記錄全部合法的狀況,同時計算出每種合法狀況的1數目
    for (int i = 0; i < 1 << n; i ++ )
        if (check(i))
        {
            state.push_back(i);
            cnt[i] = count(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 && check(a | b))
                head[i].push_back(j);
        }

    // 狀態轉移:f[i][j][a]:到第i行,使用了j個,第i行填充a
    f[0][0][0] = 1;  // 本題入口與蒙德里安的夢想不一樣是由於本題的第一列能夠填東西,而蒙德里安的夢想第一列不能夠填東西
    for (int i = 1; i <= n + 1; i ++ )
        for (int j = 0; j <= m; j ++ )
            for (int a = 0; a < state.size(); a ++ )
                for (int b : head[a])  // 第i-1行填充b
                {
                    int c = cnt[state[a]];
                    if (j >= c)
                        f[i][j][a] += f[i - 1][j - c][b];
                }

    cout << f[n + 1][m][0] << endl;  // 輸出第n+1行,使用了m個,第n+1行填充0的狀況

    return 0;
}

acwing327玉米田
題意: 農夫約翰的土地由M*N個小方格組成,如今他要在土地裏種植玉米。很是遺憾,部分土地是不育的,沒法種植。並且,相鄰的土地不能同時種植玉米,也就是說種植玉米的全部方格之間都不會有公共邊緣。如今給定土地的大小,請你求出共有多少種種植方法。土地上什麼都不種也算一種方法。1≤M,N≤12
題解: 狀壓dp模板題
代碼:spa

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;
int n, m;
int const N = 14, M = 1 << N, MOD = 1e8;
LL f[N][M];
int g[N];
vector<int> st;
vector<int> state[M];

bool check(int x)
{
    for (int i = 0; i < m - 1; ++i)
        if ((x >> i & 1) && (x >> (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 t;
            cin >> t;
            g[i] += (!t << j);
        }
    }

    // 找到全部合法方案
    for (int i = 0; i < 1 << m; ++i)
    {
        if (check(i)) st.push_back(i);
    }

    // 找到能夠互相匹配的合法方案
    for (int i = 0; i < st.size(); ++i)
        for (int j = 0; j < st.size(); ++j)
        {
            int a = st[i], b = st[j];
            if ((a & b) == 0) state[i].push_back(j);
        }

    // 初始化
    memset(f, 0, sizeof f);
    f[0][0] = 1;

    // 狀態轉移f[i][j]:第i行使用第j種合法方案
    for (int i = 1; i <= n + 1; i ++ )  // 第i行
        for (int j = 0; j < st.size(); j ++ )  // 第j種合法方案
            if (!(st[j] & g[i])) // 若是第j種合法方案和當前玉米地不匹配
                for (int k : state[j])  // 第i-1行選擇合法方案k
                    f[i][j] = (f[i][j] + f[i - 1][k]) % MOD;

    cout << f[n + 1][0] << endl;  // 第n+1行選擇合法方案0的狀況(即000000)

    return 0;
}

acwing292 炮兵陣地
題意: 司令部的將軍們打算在N * M的網格地圖上部署他們的炮兵部隊。一個N * M的地圖由N行M列組成,地圖的每一格多是山地(用」H」 表示),也多是平原(用」P」表示),以下圖。在每一格平原地形上最多能夠佈置一支炮兵部隊(山地上不可以部署炮兵部隊);一支炮兵部隊在地圖上的攻擊範圍如圖中黑色區域所示:
Image 2.jpg
若是在地圖中的灰色所標識的平原上部署一支炮兵部隊,則圖中的黑色的網格表示它可以攻擊到的區域:沿橫向左右各兩格,沿縱向上下各兩格。圖上其它白色網格均攻擊不到。從圖上可見炮兵的攻擊範圍不受地形的影響。如今,將軍們規劃如何部署炮兵部隊,在防止誤傷的前提下(保證任何兩支炮兵部隊之間不能互相攻擊,即任何一支炮兵部隊都不在其餘支炮兵部隊的攻擊範圍內),在整個地圖區域內最多可以擺放多少我軍的炮兵部隊。輸出最多能擺放的炮兵部隊的數量。N≤100,M≤10
題解:code

代碼:ci

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 10, M = 1 << 10;

int n, m;
int g[1010];
int f[2][M][M];
vector<int> state;
int cnt[M];

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;
}

int count(int state)
{
    int res = 0;
    for (int i = 0; i < m; i ++ )
        if (state >> i & 1)
            res ++ ;
    return res;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )
        for (int j = 0; j < m; j ++ )
        {
            char c;
            cin >> c;
            g[i] += (c == 'H') << j;
        }

    for (int i = 0; i < 1 << m; i ++ )
        if (check(i))
        {
            state.push_back(i);
            cnt[i] = count(i);
        }

    for (int i = 1; i <= n; i ++ )
        for (int j = 0; j < state.size(); j ++ )
            for (int k = 0; k < state.size(); k ++ )
                for (int u = 0; u < state.size(); u ++ )
                {
                    int a = state[j], b = state[k], c = state[u];
                    if (a & b | a & c | b & c) continue;
                    if (g[i] & b | g[i - 1] & a) continue;
                    f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][u][j] + cnt[b]);
                }

    int res = 0;
    for (int i = 0; i < state.size(); i ++ )
        for (int j = 0; j < state.size(); j ++ )
            res = max(res, f[n & 1][i][j]);

    cout << res << endl;

    return 0;
}

2.2 集合類dp

acwing 91. 最短Hamilton路徑
題意: 給定一張 n 個點的帶權無向圖,點從 0 ~ n-1 標號,求起點 0 到終點 n-1 的最短Hamilton路徑。 Hamilton路徑的定義是從 0 到 n-1 不重不漏地通過每一個點剛好一次。輸出一個整數,表示最短Hamilton路徑的長度。1≤n≤20,0≤a[i,j]≤10^7^
題解: 本題若是純暴力那麼共有n!種狀況
所以能夠考慮使用二進制來優化時間複雜度。第一維枚舉每一個點是否被使用過,第二維和第三維分別枚舉當前是在哪一個點上,經過不斷的三角不等式更新,最後就能夠取得最小值。這個題本質就是對floyd的一種優化
代碼:部署

#include<bits/stdc++.h>

using namespace std;

int const N = 1 << 20;
int f[N][21];
int n;
int g[21][21];
int main()
{
    // 讀入邊
    cin >> n;
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            cin >> g[i][j];
    
    // 初始化
    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;

    // 更新f數組
    for (int i = 0; i < 1 << n; ++i)  // 枚舉每一種狀況
    {
        for (int j = 0; j < n; ++j)  // 枚舉第1個點
        {
            if (i >> j & 1)
            {
                for (int k = 0; k < n; ++k)  // 枚舉第2個點
                {
                    if ((i - (1 << j)) >> k & 1)
                    {
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + g[k][j]);  // 三角不等式更新
                    }
                }
            }
        }
    }
    cout << f[(1 << n) - 1][n - 1] << endl;
    return 0;
}
相關文章
相關標籤/搜索