动态规划之插头DP入门

基于联通性的状态压缩动态规划是一类很典型的状态压缩动态规划问题,因为其压缩的本质并不像是普通的状态压缩动态规划那样用0或者1来表示未使用、使用两种状态,而是使用数字来表示类似插头的状态,因此,它又被称作插头DP。

插头DP本质上是一类状态压缩DP,因此,依然避免不了其指数级别的算法复杂度,即便如此,它依然要比普通的搜索算法快很多。

【例】Postal Vans(USACO training 6.1.1)

有一个4*n的矩阵,从左上角出发,每次可以向四个方向走一步,求经过每个格子恰好一次,再回到起点的走法数。

【算法分析】

看到此题,许多读者觉得4很小,会想到搜索算法或者是递推公式,而实际上,搜索算法是不能解决此题的,当n稍大一点,搜索算法即使写的再漂亮,也不能通过此题。本题确实有递推公式,但递推公式却不是那么好找,因此,可以考虑使用插头DP。

为了更好的了解插头DP,首先引入以下几个概念:

1.插头

对于矩阵上的任何一个格点,路径总是会穿过它,也就是从一头进入,从一头出去,这样的情况一共有6种,如下所示:

一个合法的路径需要满足的必要条件之一是:它的每一个格子上的路径插头都是上述六者之一,并且要相互匹配。相互匹配的意思是,如果一个格子上方的格子有向下的插头,那么这个格子就必须有向上的插头与它相匹配。

2.轮廓线

对于任何一个未决策的格子,仅有其上边和左边的格子对其的放置方法有影响,因此,可以根据当前已经决策的格子画出一条轮廓线,分割出已经决策和未决策的格子。

如上图就是两种典型的轮廓线,一种是基于格子的轮廓线,当前该转移的是轮廓线拐角处的格子;一种是基于行的轮廓线,当前该转移的是轮廓线下方的一整行。

对于第一种情况,涉及的插头一共有N+1个,其中N个下插头,1个右插头,需要保存的插头数量是N+1个,对于第二种情况,只有N个下插头,需要保存的插头数是N个。

3.连通性

对于这类动态规划问题,除了要保存每一个插头外,还需要记录这些插头的连通性情况。例如,使用[(1,2)(3,4)]来表示该行第1、2个格子已经连通,第3、4个格子已经连通。

如图所示,两者的下插头完全一致,但连通性却完全不同。因此,还需要在状态中表示他们的连通性。

由于插头的表示已经是指数级别的空间,表示连通性如果再需要指数型的空间,那么空间和时间的消耗将是巨大的!因此,需要有更好的办法去表示连通性,通用的一个办法我们称作“括号表示法”。

对于同一行的四个格子,假设他们都有下插头,则,他们的连通性只可能有上图两种情况[(1,2),(3,4)],[(1,4),(2,3)],而不可能是[(1,3),(2,4)],更普遍的,因为插头永远都不可能有交叉,因此,任何两个格子之间的联通性也不会存在交叉。这和括号匹配是完全一致的!

括号表示法的基本思想是三进制:

0:无插头状态,用#表示

1:左括号插头,用(表示

2:右括号插头,用)表示

图左(使用格点转移)可以表示为:(()#)

图右(使用行来转移)可以表示为:(())

在此基础上,继续来了解插头DP的状态转移方式。

1.基于格点的状态转移:

基于格点的状态转移方式每次转移仅一个格子,转移的时候需要考虑这个格子的左方以及上方是否有插头。

左方无插头,上方无插头。[****##****](*代表#()中任意一个),只能在此处加入一个形插头,状态变为[****()****]。

左方和上方只有一个插头。此时,该格必然有一个插头和这个插头匹配,另一个插头插向下方或右方,这个格子相当于延续了之前该插头的连通状态,因此,转移方式是:将插头所在的位不动(插头插向下方)或向右移动一位(插向右方)。

左方有插头,上方有插头。这种情况下相当于合并了两个连通块,需要考虑两个插头的括号表示情况:

case1:”((”,两者都是左插头,此刻要把两个两个连通分量合
并,就必须修改他们对应的右括号,使得它们对应的右括号匹配起来,例如:[#((##))],将两个’(’合并时,需要修改第二个’(’所匹配的’)’为’(’,使他们继续匹配。变为[#####()]

case2:”))”,两者都是右插头,此时,合并他们相当于直接把它们变为”##”即可。

case3:”()”,即两者本来就是匹配的,此时,合并他们相当于把它们连起来形成回路,对于需要形成一条回路的题目,只有在最后一个格子形成回路才是合法的转移方式。

其中,左方有插头,上方无插头以及左方无插头,上方有插头的情况十分类似,在实现代码的时候可以合并。

2.基于行的状态转移:

基于行的状态转移,使用搜索的办法实现,dfs每一行的合法状态,然后更新状态至下一行。容易实现但算法复杂度较高,很少有人使用。在此处不再赘述。

需要注意的是,虽然插头DP使用的是三进制的状态表示,但是往往在实现的时候,使用的却是四进制(仅使用0、1、2,3不表示任何意义)。原因是由于计算机本身的设计导致其对2的幂次方进制数计算速度很快,使用四进制可以利用到位运算,加快运行速度。

此外,由于在状态转移的过程中,需要知道每一个左括号所对应的右括号的位置,由于合法状态是很有限的,因此,可以通过预处理的方式将合法状态以及这些状态下每一个左括号所匹配的右括号的位置记录下来,利用额外空间换来时间复杂度的下降。

在实现代码时,使用一个int类型来保存每一个状态,括号在四进制中从低位到高位依次表示从左到右的每一个括号,例如:9(21)3的实际上代表了一个从左到右的匹配的括号(),请读者在阅读代码的时候注意。

本题在读入数据过大的时候会超过long long类型,因此需要用到高精度运算,实现时可以用结构体复写加法运算符的方式来实现。熟悉java的读者也可以直接使用java中的BigInteger类。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>

using namespace std;

const int n = 4;

struct Num{
	short arr[500];
	int len;
	void init(int i)
	{
		memset(arr, 0, sizeof(arr));
		arr[0] = i;
		len = 1;
	}
	void print()
	{
		for (int i = len - 1; i >= 0; i--)
		{
			cout << arr[i];
		}
		cout << endl;
	}
};

void operator += (Num &a, Num b)
{
	a.len = max(a.len, b.len);
	for (int i = 0; i < a.len; i++)
	{
		a.arr[i] += b.arr[i];
		a.arr[i + 1] += a.arr[i] / 10;
		a.arr[i] %= 10;
	}
	if (a.arr[a.len]) a.len++;
}

int N;
int stat[1110];
int brk[1110][8], stack[8], top = 0, tot = 0;
Num dp[8][1110];
int main() {
	freopen("vans.in", "r", stdin);
	freopen("vans.out", "w", stdout);
	cin >> N;
	int m = 1 << ((n + 1) << 1);
	for (int i = 0; i < m; i++)
	{
		top = 0;
		for (int j = 0; j <= n; j++)
		{
			int x = i >> (j << 1);
			if ((x & 3) == 1) stack[top++] = j;
			if ((x & 3) == 2)
				if (top--)
				{
					brk[tot][j] = stack[top];
					brk[tot][stack[top]] = j;
				} else break;
			if ((x & 3) == 3)
			{
				top = -1;
				break;
			}
		}
		if (!top) stat[tot++] = i;
	}
	Num ans;
	ans.init(0);
	memset(dp, 0, sizeof(dp));
	dp[n][0].init(1);
	for (int k = 1; k <= N; k++)
	{
		for (int i = 0; i < tot; i++)
		{
			if (stat[i] & 3) dp[0][stat[i]].init(0);
			else dp[0][stat[i]] = dp[n][stat[i] >> 2];
		}
		for (int i = 1; i <= n; i++)
		{
			int x = (i - 1) << 1;
			memset(dp[i], 0, sizeof(dp[i]));
			for (int j = 0; j < tot; j++) {
				int p = (stat[j] >> x) & 3;
				int q = (stat[j] >> (x + 2)) & 3;
				// ## -> ()
				// 9 = (21)4
				//左上都无插头
				if (!p && !q) dp[i][stat[j] | (9 << x)] += dp[i - 1][stat[j]];
				else
				//左上都有插头
				if (p && q)
				{
					//两个((或者两个))
					if (p == q)
					{
						// ((...)) ->
						// ##...()
						// 5 = (11)4 : ## = (( ^ 5
						// () = )) ^ 3
						//两个((,把其匹配位置的)改为(
						if (p == 1) dp[i][stat[j] ^ (5 << x) ^ (3 << (brk[j][i] << 1))] += dp[i - 1][stat[j]];
						// ((...)) ->
						// ()...##
						// 10 = (22)4
						//两个)),把其匹配位置的(改为)
						else
							dp[i][stat[j] ^ (10 << x) ^ (3 << (brk[j][i - 1] << 1))] += dp[i - 1][stat[j]];
					}
					else
					//()或)(
					{
						//()的情况,如果是最后一个格子,将答案加进来,否则跳过
						if (p == 1)
						{
							if (k == N && i == n && stat[j] == (9 << x))
								ans += dp[i - 1][stat[j]];
						}
						//)(的情况,直接把)(改成##
						else dp[i][stat[j] ^ (6 << x)] += dp[i - 1][stat[j]]; // )( -> ##, 6 = (12)4
					}
				}
				//只有其中一个位置有插头
				else
				{
					//当原来状态是#(或者#)时,状态不变意味着插头向右
					//当原来状态是(#或者)#时,状态不变意味着插头向下
					dp[i][stat[j]] += dp[i - 1][stat[j]];

					//当原来状态是#(或者#)时,状态交换意味着插头向下
					//当原来状态是(#或者)#时,状态交换意味着插头向右
					dp[i][stat[j] ^ (p << x) ^ (q << x + 2) | (p << x + 2) | (q << x)] += dp[i - 1][stat[j]];
				}
			}
		}
	}
	//连通之后答案要乘以2,因为一个环有两种遍历的方向
	ans += ans;
	ans.print();
	return 0;
}

本题稍作变化,就可以出很多道插头DP类的题目:例如,从(1,1)点出发,回到(1,n)点;找出若干个环把图上所有点都覆盖;从(1,1)点出发,把所有点恰好都走一次,但不需要回到(1,1)点等。本质上都是本题的一点点改变,只需要彻底理解插头DP的思想,做出这些题目都不是问题。

1.从(1,1)点出发,回到(1,n)点。我们可以人为的认为这个迷宫是从(0,1)点进入,最后回到(0,n)点,这样一来,我们只需要在转移状态的时候,强制要求(1,1)点和(1,n)点必须得有上插头,其他不变即可。

2.找出若干个环把图上所有点都覆盖。在原来的题目中,必须只有一个环,因此,只有在最后一个点(n,n)的时候,才处理了同时有左上插头并且他们是()的情况,在本问题里,只需要把这个特殊要求去掉即可,即:在任何一个点,只要满足左上都有插头且他们是(),就把它们合并。

3.从(1,1)点出发,把所有点恰好都走一次,但不需要回到(1,1)点。此时,人为的给(1,1)点添加一个上插头,使得(1,1)点只可能被经过一次,之后在状态中多加入一维状态[0 or 1]用来表示当前是否已经把某一个点作为终点,当这一维状态是0的时候,可以在某一个格子转移的时候只给他添加一个插头,然后把0修改成1,最后到(n,n)点要求必须是()才更新答案即可;如果到达最后一个节点(n,n)这一维状态依然是0,则在此处考虑两种插头的插法,并更新答案。

时间: 2024-08-02 11:13:43

动态规划之插头DP入门的相关文章

hdu 1693 Eat the Trees (插头dp入门)

Eat the Trees Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 2507    Accepted Submission(s): 1225 Problem Description Most of us know that in the game called DotA(Defense of the Ancient), Pudg

[URAL1519] Formula 1 [插头dp入门]

题面: 传送门 思路: 先理解一下题意:实际上就是要你求这个棋盘中的哈密顿回路个数,障碍不能走 看到这个数据范围,还有回路处理,就想到使用插头dp来做了 观察一下发现,这道题因为都是回路,所以联通块上方的插头一定两两配对,可以使用括号序列代替最小表示法 分情况讨论一下 情况一:当前格子上方和左方都没有插头 这种时候可以继续,也可以给当前格子加一个下插头一个右插头,相当于一个新的联通分量 情况二:上方有一个下插头,左边没有 这时有两个决策:可以向右转,也可以继续向下,操作就是分别给这个格子一个右插

插头DP入门

终于来补插头DP的坑了,咕了好久,主要是因为博猪代码实现能力太弱,而网上的大神们都只讲分类讨论... 只放代码了: zzh学长: 1 #include<bits/stdc++.h> 2 using namespace std; 3 #define ll long long 4 #define A 1100000 5 #define mod 299989 6 #define P 8 7 #define N 100000000 8 ll n,m; 9 inline ll find(ll state

[入门向选讲] 插头DP:从零概念到入门 (例题:HDU1693 COGS1283 BZOJ2310 BZOJ2331)

转载请注明原文地址:http://www.cnblogs.com/LadyLex/p/7326874.html 最近搞了一下插头DP的基础知识……这真的是一种很锻炼人的题型…… 每一道题的状态都不一样,并且有不少的分类讨论,让插头DP十分锻炼思维的全面性和严谨性. 下面我们一起来学习插头DP的内容吧! 插头DP主要用来处理一系列基于连通性状态压缩的动态规划问题,处理的具体问题有很多种,并且一般数据规模较小. 由于棋盘有很特殊的结构,使得它可以与“连通性”有很强的联系,因此插头DP最常见的应用要数

插头DP专题

建议入门的人先看cd琦的<基于连通性状态压缩的动态规划问题>.事半功倍. 插头DP其实是比较久以前听说的一个东西,当初是水了几道水题,最近打算温习一下,顺便看下能否入门之类. 插头DP建议先理解“插头”的概念.然后会HASH表(这个其实是很基础的东西,应该都会的).然后就是DP. 以及特殊题目的特殊处理. 好像一般是求N,M<=12的网格图的某种回路数或某种通路数的方案数. 大体上每个题说几句特殊处理,有问题请纠正....题目的顺序基本上难度递增 另外代码我都是用括号匹配的.因为感觉连通

POJ 2411 Mondriaan&#39;s Dream ——状压DP 插头DP

[题目分析] 用1*2的牌铺满n*m的格子. 刚开始用到动规想写一个n*m*2^m,写了半天才知道会有重复的情况. So Sad. 然后想到数据范围这么小,爆搜好了.于是把每一种状态对应的转移都搜了出来. 加了点优(gou)化(pi),然后poj上1244ms垫底. 大概的方法就是考虑每一层横着放的情况,剩下的必须竖起来的情况到下一层取反即可. 然后看了 <插头DP-从入门到跳楼> 这篇博客,怒抄插头DP 然后16ms了,自己慢慢YY了一下,写出了风(gou)流(pi)倜(bu)傥(tong)

HDU 1693 Eat the Trees 插头DP

链接:http://acm.hdu.edu.cn/showproblem.php?pid=1693 题意:给出一块r*c的地,(r,c<=11),其中有的土地上种上了树,有些没有种上树,只能在种上树的地上走,通过走若干个回路,来走遍所有种树的土地.问有多少种走法. 思路:无论如何还是要先提供一个链接:http://wenku.baidu.com/view/4fe4ac659b6648d7c1c74633.html,是国家队论文,里面对于要提及的概念讲解的十分清楚. dp的过程是从左上角的点到右下

树状DP入门

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1520 题目大意:给定一棵关系树,每个节点有个权值,子节点和父节点不能同时选,问最后能选的最大价值是多少? 解题思路:树形DP入门题.由于子节点与父节点不能同时选,有人可能会用贪心思想,二者选其一肯定最优.其实不然,有可能父节点和子节点都不选,而要选子孙节点.不过只要再往深点想下,就可以得出动态规划的解法.每个节点要么选要么不选,和大多数选不选动归一样,来个dp[i][2],0表示不选,1表示不选,那

插头DP小结

首先是CDQ<基于连通性状态压缩的动态规划问题>论文上的题目: URAL 1519 Formula 1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 using namespace std; 5 const int maxn = 15; 6 const int HASH = 30007; 7 const int SIZE = 1000010; 8 typedef long lon