1. 程式人生 > 實用技巧 >插頭DP

插頭DP

資料:oi-wiki

資料:CDQ的論文

弱弱化版:狀壓&逐格轉移&骨牌覆蓋

詳見: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]);
...

注意

  • encodedecode 的時候注意順序,不要寫成快讀式寫法了。(應該是:encode從後往前,decode從前往後)

  • 注意進位制數通常是2的正數次冪,這樣快,但是就別再老是<<=1或者>>=1