插頭DP
阿新 • • 發佈:2020-07-18
弱弱化版:狀壓&逐格轉移&骨牌覆蓋
詳見:DP學習記錄Ⅰ
弱化版:壞點,多條迴路
例題:hdu 1693 Eat the Trees
將當前輪廓線狀壓起來,0表示有插頭穿過此線,1表示沒有。簡單分類討論,由上一個狀態轉移到下一個合法狀態即可。注意一行結束後要整體左移。注意狀壓會多一位,因此要開夠陣列。需要靈活掌握二進位制操作。
正確性:因為不合法狀態只可能是一個格子出現多於兩個插頭,或者一個格子只有一個插頭,而我們並沒有這種轉移。
關鍵程式碼:
int t = 0; f[t][0] = 1; for (register int i = 0; i < n; ++i) { for (register int j = 0; j < m; ++j) { t ^= 1; memset(f[t], 0, sizeof(f[t])); int tp; read(tp); for (register int s = 0; s < 1 << (m + 1); ++s) if (f[t ^ 1][s]) { ll tmp = f[t ^ 1][s]; int lft = (s >> j) & 1, up = (s >> (j + 1)) & 1; if (!tp) { if (!up && !lft) f[t][s] += tmp; } else { f[t][s ^ (3 << j)] += tmp; if (up != lft) f[t][s] += tmp; } } } t ^= 1; memset(f[t], 0, sizeof(f[t])); for (register int s = 0; s < 1 << m; ++s) f[t][s << 1] = f[t ^ 1][s]; }
是不是很簡單?
注意
-
眾所周知,狀壓題下標從0開始顯然更方便。 -
記得輪廓線一共有m+1處!
一條迴路
例題:Gym : Pipe layout
這回我們需要知道插頭之間的對應關係了。
一種易於理解的方法是最小表示法。我們用相同的數代表在輪廓線上方聯通的插頭,但是會出一些問題:1 1 0 2 2 0 和 3 3 0 1 1 0 表示同一種情況,這時我們需要將它們看作一種狀態。方法是:
將我們遇到的第一個非零數編號為1,第二個編號為2...0永遠編號為0,第二次遇到之前出現過的數沿用之前的編號。
關鍵程式碼:
const int base = 8, mask = 7;//2^3 進位制儲存 int b[N], bb[N];//b[i]表示原陣列的第i位,bb[i]表示“i”重新編號的值。 inline int encode() { memset(bb, -1, sizeof(bb)); bb[0] = 0; int s = 0, bcnt = 0; for (register int i = m; ~i; --i) {//Attention!! if (bb[b[i]] == -1) bb[b[i]] = ++bcnt; s <<= 3; s |= bb[b[i]]; } return s; } inline void decode(int s) { for (register int i = 0; i <= m; ++i) {//注意順序 b[i] = s & mask; s >>= 3; } }
有些狀態永遠用不到,比如 1 5 3 2 3,或者 1 2 1 0 2,實際用到的狀態非常少,因此我們可以用雜湊表存現有合法狀態,優化時空複雜度,同時方便封裝一些東西。(其實用 unordered map 也可以,但是正規比賽可能不讓用(儘管正規比賽不太可能考插頭DP))
關鍵程式碼:
const int P = 9973; struct hashTable{ int head[NN], nxt[NN], ecnt;//模擬鄰接連結串列存邊 int state[NN];//狀態的最小表示 ll val[NN];//方案數 hashTable() { memset(head, 0, sizeof(head)); memset(nxt, 0, sizeof(nxt)); memset(state, 0, sizeof(state)); memset(val, 0, sizeof(val)); ecnt = 0; } inline void addedge(int s, ll v) { int x = s % P; for (register int i = head[x]; i; i = nxt[i]) if (state[i] == s) { val[i] += v; return ; } ++ecnt; nxt[ecnt] = head[x]; val[ecnt] = v; state[ecnt] = s; head[x] = ecnt; } inline void clear() { memset(head, 0, sizeof(head)); ecnt = 0; } inline void Roll() { for (register int i = 1; i <= ecnt; ++i) state[i] <<= 3; } }f[2];
這樣,我們就可以分類討論求解了。
ll tmp;
inline void Push(int j, int dn, int rg) {
//將第 j 個格子的下方插上個dn插頭,右方插上個rg插頭
b[j] = dn, b[j + 1] = rg;
f[t].addedge(encode(), tmp);
}
...
(main)
...
for (register int i = 0; i < n; ++i) {
for (register int j = 0; j < m; ++j) {
t ^= 1;
f[t].clear();
int dn = i != n - 1, rg = j != m - 1;
for (register int s = 1; s <= f[t ^ 1].ecnt; ++s) {
decode(f[t ^ 1].state[s]);
tmp = f[t ^ 1].val[s];
int lft = b[j], up = b[j + 1];
if (up && lft) {
if (up == lft) {
if (i == n - 1 && j == m - 1) Push(j, 0, 0);
} else {
for (register int k = 0; k <= m; ++k)//Attention! : k = 0
if (b[k] == lft) b[k] = up;
Push(j, 0, 0);
}
} else if (up || lft) {
int id = up | lft;
if (dn) Push(j, id, 0);
if (rg) Push(j, 0, id);
} else {
if (rg && dn) Push(j, m, m);
}
}
}
f[t].Roll();
}
if (f[t].ecnt == 0) puts("0");
else printf("%lld\n", f[t].val[1]);
...
注意
-
encode
和decode
的時候注意順序,不要寫成快讀式寫法了。(應該是:encode從後往前,decode從前往後) -
注意進位制數通常是2的正數次冪,這樣快,但是就別再老是
<<=1
或者>>=1
了