動態規劃_狀態機與狀態壓縮DP
阿新 • • 發佈:2020-07-18
狀態機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]\)
- 前一天手中無貨:\(dp[i - 1][j][0]\)
- 前一天買入:\(dp[i - 1][j][1] - w[i]\)
- \(dp[i][j][1]\)
- 前一天手中有貨:\(dp[i - 1][j][1]\)
- 前一天買入:\(dp[i - 1][j - 1][0] - w[i]\)
結果:求最多交易\(k\)次,所獲得的最大利益
1058. 股票買賣 V
狀態表示:
- \(dp[i][0]\): 在前\(i\)天內, 手中有存貨
- \(dp[i][1]\):在前\(i\)天內,手中無存貨的第一天
- \(dp[i][2]\)
狀態計算:
- \(dp[i][0]\)
- \(dp[i - 1][2] - w[i]\):手中無存貨了好幾天,買入
- \(dp[i][0]\): 手中無存貨
-
\(dp[i][1]\)
\(dp[i - 1][0] + w[i]\):前一天手中有存貨,賣掉了 -
\(dp[i][2]\)
- \(dp[i - 1][1]\):前一天是沒有存貨的第一天
- \(dp[i - 1][2]\):前一天也是沒存貨好幾天了
狀態壓縮DP
- 基於連通性的DP(棋盤類)
291. 蒙德里安的夢想
-
如果橫放的長方形擺放好了,那麼豎放的長方形的擺放的方案數就確定了,因此只需考慮橫放的數量即可
-
\(dp[i][j]\)表示第\(i\)列,上一列中那些行伸出來的小方格狀態數,伸出來記為1,否則值為零,由此產生的二進位制數的十進位制表示
-
兩個轉移條件:
- \(i\) 列和 \(i - 1\)列同一行不同時捅出來:\((j & k) == 0\)
- 本列伸出來的狀態\(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,所形成的的十進位制數
- 合法方案:
- 第\(i - 1\)行內部不能有兩個1相鄰
- 第\(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])\)
- 判斷條件:
- 當前為平地
- 當前行、當前行的上一層、當前行的上兩層,之間不能有交集
#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;
}
- 集合類狀態壓縮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];