zoukankan      html  css  js  c++  java
  • 插头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
    ", 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]])

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

  • 相关阅读:
    注册验证
    翻页加输入框
    php面向对象
    封装数据库
    浅谈BFC和haslayout
    总结JS面向对象
    总结(JS)
    转载6
    总结(5.30)
    总结(5.29)
  • 原文地址:https://www.cnblogs.com/JiaZP/p/13336603.html
Copyright © 2011-2022 走看看