基于联通性的状态压缩动态规划是一类非常典型的状态压缩动态规划问题,由于其压缩的本质并不像是普通的状态压缩动态规划那样用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每一行的合法状态,然后更新状态至下一行。easy实现但算法复杂度较高,非常少有人使用。在此处不再赘述。

须要注意的是,尽管插头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,则在此处考虑两种插头的插法,并更新答案。

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

  1. 动态规划:插头DP

    这种动归有很多名字,插头DP是最常见的 还有基于连通性的动态规划 轮廓线动态规划等等 超小数据范围,网格图,连通性 可能算是状态压缩DP的一种变式 以前我了解的状压DP用于NP难题的小数据范围求解 这 ...

  2. hdu 1693 插头dp入门

    hdu1693 Eat the Trees 题意 在\(n*m\)的矩阵中,有些格子有树,没有树的格子不能到达,找一条或多条回路,吃完所有的树,求有多少种方法. 解法 这是一道插头dp的入门题,只需要 ...

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

    题面: 传送门 思路: 插头dp基础教程 先理解一下题意:实际上就是要你求这个棋盘中的哈密顿回路个数,障碍不能走 看到这个数据范围,还有回路处理,就想到使用插头dp来做了 观察一下发现,这道题因为都是 ...

  4. HDU 1693 插头dp入门详解

    放题目链接   https://vjudge.net/problem/22021/origin 给出一个n*m的01矩阵,1可走0不可通过,要求走过的路可以形成一个环且可以有多个环出现,问有多少不同的 ...

  5. hdu 1693 : Eat the Trees 【插头dp 入门】

    题目链接 题意: 给出一个n*m大小的01矩阵,在其中画线连成封闭图形,其中对每一个值为1的方格,线要恰好穿入穿出共两次,对每一个值为0的方格,所画线不能经过. 参考资料: <基于连通性状态压缩 ...

  6. 插头DP题目泛做(为了对应WYD的课件)

    题目1:BZOJ 1814 URAL 1519 Formula 1 题目大意:给定一个N*M的棋盘,上面有障碍格子.求一个经过所有非障碍格子形成的回路的数量. 插头DP入门题.记录连通分量. #inc ...

  7. POJ 2411 Mondriaan's Dream 插头dp

    题目链接: http://poj.org/problem?id=2411 Mondriaan's Dream Time Limit: 3000MSMemory Limit: 65536K 问题描述 S ...

  8. hdu1693 Eat the Trees 【插头dp】

    题目链接 hdu1693 题解 插头\(dp\) 特点:范围小,网格图,连通性 轮廓线:已决策点和未决策点的分界线 插头:存在于网格之间,表示着网格建的信息,此题中表示两个网格间是否连边 状态表示:当 ...

  9. [Hdu1693]Eat the Trees(插头DP)

    Description 题意:在n*m(1<=N, M<=11 )的矩阵中,有些格子有树,没有树的格子不能到达,找一条或多条回路,吃完所有的树,求有多少种方法. Solution 插头DP ...

随机推荐

  1. jQuery EasyUI 数字框(NumberBox)用法

    这里的options是选项,可以参考下表: 选项名 类型 描述 默认值 min 数字 文本框中可允许的最小值 null max 数字 文本框中可允许的最大值 null precision 数字 最高可 ...

  2. POJ 3228Gold Transportation(二分+最大流)

    题目地址:POJ3288 这个题跟之前的一道题混了,感觉是一样的,所以连想都没怎么想就拆点然后求最短路然后二分求最大流了.结果连例子都只是,还一直以为又是哪里手残了..结果看了看例子,手算也确实不正确 ...

  3. U盘安装Ubuntu14.4时遇到分区问题记录

    1.在安装Ubuntu14.4时,遇到如果先分出 / 跟挂载的主分区时,后面只能再分一个swap,或者挂载一个/home,或者一个/ boot 时不能继续分区,当然想安装也是不能不能成功的. 解决办法 ...

  4. tomcat的集群配置

    配置环境需要:1.Apache服务器,下载地址:http://httpd.apache.org/download.cgi#apache22 2.tomcat6.0或者tomcat7.0,(集群中tom ...

  5. webform基础介绍及页面传值(session,cookie)、跳转页面

    一,IIS 1.首先知道IIS是个什么东西:它是web服务器软件,安装在服务器上,接受客户端发来的请求,并传送给服务器端,然后响应请求并送回给客户端.类似于饭店里的服务员. 2.会安装IIS——控制面 ...

  6. Enze fourth day(循环语句 一)

    哈喽,大家好.又到了总结知识的时间了.今天在云和学院自学了一下循环语句,下面是自己总的一些知识点. 先补充一下选择结构中的switch语句. 理论:switch语句是一种多分支选择语句,当需要测试大量 ...

  7. Enze Third day(c#中选择结构【if...else】)

    哈喽,又到了我总结课堂知识的时间了.今天在云和学院学的是C#中的“选择结构”下的If语句.下面就来总结一下今天所学的吧. 理论:If语句是最常用的选择结构语句.它主要根据所给定的条件(常由关系表达式和 ...

  8. iOS开发中遇到的bug

    报错:The operation couldn’t be completed. (LaunchServicesError error 0.) 解决办法:重置模拟器

  9. 伪静态 apache重写

    mod_rewrite是Apache的一个非常强大的功能,它可以实现伪静态页面 下面我详细说说它的使用方法 A.检测Apache是否支持mod_rewrite 通过php提供的phpinfo()函数查 ...

  10. STL之set和multiset(集合)

    set和multiset会根据特定的排序准则,自动将元素进行排序.不同的是后者允许元素重复而前者不允许. constructing sets #include #include using names ...