动态规划之状态压缩
状态压缩动态规划状压DP
状态压缩动态规划状压DP总述状态压缩动态规划,就是我们俗称的状压DP,是利⽤计算机⼆进制的性质来描述状态的⼀种DP⽅式很多棋盘问题都运⽤到了状压,同时,状压也很经常和BFS及DP连⽤,例题⾥会给出介绍有了状态,DP就⽐较容易了举个例⼦:有⼀个⼤⼩为n*n的农⽥,我们可以在任意处种⽥,现在来描述⼀下某⼀⾏的某种状态:设n = 9;有⼆进制数 100011011(九位),每⼀位表⽰该农⽥是否被占⽤,1表⽰⽤了,0表⽰没⽤,这样⼀种状态就被我们表⽰出来了:见下表列数123456789⼆进制100011011是否⽤√×××√√×√√所以我们最多只需要 2n+1−1 的⼗进制数就好(左边那个数的⼆进制形式是n个1)现在我们有了表⽰状态的⽅法,但⼼⾥也会有些不安:上⾯⽤⼗进制表⽰⼆进制的数,枚举了全部的状态,DP起来复杂度岂不是很⼤?没错,状压其实是⼀种很暴⼒的算法,因为他需要遍历每个状态,所以将会出现2^n的情况数量,不过这并不代表这种⽅法不适⽤:⼀些题⽬可以依照题意,排除不合法的⽅案,使⼀⾏的总⽅案数⼤⼤减少从⽽减少枚举位运算有了状态,我们就需要对状态进⾏操作或访问可是问题来了:我们没法对⼀个⼗进制下的信息访问其内部存储的⼆进制信息,怎么办呢?别忘了,操作系统是⼆进制的,编译器中同样存在⼀种运算符:位运算能帮你解决这个问题(基础,这⾥不打算⾃⼰写了,参照,以下内容也复制⾃qxAi的这篇博客,这⾥谢谢博主)为了更好的理解状压dp,⾸先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个⼗进制数在⼆进制下进⾏与运算,然后返回其⼗进制下的值。
例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个⼗进制数在⼆进制下进⾏或运算,然后返回其⼗进制下的值。
例如3(11)|2(10)=3(11)。
3.’’符号,x y,会将两个⼗进制数在⼆进制下进⾏异或运算,然后返回其⼗进制下的值。
资料:动态规划状态压缩模型
动态规划状态压缩模型有一些动态规划的状态并不是放与不放,取与不取,选与不选那么简单。
状态可能因为情况而变得很多,比如在一个n*m(n<=500,m<=10)的棋盘里面放棋子,有些格子被挖掉不能放棋子,并且任何两个棋子不得有上下左右的相邻,问最多放多少个棋,该怎么做呢?注意到m出奇的小,这就提示我们使用状态压缩模型来做。
先分析是否有后效性,发现任何一行在其下一行没有被放置的时候,都只受上一行的影响,于是满足了无后效性。
具体的做法是,把每一行的摆放情况看成一个二进制数,放了的地方是1,不放的地方是0,因此,每一种状态都可以用唯一一个数字来表示,于是就可以记录当前状态最多可以放多少个棋子了。
这里有一个优化,有些状态本身就是不合法的,如23(10111)在同行中就不满足,应该完全不考虑。
所以,先进行预处理把所有可能的状态求出来是很必要的。
第一行的每一个状态的棋子数等于本身这个状态所拥有的棋子数。
在状态转移中,第k 行的第s个状态可以为k-1行所有可能的状态数的和加上本身状态所拥有的棋子数,至于两状态是否冲突,可以使用位运算判断。
最后,选择第n行记录最大数字的状态就是答案。
本算法的时间复杂度为O(n*(2^m)^2),当m比较小时,此算法还是很快了。
由于状态压缩中使用的空间比较大,通常是指数级别的,所以推荐使用滚动数组来记录。
有一些状态压缩模型并不是描述前一行的状态就可以了这么简单,如:炮兵阵地(NOI2001)【题目描述】司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队。
一个N*M的地图由N 行M列组成,地图的每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示)。
在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队)。
一支炮兵部队能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
炮兵的攻击范围不受地形的影响。
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
【最新推荐】状态压缩动态规划-范文模板 (13页)
本文部分内容来自网络整理,本司不为其真实性负责,如有异议或侵权请及时联系,本司将立即删除!== 本文为word格式,下载后可方便编辑和修改! ==状态压缩动态规划篇一:一道状态压缩DP的详细思路一道状态压缩DP的详细思路河北省衡水中学高亚在动态规划的过程中,状态的表示有时候很是恶心,不容易表示出来,所以,我们需要用一些编码技术,将这些状态表示出来,最常用的方法是用一个二进制数来表示一个集合的状态,下面通过一道例题来对状态压缩DP进行分析小keke同学非常喜欢玩俄罗斯方块(**),他最近发现传统的俄罗斯方块很无趣,于是他想到了一个新规则的游戏来恶心你(……,没素质啊)。
游戏是这样的:给定你一个宽度为w的游戏场地,我们设高度为正无穷。
现在给你3种俄罗斯方块: 1*2的方块2*2的方块2*2的方块去掉一个1*1的方块如果你明白俄罗斯方块的规则的话,方块在下落过程中是可以随便旋转的。
而且是从上往下落,上面的落在下面的上面(废话!!!)现在给定你一个高度h,让你求出有多少种游戏的方法,使得最后恰好落满h 的高度(最上层是齐平的)。
因为这样可以得巨多分!巨!舒服~~~~~两个整数h,w含义如题所述一个整数,为能达到要求的游戏方法的总数。
1<=h,w<=9,注意答案有可能很大(你懂得,用不到高精度)首先,先根据题意,将这个俄罗斯方块的所有形状都画出来(注意这些方块已经标上号了,下面直接引用不加说明)观察到只有七种情况,并且它们的高度只有两行,所以,一个俄罗斯方块最多只会影响上一行或下一行,而不会影响其他行题目中给出的高度,宽度最多只有9,用二进制位完全可以满足要求,所以可以用0表示这个地方是空的,1表示这个地方已经被填充了木块,那么可以很方便的用两个十进制数来表示两行的状态设f[i][j]表示前i行,并且第i行状态为j的最多方案数,那么因为第i行的放置方案仅仅只会影响上一行,所以可以得以下方程:f[i][j]=∑f[i?1][j′]其中,要求第i-1行的j'状态能够推到j这个状态来个最简单的例子,一个2*2的方格,从空的状态到把它填满,方案数是多少?(其实就是样例)可以看到,如果用刚才的方法,直接用0表示不填,用1表示填,那么这三种方案的状态表示是一样的,所以如果只是简单的求和,方案数就只是简单的1,并不是我们想要的结果,如何满足这一点的要求呢?其实可以采用预处理的方法,预处理出从状态i到状态j的方案数有多少种,记录到g[i][j]数组中(如果不能转移到,那么g[i][j] = 0),然后在方程转移的时候,直接采用下面的方程:f[i][j]=∑g[j′][j]?f[i?1][j′]其中j'还是老规矩,能从j'状态推到j这个状态因为有了g[j'][j]这个从j'状态推到j状态的方案数,所以在i-1行状态为j'时,想推到第i行状态为j,共有g[j'][j]种方案可以选择,根据乘法原理,所以f[i-1][j']的方案数乘以g[j'][j]就可以得到f[i][j],最后针对每种状态,求和即可好了,我主要想说的是如何进行预处理,最后还想说说方程中f数组的初值问题,因为这两个问题是我都犯过错的,或许能引以为戒预处理的目的是求出任何一个状态i到任何一个状态j的方案数一共有多少,那么显然,i和j都不大,搜索即可需要注意的是,搜索中有许多细节问题需要处理,相当考验细心搜索的时候,我们枚举的是状态i,然后搜索状态i能够推出什么样的状态j,针对推出的每一种合法的i, j,将相应的g[i][j]加1枚举状态i就不用多说了,一个for循环就可以了,注意区间是[0, 2w) (左闭右开)当有了状态i,那么我们的目标是将状态i填满(变成11111111这种形式),并且记录得到的相应的状态j,这个步骤的伪代码如下:基本上就是这些,注意边界的处理就好了第一次打这个搜索的时候,没有弄清搜索的实质是要干什么,导致position的加减总是不对,并且,状态考虑重复,造成了答案偏大基本的递推就是三重for循环,这个没什么问题,主要是那个f数组的初值问题这里必须且只能赋f[ 1][ 0] = 1,为什么?首先,f[ 1][ 0] = 1的含义是第一行什么都不填的方案数是1,这个应该没什么问题,但为什么不将f[ 1][j]的其它状态的初值赋上?还是先考虑一下赋初始值的目的,显然是将f[ 2][j]全部计算出来,对于任意的一个j,会枚举出所有的第一行的状态,并将它们全部累加,但其实第一行的任何合法状态都只能是一种方案,只将f[ 1][ 0]赋值就可以保证不会重复计算,如果其它的f[ 1][j]也有值的话,就会重复累加很多方案数,但其实并没有这么多综上所述,这里必须且只能赋f[ 1][ 0] = 1代码还可以吧,配合着上面的伪代码解析,应该挺容易懂的篇二:动态规划_状态转移方程-我们将人生划为诡异的阶段·我们把这个世界表为丰富的状态1. 资源问题1-----机器分配问题F[I,j]:=max(f[i-1,k]+w[i,j-k])2. 资源问题2 ------01背包问题 F[I,j]:=max(f[i-1,j-v[i]]+w[i],f[i-1,j]); 3. 线性动态规划1-----朴素最长非降子序列 F[i]:=max{f[j]+1}4. 剖分问题1-----石子合并 F[i,j]:=min(f[i,k]+f[k+1,j]+sum[i,j]);5. 剖分问题2 -----多边形剖分 F[I,j]:=min(f[i,k]+f[k,j]+a[k]*a[j]*a[i]);6. 剖分问题3 ------乘积最大 f[i,j]:=max(f[k,j-1]*mult[k,i]);7. 资源问题3 -----系统可靠性(完全背包) F[i,j]:=max{f[i-1,j-c[i]*k]*P[I,x]} 8. 贪心的动态规划1-----快餐问题F[i,j,k]:=max{f[i-1,j',k']+(T[i]-(j-j')*p1-(k-k')*p2) div p3} 9. 贪心的动态规划2 -----过河f[i]=min{{f(i-k)} (not stone[i]){f(i-k)}+1} (stone[i]); +贪心压缩状态10. 剖分问题4-----多边形-讨论的动态规划F[i,j]:=max{正正 f[I,k]*f[k+1,j]; 负负 g[I,k]*f[k+1,j]; 正负g[I,k]*f[k+1,j];负正 f[I,k]*g[k+1,j];} g为min 11. 树型动态规划1 -----加分二叉树 (从两侧到根结点模型) F[I,j]:=max{f[I,k-1]*f[k+1,j]+c[k]} 12. 树型动态规划2-----选课 (多叉树转二叉树,自顶向下模型)F[I,j]表示以i为根节点选j门功课得到的最大学分f[i,j]:=max{f[t[i].l,k]+f[t[i].r,j-k-1]+c[i]}13. 计数问题1 -----砝码称重 f[f[0]+1]=f[j]+k*w[j]; (1<=i<=n;1<=j<=f[0]; 1<=k<=a[i];) 14. 递推天地1 ------核电站问题f[-1]:=1;f[0]:=1; f[i]:=2*f[i-1]-f[i-1-m] 15. 递推天地2 ------数的划分f[i,j]:=f[i-j,j]+f[i-1,j-1]; 16. 最大子矩阵1 -----一最大01子矩阵f[i,j]:=min(f[i-1,j],v[i,j-1],v[i-1,j-1])+1; ans:=maxvalue(f); 17. 判定性问题1 -----能否被4整除g[1,0]:=true; g[1,1]:=false;g[1,2]:=false; g[1,3]:=false; g[i,j]:=g[i-1,k] and ((k+a[i,p]) mod 4 = j) 18. 判定性问题2 -----能否被k整除f[I,j±n[i] mod k]:=f[i-1,j];-k<=j<=k; 1<=i<=n20. 线型动态规划2-----方块消除游戏f[i,i-1,0]:=0 f[i,j,k]:=max{f[i,j-1,0]+sqr(len(j)+k),f[i,p,k+len[j]]+f[p+1,j-1,0]}ans:=f[1,m,0] 21. 线型动态规划3 -----最长公共子串,LCS问题 f[i,j]={0 (i=0)&(j=0); (i>0,j>0,x[i]=y[j]); f[i-1,j-1]+1 max{f[i,j-1]+f[i-1,j]}} (i>0,j>0,x[i]<>y[j]); 22. 最大子矩阵2-----最大带权01子矩阵O(n^2*m)。
如何提高动态规划与状态压缩算法的解题能力
如何提高动态规划与状态压缩算法的解题能力动态规划与状态压缩算法在计算机科学中是非常重要的算法技术。
通过合理地设计和优化算法,可以提高解题能力并降低时间和空间复杂度。
本文将介绍如何提高动态规划与状态压缩算法的解题能力,并探讨一些实用技巧和方法。
一、理解动态规划与状态压缩算法的基本原理动态规划是一种通过将问题拆解为子问题,并将子问题的解保存起来,以便在需要时进行查找的算法技术。
在解题过程中,动态规划算法可以帮助我们避免重复计算,提高计算效率。
状态压缩算法则是一种将状态编码为二进制表示,以减少存储空间和提高运算速度的技术。
二、学习经典的动态规划与状态压缩算法问题为了提高解题能力,我们可以学习并熟悉一些经典的动态规划与状态压缩算法问题。
例如,0-1背包问题、最长递增子序列问题、最短路径问题等都是经典的动态规划问题,而旅行商问题、集合覆盖问题等则是常见的使用状态压缩算法解决的问题。
通过研究和练习这些问题,我们可以提高对动态规划与状态压缩算法的理解和应用能力。
三、掌握常见的动态规划与状态压缩算法技巧1. 状态定义:合理定义状态是动态规划与状态压缩算法的关键。
在解决问题时,我们需要明确需要保存哪些状态信息,并将其定义为动态规划的状态变量或状态压缩的二进制位。
2. 状态转移方程:动态规划算法的核心在于状态转移方程的定义。
我们需要找到问题的递推关系,并通过状态转移方程将子问题的解与当前问题的解建立关联。
3. 空间优化:对于一些具有重叠子问题特性的问题,我们可以采用空间优化的技巧,例如使用一维数组代替二维数组,或者使用状态压缩的方法来减少存储空间。
4. 剪枝策略:动态规划和状态压缩算法中都存在冗余计算的问题,为了提高算法的效率,我们可以通过剪枝策略来减少不必要的计算。
5. 动态规划的优化:有时候,我们可以通过引入一些启发式的方法或贪心策略,对动态规划算法进行一定的优化,以提高解题能力和效率。
四、多做练习和实践要提高动态规划与状态压缩算法的解题能力,多做练习和实践是非常重要的。
状态压缩入门
例一: 例一:多米诺游戏
例一: 例一:多米诺游戏
这样,我们虽然把状态表示出来了,但是 10101该怎么作为数组的下标呢?显然不能 10101该怎么作为数组的下标呢?显然不能 开一个f[0..n,0..11111]的数组吧. 开一个f[0..n,0..11111]的数组吧.
例一: 例一:多米诺游戏
复习: 复习:状态的表示
f[1,0]:=1; f[1,3]:=1; f[i,0]:=f[if[i,0]:=f[i-1,3]; f[i,1]:=(f[i,1]+f[i-1,0]+f[if[i,1]:=(f[i,1]+f[i-1,0]+f[i-1,2]); f[i,2]:=(f[i,2]+f[i-1,0]+f[if[i,2]:=(f[i,2]+f[i-1,0]+f[i-1,1]); f[i,3]:=(f[i,3]+f[i-1,1]+f[i-1,2]+f[i-1,0]+f[if[i,3]:=(f[i,3]+f[i-1,1]+f[i-1,2]+f[i-1,0]+f[i-1,3]);
状态压缩入门
——长乐市第一中学 ——长乐市第一中学 内部材料
摘要
动态规划作为一个高效的算法已经在 多个领域得到了广泛应用,而基于状态压 缩的动态规划则是一种特殊的动态规划. 近年来在信息学竞赛中也涌现出个个元素 作为状态,或是以一整块作为状态,这样 虽然直观清楚,但记录的信息太少,因此 就会导致后效性的存在.而状态压缩模型 则是把许多个元素的状态(比如一行)作 为一个整体来表示状态,这样就为下面的 计算提供了更多的信息,从而保证了正确 性.但是由于是多个元素的集合体,所以 要在计算机上储存就要把它压缩,具体的 方法将在后面详细描述.
状态压缩动态规划学习笔记
状态压缩动态规划学习笔记状态压缩动态规划学习笔记算法介绍状态压缩动态规划是近些年来NOIP提⾼组常考的算法,也是⽇后ACM必备的算法之⼀,因此我们有必须要学习此类⾼级算法.⽽且此类算法往往是NP算法的最强优化之⼀.算法思想状态压缩动态规划,顾名思义也就是,将动态规划中的状态数组进⾏了压缩.那么想到压缩,我们不免就要想到⼀种常⽤的时间空间优化技巧,或者说⼀种特殊的算法,也就是位运算.卡常算法就是它,⾼端暴⼒就是它,奇迹算法还是它.位运算,也就是⼆进制的运算,⽽且我们的⼆进制,是⼀种计算机中最为核⼼的编码,这也就是为什么,电脑对于这种编码运算速度最快.状态压缩动态规划,就是利⽤了位运算,的这三⼤优化性质,来起到简化代码,优化代码,解决难题的⽬的.位运算基础&|^<<>>中⽂意思并或异或右移左移举例说明1&1=11|0=11^0=11<<1=1010>>1=11&0=00|0=01^1=011<<1=110101>>1=10判断算法⾸先拿到⼀道题⽬,我们第⼀步就是要看数据范围,题⽬描述. ⽽且当你发现数据范围和题⽬描述具有以下三⼤特点的时候,那么我们就可以初步判断这道题⽬需要使⽤状态压缩动态规划.1. 数据中的N,M范围很⼩,基本上不超过30,20.(N,M⼴义理解)2. 题⽬似乎要我们求⽅案数,或者说极值问题.3. 题⽬似乎是个棋盘覆盖这种类型的问题.算法处理⼀般来说,状态压缩动态规划算法,最为困难,也是最为关键的⼀步,就在于状态如何以⼆进制表⽰那么接下来我就来详细解说,状态压缩的状态到底如何设置.⼀般来说状态设置,往往是⼀个整数,表⽰⼀个⼆进制决策集合.⽐如说13,它就可以表⽰为1011,那么我们⼀般来说可以表⽰第⼀个点,第⼆点,第四个点已经选择这个意思.因为我们可以确定算法为状态压缩,那么我们现在的主⼒攻击,就是状态设置,既然现在我们已经有了这个⽬标,显然我们就是尽量地将题⽬的条件进⾏转化,在这⾥我们具体以棋盘类型来分析.对于条件⽽⾔的话,我们需要捕捉到关键点.1. 如果说题⽬中出现了这⼀个点不可以选择,那么你的神经中枢第⼀时间就要条件反射地,对⾃⼰的内⼼说⼀句,这⾥是1.这⾥是1到底是什么意思?其实这个意思,就是告诉我们这个点不可以选择,我们可以通过开⼀个特殊数组来保存,那么到了以后,对于我们枚举的⼀个决策集合,那么我们可以通过&运算,来判断这个点是否可以选择.⽐如说我们现在要求第五个点不可以选择,那么我们可以构造⼀个判断数组.10000表⽰第五个点不可以选择.那么假如说我们当前枚举的状态决策集合是11000,这个意思是,我们当前选择第四个点和第五个点.那么我们可以通过&运算,来进⾏判断.11000的⼗进制表⽰为24,⽽我们10000表⽰为16.那么我们进⾏&运算.24&16 ==> 11000 & 10000 ==> 16 ==> 10000总结:所以说我们可以通过&运算,进⾏判断是否选择.同理,如果题⽬说必须选择,显然&运算也可以发挥作⽤.其他运算操作以后再慢慢补充吧,我们先来⼏道题⽬感受感受.题⽬选讲第⼀题题⽬描述求把N×M的棋盘分割成若⼲个1×2的的长⽅形,有多少种⽅案。
基于连通性状态压缩的动态规划问题
基于连通性状态压缩的动态规划问题基于连通性状态压缩的动态规划问题基于状态压缩的动态规划问题是⼀类以集合信息为状态且状态总数为指数级的特殊的动态规划问题.在状态压缩的基础上,有⼀类问题的状态中必须要记录若⼲个元素的连通情况,我们称这样的问题为基于连通性状态压缩的动态规划问题,本⽂着重对这类问题的解法及优化进⾏探讨和研究.本⽂主要从动态规划的⼏个步骤——划分阶段,确⽴状态,状态转移以及程序实现来介绍这类问题的⼀般解法,会特别针对到⽬前为⽌信息学竞赛中涌现出来的⼏类题型的解法作⼀个探讨.结合例题,本⽂还会介绍作者在减少状态总数和降低转移开销两个⽅⾯对这类问题优化的⼀些⼼得.总结⾃序⾔先看⼀个⾮常经典的问题——旅⾏商问题(即TSP问题,Traveling Salesman Problem):⼀个n(≤15)个点的带权完全图,求权和最⼩的经过每个点恰好⼀次的封闭回路.这个问题已经被证明是NP完全问题,那么对于这样⼀类⽆多项式算法的问题,搜索算法是不是解决问题的唯⼀途径呢? 答案是否定的.不难发现任何时候我们只需要知道哪些点已经被遍历过⽽遍历点的具体顺序对以后的决策是没有影响的,因此不妨以当前所在的位置i,遍历过的点的集合S为状态作动态规划:动态规划的时间复杂度为,虽然为指数级算法,但是对于n = 15的数据规模来说已经⽐朴素的的搜索算法⾼效很多了.我们通常把这样⼀类以⼀个集合内的元素信息作为状态且状态总数为指数级别的动态规划称为基于状态压缩的动态规划或集合动态规划.基于状态压缩的动态规划问题通常具有以下两个特点:1.数据规模的某⼀维或⼏维⾮常⼩;2.它需要具备动态规划问题的两个基本性质:最优性原理和⽆后效性.⼀般的状态压缩问题,压缩的是⼀个⼩范围内每个元素的决策,状态中元素的信息相对独⽴.⽽有些问题,仅仅记录每个元素的决策是不够的,不妨再看⼀个例⼦:给你⼀个m * n (m,n≤9) 的矩阵,每个格⼦有⼀个价值,要求找⼀个连通块使得该连通块内所有格⼦的价值之和最⼤.按从上到下的顺序依次考虑每个格⼦选还是不选,下图为⼀个极端情况,其中⿊⾊的格⼦为所选的连通块.只考虑前5⾏的时候,所有的⿊⾊格⼦形成了三个连通块,⽽最后所有的⿊⾊格⼦形成⼀个连通块.如果状态中只单纯地记录前⼀⾏或前⼏⾏的格⼦选还是不选,是⽆法准确描述这个状态的,因此压缩的状态中我们需要增加⼀维,记录若⼲个格⼦之间的连通情况.我们把这⼀类必须要在状态中记录若⼲个元素之间的连通信息的问题称为基于连通性状态压缩的动态规划问题.本⽂着重对这类问题进⾏研究.连通是图论中⼀个⾮常重要的概念,在⼀个⽆向图中,如果两个顶点之间存在⼀条路径,则称这两个点连通.⽽基于连通性状态压缩的动态规划问题与图论模型有着密切的关联,⽐如后⽂涉及到的哈密尔顿回路、⽣成树等等.通常这类问题的本⾝与连通性有关或者隐藏着连通信息.全⽂共有六个章节.第⼀章,问题的⼀般解法,介绍解决基于连通性状态压缩的动态规划问题的⼀般思路和解题技巧;第⼆章,⼀类简单路径问题,介绍⼀类基于棋盘模型的简单路径问题的状态表⽰的改进——括号表⽰法以及提出⼴义的括号表⽰法;第三章,⼀类棋盘染⾊问题,介绍解决⼀类棋盘染⾊问题的⼀般思路;第四章,⼀类基于⾮棋盘模型的问题,介绍解决⼀类⾮棋盘模型的连通性状态压缩问题的⼀般思路;第五章,⼀类最优性问题的剪枝技巧,本章的重点是优化,探讨如何通过剪枝来减少扩展的状态的总数从⽽提⾼算法的效率;第六章,总结,回顾前⽂,总结解题⽅法.⼀. 问题的⼀般解法基于连通性状态压缩的动态规划问题通常具有⼀个⽐较固定的模式,⼏乎所有的题⽬都是在这个模式的基础上变形和扩展的.本章选取了⼀个有代表性的例题来介绍这⼀类问题的⼀般解法.【例1】Formula 1问题描述给你⼀个m * n的棋盘,有的格⼦是障碍,问共有多少条回路使得经过每个⾮障碍格⼦恰好⼀次.m, n ≤ 12.Ural1519, Timus Top Coders : Third Challenge如图,m = n = 4,(1, 1), (1, 2)是障碍,共有2条满⾜要求的回路.算法分析【划分阶段】这是⼀个典型的基于棋盘模型的问题,棋盘模型的特殊结构,使得它成为连通性状态压缩动态规划问题最常见的“舞台”.通常来说,棋盘模型有三种划分阶段的⽅法:逐⾏,逐列,逐格.顾名思义,逐⾏即从上到下或从下到上依次考虑每⼀⾏的状态,并转移到下⼀⾏;逐列即从左到右或从右到左依次考虑每⼀列的状态,并转移到下⼀列;逐格即按⼀定的顺序(如从上到下,从左到右)依次考虑每⼀格的状态,并转移到下⼀个格⼦.对于本题来说,逐⾏递推和逐列递推基本类似,接下来我们会对逐⾏递推和逐格递推的状态确⽴,状态转移以及程序实现⼀⼀介绍.有的题⽬, 逐⾏递推和逐列递推的状态表⽰有较⼤的区别, ⽐如本⽂后⾯会讲到的Rocket Mania⼀题【确⽴状态】先提出⼀个⾮常重要的概念——“插头”.对于⼀个4连通的问题来说,它通常有上下左右4个插头,⼀个⽅向的插头存在表⽰这个格⼦在这个⽅向可以与外⾯相连.本题要求回路的个数,观察可以发现所有的⾮障碍格⼦⼀定是从⼀个格⼦进来,另⼀个格⼦出去,即4个插头恰好有2个插头存在,共6种情况.逐⾏递推不妨按照从上到下的顺序依次考虑每⼀⾏.分析第i ⾏的哪些信息对第i + 1⾏有影响:我们需要记录第i⾏的每个格⼦是否有下插头,这决定了第i+1⾏的每个格⼦是否有上插头.仅仅记录插头是否存在是不够的,可能导致出现多个回路 (如图),⽽本题要求⼀个回路,也就隐含着最后所有的⾮障碍格⼦通过插头连接成了⼀个连通块,因此还需要记录第i⾏的n个格⼦的连通情况.我们称图中的蓝线为轮廓线,任何时候只有轮廓线上⽅与其直接相连的格⼦和插头才会对轮廓线以下的格⼦产⽣直接的影响.通过上⾯的分析,可以写出动态规划的状态:表⽰前i⾏,第i⾏的n个格⼦是否具有下插头的⼀个n位的⼆进制数为,第i⾏的n个格⼦之间的连通性为的⽅案总数.如何表⽰n个格⼦的连通性呢? 通常给每⼀个格⼦标记⼀个正数,属于同⼀个的连通块的格⼦标记相同的数.⽐如{1,1,2,2}和{2,2,1,1}都表⽰第1,2个格⼦属于⼀个连通块,第3,4个格⼦属于⼀个连通块.为了避免出现同⼀个连通信息有不同的表⽰,⼀般会使⽤最⼩表⽰法.⼀种最⼩表⽰法为:所有的障碍格⼦标记为0,第⼀个⾮障碍格⼦以及与它连通的所有格⼦标记为1,然后再找第⼀个未标记的⾮障碍格⼦以及与它连通的格⼦标记为2,……,重复这个过程,直到所有的格⼦都标记完毕.⽐如连通信息((1,2,5),(3,6),(4))表⽰为{1,1,2,3,1,2}.还有⼀种最⼩表⽰法,即⼀个连通块内所有的格⼦都标记成该连通块最左边格⼦的列编号,⽐如上⾯这个例⼦,我们表⽰为{1,1,3,4,1,3}.两种表⽰⽅法在转移的时候略有不同,本⽂后⾯将会提到.如上图三个状态我们可以依次表⽰为,,.状态表⽰的优化通过观察可以发现如果轮廓线上⽅的n个格⼦中某个格⼦没有下插头,那么它就不会再与轮廓线以下的格⼦直接相连,它的连通性对轮廓线以下的格⼦不会再有影响,也就成为了“冗余”信息.不妨将记录格⼦的连通性改成记录插头的连通性,如果这个插头存在,那么就标记这个插头对应的格⼦的连通标号,如果这个插头不存在,那么标记为0.这样状态就从精简为,上图三个状态表⽰为,,.优化后不仅状态表⽰更加简单,⽽且状态总数将会⼤⼤减少.因为第⼀种表⽰法更加直观, 本⽂如果不作特殊说明, 默认使⽤第⼀种最⼩表⽰法逐格递推按照从上到下,从左到右的顺序依次考虑每⼀格.分析转移完(i, j)这个格⼦后哪些信息对后⾯的决策有影响:同样我们可以刻画出轮廓线,即轮廓线上⽅是已决策格⼦,下⽅是未决策格⼦.由图可知与轮廓线直接相连的格⼦有n个,直接相连的插头有n+1个,包括n个格⼦的下插头以及(i, j)的右插头.为了保持轮廓线的“连贯性”,不妨从左到右依次给n个格⼦标号,n+1个插头标号.类似地,我们需要记录与轮廓线直接相连的n+1个插头是否存在以及n个格⼦的连通情况.通过上⾯的分析,很容易写出动态规划的状态:表⽰当前转移完(i, j)这个格⼦,n+1个插头是否存在表⽰成⼀个n+1位的⼆进制数S0,以及n个格⼦的连通性为S1的⽅案总数.逐⾏递推的时候我们提到了状态的优化,同样地,我们也可以把格⼦的连通性记录在插头上,新的状态为,上图3个状态依次为,,.【转移状态】状态的转移开销主要包含两个⽅⾯:每个状态转移的状态数,计算新的状态的时间.逐⾏递推 假设从第i⾏转移到第i+1⾏,我们需要枚举第i+1⾏的每个格⼦的状态(共6种情况),对于任何⼀个⾮障碍格⼦,它是否有上插头和左插头已知,因此最多只有2种情况,状态的转移数≤2n.枚举完第i+1⾏每个格⼦的状态后,需要计算第i+1⾏n个格⼦之间的连通性的最⼩表⽰,通常可以使⽤并查集的Father数组对其重新标号或者重新执⾏⼀次BFS/DFS,时间复杂度为O(n),最后将格⼦的连通性转移到插头的连通性上.特别需要注意的是在转移的过程中,为了避免出现多个连通块,除了最后⼀⾏,任何时候⼀个连通分量内⾄少有⼀个格⼦有下插头.逐格递推 仔细观察下⾯这个图,当转移到时,轮廓线上n个格⼦只有(i-1, j)被改成(i, j),n+1个插头只有2个插头被改动,即(i, j-1)的右插头修改成(i, j)的下插头和(i-1,j)的下插头修改成(i, j)的右插头.转移的时候枚举(i, j)的状态分情况讨论.⼀般棋盘模型的逐格递推转移有3类情况:新建⼀个连通分量,合并两个连通分量,以及保持原来的连通分量.下⾯针对本题进⾏分析:情况1 新建⼀个连通分量,这种情况出现在(i, j)有右插头和下插头.新建的两个插头连通且不与其它插头连通,这种情况下需要将这两个插头连通分量标号标记成⼀个未标记过的正数,重新O(n)扫描保证新的状态满⾜最⼩表⽰.情况2 合并两个连通分量,这种情况出现在(i, j)有上插头和左插头.如果两个插头不连通,那么将两个插头所处的连通分量合并,标记相同的连通块标号,O(n)扫描保证最⼩表⽰;如果已经连通,相当于出现了⼀个回路,这种情况只能出现在最后⼀个⾮障碍格⼦.情况3 保持原来的连通分量,这种情况出现在(i, j)的上插头和左插头恰好有⼀个,下插头和右插头也恰好有⼀个.下插头或右插头相当于是左插头或上插头的延续,连通块标号相同,并且不会影响到其他的插头的连通块标号,计算新的状态的时间为O(1).注意当从⼀⾏的最后⼀个格⼦转移到下⼀⾏的第⼀个格⼦的时候,轮廓线需要特殊处理.值得⼀提的是,上⾯三种情况计算新的状态的时间分别为O(n), O(n), O(1),如果使⽤前⾯提到的第⼆种最⼩表⽰⽅法,情况1只需要O(1),但是情况3可能需要O(n)重新扫描.⽐较⼀下逐⾏递推和逐格递推的状态的转移,逐⾏递推的每⼀个转移的状态总数为指数级,⽽逐格递推为O(1),每次计算新的状态的时间两者最坏情况都为O(n),但是逐⾏递推的常数要⽐逐格递推⼤,从转移开销这个⾓度来看,逐格递推的优势是⽏庸置疑的.【程序实现】逐⾏递推和逐格递推的程序实现基本⼀致,下⾯以逐格递推为例来说明.⾸先必须解决的⼀个问题是,对于像这样的⼀个状态我们该如何存储,可以开⼀个长度为n+1的数组来存取n+1个插头的连通性,但是数组判重并不⽅便,⽽且空间较⼤.不妨将n+1个元素进⾏编码,⽤⼀个或⼏个整数来存储,当我们需要取⼀个状态出来对它进⾏修改的时候再进⾏解码.编码最简单的⽅法就是表⽰成⼀个n+1位的p进制数,p可以取能够达到的最⼤的连通块标号加1,对本题来说,最多出现个连通块,不妨取p = 7.在不会超过数据类型的范围的前提下,建议将p改成2的幂,因为位运算⽐普通的运算要快很多,本题最好采⽤8进制来存储.如需⼤范围修改连通块标号,最好将状态O(n) 解码到⼀个数组中,修改后再O(n)计算出新的p进制数,⽽对于只需要局部修改⼏个标号的情况下,可以直接⽤(x div p i-1) mod p来获取第i位的状态,⽤直接对第i位进⾏修改.最后我们探讨⼀下实现的⽅法,⼀般有两种⽅法:1.对所有可能出现的状态进⾏编码,枚举编码⽅式:预处理将所有可能的连通性状态搜索出来,依次编号1, 2, 3, …,Tot,那么状态为表⽰转移完(i, j)后轮廓线状态编号为k的⽅案总数.将所有状态存⼊Hash表中,使得每个状态与编号⼀⼀对应,程序框架如下:1 For i ←1 to m2 For j ←1 to n3 For k ←1 to Tot4 For x ← (i, j, State[k]) 的所有转移后的状态5←状态x的编号6,为的后继格⼦.7 End For因为还要把0留出来存没有插头的情况2.记忆化宽度优先搜索:将初始状态放⼊队列中,每次取队⾸元素进⾏扩展,并⽤Hash对扩展出来的新的状态判重.程序框架如下:1Queue.Push(所有初始状态)2While not Empty(Queue)3 p ← Queue.Pop()4 For x ← p的所有转移后的状态5If x之前扩展过 Then6 Sum [x] ← Sum[x] + Sum[p]7Else8Queue.Push(x)9Sum[x] ← Sum[p]10 End If11 End For12 End While⽐较上述两种实现⽅法,直接编码的⽅法实现简单,结构清晰,但是有⼀个很⼤的缺点:⽆效状态可能很多,导致了很多次空循环,⽽⼤⼤影响了程序的效率.下⾯是⼀组实验的⽐较数据:表1.直接编码与宽度优先搜索扩展状态总数⽐较可以看出直接编码扩展的⽆效状态的⽐率⾮常⾼,对于障碍较多的棋盘其对⽐更加明显,因此通常来说宽度优先搜索扩展⽐直接编码实现效率要⾼.Hash判重的优化:使⽤⼀个HashSize较⼩的Hash表,每转移⼀个(i, j)清空⼀次,每次判断状态x是否扩展过的程序效率⽐⽤⼀个HashSize较⼤的Hash表每次判断状态(i, j, x)⾼很多.类似地,在不需要记录路径的情况下,也可以使⽤滚动的扩展队列来代替⼀个⼤的扩展队列.最后我们⽐较⼀下,不同的实现⽅法对程序效率的影响:Program 1 :8-Based,枚举编码⽅式.Program 2 :8-Based,队列扩展,HashSize = 3999997.Program 3 :8-Based,队列扩展,HashSize = 4001,Hash表每次清空.Program 4 :7-Based,队列扩展,HashSize = 4001,Hash表每次清空.表2.不同的实现⽅法的程序效率的⽐较测试环境: Intel Core2 Duo T7100, 1.8GHz, 1G内存⼩结本章从划分阶段,确⽴状态,状态转移以及程序实现四个⽅⾯介绍了基于连通性状态压缩动态规划问题的⼀般解法,并在每个⽅⾯归纳了⼀些不同的⽅法,最后对不同的算法的效率进⾏⽐较.在平时的解题过程中我们要学会针对题⽬的特点和数据规模“对症下药”,选择最合适的⽅法⽽达到最好的效果.由于逐格递推的转移开销⽐逐⾏递推⼩很多,下⽂如果不作特殊说明,我们都采⽤逐格的阶段划分.⼆. ⼀类简单路径问题这⼀章我们会针对⼀类基于棋盘模型的简单回路和简单路径问题的解法作⼀个探讨.简单路径,即除了起点和终点可能相同外,其余顶点均不相同的路径,⽽简单回路为起点和终点相同的简单路径.Formula 1是⼀个典型的棋盘模型的简单回路问题,这⼀章我们继续以这个题为例来说明.⾸先我们分析⼀下简单回路问题有什么特点:仔细观察上⾯的图,可以发现轮廓线上⽅是由若⼲条互不相交的路径构成的,⽽每条路径的两个端⼝恰好对应了轮廓线上的两个插头! ⼀条路径上的所有格⼦对应的是⼀个连通块,⽽每条路径的两个端⼝对应的两个插头是连通的⽽且不与其他任何⼀个插头连通.在上⼀章我们提到了逐格递推转移的时候的三种情况:新建⼀个连通分量,合并两个连通分量,保持原来的连通分量,它们分别等价于两个插头成为了⼀条新的路径的两端,两条路径的两个端⼝连接起来形成⼀条更长的路径或⼀条路径的两个端⼝连接起来形成⼀个回路以及延长原来的路径.通过上⾯的分析我们知道了简单回路问题⼀定满⾜任何时候轮廓线上每⼀个连通分量恰好有2个插头,那么这些插头之间有什么性质呢? 【性质】轮廓线上从左到右4个插头a, b, c, d,如果a, c连通,并且与b不连通,那么b, d⼀定不连通.证明:反证法,如果a, c连通,b, d连通,那么轮廓线上⽅⼀定⾄少存在⼀条a到c的路径和⼀条b到d的路径.如图,两条路径⼀定会有交点,不妨设两条路径相交于格⼦P,那么P既与a, c连通,⼜与b, d连通,可以推出a, c与b, d连通,⽭盾,得证.这个性质对所有的棋盘模型的问题都适⽤.“两两匹配”,“不会交叉”这样的性质,我们很容易联想到括号匹配.将轮廓线上每⼀个连通分量中左边那个插头标记为左括号,右边那个插头标记为右括号,由于插头之间不会交叉,那么左括号⼀定可以与右括号⼀⼀对应.这样我们就可以使⽤3进制——0表⽰⽆插头,1表⽰左括号插头,2表⽰右括号插头记录下所有的轮廓线信息.不妨⽤#表⽰⽆插头,那么上⾯的三幅图分别对应的是(())#(),(()#)(),(()###),即,我们称这种状态的表⽰⽅法为括号表⽰法.依然分三类情况来讨论状态的转移:为了叙述⽅便,不妨称(i,j-1)的右插头为p,(i-1, j)的下插头为q,(i, j)的下插头为p',右插头为q',那么每次转移相当于轮廓线上插头p的信息修改成的信息p',插头q的信息修改成的信息q',设W(x) = 0, 1, 2表⽰插头x的状态.情况1 新建⼀个连通分量,这种情况下W(p) = 0,W(q) = 0,p',q'两个插头构建了⼀条新的路径,p'相当于为左括号,q'为右括号,即W(p')← 1,W(q')← 2,计算新的状态的时间为O(1).情况2 合并两个连通分量,这种情况下W(p) > 0,W(q) > 0,W(p')← 0,W(q')← 0,根据p, q为左括号还是右括号分四类情况讨论:情况2.1 W(p) = 1,W(q) = 1.那么需要将q这个左括号与之对应的右括号v修改成左括号,即W(v) ← 1.情况2.2 W(p) = 2,W(q) = 2.那么需要将p这个右括号与之对应的左括号v修改成右括号,即W(v)← 2.情况2.3 W(p) = 1,W(q) = 2,那么p和q是相对应的左括号和右括号,连接p, q相当于将⼀条路径的两端连接起来形成⼀个回路,这种情况下只能出现在最后⼀个⾮障碍格⼦. 情况2.4 W(p) = 2,W(q) = 1,那么p和q连接起来后,p对应的左括号和q对应的右括号恰好匹配,不需要修改其他的插头的状态.情况2.1, 2.2需要计算某个左括号或右括号与之匹配的括号,这个时候需要对三进制状态解码,利⽤类似模拟栈的⽅法.因此情况2.1, 2.2计算新的状态的时间复杂度为O(n),2.3, 2.4时间复杂度为O(1).情况3 保持原来的连通分量,W(p),W(q)中恰好⼀个为0,p',q'中也恰好⼀个为0.那么⽆论p',q'中哪个插头存在,都相当于是p, q中那个存在的插头的延续,括号性质⼀样,因此W(p')←W(p) + W(q),W(q')← 0或者W(q')←W(p) + W(q),W(p')← 0.计算新的状态的时间复杂度为O(1).通过上⾯的分析可以看出,括号表⽰法利⽤了简单回路问题的“⼀个连通分量内只有2个插头”的特殊性质巧妙地⽤3进制状态存储下完整的连通信息,插头的连通性标号相对独⽴,不再需要通过O(n)扫描⼤范围修改连通性标号.实现的时候,我们可以⽤4进制代替3进制⽽提⾼程序运算效率,下⾯对最⼩表⽰法与括号表⽰法的程序效率进⾏⽐较:表3.不同的状态表⽰的程序效率的⽐较可以看出,括号表⽰法的优势⾮常明显,加上它的思路清晰⾃然,实现也更加简单,因此对于解决这样⼀类简单回路问题是⾮常有价值的.类似的问题还有:NWERC 2004 Pipes,Hnoi2004 Postman,Hnoi2007 Park,还有⼀类⾮回路问题也可以通过棋盘改造后⽤简单回路问题的⽅法解决,⽐如 POJ 1739 Tony’s Tour:给⼀个m * n棋盘,有的格⼦是障碍,要求从左下⾓⾛到右下⾓,每个格⼦恰好经过⼀次,问⽅案总数.(m, n ≤ 8)只需要将棋盘改造⼀下,问题就等价于Formula 1了........#.. 改造成 .#####.... .##..#........介绍完简单回路问题的解法,那么⼀般的简单路径问题⼜如何解决呢?【例2】Formula 2问题描述给你⼀个m * n的棋盘,有的格⼦是障碍,要求从⼀个⾮障碍格⼦出发经过每个⾮障碍格⼦恰好⼀次,问⽅案总数.m, n ≤ 10.改编⾃Formula 1如图,⼀个2 * 2的⽆障碍棋盘,共有4条满⾜要求的路径.算法分析确⽴状态:按照从上到下,从左到右依次考虑每⼀个格⼦,设表⽰转移完(i, j)这个格⼦,轮廓线状态为S的⽅案总数.如果⽤⼀般的最⼩表⽰法,不仅需要记录每个插头的连通情况,还需要额外记录每个插头是否连接了路径的⼀端,状态表⽰相当复杂.依然从括号表⽰法这个⾓度来思考如何来存储轮廓线的状态:这个问题跟简单回路问题最⼤的区别为:不是所有的插头都两两匹配,有的插头连接的路径的另⼀端不是⼀个插头⽽是整条路径的⼀端,我们称这样的插头为独⽴插头.不妨将原来的3进制状态修改成4进制——0表⽰⽆插头,1表⽰左括号插头,2表⽰右括号插头,3表⽰独⽴插头,这样我们就可以⽤4进制完整地记录下轮廓线的信息,图中状态表⽰为(1203)4.状态转移:依然设(i, j-1)的右插头为p,(i-1, j)的下插头为q,(i, j)的下插头为p',右插头为q'.部分转移同简单回路问题完全⼀样,这⾥不再赘述,下⾯分三类情况讨论与独⽴插头有关的转移:情况1 W(p) = 0,W(q) = 0.当前格⼦可能成为路径的⼀端,即右插头或下插头是独⽴插头,因此W(p')←3,W(q')← 0或者W(q')← 3,W(p')← 0.情况2 W(p) > 0,W(q) > 0,那么W(p')← 0,W(q')← 0 情况2.1 W(p) =3,W(q) = 3,将插头p和q连接起来就相当于形成了⼀条完整的路径,这种情况只能出现在最后⼀个⾮障碍格⼦. 情况2.2 W(p) ,W(q) 中有⼀个为3,如果p为独⽴插头,那么⽆论q是左括号插头还是右括号插头,与q相匹配的插头v成为了独⽴插头,因此,W(v)←3.如果q为独⽴插头,类似处理.情况3 W(p) ,W(q) 中有⼀个>0,即p, q中有⼀个插头存在. 情况3.1 如果这个插头为独⽴插头,若在最后⼀个⾮障碍格⼦,这个插头可以成为路径的⼀端,否则可以⽤右插头或下插头来延续这个独⽴插头. 情况3.2 如果这个插头是左括号或右括号,那么我们以将这个插头“封住”,使它成为路径的⼀端,需要将这个插头所匹配的另⼀个插头的状态修改成为独⽴插头.情况2.2, 3.2需要计算某个左括号或右括号与之匹配的括号,计算新的状态的时间复杂度为O(n),其余情况计算新的状态的时间复杂度为O(1).特别需要注意,任何时候轮廓线上独⽴插头的个数不可以超过2个.⾄此问题完整解决,m = n = 10的⽆障碍棋盘,扩展的状态总数为3493315,完全可以承受.上⾯两类题⽬我们⽤括号表⽰法取得了很不错的效果,但是它存在⼀定的局限性,即插头必须满⾜两两匹配.那么对于更加⼀般的问题,⼀个连通分量内出现⼤于2个插头,上述的括号表⽰⽅法显得束⼿⽆策.下⾯将介绍⼀种括号表⽰法的变形,它可以适⽤于出现连通块内⼤于2个插头的问题,我们称之为⼴义的括号表⽰法:假设⼀个连通分量从左到右有多个插头,不妨将最左边的插头标记为“(”,最右边的插头标记为“)”,中间的插头全部标记为“)(”,那么能够匹配的括号对应的插头连通.如果问题中可能出现⼀个连通分量只有⼀个插头,那么这个插头标记为“( )”,这样插头之间的连通性可⽤括号序列完整地记录下来,⽐如对于⼀个连通性状态为{1,2,2,3,4,3,2,1},我们可以⽤(-(-)(-(-()-)-)-)记录.这种⼴义的括号表⽰⽅法需要⽤4进制甚⾄5进制存储状态,⽽且直接对状态连通性进⾏修改情况⾮常多,最好还是将状态进⾏解码,修改后再重新编码.下⽂我们将会运⽤⼴义的括号表⽰法解决⼀些具体的问题.⼩结本章针对⼀类简单路径问题,提出了⼀种新的状态表⽰⽅法——括号表⽰法,最后提出了⼴义的括号表⽰⽅法.相⽐普通的最⼩表⽰法,括号表⽰法巧妙地把连通块与括号匹配⼀⼀对应,使得状态更加简单明了,虽然不会减少扩展的状态总数,但是转移开销的常数要⼩很多,是⼀个不错的⽅法.三. ⼀类棋盘染⾊问题有⼀类这样的问题——给你⼀个m * n的棋盘,要求给每个格⼦染上⼀种颜⾊(共k种颜⾊),每种颜⾊的格⼦相互连通 (4连通).本章主要对这类问题的解法进⾏探讨,我们从⼀个例题说起:【例3】Black & White问题描述⼀个m * n的棋盘,有的格⼦已经染上⿊⾊或⽩⾊,现在要求将所有的未染⾊格⼦染上⿊⾊或⽩⾊,使得满⾜以下2个限制:1) 所有的⿊⾊的格⼦是连通的,所有的⽩⾊格⼦也是连通的.2) 不会有⼀个2 * 2的⼦矩阵的4个格⼦的颜⾊全部相同.问⽅案总数.(m, n ≤ 8)如下图,m = 2,n = 3,灰⾊格⼦为未染⾊格⼦,共有9种染⾊⽅案.Source : Uva10572算法分析这是⼀个典型的棋盘染⾊问题,着⾊规则有:1) 只有⿊⽩两种颜⾊,即k = 2,并且同⾊的格⼦互相连通.2) 没有同⾊的2 * 2的格⼦.对于简单路径问题来说,相邻的格⼦是否连通取决于它们之间的插头是否存在,状态记录轮廓线上每个插头是否存在以及插头之间的连通性;⽽棋盘染⾊问题相邻的格⼦是否连通取决于它们的颜⾊是否相同,这就需要记录轮廓线上⽅n个格⼦的颜⾊以及格⼦之间的连通性.确⽴状态 设当前转移完Q(i, j)这个格⼦,对以后的决策产⽣影响的信息有:轮廓线上⽅n个格⼦的染⾊情况以及它们的连通性,由第2条着⾊规则“没有同⾊的2 * 2的格⼦”可知P(i-1, j)的颜⾊会影响到(i, j+1)着⾊,因此我们还需要额外记录格⼦的颜⾊.动态规划的状态为:表⽰转移完(i, j),轮廓线上从左到右n个格⼦的染⾊情况为S0 (0 ≤ S0 < 2n),连通性状态为S1,格⼦的颜⾊为cp(0或1)的⽅案总数.状态的精简 如果相邻的2个格⼦不属于同⼀个连通块,那么它们必然不同⾊,因此只需要记录(i, 1)和(i-1, j+1)两个格⼦的颜⾊,利⽤S1就可以推出n个格⼦的颜⾊.这个精简不会减少状态的总数,仍然需要⼀个变量来记录两个格⼦的颜⾊,因此意义并不⼤,这⾥只是提⼀下.。
状态压缩类型动态规划
样例
• 如下图,当N=3,K=5时,只有2种方案。 如下图, , 时 只有2种方案。
分析
• 类似第1题,我们按行放马,对于上一行的某种 类似第1 我们按行放马, 状态,看下一行有多少种放法。 状态,看下一行有多少种放法。 • 因为这里马的个数给定,因此需要增加一维马的 因为这里马的个数给定, 个数限制。 个数限制。 • 由于马的特殊型,马可以控制上下两行,因此每 由于马的特殊型,马可以控制上下两行, 一行的状态是与前两行相关联的, 一行的状态是与前两行相关联的,为了计数的方 采取两行一压的方式,用一个16位整数保存 便,采取两行一压的方式,用一个 位整数保存 两行的状态。规定高8位保存第一行的状态 位保存第一行的状态, 两行的状态。规定高 位保存第一行的状态,低八 位保存第二行的状态。 位保存第二行的状态。 • 每个格子对应的二进制位如果是 就表示这个格子 每个格子对应的二进制位如果是1就表示这个格子 里放了一个骑士,否则就是没有放。 里放了一个骑士,否则就是没有放。
W=4,h=2的求解过程 , 的求解过程
void dfs(int i, int s1, int s2, int d) { if (s == m) //如第 行已经做完,则累加 如第i行已经做完 如第 行已经做完, f[i + 1][s2] += f[i][s1]; else if ((jj & (1 << d)) == 0) //第d位为 位为0 第 位为 { dfs(i,s1, s2| (1 << d), d + 1); //将第 位变为 ,并右移 位 将第d位变为 将第 位变为1,并右移1位 //如果第 如果第d+1位也为 ,则直接搜索 位也为0,则直接搜索d+2位 如果第 位也为 位 if (s2 < m - 1 && (jj & (1 << (d + 1))) == 0) dfs(i,s1, s2, d + 2); } else dfs(i, s1,s2 & ~(1 << d), d + 1); //将第 位变为 并右移 位 将第d位变为 并右移1位 将第 位变为0,并右移 }
基于连通性状态压缩的动态规划问题
基于连通性状态压缩的动态规划问题长沙市雅礼中学陈丹琦【摘要】基于状态压缩的动态规划问题是一类以集合信息为状态且状态总数为指数级的特殊的动态规划问题.在状态压缩的基础上,有一类问题的状态中必须要记录若干个元素的连通情况,我们称这样的问题为基于连通性状态压缩的动态规划问题,本文着重对这类问题的解法及优化进行探讨和研究.本文主要从动态规划的几个步骤——划分阶段,确立状态,状态转移以及程序实现来介绍这类问题的一般解法,会特别针对到目前为止信息学竞赛中涌现出来的几类题型的解法作一个探讨.结合例题,本文还会介绍作者在减少状态总数和降低转移开销两个方面对这类问题优化的一些心得.【关键词】状态压缩连通性括号表示法轮廓线插头棋盘模型【目录】【序言】 (3)【正文】 (5)一. 问题的一般解法 (5)【例1】Formula 1 (5)问题描述 (5)算法分析 (5)小结 (11)二. 一类简单路径问题 (12)【例2】Formula 2 (15)问题描述 (15)算法分析 (15)小结 (16)三. 一类棋盘染色问题 (17)【例3】Black & White (17)问题描述 (17)算法分析 (17)小结 (19)四. 一类基于非棋盘模型的问题 (20)【例4】生成树计数 (20)问题描述 (20)算法分析 (20)小结 (21)五. 一类最优性问题的剪枝技巧 (22)【例5】Rocket Mania (22)问题描述 (22)算法分析 (23)小结 (25)六.总结 (25)【参考文献】 (26)【感谢】 (26)【附录】 (26)【序言】先看一个非常经典的问题——旅行商问题(即TSP 问题,Traveling Salesman Problem):一个n(≤15)个点的带权完全图,求权和最小的经过每个点恰好一次的封闭回路.这个问题已经被证明是NP 完全问题,那么对于这样一类无多项式算法的问题,搜索算法是不是解决问题的唯一途径呢? 答案是否定的.不难发现任何时候我们只需要知道哪些点已经被遍历过而遍历点的具体顺序对以后的决策是没有影响的,因此不妨以当前所在的位置i ,遍历过的点的集合S 为状态作动态规划:(,)min{(,{})(,)}f i S f j S i dist j i =-+,其中j<>i ,i ,j in S .动态规划的时间复杂度为2(2*)n O n ,虽然为指数级算法,但是对于n = 15的数据规模来说已经比朴素的(!)O n 的搜索算法高效很多了.我们通常把这样一类以一个集合内的元素信息作为状态且状态总数为指数级别的动态规划称为基于状态压缩的动态规划或集合动态规划.基于状态压缩的动态规划问题通常具有以下两个特点:1.数据规模的某一维或几维非常小;2.它需要具备动态规划问题的两个基本性质:最优性原理和无后效性.一般的状态压缩问题,压缩的是一个小范围内每个元素的决策,状态中元素的信息相对独立.而有些问题,仅仅记录每个元素的决策是不够的,不妨再看一个例子:给你一个m * n (m, n ≤9) 的矩阵,每个格子有一个价值,i j V ,要求找一个连通块使得该连通块内所有格子的价值之和最大.按从上到下的顺序依次考虑每个格子选还是不选,下图为一个极端情况,其中黑色的格子为所选的连通块.只考虑前5行的时候,所有的黑色格子形成了三个连通块,而最后所有的黑色格子形成一个连通块.如果状态中只单纯地记录前一行或前几行的格子选还是不选,是无法准确描述这个状态的,因此压缩的状态中我们需要增加一维,记录若干个格子之间的连通情况.我们把这一类必须要在状态中记录若干个元素之间的连通信息的问题称为基于连通性状态压缩的动态规划问题.本文着重对这类问题进行研究.连通是图论中一个非常重要的概念,在一个无向图中,如果两个顶点之间存在一条路径,则称这两个点连通.而基于连通性状态压缩的动态规划问题与图论模型有着密切的关联,比如后文涉及到的哈密尔顿回路、生成树等等.通常这类问题的本身与连通性有关或者隐藏着连通信息.全文共有六个章节.第一章,问题的一般解法,介绍解决基于连通性状态压缩的动态规划问题的一般思路和解题技巧;第二章,一类简单路径问题,介绍一类基于棋盘模型的简单路径问题的状态表示的改进——括号表示法以及提出广义的括号表示法;第三章,一类棋盘染色问题,介绍解决一类棋盘染色问题的一般思路;第四章,一类基于非棋盘模型的问题,介绍解决一类非棋盘模型的连通性状态压缩问题的一般思路;第五章,一类最优性问题的剪枝技巧,本章的重点是优化,探讨如何通过剪枝来减少扩展的状态的总数从而提高算法的效率;第六章,总结,回顾前文,总结解题方法.【正文】一. 问题的一般解法基于连通性状态压缩的动态规划问题通常具有一个比较固定的模式,几乎所有的题目都是在这个模式的基础上变形和扩展的.本章选取了一个有代表性的例题来介绍这一类问题的一般解法.【例1】Formula 11问题描述给你一个m * n的棋盘,有的格子是障碍,问共有多少条回路使得经过每个非障碍格子恰好一次.m, n ≤ 12.如图,m = n = 4,(1, 1), (1, 2)是障碍,共有2条满足要求的回路.算法分析【划分阶段】这是一个典型的基于棋盘模型的问题,棋盘模型的特殊结构,使得它成为连通性状态压缩动态规划问题最常见的“舞台”.通常来说,棋盘模型有三种划分阶段的方法:逐行,逐列,逐格.顾名思义,逐行即从上到下或从下到上依次考虑每一行的状态,并转移到下一行;逐列即从左到右或从右到左依次考虑每一列的状态,并转移到下一列;逐格即按一定的顺序(如从上到下,从左到右)依次考虑每一格的状态,并转移到下一个格子.对于本题来说,逐行递推和逐列递推基本类似2,接下来我们会对逐行递推和逐格递推的状态确立,状态转移以及程序实现一一介绍.1Ural1519, Timus Top Coders : Third Challenge2有的题目, 逐行递推和逐列递推的状态表示有较大的区别, 比如本文后面会讲到的Rocket Mania一题【确立状态】 先提出一个非常重要的概念——“插头”.对于一个4连通的问题来说,它通常有上下左右4个插头,一个方向的插头存在表示这个格子在这个方向可以与外面相连.本题要求回路的个数,观察可以发现所有的非障碍格子一定是从一个格子进来,另一个格子出去,即4个插头恰好有2个插头存在,共6种情况.逐行递推 不妨按照从上到下的顺序依次考虑每一行.分析第i 行的哪些信息对第i + 1行有影响:我们需要记录第i 行的每个格子是否有下插头,这决定了第i+1行的每个格子是否有上插头.仅仅记录插头是否存在是不够的,可能导致出现多个回路 (如右图),而本题要求一个回路,也就隐含着最后所有的非障碍格子通过插头连接成了一个连通块,因此还需要记录第i 行的n 个格子的连通情况.插头:0011 插头:1111 插头:10013连通性:(3,4) 连通性:(1,2) (3,4) 连通性:(1,2,3,4)4我们称图中的蓝线为轮廓线,任何时候只有轮廓线上方与其直接相连的格子和插头才会对轮廓线以下的格子产生直接的影响.通过上面的分析,可以写出动态规划的状态:01(,,)f i S S 表示前i 行,第i 行的n 个格子是否具有下插头的一个n 位的二进制数为0S ,第i 行的n 个格子之间的连通性为1S 的方案总数.如何表示n 个格子的连通性呢? 通常给每一个格子标记一个正数,属于同一个的连通块的格子标记相同的数.比如{1,1,2,2}和{2,2,1,1}都表示第1,2个格子属于一个连通块,第3,4个格子属于一个连通块.为了避免出现同一个连通信息有不同的表示,一般会使用最小表示法.一种最小表示法为:所有的障碍格子标记为0,第一个非障碍格子以及与它连通的所有格子标记为1,然后再找第一个未标记的非障碍格子以及与它连通的格子标记为2,……,重复这个过程,直到所有的格子都标记完毕.比如连通信息((1,2,5),(3,6),(4))表示为{1,1,2,3,1,2}.还有一种最小表示法,即一个连通块内所有的格子都标记成该连通块最左边格子的列编号,比如上面这个例子,我们表3从左到右, 0表示无插头, 1表示有插头 4 括号内的数表示的是格子的列编号, 一个括号内的格子属于一个连通块示为{1,1,3,4,1,3}.两种表示方法在转移的时候略有不同,本文后面将会提到5.如上图三个状态我们可以依次表示为2(1,(0011),{0,0,1,1})f ,2(2,(1111),{1,1,2,2})f ,2(3,(1001),{1,1,1,1})f .状态表示的优化 通过观察可以发现如果轮廓线上方的n 个格子中某个格子没有下插头,那么它就不会再与轮廓线以下的格子直接相连,它的连通性对轮廓线以下的格子不会再有影响,也就成为了“冗余”信息.不妨将记录格子的连通性改成记录插头的连通性,如果这个插头存在,那么就标记这个插头对应的格子的连通标号,如果这个插头不存在,那么标记为0.这样状态就从01(,,)f i S S 精简为(,)f i S ,上图三个状态表示为(1,{0,0,1,1})f ,(2,{1,1,2,2})f ,(3,{1,0,0,1})f .优化后不仅状态表示更加简单,而且状态总数将会大大减少.逐格递推 按照从上到下,从左到右的顺序依次考虑每一格.分析转移完(i, j)这个格子后哪些信息对后面的决策有影响:同样我们可以刻画出轮廓线,即轮廓线上方是已决策格子,下方是未决策格子.由图可知与轮廓线直接相连的格子有n 个,直接相连的插头有n+1个,包括n个格子的下插头以及(i, j)的右插头.为了保持轮廓线的“连贯性”,不妨从左到右依次给n 个格子标号,n+1个插头标号.类似地,我们需要记录与轮廓线直接相连的n+1个插头是否存在以及n 个格子的连通情况.通过上面的分析,很容易写出动态规划的状态:01(,,,)f i j S S 表示当前转移完(i, j)这个格子,n+1个插头是否存在表示成一个n+1位的二进制数S 0,以及n 个格子的连通性为S 1的方案总数.2(3,1,(10111),{1,1,2,2})f 2(3,2,(10111),{1,1,2,2})f 2(3,3,(10001),{1,1,1,1})f 逐行递推的时候我们提到了状态的优化,同样地,我们也可以把格子的连通性记录在插头上,新的状态为(,,)f i j S ,上图3个状态依次为(3,1,{1,0,1,2,2})f ,(3,2,{1,0,1,2,2})f ,(3,3,{1,0,0,0,1})f .5因为第一种表示法更加直观, 本文如果不作特殊说明, 默认使用第一种最小表示法【转移状态】状态的转移开销主要包含两个方面:每个状态转移的状态数,计算新的状态的时间.逐行递推 假设从第i 行转移到第i+1行,我们需要枚举第i+1行的每个格子的状态(共6种情况),对于任何一个非障碍格子,它是否有上插头和左插头已知,因此最多只有2种情况,状态的转移数≤2n .枚举完第i+1行每个格子的状态后,需要计算第i+1行n 个格子之间的连通性的最小表示,通常可以使用并查集的Father 数组对其重新标号或者重新执行一次BFS/DFS ,时间复杂度为O(n),最后将格子的连通性转移到插头的连通性上.特别需要注意的是在转移的过程中,为了避免出现多个连通块,除了最后一行,任何时候一个连通分量内至少有一个格子有下插头.逐格递推 仔细观察下面这个图,当(,1,)f i j S 转移到(,,')f i j S 时,轮廓线上n 个格子只有(i-1, j)被改成(i, j),n+1个插头只有2个插头被改动,即(i, j-1)的右插头修改成(i, j)的下插头和(i-1,j)的下插头修改成(i, j)的右插头.转移的时候枚举(i, j)的状态分情况讨论.一般棋盘模型的逐格递推转移有3类情况:新建一个连通分量,合并两个连通分量,以及保持原来的连通分量.下面针对本题进行分析:情况1 新建一个连通分量,这种情况出现在(i, j)有右插头和下插头.新建Condition I Condition IIICondition II的两个插头连通且不与其它插头连通,这种情况下需要将这两个插头连通分量标号标记成一个未标记过的正数,重新O(n)扫描保证新的状态满足最小表示.情况2 合并两个连通分量,这种情况出现在(i, j)有上插头和左插头.如果两个插头不连通,那么将两个插头所处的连通分量合并,标记相同的连通块标号,O(n)扫描保证最小表示;如果已经连通,相当于出现了一个回路,这种情况只能出现在最后一个非障碍格子.情况3 保持原来的连通分量,这种情况出现在(i, j)的上插头和左插头恰好有一个,下插头和右插头也恰好有一个.下插头或右插头相当于是左插头或上插头的延续,连通块标号相同,并且不会影响到其他的插头的连通块标号,计算新的状态的时间为O(1).注意当从一行的最后一个格子转移到下一行的第一个格子的时候,轮廓线需要特殊处理.值得一提的是,上面三种情况计算新的状态的时间分别为O(n), O(n), O(1),如果使用前面提到的第二种最小表示方法,情况1只需要O(1),但是情况3可能需要O(n)重新扫描.比较一下逐行递推和逐格递推的状态的转移,逐行递推的每一个转移的状态总数为指数级,而逐格递推为O(1),每次计算新的状态的时间两者最坏情况都为O(n),但是逐行递推的常数要比逐格递推大,从转移开销这个角度来看,逐格递推的优势是毋庸置疑的.【程序实现】逐行递推和逐格递推的程序实现基本一致,下面以逐格递推为例来说明.首先必须解决的一个问题是,对于像(3,2,{1,0,1,2,2})f 这样的一个状态我们该如何存储,可以开一个长度为n+1的数组来存取n+1个插头的连通性,但是数组判重并不方便,而且空间较大.不妨将n+1个元素进行编码,用一个或几个整数来存储,当我们需要取一个状态出来对它进行修改的时候再进行解码.编码最简单的方法就是表示成一个n+1位的p 进制数,p 可以取能够达到的最大的连通块标号加16,对本题来说,最多出现/26n ≤⎢⎥⎣⎦个连通块,不妨取p =7.在不会超过数据类型的范围的前提下,建议将p 改成2的幂,因为位运算比普通的运算要快很多,本题最好采用8进制来存储.如需大范围修改连通块标号,最好将状态O(n) 解码到一个数组中,修改后再O(n)计算出新的p 进制数,而对于只需要局部修改几个标号的情况下,可以直接用(x div p i-1) mod p 来获取第i 位的状态,用1*i k p -±直接对第i 位进行修改.最后我们探讨一下实现的方法,一般有两种方法:6 因为还要把0留出来存没有插头的情况1.对所有可能出现的状态进行编码,枚举编码方式:预处理将所有可能的 连通性状态搜索出来,依次编号1, 2, 3, …,Tot ,那么状态为(,,)f i j k 表示转移完(i, j)后轮廓线状态编号为k 的方案总数.将所有状态存入Hash 表中,使得每个状态与编号一一对应,程序框架如下:2.记忆化宽度优先搜索:将初始状态放入队列中,每次取队首元素进行扩展,并用Hash 对扩展出来的新的状态判重.程序框架如下:比较上述两种实现方法,直接编码的方法实现简单,结构清晰,但是有一个很大的缺点:无效状态可能很多,导致了很多次空循环,而大大影响了程序的效率.下面是一组实验的比较数据:表1.直接编码与宽度优先搜索扩展状态总数比较可以看出直接编码扩展的无效状态的比率非常高,对于障碍较多的棋盘其对比更加明显,因此通常来说宽度优先搜索扩展比直接编码实现效率要高.Hash判重的优化:使用一个HashSize较小的Hash表,每转移一个(i, j)清空一次,每次判断状态x是否扩展过的程序效率比用一个HashSize较大的Hash 表每次判断状态(i, j, x)高很多.类似地,在不需要记录路径的情况下,也可以使用滚动的扩展队列来代替一个大的扩展队列.最后我们比较一下,不同的实现方法对程序效率的影响7:Program 1 :8-Based,枚举编码方式.Program 2 :8-Based,队列扩展,HashSize = 3999997.Program 3 :8-Based,队列扩展,HashSize = 4001,Hash表每次清空.Program 4 :7-Based,队列扩展,HashSize = 4001,Hash表每次清空.表2.不同的实现方法的程序效率的比较小结本章从划分阶段,确立状态,状态转移以及程序实现四个方面介绍了基于连通性状态压缩动态规划问题的一般解法,并在每个方面归纳了一些不同的方法,最后对不同的算法的效率进行比较.在平时的解题过程中我们要学会针对题目的特点和数据规模“对症下药”,选择最合适的方法而达到最好的效果.由于逐格递推的转移开销比逐行递推小很多,下文如果不作特殊说明,我们都采用逐格的阶段划分.7测试环境: Intel Core2 Duo T7100, 1.8GHz, 1G内存二. 一类简单路径问题这一章我们会针对一类基于棋盘模型的简单回路和简单路径问题的解法作一个探讨.简单路径,即除了起点和终点可能相同外,其余顶点均不相同的路径,而简单回路为起点和终点相同的简单路径.Formula 1是一个典型的棋盘模型的简单回路问题,这一章我们继续以这个题为例来说明.首先我们分析一下简单回路问题有什么特点:仔细观察上面的图,可以发现轮廓线上方是由若干条互不相交的路径构成的,而每条路径的两个端口恰好对应了轮廓线上的两个插头! 一条路径上的所有格子对应的是一个连通块,而每条路径的两个端口对应的两个插头是连通的而且不与其他任何一个插头连通.在上一章我们提到了逐格递推转移的时候的三种情况:新建一个连通分量,合并两个连通分量,保持原来的连通分量,它们分别等价于两个插头成为了一条新的路径的两端,两条路径的两个端口连接起来形成一条更长的路径或一条路径的两个端口连接起来形成一个回路以及延长原来的路径.通过上面的分析我们知道了简单回路问题一定满足任何时候轮廓线上每一个连通分量恰好有2个插头,那么这些插头之间有什么性质呢?【性质】轮廓线上从左到右4个插头a, b, c, d,如果a, c连通,并且与b不连通,那么b, d一定不连通.证明:反证法,如果a, c连通,b, d连通,那么轮廓线上方一定至少存在一条a到c的路径和一条b到d的路径.如图,两条路径一定会有交点,不妨设两条路径相交于格子P,那么P既与a, c连通,又与b, d连通, dca可以推出a, c 与b, d 连通,矛盾,得证.这个性质对所有的棋盘模型的问题都适用.“两两匹配”,“不会交叉”这样的性质,我们很容易联想到括号匹配.将轮廓线上每一个连通分量中左边那个插头标记为左括号,右边那个插头标记为右括号,由于插头之间不会交叉,那么左括号一定可以与右括号一一对应.这样我们就可以使用3进制——0表示无插头,1表示左括号插头,2表示右括号插头记录下所有的轮廓线信息.不妨用#表示无插头,那么上面的三幅图分别对应的是(())#(),(()#)(),(()###),即333(1122012),(1120212),(1120002),我们称这种状态的表示方法为括号表示法.依然分三类情况来讨论状态的转移:为了叙述方便,不妨称(i,j-1)的右插头为p ,(i-1, j)的下插头为q ,(i, j)的下插头为p ',右插头为q ',那么每次转移相当于轮廓线上插头p 的信息修改成p '的信息,插头q 的信息修改成q '的信息,设W(x) = 0, 1, 2表示插头x 的状态.情况1 新建一个连通分量,这种情况下W(p) = 0,W(q) = 0,p ',q '两个插头构建了一条新的路径,相当于p '为左括号,q '为右括号,即()W p '← 1,()W q '← 2,计算新的状态的时间为O(1).情况2 合并两个连通分量,这种情况下W(p) > 0,W(q) > 0,()W p '← 0,()W q '← 0,根据p, q 为左括号还是右括号分四类情况讨论:情况2.1 W(p) = 1,W(q) = 1.那么需要将q 这个左括号与之对应的右括号v 修改成左括号,即W(v) ← 1.情况2.2 W(p) = 2,W(q) = 2.那么需要将p 这个右括号与之对应的左括号v 修改成右括号,即W(v)← 2.情况2.3 W(p) = 1,W(q) = 2,那么p 和q 是相对应的左括号和右括号,连接p, q 相当于将一条路径的两端连接起来形成一个回路,这种情况下只能出现在最后一个非障碍格子.情况2.4 W(p) = 2,W(q) = 1,那么p 和q 连接起来后,p 对应的左括号和q 对应的右括号恰好匹配,不需要修改其他的插头的状态.情况2.1图 v ) 情况2.2图情况2.1, 2.2需要计算某个左括号或右括号与之匹配的括号,这个时候需要对三进制状态解码,利用类似模拟栈的方法.因此情况2.1, 2.2计算新的状态的时间复杂度为O(n),2.3, 2.4时间复杂度为O(1).情况3 保持原来的连通分量,W(p),W(q)中恰好一个为0,()W p ',()W q '中也恰好一个为0.那么无论p ',q '中哪个插头存在,都相当于是p, q 中那个存在的插头的延续,括号性质一样,因此()W p '← W(p) + W(q),()W q '← 0或者()W q '← W(p) + W(q),()W p '← 0.计算新的状态的时间复杂度为O(1).通过上面的分析可以看出,括号表示法利用了简单回路问题的“一个连通分量内只有2个插头”的特殊性质巧妙地用3进制状态存储下完整的连通信息,插头的连通性标号相对独立,不再需要通过O(n)扫描大范围修改连通性标号.实现的时候,我们可以用4进制代替3进制而提高程序运算效率,下面对最小表示法与括号表示法的程序效率进行比较:表3.不同的状态表示的程序效率的比较可以看出,括号表示法的优势非常明显,加上它的思路清晰自然,实现也更加简单,因此对于解决这样一类简单回路问题是非常有价值的.类似的问题还有:NWERC 2004 Pipes ,Hnoi2004 Postman ,Hnoi2007 Park ,还有一类非回路问题也可以通过棋盘改造后用简单回路问题的方法解决,比如 POJ 1739 Tony ’s Tour :给一个m * n 棋盘,有的格子是障碍,要求从左下角走到右下角,每个格子恰好经过一次,问方案总数.(m, n ≤ 8)只需要将棋盘改造一下,问题就等价于Formula 1了........#.. 改造成 .#####.情况2.4图... .##..#........介绍完简单回路问题的解法,那么一般的简单路径问题又如何解决呢?【例2】Formula 28问题描述给你一个m * n的棋盘,有的格子是障碍,要求从一个非障碍格子出发经过每个非障碍格子恰好一次,问方案总数.m, n ≤ 10.如图,一个2 * 2的无障碍棋盘,共有4条满足要求的路径.算法分析确立状态:按照从上到下,从左到右依次考虑每一个格子,设(,,)f i j S表示转移完(i, j)这个格子,轮廓线状态为S的方案总数.如果用一般的最小表示法,不仅需要记录每个插头的连通情况,还需要额外记录每个插头是否连接了路径的一端,状态表示相当复杂.依然从括号表示法这个角度来思考如何来存储轮廓线的状态:这个问题跟简单回路问题最大的区别为:不是所有的插头都两两匹配,有的插头连接的路径的另一端不是一个插头而是整条路径的一端,我们称这样的插头为独立插头.不妨将原来的3进制状态修改成4进制——0表示无插头,1表示左括号插头,2表示右括号插头,3表示独立插头,这样我们就可以用4进制完整地记录下轮廓线的信息,图中状态表示为(1203)4.状态转移:依然设(i, j-1)的右插头为p,(i-1, j)的下插头为q,(i, j)的下插头为p',右插头为q'.部分转移同简单回路问题完全一样,这里不再赘述,下面分三类情况讨论与独立插头有关的转移:情况1 W(p) = 0,W(q) = 0.当前格子可能成为路径的一端,即右插头或8改编自Formula 1。
压缩型动态规划
状态压缩Abstract信息学发展势头迅猛,信息学奥赛的题目来源遍及各行各业,经常有一些在实际应用中很有价值的问题被引入信息学并得到有效解决。
然而有一些问题却被认为很可能不存在有效的(多项式级的)算法,本文以对几个例题的剖析,简述状态压缩思想及其应用。
Keywords状态压缩、集合、Hash、NPCContentIntroducti o n作为OIers,我们不同程度地知道各式各样的算法。
这些算法有的以O(logn)的复杂度运行,如二分查找、欧几里德GCD算法(连续两次迭代后的余数至多为原数的一半)、平衡树,有的以O(n)运行,例如二级索引、块状链表,再往上有O(n)、O(n p log q n)……大部分问题的算法都有一个多项式级别的时间复杂度上界1,我们一般称这类问题2为P类(deterministic Polynomial-time)问题,例如在有向图中求最短路径。
然而存在几类问题,至今仍未被很好地解决,人们怀疑他们根本没有多项式时间复杂度的算法,它们是NPC(NP-Complete)和NPH(NP-Hard)类,例如问一个图是否存在哈密顿圈(NPC)、问一个图是否不存在哈密顿圈(NPH)、求一个完全图中最短的哈密顿圈(即经典的Traveling Salesman Problem货郎担问题,NPH)、在有向图中求最长(简单)路径(NPH),对这些问题尚不知有多项式时间的算法存在。
P和NPC都是NP(Non-deterministic Polynomial-time)的子集,NPC则代表了NP类中最难的一类问题,所有的NP类问题都可以在多项式时间内归约到NPC问题中去。
NPH包含了NPC和其他一些不属于NP(也更难)的问题(即NPC是NP与NPH的交集), NPC问题的最优化版本一般是NPH的,例如问一个图是否存在哈密顿圈是NPC的,但求最短的哈密顿圈则是NPH的,原因在于我们可以在多项式时间内验证一个回路是否真的是哈密顿回路,却无法在多项式时间内验证其是否是最短的,NP类要求能在多项式时间内验证问题的一个解是否真的是一个解,所以最优化TSP问题不是NP的,而是NPH的。
基于连通性状态压缩的动态规划问题
基于连通性状态压缩的动态规划问题长沙市雅礼中学陈丹琦【摘要】基于状态压缩的动态规划问题是一类以集合信息为状态且状态总数为指数级的特殊的动态规划问题.在状态压缩的基础上,有一类问题的状态中必须要记录若干个元素的连通情况,我们称这样的问题为基于连通性状态压缩的动态规划问题,本文着重对这类问题的解法及优化进行探讨和研究.本文主要从动态规划的几个步骤——划分阶段,确立状态,状态转移以及程序实现来介绍这类问题的一般解法,会特别针对到目前为止信息学竞赛中涌现出来的几类题型的解法作一个探讨.结合例题,本文还会介绍作者在减少状态总数和降低转移开销两个方面对这类问题优化的一些心得.【关键词】状态压缩连通性括号表示法轮廓线插头棋盘模型【目录】【序言】 (3)【正文】 (5)一. 问题的一般解法 (5)【例1】Formula 1 (5)问题描述 (5)算法分析 (5)小结 (11)二. 一类简单路径问题 (12)【例2】Formula 2 (15)问题描述 (15)算法分析 (15)小结 (16)三. 一类棋盘染色问题 (17)【例3】Black & White (17)问题描述 (17)算法分析 (17)小结 (19)四. 一类基于非棋盘模型的问题 (20)【例4】生成树计数 (20)问题描述 (20)算法分析 (20)小结 (21)五. 一类最优性问题的剪枝技巧 (22)【例5】Rocket Mania (22)问题描述 (22)算法分析 (22)小结 (25)六.总结 (25)【参考文献】 (26)【感谢】 (26)【附录】 (26)【序言】先看一个非常经典的问题——旅行商问题(即TSP 问题,Traveling Salesman Problem):一个n (≤15)个点的带权完全图,求权和最小的经过每个点恰好一次的封闭回路.这个问题已经被证明是NP 完全问题,那么对于这样一类无多项式算法的问题,搜索算法是不是解决问题的唯一途径呢? 答案是否定的.不难发现任何时候我们只需要知道哪些点已经被遍历过而遍历点的具体顺序对以后的决策是没有影响的,因此不妨以当前所在的位置i ,遍历过的点的集合S 为状态作动态规划:(,)m i n {(,{})(f i S f j S i d i s t j i =-+,其中j<>i ,i ,j in S .动态规划的时间复杂度为2(2*)n O n ,虽然为指数级算法,但是对于n = 15的数据规模来说已经比朴素的(!)O n 的搜索算法高效很多了.我们通常把这样一类以一个集合内的元素信息作为状态且状态总数为指数级别的动态规划称为基于状态压缩的动态规划或集合动态规划.基于状态压缩的动态规划问题通常具有以下两个特点:1.数据规模的某一维或几维非常小;2.它需要具备动态规划问题的两个基本性质:最优性原理和无后效性.一般的状态压缩问题,压缩的是一个小范围内每个元素的决策,状态中元素的信息相对独立.而有些问题,仅仅记录每个元素的决策是不够的,不妨再看一个例子:给你一个m * n (m , n ≤9) 的矩阵,每个格子有一个价值,i j V ,要求找一个连通块使得该连通块内所有格子的价值之和最大.按从上到下的顺序依次考虑每个格子选还是不选,下图为一个极端情况,其中黑色的格子为所选的连通块.只考虑前5行的时候,所有的黑色格子形成了三个连通块,而最后所有的黑色格子形成一个连通块.如果状态中只单纯地记录前一行或前几行的格子选还是不选,是无法准确描述这个状态的,因此压缩的状态中我们需要增加一维,记录若干个格子之间的连通情况.我们把这一类必须要在状态中记录若干个元素之间的连通信息的问题称为基于连通性状态压缩的动态规划问题.本文着重对这类问题进行研究.连通是图论中一个非常重要的概念,在一个无向图中,如果两个顶点之间存在一条路径,则称这两个点连通.而基于连通性状态压缩的动态规划问题与图论模型有着密切的关联,比如后文涉及到的哈密尔顿回路、生成树等等.通常这类问题的本身与连通性有关或者隐藏着连通信息.全文共有六个章节.第一章,问题的一般解法,介绍解决基于连通性状态压缩的动态规划问题的一般思路和解题技巧;第二章,一类简单路径问题,介绍一类基于棋盘模型的简单路径问题的状态表示的改进——括号表示法以及提出广义的括号表示法;第三章,一类棋盘染色问题,介绍解决一类棋盘染色问题的一般思路;第四章,一类基于非棋盘模型的问题,介绍解决一类非棋盘模型的连通性状态压缩问题的一般思路;第五章,一类最优性问题的剪枝技巧,本章的重点是优化,探讨如何通过剪枝来减少扩展的状态的总数从而提高算法的效率;第六章,总结,回顾前文,总结解题方法.【正文】一. 问题的一般解法基于连通性状态压缩的动态规划问题通常具有一个比较固定的模式,几乎所有的题目都是在这个模式的基础上变形和扩展的.本章选取了一个有代表性的例题来介绍这一类问题的一般解法.【例1】Formula 11问题描述给你一个m * n的棋盘,有的格子是障碍,问共有多少条回路使得经过每个非障碍格子恰好一次.m, n≤ 12.如图,m = n = 4,(1, 1), (1, 2)是障碍,共有2条满足要求的回路.算法分析【划分阶段】这是一个典型的基于棋盘模型的问题,棋盘模型的特殊结构,使得它成为连通性状态压缩动态规划问题最常见的“舞台”.通常来说,棋盘模型有三种划分阶段的方法:逐行,逐列,逐格.顾名思义,逐行即从上到下或从下到上依次考虑每一行的状态,并转移到下一行;逐列即从左到右或从右到左依次考虑每一列的状态,并转移到下一列;逐格即按一定的顺序(如从上到下,从左到右)依次考虑每一格的状态,并转移到下一个格子.对于本题来说,逐行递推和逐列递推基本类似2,接下来我们会对逐行递推和逐格递推的状态确立,状态转移以及程序实现一一介绍.1Ural1519, Timus Top Coders : Third Challenge2有的题目, 逐行递推和逐列递推的状态表示有较大的区别, 比如本文后面会讲到的Rocket Mania一题【确立状态】 先提出一个非常重要的概念——“插头”.对于一个4连通的问题来说,它通常有上下左右4个插头,一个方向的插头存在表示这个格子在这个方向可以与外面相连.本题要求回路的个数,观察可以发现所有的非障碍格子一定是从一个格子进来,另一个格子出去,即4个插头恰好有2个插头存在,共6种情况.逐行递推 不妨按照从上到下的顺序依次考虑每一行.分析第i 行的哪些信息对第i + 1行有影响:我们需要记录第i 行的每个格子是否有下插头,这决定了第i +1行的每个格子是否有上插头.仅仅记录插头是否存在是不够的,可能导致出现多个回路 (如右图),而本题要求一个回路,也就隐含着最后所有的非障碍格子通过插头连接成了一个连通块,因此还需要记录第i 行的n 个格子的连通情况.插头:0011 插头:1111 插头:10013连通性:(3,4) 连通性:(1,2) (3,4) 连通性:(1,2,3,4)4我们称图中的蓝线为轮廓线,任何时候只有轮廓线上方与其直接相连的格子和插头才会对轮廓线以下的格子产生直接的影响.通过上面的分析,可以写出动态规划的状态:01(,,)f i S S 表示前i 行,第i 行的n 个格子是否具有下插头的一个n 位的二进制数为0S ,第i 行的n 个格子之间的连通性为1S 的方案总数.如何表示n 个格子的连通性呢? 通常给每一个格子标记一个正数,属于同一个的连通块的格子标记相同的数.比如{1,1,2,2}和{2,2,1,1}都表示第1,2个格子属于一个连通块,第3,4个格子属于一个连通块.为了避免出现同一个连通信息有不同的表示,一般会使用最小表示法.一种最小表示法为:所有的障碍格子标记为0,第一个非障碍格子以及与它连通的所有格子标记为1,然后再找第一个未标记的非障碍格子以及与它连通的格子标记为2,……,重复这个过程,直到所有的格子都标记完毕.比如连通信息((1,2,5),(3,6),(4))表示为{1,1,2,3,1,2}.还有一种最小表示法,即一个连通块内所有的格子都标记成该连通块最左边格子的列编号,比如上面这个例子,我们表3从左到右, 0表示无插头, 1表示有插头 4 括号内的数表示的是格子的列编号, 一个括号内的格子属于一个连通块示为{1,1,3,4,1,3}.两种表示方法在转移的时候略有不同,本文后面将会提到5.如上图三个状态我们可以依次表示为2(1,(0011),{0,0,1,1})f ,2(2,(1111),{1,1,2,2})f ,2(3,(1001),{1,1,1,1})f .状态表示的优化 通过观察可以发现如果轮廓线上方的n 个格子中某个格子没有下插头,那么它就不会再与轮廓线以下的格子直接相连,它的连通性对轮廓线以下的格子不会再有影响,也就成为了“冗余”信息.不妨将记录格子的连通性改成记录插头的连通性,如果这个插头存在,那么就标记这个插头对应的格子的连通标号,如果这个插头不存在,那么标记为0.这样状态就从01(,,)f i S S 精简为(,)f i S ,上图三个状态表示为(1,{0,0,1,1})f ,(2,{1,1,2,2})f ,(3,{1,0,0,1})f . 优化后不仅状态表示更加简单,而且状态总数将会大大减少.逐格递推 按照从上到下,从左到右的顺序依次考虑每一格.分析转移完(i , j )这个格子后哪些信息对后面的决策有影响:同样我们可以刻画出轮廓线,即轮廓线上方是已决策格子,下方是未决策格子.由图可知与轮廓线直接相连的格子有n个,直接相连的插头有n +1个,包括n 个格子的下插头以及(i , j )的右插头.为了保持轮廓线的“连贯性”,不妨从左到右依次给n 个格子标号,n +1个插头标号.类似地,我们需要记录与轮廓线直接相连的n +1个插头是否存在以及n 个格子的连通情况.通过上面的分析,很容易写出动态规划的状态:01(,,,)f i j S S 表示当前转移完(i , j )这个格子,n +1个插头是否存在表示成一个n +1位的二进制数S 0,以及n 个格子的连通性为S 1的方案总数.2(3,1,(10111),{1,1,2,2})f 2(3,2,(10111),{1,1,2,2})f 2(3,3,(10001),{1,1,1,1})f 逐行递推的时候我们提到了状态的优化,同样地,我们也可以把格子的连通性记录在插头上,新的状态为(,,)f i j S ,上图3个状态依次为(3,1,{1,0,1,2,2})f ,(3,2,{1,0,1,2,2})f ,(3,3,{1,0,0,0,1})f .5因为第一种表示法更加直观, 本文如果不作特殊说明, 默认使用第一种最小表示法【转移状态】状态的转移开销主要包含两个方面:每个状态转移的状态数,计算新的状态的时间.逐行递推 假设从第i 行转移到第i +1行,我们需要枚举第i +1行的每个格子的状态(共6种情况),对于任何一个非障碍格子,它是否有上插头和左插头已知,因此最多只有2种情况,状态的转移数≤2n .枚举完第i +1行每个格子的状态后,需要计算第i +1行n 个格子之间的连通性的最小表示,通常可以使用并查集的Father 数组对其重新标号或者重新执行一次BFS/DFS ,时间复杂度为O (n ),最后将格子的连通性转移到插头的连通性上.特别需要注意的是在转移的过程中,为了避免出现多个连通块,除了最后一行,任何时候一个连通分量内至少有一个格子有下插头.逐格递推 仔细观察下面这个图,当(,1,)f i j S 转移到(,,')f i j S 时,轮廓线上n 个格子只有(i -1, j)被改成(i , j ),n +1个插头只有2个插头被改动,即(i , j -1)的右插头修改成(i , j )的下插头和(i -1,j )的下插头修改成(i , j )的右插头.转移的时候枚举(i , j )的状态分情况讨论.一般棋盘模型的逐格递推转移有3类情况:新建一个连通分量,合并两个连通分量,以及保持原来的连通分量.下面针对本题进行分析:情况1 新建一个连通分量,这种情况出现在(i , j )有右插头和下插头.新建的两个插头连通且不与其它插头连通,这种情况下需要将这两个插头连通分量标号标记成一个未标记过的正数,重新O (n )扫描保证新的状态满足最小表示.Condition ICondition IIICondition II情况2 合并两个连通分量,这种情况出现在(i , j )有上插头和左插头.如果两个插头不连通,那么将两个插头所处的连通分量合并,标记相同的连通块标号,O (n )扫描保证最小表示;如果已经连通,相当于出现了一个回路,这种情况只能出现在最后一个非障碍格子.情况3 保持原来的连通分量,这种情况出现在(i , j )的上插头和左插头恰好有一个,下插头和右插头也恰好有一个.下插头或右插头相当于是左插头或上插头的延续,连通块标号相同,并且不会影响到其他的插头的连通块标号,计算新的状态的时间为O (1).注意当从一行的最后一个格子转移到下一行的第一个格子的时候,轮廓线需要特殊处理.值得一提的是,上面三种情况计算新的状态的时间分别为O (n ), O (n ), O (1),如果使用前面提到的第二种最小表示方法,情况1只需要O (1),但是情况3可能需要O (n )重新扫描.比较一下逐行递推和逐格递推的状态的转移,逐行递推的每一个转移的状态总数为指数级,而逐格递推为O (1),每次计算新的状态的时间两者最坏情况都为O (n ),但是逐行递推的常数要比逐格递推大,从转移开销这个角度来看,逐格递推的优势是毋庸置疑的.【程序实现】逐行递推和逐格递推的程序实现基本一致,下面以逐格递推为例来说明.首先必须解决的一个问题是,对于像(3,2,{1,0,1,2,2})f 这样的一个状态我们该如何存储,可以开一个长度为n +1的数组来存取n +1个插头的连通性,但是数组判重并不方便,而且空间较大.不妨将n +1个元素进行编码,用一个或几个整数来存储,当我们需要取一个状态出来对它进行修改的时候再进行解码.编码最简单的方法就是表示成一个n +1位的p 进制数,p 可以取能够达到的最大的连通块标号加16,对本题来说,最多出现/26n ≤⎢⎥⎣⎦个连通块,不妨取p =7.在不会超过数据类型的范围的前提下,建议将p 改成2的幂,因为位运算比普通的运算要快很多,本题最好采用8进制来存储.如需大范围修改连通块标号,最好将状态O (n ) 解码到一个数组中,修改后再O (n )计算出新的p 进制数,而对于只需要局部修改几个标号的情况下,可以直接用(x div p i -1) mod p 来获取第i 位的状态,用1*i k p -±直接对第i 位进行修改.最后我们探讨一下实现的方法,一般有两种方法:1.对所有可能出现的状态进行编码,枚举编码方式:预处理将所有可能的 连通性状态搜索出来,依次编号1, 2, 3, …,Tot ,那么状态为(,,)f i j k 表示转移完6 因为还要把0留出来存没有插头的情况(i , j )后轮廓线状态编号为k 的方案总数.将所有状态存入Hash 表中,使得每个状态与编号一一对应,程序框架如下:2.记忆化宽度优先搜索:将初始状态放入队列中,每次取队首元素进行扩展,并用Hash 对扩展出来的新的状态判重.程序框架如下:比较上述两种实现方法,直接编码的方法实现简单,结构清晰,但是有一个很大的缺点:无效状态可能很多,导致了很多次空循环,而大大影响了程序的效率.下面是一组实验的比较数据:表1.直接编码与宽度优先搜索扩展状态总数比较 测试数据 宽度优先搜索 扩展状态总数直接编码 Tot Tot * m * n 无效状态比率 m = 9, n = 9 (1,1)为障碍 309302188 177228 82.5% m = 10, n = 10 无障碍 1340115798 579800 76.8% m = 11, n = 11 (1,1)为障碍 33326415511 1876831 82.2% m = 12, n = 12无障碍 1333113 41835 6024240 77.9%For i ← 1 to mFor j ←1 to nFor k ← 1 to TotFor x ← (i , j , State [k ]) 的所有转移后的状态'k ← 状态x 的编号(,,)(,,)(,,)f i j k f i j k f i j k ''''''←+,(,)i j ''为(,)i j 的后继格子.End ForQueue.Push(所有初始状态) While not Empty(Queue) p ← Queue.Pop() For x ← p 的所有转移后的状态 If x 之前扩展过 Then Sum [x ] ← Sum[x ] + Sum[p ] Else Queue.Push(x ) Sum[x ] ← Sum[p ] End If End For End While可以看出直接编码扩展的无效状态的比率非常高,对于障碍较多的棋盘其对比更加明显,因此通常来说宽度优先搜索扩展比直接编码实现效率要高.Hash判重的优化:使用一个HashSize较小的Hash表,每转移一个(i, j)清空一次,每次判断状态x是否扩展过的程序效率比用一个HashSize较大的Hash表每次判断状态(i, j, x)高很多.类似地,在不需要记录路径的情况下,也可以使用滚动的扩展队列来代替一个大的扩展队列.最后我们比较一下,不同的实现方法对程序效率的影响7:Program 1 :8-Based,枚举编码方式.Program 2 :8-Based,队列扩展,HashSize = 3999997.Program 3 :8-Based,队列扩展,HashSize = 4001,Hash表每次清空.Program 4 :7-Based,队列扩展,HashSize = 4001,Hash表每次清空.表2.不同的实现方法的程序效率的比较测试数据Program 1 Program 2 Program 3 Program 4m = 10, n = 1046ms 31ms 15ms 31ms 无障碍棋盘m = 11, n = 11140ms 499ms 109ms 187ms (1,1)为障碍m = 12, n = 12624ms 1840ms 499ms 873ms 无障碍小结本章从划分阶段,确立状态,状态转移以及程序实现四个方面介绍了基于连通性状态压缩动态规划问题的一般解法,并在每个方面归纳了一些不同的方法,最后对不同的算法的效率进行比较.在平时的解题过程中我们要学会针对题目的特点和数据规模“对症下药”,选择最合适的方法而达到最好的效果.由于逐格递推的转移开销比逐行递推小很多,下文如果不作特殊说明,我们都采用逐格的阶段划分.7测试环境: Intel Core2 Duo T7100, 1.8GHz, 1G内存二. 一类简单路径问题这一章我们会针对一类基于棋盘模型的简单回路和简单路径问题的解法作一个探讨.简单路径,即除了起点和终点可能相同外,其余顶点均不相同的路径,而简单回路为起点和终点相同的简单路径.Formula 1是一个典型的棋盘模型的简单回路问题,这一章我们继续以这个题为例来说明.首先我们分析一下简单回路问题有什么特点:仔细观察上面的图,可以发现轮廓线上方是由若干条互不相交的路径构成的,而每条路径的两个端口恰好对应了轮廓线上的两个插头! 一条路径上的所有格子对应的是一个连通块,而每条路径的两个端口对应的两个插头是连通的而且不与其他任何一个插头连通.在上一章我们提到了逐格递推转移的时候的三种情况:新建一个连通分量,合并两个连通分量,保持原来的连通分量,它们分别等价于两个插头成为了一条新的路径的两端,两条路径的两个端口连接起来形成一条更长的路径或一条路径的两个端口连接起来形成一个回路以及延长原来的路径.通过上面的分析我们知道了简单回路问题一定满足任何时候轮廓线上每一个连通分量恰好有2个插头,那么这些插头之间有什么性质呢?【性质】轮廓线上从左到右4个插头a , b , c , d ,如果a , c 连通,并且与b 不连通,那么b , d 一定不连通.证明:反证法,如果a , c 连通,b , d 连通,那么轮廓线上方一定至少存在一条a 到c 的路径和一条b 到d的路径.如图,两条路径一定会有交点,不妨设两条路径相交于格子P ,那么P 既与a , c 连通,又与b , d 连通,可以推出a , c 与b , d 连通,矛盾,得证.这个性质对所有的棋盘模型的问题都适用.“两两匹配”,“不会交叉”这样的性质,我们很容易联想到括号匹配.将轮廓线上每一个连通分量中左边那个插头标记为左括号,右边那个插头标记为右括号,由于插头之间不会交叉,那么左括号一定可以与右括号一一对应.这样我们 dc a b就可以使用3进制——0表示无插头,1表示左括号插头,2表示右括号插头记录下所有的轮廓线信息.不妨用#表示无插头,那么上面的三幅图分别对应的是(())#(),(()#)(),(()###),即333(1122012),(1120212),(1120002),我们称这种状态的表示方法为括号表示法.依然分三类情况来讨论状态的转移:为了叙述方便,不妨称(i,j-1)的右插头为p,(i-1, j)的下插头为q,(i, j)的下插头为p',右插头为q',那么每次转移相当于轮廓线上插头p的信息修改成p'的信息,插头q的信息修改成q'的信息,设W(x) = 0, 1, 2表示插头x的状态.情况1 新建一个连通分量,这种情况下W(p) = 0,W(q) = 0,p',q'两个插头构建了一条新的路径,相当于p'为左括号,q'为右括号,即()W p'←1,()W q'← 2,计算新的状态的时间为O(1).情况2 合并两个连通分量,这种情况下W(p) > 0,W(q) > 0,()W p'← 0,()W q'← 0,根据p, q为左括号还是右括号分四类情况讨论:情况2.1W(p) = 1,W(q) = 1.那么需要将q这个左括号与之对应的右括号v修改成左括号,即W(v) ← 1.情况2.2W(p) = 2,W(q) = 2.那么需要将p这个右括号与之对应的左括号v修改成右括号,即W(v)← 2.情况2.3W(p) = 1,W(q) = 2,那么p和q是相对应的左括号和右括号,连接p, q相当于将一条路径的两端连接起来形成一个回路,这种情况下只能出现在最后一个非障碍格子.情况2.4W(p) = 2,W(q) = 1,那么p和q连接起来后,p对应的左括号和q对应的右括号恰好匹配,不需要修改其他的插头的状态.pq 情况2.3图pq()情况2.4图qp v( ) 情况2.1图qp (v)情况2.2图情况2.1, 2.2需要计算某个左括号或右括号与之匹配的括号,这个时候需要对三进制状态解码,利用类似模拟栈的方法.因此情况2.1, 2.2计算新的状态的时间复杂度为O(n),2.3, 2.4时间复杂度为O(1).情况3 保持原来的连通分量,W(p),W(q)中恰好一个为0,()W p',()W q'中也恰好一个为0.那么无论p',q'中哪个插头存在,都相当于是p, q中那个存在的插头的延续,括号性质一样,因此()W p'←W(p) + W(q),()W q'←0或者()W q'←W(p) + W(q),()W p'← 0.计算新的状态的时间复杂度为O(1).通过上面的分析可以看出,括号表示法利用了简单回路问题的“一个连通分量内只有2个插头”的特殊性质巧妙地用3进制状态存储下完整的连通信息,插头的连通性标号相对独立,不再需要通过O(n)扫描大范围修改连通性标号.实现的时候,我们可以用4进制代替3进制而提高程序运算效率,下面对最小表示法与括号表示法的程序效率进行比较:表3.不同的状态表示的程序效率的比较测试数据最小表示法7Based最小表示法8Based括号表示法3Based括号表示法4Basedm = 10, n = 10无障碍棋盘31ms 15ms 0ms 0ms m = 11, n = 11(1,1)为障碍187ms 109ms 46ms 31ms m = 12, n = 12无障碍873ms 499ms 265ms 140ms 可以看出,括号表示法的优势非常明显,加上它的思路清晰自然,实现也更加简单,因此对于解决这样一类简单回路问题是非常有价值的.类似的问题还有:NWERC 2004 Pipes,Hnoi2004 Postman,Hnoi2007 Park,还有一类非回路问题也可以通过棋盘改造后用简单回路问题的方法解决,比如POJ 1739 Tony’s Tour:给一个m * n棋盘,有的格子是障碍,要求从左下角走到右下角,每个格子恰好经过一次,问方案总数.(m, n ≤ 8)只需要将棋盘改造一下,问题就等价于Formula 1了........#.. 改造成 .#####.... .##..#........介绍完简单回路问题的解法,那么一般的简单路径问题又如何解决呢?【例2】Formula 28问题描述给你一个m * n的棋盘,有的格子是障碍,要求从一个非障碍格子出发经过每个非障碍格子恰好一次,问方案总数.m, n≤ 10.如图,一个2 * 2的无障碍棋盘,共有4条满足要求的路径.算法分析确立状态:按照从上到下,从左到右依次考虑每一个格子,设(,,)f i j S表示转移完(i, j)这个格子,轮廓线状态为S的方案总数.如果用一般的最小表示法,不仅需要记录每个插头的连通情况,还需要额外记录每个插头是否连接了路径的一端,状态表示相当复杂.依然从括号表示法这个角度来思考如何来存储轮廓线的状态:这个问题跟简单回路问题最大的区别为:不是所有的插头都两两匹配,有的插头连接的路径的另一端不是一个插头而是整条路径的一端,我们称这样的插头为独立插头.不妨将原来的3进制状态修改成4进制——0表示无插头,1表示左括号插头,2表示右括号插头,3表示独立插头,这样我们就可以用4进制完整地记录下轮廓线的信息,图中状态表示为(1203)4.状态转移:依然设(i, j-1)的右插头为p,(i-1, j)的下插头为q,(i, j)的下插头为p',右插头为q'.部分转移同简单回路问题完全一样,这里不再赘述,下面分三类情况讨论与独立插头有关的转移:情况1 W(p) = 0,W(q) = 0.当前格子可能成为路径的一端,即右插头或下插头是独立插头,因此()W p'← 0.W q'← 3,()W q'← 0或者()W p'←3,()情况2 W(p) > 0,W(q) > 0,那么()W p'← 0,()W q'← 0情况2.1 W(p) =3,W(q) = 3,将插头p和q连接起来就相当于形成了8改编自Formula 1。
状态压缩动态规划浅谈
最优性原理、无后效性。 2、数据某几维规模比较小。
传统集合动态规划
例题一: 给定n个点的有向带权图,求一条经过每个
点一次的回路,并要求权和最小。 范围n<=15。
传统集合动态规划
显然对于某一个中间状态,影响它的最后 结果的仅仅是当前所在点以及之前已经经 过的点。而之前的路径行走情况与之后的 解无关。
性(染色法记录属于第几个连通块,最小表示)。 对于本题最多同时存在6个连通块的插头。
实现细节
转移:分类讨论插头方向。 1、当前格上方左方均有插头:只能将这两个连通
块连接。(1种) 2、当前格只有上方有插头:将这个插头向下向右
延伸。(2种) 3、当前格只有左方有插头:将这个插头向下向右
剪枝二:如果轮廓线上有一个插头p,它没 有被火柴点燃且没有其它的插头与它连通, 那么这个插头可以认为是“无效”插头。 这个状态可以剪枝。(必然存在不存在这 个“无效”插头的状态)
状态压缩动态规划中的剪枝
剪枝三:对于一个格子(i, j)的两个状态 (opt1,fired1)(opt2,fired2),如果第一个状 态的每一个存在的插头在第二个状态中不 仅存在而且都被点燃,那么第一个状态可 以剪枝。(一个状态必然不比第二个状态 优)
延伸。(2种) 4、当前格周围无插头:若当前格为障碍物,则无
插头,否则插入一个折线形插头。
实现细节
合并连通块: 对于第一种情况,需要合并连通块。若不加限制,
则会计算出包含多条回路的情况。 限制:和并连通块时,若两个插头属于同一个连
通块,则当且仅当在最后一个有效格子中可以将 这两个插头连接。 最后统计:计算到最后一个有效格子时,需要统 计答案。此时,要保证当前状态没有剩余的插头。
动态规划算法的优化和拓展
动态规划算法的优化和拓展动态规划算法是一种常见的算法思想,常用于解决最优化问题。
它的优点在于可以有效地避免重复计算和枚举,从而提高了计算效率。
但是,由于数据规模的增加,我们需要进一步优化和拓展动态规划算法,以适应更复杂的问题。
一、优化空间复杂度在一些需要处理海量数据的问题中,空间复杂度往往成为瓶颈。
因此,优化空间复杂度是很有必要的。
1. 状态压缩状态压缩是一种将状态用二进制数表示的方法,可以大大减少状态所用的空间。
例如,在01背包问题中,我们可以用一个一维数组来表示当前背包里物品的状态,以便降低所需的空间复杂度。
2. 滚动数组滚动数组是一种通过滚动复用数组空间的方法,从而减少空间复杂度。
在一些状态转移方程中,我们只需要保存前一行或前一列的状态即可,因此我们可以用一个大小为2的数组滚动更新,从而节省空间。
二、优化时间复杂度在处理数据规模较大的问题时,时间复杂度往往是我们最关注的问题。
因此,我们需要进一步优化动态规划算法,以提高计算效率。
1. 前缀和优化前缀和可以用来处理连续区间的和,可以大大降低计算量。
在某些动态规划问题中,我们可以用前缀和的方式去避免重复计算,从而提高计算效率。
2. 近似算法一些问题中,我们不必求出精确的答案,而是可以用一种近似算法求解。
例如,针对旅行商问题,我们可以用模拟退火的算法来寻找一个较优的解决方案,从而省去一些不必要的计算。
三、拓展动态规划算法除了优化动态规划算法,我们还需要拓展动态规划算法,以解决更为复杂的问题。
1. 多阶段决策多阶段决策是指一个问题可以分解为多个相互关联的阶段,每个阶段都有不同的决策和状态。
在这种情况下,我们需要用多阶段动态规划来解决问题。
例如,对于双倍经验的游戏中,我们需要设计一个策略来最大化收益。
这个问题可以分为多个阶段,每个阶段都需要做出不同的决策。
因此,我们可以用多阶段动态规划来解决这个问题。
2. 组合优化问题组合优化问题是指将要做决策的数据中存在多个值,且每个值之间的关系将会影响到最终的结果。
状态压缩DP入门
THale Waihona Puke P最后的结果是: min( dp[( 1<<n ) – 1][j] ) ( 0 <= j < n );
技巧:利用2进制,使得一个整数表示一个点 集,这样集合的操作可以用位运算来实现。 例如从集合i中去掉点j: k = i & ( ~( 1 << j ) ) 或者 k = i - ( 1 << j )
遍历点集i中都包含哪些点 for(j=0;(1<<j)<=i;j++) {
if(((1<<j)&i)!=0)
点集i就包含点j
} 把点j加入点集i i=(i|(1<<j));
TSP
如何表示一个点集: 由于只有16个点,所以我们用一个整数表示 一个点集: 例如: 5 = 0000000000000101;(2进制表示) 它的第0位和第2位是1,就表示这个点集里有 2个点,分别是点0和点2。 31 = 0000000000011111; (2进制表示) 表示这个点集里有5个点,分别是0,1,2,4, 5;
经典入门
状态压缩动态规划
状态压缩动态规划
状态压缩动态规划: 动态规划的状态有时候比较恶心,不容 易表示出来,需要用一些编码技术,把 状态压缩的用简单的方式表示出来。
典型方式:当需要表示一个集合有哪些 元素时,往往利用2进制用一个整数表示。
经典问题:TSP
一个n个点的带权的有向图,求一条路径, 使得这条路经过每个点恰好一次,并且 路径上边的权值和最小(或者最大)。 或者求一条具有这样性质的回路,这是 经典的TSP问题。 n <= 16 (重要条件,状态压缩的标志) 今天讲第一个问题的状态压缩动态规划 的解法,第2个问题大同小异。
分组背包问题常见解法
分组背包问题常见解法
常见的解法包括动态规划和状态压缩。
动态规划解法:
1. 定义`dp[i][j]`表示前i个物品选择j个分组所能获得的最大价值。
2. 初始化`dp`数组为0。
3. 设置状态转移方程为`dp[i][j] = max(dp[i-1][j], dp[i-1][j-group[i].size] + value[i])`,其中`group[i].size`表示第i个物品所属的分组包含的物品数量,`value[i]`表示第i个物品的价值。
4. 依次考虑物品和分组,更新`dp`数组。
5. 最终结果为`dp[n][m]`,其中n表示物品的个数,m表示分组的个数。
状态压缩解法:
1. 定义一个一维数组`dp`表示前i个物品选择j个分组所能获得的最大价值。
2. 初始化`dp`数组为0。
3. 设置状态转移方程为`dp[j] = max(dp[j], dp[j-group[i].size] + value[i])`,其中`group[i].size`表示第i个物品所属的分组包含的物品数量,`value[i]`表示第i个物品的价值。
4. 依次考虑物品和分组,更新`dp`数组。
5. 最终结果为`dp[m]`,其中m表示分组的个数。
动态规划之状态压缩经典笔记
1、求一个数的第四位
int x = m & 0xf;
2、求一个数的高四位
int y = m >> 4;
3、用一个数保存两个数的数据
int m = (x << 4) | y;
4、取最后一个非零位,求一个数的(二进制形式)最后一个1的值
int m = x & (-x);
*5、在状态压缩中,x&~(1<<k)与x^((1<<k)-1)等价,但是第二种好理解
如果x所表示的二进制是一个集合,那么表示1的位置代表访问过, 那么上式结果就表示访问过的地方
6、x&(x>>1)==0用于判断是否有相邻的两个1
7、测试x的第bit位(bit从0开始计数)是否为1
x&(1<<bit) != 0 或者 (x >>bit)&1 != 0
while (x) {x = (x-1)&A;}
8、置位(设置x的第bit位为1,bit从0开始计数)
x|=1<<bit
9、清位(清除x的第bit位为0,bit从0ቤተ መጻሕፍቲ ባይዱ始计数)
x&=~(1<<bit)
10、统计非零位
for(; x!=0; x-=x&(-x)) ++cnt;
11、取所有子集
x=A;
状态压缩动态规划中的状态与时间
状态压缩动态规划中的状态与时间大丰市高级中学韩旭目录:【关键字】【概述】【正文】1.基础知识1.1 动态规划的概述 (1)1.1.1 动态规划的概念 (1)1.1.2 一些常见术语 (1)1.1.3 和递推的区别 (1)1.1.4 一些必要性质 (1)1.2 状态压缩动态规划的使用动机及其特点1.2.1 状态压缩动态规划的使用动机 (1)1.2.2 状态压缩动态规划的特点 (2)1.3 位运算的引入及其优化效果1.3.1 位运算的引入 (2)1.3.2 位运算的特殊用法及其优化效果 (2)2.常见的状态压缩类型概述2.1 常见集合型的状态压缩2.1.1 含义 (2)2.1.2 性能极其特点 (2)2.1.3 例题 (2)2.2 基于联通性的状态压缩2.2.1 含义 (3)2.2.2 性能极其特点 (3)2.2.3 例题 (3)2.3 小结3.典型例题的优化及其效果3.1 改变状态及其优化效果 (5)3.1.1 含义 (5)3.1.2 例题 (5)3.2 去重消冗及其优化效果 (8)3.2.1 含义 (8)3.2.2 例题 (8)3.3 小结【参考文献】【附件】【关键字】动态规划状态压缩去重消冗改变状态降低时耗【概述】随着社会的发展以及社会生产技术的变革,世界也在从工业化转向信息化。
与之而来的也就是信息学的快速发展,即其在生产生活中的大范围运用。
但是很多的实际问题到目前为止并没有太多十分有效的算法,也就是无法在多项式时间复杂度内得出结果,但却依然需要快速解决,状态压缩的概念也就被引入进来并用以解决特定的一定范围内的问题。
然而繁琐的状态压缩方法以及冗余的状态表述依然是制约效率的因素之一。
于是去重消冗与改变状态来提高时空效率也就是我们需要做的,通过下面题目的分析,希望能对大家起到抛砖引玉的作用。
(ps:基础知识可直接跳过,常见的状态压缩类型举例可以适当跳过)【正文】【第一部分】基础知识1.1.1 动态规划的概念:动态规划是运筹学的一个分支,是求解决策过程最优化的数学方法。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
状态压缩Abstract信息学发展势头迅猛,信息学奥赛的题目来源遍及各行各业,经常有一些在实际应用中很有价值的问题被引入信息学并得到有效解决。
然而有一些问题却被认为很可能不存在有效的(多项式级的)算法,本文以对几个例题的剖析,简述状态压缩思想及其应用。
Keywords状态压缩、Hash、动态规划、递推ContentIntroducti o n作为OIers,我们不同程度地知道各式各样的算法。
这些算法有的以O(logn)的复杂度运行,如二分查找、欧几里德GCD算法(连续两次迭代后的余数至多为原数的一半)、平衡树,有的以)运行,例如二级索引、块状链表,再往上有O(n)、O(n p log q n)……大部分问题的算法都有一个多项式级别的时间复杂度上界1,我们一般称这类问题2为P (deterministic Polynomial-time)类问题,例如在有向图中求最短路径。
然而存在几类问题,至今仍未被很好地解决,人们怀疑他们根本没有多项式时间复杂度的算法,NPC(NP-Complete)和NPH(NP-Hard)就是其中的两类,例如问一个图是否存在哈密顿圈(NPC)、问一个图是否不存在哈密顿圈(NPH)、求一个完全图中最短的哈密顿圈(即经典的Traveling Salesman Problem货郎担问题,NPH)、在有向图中求最长(简单)路径(NPH),对这些问题尚不知有多项式时间的算法存在。
P和NPC都是NP(Non-deterministic Polynomial-time)的子集,NPC则代表了NP类中最难的一类问题,所有的NP类问题都可以在多项式时间内归约到NPC问题中去。
NPH包含了NPC和其他一些不属于NP(也更难)的问题,NPC问题的函数版本(相对于判定性版本)一般是NPH的,例如问一个图是否存在哈密顿圈是NPC的,但求最短的哈密顿圈则是NPH的,原因在于我们可以在多项式时间内验证一个回路是否真的是哈密顿回路,却无法在多项式时间内验证其是否是最短的,NP类要求能在多项式时间内验证问题的一个解是否真的是一个解,所以最优化TSP问题不是NP的,而是NPH的。
存在判定性TSP问题,它要求判定给定的完全图是否存在权和小于某常数v的哈密顿圈,这个问题的解显然可以在多项式时间内验证,因此它是NP1请注意,大O符号表示上界,即O(n)的算法可以被认为是O(n2)的,O(n p log q n)可以被认为是O(n p+1)的。
2在更正式的定义中,下面提到的概念都只对判定性问题或问题的判定版本才存在(NPH除外)。
Levin给出了一个适用于非判定问题的更一般的概念,但他的论文比Cook的晚发表2年。
的,更精确地说是NPC的。
1如上所述,对于NPC和NPH问题,至今尚未找到多项式时间复杂度的算法。
然而它们的应用又是如此的广泛,我们不得不努力寻找好的解决方案。
毫无疑问,对于这些问题,使用暴力的搜索是可以得到正确的答案的,但在信息学竞赛那有限的时间内,很难写出速度可以忍受的暴力搜索。
例如对于TSP问题,暴力搜索的复杂度是O(n!),如此高的复杂度使得它对于高于10的数据规模就无能为力了。
那么,有没有一种算法,它可以在很短的时间内实现,而其最坏情况下的表现比搜索好呢?答案是肯定的——状态压缩(States Compression,SC)。
作为对下文的准备,这里先为使用Pascal的OIers简要介绍一下C/C++样式的位运算(bitwise operation)。
一、基本运算符名称 C/C++样式 Pascal样式简记法则按位与(bitwise AND) & and 全一则一否则为零按位或(bitwise OR) | or 有一则一否则为零按位取反(bitwise NOT) ~ not 是零则一是一则零按位异或(bitwise XOR) ^ xor 不同则一相同则零以上各运算符的优先级从高到低依次为:~,&,^,|二、特殊应用a)and:i.用以取出一个数的某些二进制位ii.取出一个数二进制中的最后一个1(lowbit)2:x&-xb)or:用以将一个数的某些位设为1c)not:用以间接构造一些数:~0u=4294967295=232-1d)xor:i.不使用中间变量交换两个数:a=a^b;b=a^b;a=a^b;ii.将一个数的某些位取反有了这些基础,就可以开始了。
1不应该混淆P、NP、NPC、NPH的概念。
这里只是粗略介绍,详见关于算法分析的书籍(实际上一篇paper 根本讲不完,因为还有co-NP、co-NPC、#P、#PC……后文会给出一个#PC的例子),这会使新手读者的理论水平上一个台阶。
弄不明白也没关系,基本不影响对本文其他部分的理解^_^2具有同样作用的还有(x-1)&x^x,这个的道理更容易理解。
lowbit在树状数组(某种数据结构)中可以用到,这里不再单独介绍,感兴趣的可以参阅各牛的论文,或者加我QQ。
建议掌握,否则可能会看不懂我的部分代码。
Getting Started我们暂时避开状态压缩的定义,先来看一个小小的例题。
【引例】1在n*n(n≤20)的方格棋盘上放置n 个车(可以攻击所在行、列),求使它们不能互相攻击的方案总数。
【分析】这个题目之所以是作为引例而不是例题,是因为它实在是个非常简单的组合学问题:我们一行一行放置,则第一行有n 种选择,第二行n -1,……,最后一行只有1种选择,根据乘法原理,答案就是n!。
这里既然以它作为状态压缩的引例,当然不会是为了介绍组合数学。
我们下面来看另外一种解法:状态压缩递推(States Compressing Recursion,SCR)。
我们仍然一行一行放置。
取棋子的放置情况作为状态,某一列如果已经放置棋子则为1,否则为0。
这样,一个状态就可以用一个最多20位的二进制数2表示。
例如n=5,第1、3、4列已经放置,则这个状态可以表示为01101(从右到左)。
设f s 为达到状态s 的方案数,则可以尝试建立f 的递推关系。
考虑n=5,s=01101。
这个状态是怎么得到的呢?因为我们是一行一行放置的,所以当达到s 时已经放到了第三行。
又因为一行能且仅能放置一个车,所以我们知道状态s 一定来自:①前两行在第3、4列放置了棋子(不考虑顺序,下同),第三行在第1列放置;②前两行在第1、4列放置了棋子,第三行在第3列放置;③前两行在第1、3列放置了棋子,第三行在第4列放置。
上图依次为①②③这三种情况,(红,蓝),(红,绿)分别代表两种顺序。
这三种情况互不相交,且只可能有这三种情况,根据加法原理,f s 应该等于这三种情况的和。
写成递推式就是:01101011000100100101f f f f =++根据上面的讨论思路推广之,得到引例的解决办法:021−==∑i s s f f f其中s 的右起第i +1位为1 (其实就是在枚举s 的二进制表示中的1) 3。
反思这个算法,其正确性毋庸置疑(可以和n!对比验证)。
但是算法的时间复杂度为O(n2n ),空间复杂度O(2n ),是个指数级的算法,比循环计算n!差了好多,它有什么优势?较大的推广空间。
4 1本文所有例题的C++代码均可以在附件中找到。
2 方便、整齐起见,本文中不在数的后面标明进制。
3 考虑上节介绍的位运算的特殊应用,可以更精巧地实现。
4 还有一个很…的用处,即对新手说:“来看看我这个计算n!的程序,连这都看不懂就别OI 了~”Sample Problems【例1】1在n*n(n≤20)的方格棋盘上放置n 个车,某些格子不能放,求使它们不能互相攻击的方案总数。
【分析】对于这个题目,如果组合数学学得不够扎实,是否还能一眼看出解法?应该很难。
对于这个题目,确实存在数学方法(容斥原理),但因为和引例同样的理由,这里不再赘述。
联系引例的思路,发现我们并不需要对算法进行太大的改变。
引例的算法是在枚举当前行(即s 中1的个数,设为r )的放置位置(即枚举每个1),而对于例1,第r 行可能存在无法放置的格子,怎么解决这个问题呢?枚举1的时候判断一下嘛!事实的确是这样,枚举1的时候判断一下是否是不允许放置的格子即可。
但是对于n=20,O(n2n )的复杂度已经不允许我们再进行多余的判断。
所以实现这个算法时应该应用一些技巧。
对于第r 行,我们用a r 表示不允许放置的情况,如果第r 行某一位不允许放置则此位为0,否则为1,这可以在读入数据阶段完成。
运算时,对于状态s ,用tmps=s&a r 来代替s 进行枚举,即不枚举s 中的1转而枚举tmps 中的1。
因为tmps 保证了不允许放置的位为0,这样就可以不用多余的判断来实现算法,代码中只增加了计算a 数组和r 的部分,而时间复杂度没有太大变化。
这样,我们直接套用引例的算法就使得看上去更难的例1得到了解决。
读者可能会说,这题用容斥原理更快。
没错,的确是这样。
但是,容斥原理在这题上只有当棋盘为正方形、放入的棋子个数为n 、且棋盘上禁止放置的格子较少时才有简单的形式和较快的速度。
如果再对例1进行推广,要在m*n 的棋盘上放置k 个车,那么容斥原理是无能为力的,而SCR 算法只要进行很少的改变就可以解决问题2。
这也体现出了引例中给出的算法具有很大的扩展潜力。
棋盘模型是状态压缩最好的展示舞台之一。
下面再看几个和棋盘有关的题目。
【例2】3给出一个n*m 的棋盘(n 、m≤80,n*m ≤80),要在棋盘上放k(k ≤20)个棋子,使得任意两个棋子不相邻。
每次试验随机分配一种方案,求第一次出现合法方案时试验的期望次数,答案用既约分数表示。
【分析】显然,本题中的期望次数应该为出现合法方案的概率的倒数,则问题转化为求出现合法方案的概率。
而概率=方案总数合法方案数,方案总数显然为C(n*m,k),则问题转化为求合法方案数。
整理一下,现在的问题是:在n*m 的棋盘上放k 个棋子,求使得任意两个棋子不相邻的放置方案数。
1题目来源:经典组合学问题。
2 如果这样改编,则状态的维数要增加,如有疑问可以参考下面的几个例子,这里不再赘述。
3 题目来源:经典问题改编。
这个题目的状态压缩模型是比较隐蔽的。
观察题目给出的规模,n 、m≤80,这个规模要想用SC 是困难的,若同样用上例的状态表示方法(放则为1,不放为0),280无论在时间还是在空间上都无法承受。
然而我们还看到n*m ≤80,这种给出数据规模的方法是不多见的,有什么玄机呢?能把状态数控制在可以承受的范围吗?稍微一思考,我们可以发现:9*9=81>80,即如果n,m 都大于等于9,将不再满足n*m ≤80这一条件。
所以,我们有n 或m 小于等于8,而28是可以承受的。