狀壓 dp

hhc0001發表於2024-03-28

引入

先看一道例題:(可能 r18)

\(N\) 個男生和 \(N\) 個女生。小 A 喜歡磕 CP,現在小 A 想要磕 \(N\) 對 CP。不過每一個人都有自己的 npy,也不是隨隨便便就能磕成一對。現在小 A 找到了你,要你求出有多少種磕 CP 的方式。

我們顯然可以暴力列舉每一個男生跟誰組 CP 然後判斷是否合法。但是這方法 \(O(N!)\),在 \(N = 20\) 的情況下顯然不可透過。

我們可以設計出一個 pure and simple 的狀態:\((i, 0/1, 0/1, ......, 0/1)\)(其中共有 \(N\)\(0/1\)),表示目前配對了 \(i\) 個男生,女生是否被配對過。

轉移不難想:只要第 \(i + 1\) 個男生願意與某個沒有配對的女生磕 CP,就可以轉移。

但是,你還要寫 \(20\)if 用於輸出答案(或許不用?),誰想啊......

考慮到 \(2^{20} - 1\)int 存的下,所以嘗試將之前二十個維度壓成一個維度。

此即“狀態壓縮”dp,簡稱 狀壓 dp。

狀壓 dp 常見套路

可行性問題轉換最最佳化問題

Moovie Mooving G

奶牛 Bessie 想連續看 \(L (1 \le L \le 10^8)\) 分鐘的電影,有 \(N (1 \le N \le 20)\) 部電影可供選擇,每部電影會在一天的不同時段放映。

Bessie 可以在一部電影播放過程中的任何時間進入或退出放映廳。但她不願意重複看到一部電影,所以每部電影她最多看到一次。她也不能在看一部電影的過程中,換到另一個正在播放相同電影的放映廳。

請幫 Bessie 計算她能夠做到從 \(0\)\(L\) 分鐘連續不斷地觀看電影,如果能,請計算她最少看幾部電影就行了。

很顯然,直接做似乎不行了......

我們發現,只要某個方案能夠耗掉 \(L\) 分鐘,就可以了。所以我們對於一個要觀看的電影集合,只保留這些電影最長能耗多少時間。

最小化揹包數量

\(\infty\) 個容量為 \(W\) 的揹包,還有 \(N\) 個體積為 \(W_1, W_2, \cdots, W_N\) 的物品。

現在我們要將這些物品放進某些揹包滿足沒有揹包超載。求最少有多少個揹包裡面有物品。

Naive

定義 \(dp_{state}\) 為只考慮 \(state\) 裡面的元素所需要的最小揹包數量。

列舉開一個新揹包裡面裝某些東西。

列舉原先集合 \(O(2^N)\),每一個原先集合有 \(2^N\) 種轉移,總共 \(O(2^{(2N)}) = O(4^N)\)

Improvment 1

考慮到我們只需要對沒放的東西新開一個揹包。

所以僅需列舉剩下東西的子集。

時間複雜度 \(O(3^N)\),證明如下:

考慮選了正好 \(i\) 個物品對時間複雜度的貢獻。

\[\sum \limits_{i = 0}^N \binom{N}{i}2^{N - i} \]

\[= \sum \limits_{i = 0}^N \binom{N}{i}1^i2^{N - i} \]

\[= (1 + 2)^N = 3^N \]

Improvment 2

其實我們只需要列舉下一個要放什麼元素,然後轉移即可。

時間複雜度純粹的 \(O(N2^N)\)

按行 dp

\(N \cdot M\) 的網格,每個網格內包含 "0", "1", 或 "?" 。每個 "?" 會被等機率替換為 "0" 或 "1" 。請計算網格圖中不存在兩個相鄰網格均為 "1" 的 機率。 本題中我們認為網格 \((i,j)\) 與網格 \((i + 1, j), (i − 1, j), (i, j + 1), (i, j − 1)\) 相鄰 。答案對 \(998244353\) 取模。

我們定義 \(dp_{i, state}\) 為當前在第 \(i\) 行,第 \(i\) 行狀態為 \(state\) 的方案數。

Naive

直接列舉當前行和之前行,校驗。

\(O(N4^MM^2)\)

Inprovment 1

這一行的 1 上一行不會有,列舉子集。

\(O(N3^MM^2)\)

Improvment 2

校驗的環節可以 \(O(N2^M)\) 預處理。

總時間複雜度 \(O(N3^M)\)

Code:

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

const int kMod = 998244353;

int n, m, dp[16][1 << 16], cnt, sm, cz[16][1 << 16];
char c[22][22];

bool check(int x, int y) {
  for(int j = 0; j < m; j++) {
    //cout << c[x][j]
    if((((y >> j) & 1) && c[x][j] == '0') || (!((y >> j) & 1) && c[x][j] == '1')) {
      return 0;
    }
  }
  return !(y & (y >> 1));
}

bool check2(int x, int y) {
  for(int j = 0; j < m; j++) {
    if(((x >> j) & 1) && ((y >> j) & 1)) {
      return 0;
    }
  }
  return 1;
}

int qpow(int x, int y) {
  int ans = 1;
  for(; y; y >>= 1) {
    if(y & 1) {
      ans = 1ll * ans * x % kMod;
    }
    x = 1ll * x * x % kMod;
  }
  return ans;
}

int qinv(int x) {
  return qpow(x, kMod - 2);
}

int main() {
  cin >> n >> m;
  for(int i = 1; i <= n; i++) {
    for(int j = 0; j < m; j++) {
      cin >> c[i][j];
      cnt += c[i][j] == '?';
    }
  }
  dp[0][0] = 1;
  int tmp = 0;
  //cout << check(1, 0) << '\n';
  for(int i = 1; i <= n; i++) {
    for(int j = 0; j < (1 << m); j++) {
      cz[i][j] = check(i, j);
    }
  }
  cz[0][0] = 1;
  for(int i = 1; i <= n; i++) {
    for(int j = 0; j < (1 << m); j++) {
      if(cz[i][j]) {
        for(int k = (1 << m) - 1 - j; ; k = (k - 1) & ((1 << m) - 1 - j)) {
          //cout << i << ' ' << j << ' ' << k << '\n';
          if(cz[i - 1][k]) {
            //cout << i << ' ' << k << '\n';
            dp[i][j] = (dp[i][j] + dp[i - 1][k]) % kMod;
          }
          if(!k) {
            break;
          }
        }
      }
      if(i == n) {
        tmp = (tmp + dp[i][j]) % kMod;
      }
      //cout << dp[i][j] << ' ';
    }
    //cout << '\n';
  }
  cout << 1ll * tmp * qinv(qpow(2, cnt)) % kMod << '\n';
  return 0;
}

(CodeBlocks 炸了,所以沒有換行)

利用轉移的特殊性狀壓轉移

一排 \(N\) 個玩家,第 \(i\) 名玩家有一個戰力值 \(A_i\)。當滿足以下全部條件時,玩家 \(i\) 和玩家 \(j\) 可以組隊:

  • \(A_i\)\(A_j\) 互質;

  • \(|i - j| \le K\)

請問你至多可以組出幾隻二人隊伍。每個玩家不能加入超過一個隊伍。

注意到 \(K\) 只有 \(8\)

直接狀壓 \(i\)\(i - k + 1\)\(i - k\) 無需維護)的配對情況,狀壓轉移。

\(A_i\)\(A_j\) 互質的條件可以預處理。

不過這題實現起來超級噁心,所以我放一個程式碼用來查錯:

    //cout << dp[n][j] << ' ';

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

int n, k, arr[100010], dp[100010][1 << 8], xg[100010][10];

int gcd(int x, int y) {
  return !y? x : gcd(y, x % y);
}

int main() {
  cin >> n >> k;
  for(int i = 1; i <= n; i++) {
    cin >> arr[i];
    for(int j = 1; j <= min(i - 1, k); j++) {
      xg[i][j] = gcd(arr[i], arr[i - j]) == 1;
    }
  }
  // 這裡寫了擴散
  for(int i = 1; i <= n; i++) {
    for(int j = 0; j < (1 << min(k, i)); j++) { // i 可以小於 K
      int tmp = (j << 1) & ((1 << k) - 1);
      dp[i + 1][tmp] = max(dp[i + 1][tmp], dp[i][j]);
      for(int x = 1; x <= min(i, k); x++) {
        if(!((j >> (x - 1)) & 1) && xg[i + 1][x]) { // 還可以配對
          dp[i + 1][(tmp | (1 << x) | 1) & ((1 << k) - 1)] = max(dp[i + 1][(tmp | (1 << x) | 1) & ((1 << k) - 1)], dp[i][j] + 1); // 注意這裡 & ((1 << k) - 1),否則 WA
        }
      }
    }
  }
  int ans = 0;
  for(int j = 0; j < (1 << min(k, n)); j++) { // N 可以小於 K
    ans = max(ans, dp[n][j]);
    //cout << dp[n][j] << ' ';
  }
  cout << ans << '\n';
  return 0;
}

祝大家 for(;;) rp += INT_MAX;

相關文章