插头DP

资料:oi-wiki

资料:CDQ的ppt

弱弱化版:状压&逐格转移&骨牌覆盖

详见: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]);
...

例题:P5056 【模板】插头dp

这回还有坏点。

与之前吃树题类似,如果当前格子是坏点,就只保存 0 0 -> 0 0 的转移。

另外,这回不一定是最后一行最后一列的那个格子才能接受 up == rg的情况。仔细考虑发现 up == rg 实际上就是轮廓线上方出现回路的那一瞬间,并且以后都在其它的回路上转移。因此我们只需要允许且仅允许我们最后遍历的那个格子接受up == rg 的转移即可保证仅有一条回路。

一条路径

由于路径有两个端点,插头可以突然出现或者突然消失。不过一条路径只会有两个“独立插头”(指的是突然蹦出来的端点),并且转移过程中也不会超过两个。因此我们直接将“独立插头”数量计入状态转移即可,最终答案从“独立插头”数量为2的那些状态中转移即可。

与一条回路不同的是:

  • 这次转移我们还需要多枚举一个“独立插头”数量;

  • 我们无法合并两个相同来源的插头(否则会出现回路);

  • 当我们发现左,上只有一个位置有插头的时候,可以让它停止;

  • 当我们发现左,上一个都没有插头的时候,可以多一个下插头或右插头。

例题:Beautiful Meadow (ZOJ - 3213)

要求最大化路径上的权值和。有障碍点。

障碍点直接继承即可。因为之前我们已经特判过障碍点了,因此不会再出现不合法状态了。

for (register int c = 0; c <= 2; ++c) {
	for (register int s = 1; s <= f[t ^ 1][c].ecnt; ++s) {
		decode(f[t ^ 1][c].state[s]);
		tmp = v + f[t ^ 1][c].val[s];
		int lft = b[j], up = b[j + 1];
		if (lft && up) {
			if (lft == up) {
				//can't merge
			} else {
				for (register int k = 0; k <= m; ++k)
					if (b[k] == up)	b[k] = lft;
				Push(c, j, 0, 0);
			}
		} else if (lft || up) {
			int id = lft | up;
			if (dn)	Push(c, j, id, 0);
			if (rg)	Push(c, j, 0, id);
			if (c < 2)	Push(c + 1, j, 0, 0);
		} else {
			tmp -= v;
			Push(c, j, 0, 0);
			tmp += v;
			if (dn && rg)	Push(c, j, m, m);
			if (c < 2) {
				if (dn)	Push(c + 1, j, m, 0);
				if (rg)	Push(c + 1, j, 0, m);
			}
		}
	}
}

注意

  • encodedecode 的时候注意顺序,不要写成快读式写法了。(应该是:encode从后往前,decode从前往后)

  • 注意进制数通常是2的正数次幂,这样快,但是就别再老是<<=1或者>>=1

  • 哈希表中如果查找到了 \(s\) 对应的那个 \(x\),记得加权值后退出函数,而不能简单地break

  • 注意 encode()decode(int s) 都是从0到m的,因为轮廓线多了一个竖线。

  • encode() 中是 if (bb[b[i]] == -1) 而不是 if (!bb[b[i]])

  • 注意一开始要加上个全零(没有插头)的情况。

posted @ 2020-07-18 17:34  JiaZP  阅读(470)  评论(0编辑  收藏  举报