1. 程式人生 > 實用技巧 >動態規劃_狀態機與狀態壓縮DP

動態規劃_狀態機與狀態壓縮DP

狀態機DP

  • 與揹包dp不同,處在每個位置或時刻,可以有多種狀態

1049. 大盜阿福

  • 對於每個狀態,有選擇和不選兩種狀態
  • 對於當前位置,如果選擇,根據題目要求,前一個位置必須不能選
  • 如果不選當前位置,可以從前一個狀態中轉移過來,也可以從前兩個狀態轉移過來

1057. 股票買賣 IV

初始化:

  • \(dp[i][j][1]\)初始時均為不合法,設定為負無窮
  • \(dp[i][j][0]\)設定為零

狀態表示:

  • \(dp[i][j][0]\): 在前\(i\)天內,最多執行\(k\)此交易,當前手中無存貨時,所得的最大利益
  • \(dp[i][j][1]\): 在前\(i\)天內,最多執行\(k\)
    此交易,當前手中有存貨時,所得的最大利益

狀態計算:

  • \(dp[i][j][0]\)
  1. 前一天手中無貨:\(dp[i - 1][j][0]\)
  2. 前一天買入:\(dp[i - 1][j][1] - w[i]\)
  • \(dp[i][j][1]\)
  1. 前一天手中有貨:\(dp[i - 1][j][1]\)
  2. 前一天買入:\(dp[i - 1][j - 1][0] - w[i]\)

結果:求最多交易\(k\)次,所獲得的最大利益

1058. 股票買賣 V

狀態表示:

  • \(dp[i][0]\): 在前\(i\)天內, 手中有存貨
  • \(dp[i][1]\):在前\(i\)天內,手中無存貨的第一天
  • \(dp[i][2]\)
    :在前\(i\)天內,手中無存貨了好幾天(多於一天)

狀態計算:

  • \(dp[i][0]\)
  1. \(dp[i - 1][2] - w[i]\):手中無存貨了好幾天,買入
  2. \(dp[i][0]\): 手中無存貨
  • \(dp[i][1]\)
    \(dp[i - 1][0] + w[i]\):前一天手中有存貨,賣掉了

  • \(dp[i][2]\)

  1. \(dp[i - 1][1]\):前一天是沒有存貨的第一天
  2. \(dp[i - 1][2]\):前一天也是沒存貨好幾天了

狀態壓縮DP

  1. 基於連通性的DP(棋盤類)

291. 蒙德里安的夢想

  • 如果橫放的長方形擺放好了,那麼豎放的長方形的擺放的方案數就確定了,因此只需考慮橫放的數量即可

  • \(dp[i][j]\)表示第\(i\)列,上一列中那些行伸出來的小方格狀態數,伸出來記為1,否則值為零,由此產生的二進位制數的十進位制表示

  • 兩個轉移條件:

  1. \(i\) 列和 \(i - 1\)列同一行不同時捅出來:\((j & k) == 0\)
  2. 本列伸出來的狀態\(j\)和上列捅出來的狀態\(k\)求或,得到上列是否為偶數空行狀態,如果是奇數空行不轉移:
#include <iostream>
#include <cstring>
#include <vector>

using namespace std;
const int N = 12, M = 1 << N;
int n, m;
long long dp[N][M];
bool st[M];
vector<int> states[M];

int main() {
    while(cin >> n >> m , n || m) {
        for(int i = 0; i < 1 << n; i++) {
            st[i] = true;
            int cnt = 0;
            for(int j = 0; j < n; j++) {
                if(i >> j & 1) {
                    if(cnt & 1) {
                        st[i] = false;
                        break;
                    }
                }
                else cnt++;
            }
            if(cnt & 1) st[i] = false;
        }
        // cout << endl;
        for(int i = 0; i < 1 << n; i++) {
            states[i].clear();
            for(int j = 0; j < 1 << n; j++) {
                if((i & j) == 0 && st[i | j]) 
                    states[i].push_back(j);
            }
        }
        memset(dp, 0, sizeof dp);
        dp[0][0] = 1;
        for(int i = 1; i <= m; i++) {
            for(int j = 0; j < 1 << n; j++) {
                for(auto k : states[j]) {
                    dp[i][j] += dp[i - 1][k];
                }
            }
        }
        cout << dp[m][0] << endl;
    }
    return 0;
}

1064. 小國王

  • 狀態表示:\(dp[i][j][s]\) 表示當前列舉到第\(i\)行,已經用了\(k\)個棋子,前一行棋子擺放的狀態是s, 如果擺放記為1否則記為0,所形成的的十進位制數
  • 合法方案:
    1. \(i - 1\)行內部不能有兩個1相鄰
    2. \(i - 1\)行和第\(i\)行之間不能互相攻擊到
  • 狀態計算:
    已經擺完前\(i\)排,第\(i\)排狀態為\(a\),第\(i - 1\)排狀態為\(b\),已經擺了\(j\)個國王的所有方案。
    已經擺完前\(i - 1\)排,第\(i - 1\)排狀態為\(b\),已經擺了\(j - count(a)\)個國王的所有方案, \(dp[i - 1][j - count(a)][b]\)
#include <iostream>
#include <cstring>
#include <vector>

using namespace std;

typedef long long LL;
const int N = 12, M = 1 << 10, K = 110;
LL dp[N][K][M];
int cnt[M], n, m;
vector<int> h[M];
vector<int> states;

// 判斷每行中是否中所有合法的狀態:不含有相鄰的不為1的數
bool check(int state) {
    for(int i = 0; i < n; i++) 
        if((state >> i & 1) && (state >> i + 1 & 1)) 
            return false;
    return true;
}

// 計算該狀態在一行中放置的棋子數量
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;
    
    // 預處理各種可行的狀態,存入states陣列
    for(int i = 0; i < 1 << n; i++) {
        if(check(i)) {
            states.push_back(i);
            cnt[i] = count(i);
        }
    }
    
    // 列舉可行的狀態,將兩者可以作為上下行的存入h陣列
    for(int i = 0; i < states.size(); i++) {
        for(int j = 0; j < states.size(); j++) {
            int a = states[i], b = states[j];
            if((a & b) == 0 && check(a | b)) 
                h[i].push_back(j);
        }
    }
    // 什麼都沒放的方案數為1
    dp[0][0][0] = 1;
    // 列舉每行
    for(int i = 1; i <= n + 1; i ++) {
        // 列舉所有的棋子
        for(int j = 0; j <= m; j++) {
            // 遍歷所有的可行的狀態
            for(int a = 0; a < states.size(); a++) {
                for(auto b : h[a]) {
                    int c = cnt[states[a]];
                    if(j >= c) 
                        dp[i][j][a] += dp[i - 1][j - c][b];
                }
            }
        }
    }
    cout << dp[n + 1][m][0];
    return 0;
}

292. 炮兵陣地

  • 狀態表示:\(dp[i][j][k]\) 表示列舉到第\(i\)行,其上一行擺放的狀態是\(j\), 上兩行的狀態為\(k\)
  • 狀態計算:如果條件合法,\(dp[i][j][k] = max(dp[i - 1][j][l] + cnt[states[l]], dp[i][j][k])\)
  • 判斷條件:
    1. 當前為平地
    2. 當前行、當前行的上一層、當前行的上兩層,之間不能有交集
#include <iostream>
#include <vector>

using namespace std;
const int N = 110, M = 12;
int g[N], cnt[1 << M];
int n, m;
int dp[N][1 << M][1 << M];

vector<int> states;
vector<int> h[1 << 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++) 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++) {
            char c; cin >> c;
            // 此處為山地,不能擺放炮團,標記為1
            g[i] += (c == 'H') << j;
        }
    }
    for(int i = 0; i < 1 << m; i++)
        if(check(i)) {
            states.push_back(i);
            cnt[i] = count(i);
        }
        

    for(int i = 1; i <= n + 2; i++)
        // 列舉i行狀態
        for(int j = 0; j < states.size(); j++)
            // i - 1狀態
            for(int k = 0; k < states.size(); k++)
                // i - 2狀態
                for(int l = 0; l < states.size(); l++) {
                    int curr = states[j], r1 = states[k], r2 = states[l];
                    if((r1 & r2) | (r1 & curr) | (curr & r2)) continue;
                    if((g[i] & curr) | (g[i - 1] & r1))  continue;
                    dp[i & 1][j][k] = max(dp[i & 1][j][k], dp[i - 1 & 1][k][l] + cnt[curr]);
                }
    cout << dp[n + 2 & 1][0][0];
    return 0;
}
  1. 集合類狀態壓縮DP

91. 最短Hamilton路徑

  • 將途徑的點的狀態壓縮,途徑的點記為1,未途徑為0
  • \(dp[i][j]\) 表示到達\(j\)這個點時,最短的路徑長度
  • 狀態計算:\(dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + a[k][j])\):
    找到更短的路徑,從\(k\)轉移過來,應該不途徑第\(j\)個點,把j從經過的點集中去掉再加上從\(k\)\(j\)的距離
// 初始化
memset(dp, 0x3f, sizeof dp);
// 開始,途徑第零個點
dp[1][0] = 0;
for(int i = 0; i < 1 << n; i++) 
    for(int j = 0; j < n; j++) 
        // 如果經過這個點的話
        if(i >> j & 1) {
            for(int k = 0; k < n; k++) {
                if(((i - (1 << j)) >> k) & 1) 
                    dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + a[k][j]);
            }
        }
cout << dp[(1 << n) - 1][n - 1];

524. 憤怒的小鳥