状态压缩入门 摆放车子

 

 

声明:此为转载,原文链接:BUCT OJ   网友chdh14b

          另外此博客有详细完整代码:http://blog.csdn.net/math_coder/article/details/9671581


大家好~我和大家分享一下我对状态压缩类的动态规划题的一些看法。

顾名思义,状态压缩类的动态规划的本质是动态规划,所以要处理好状态压缩问题首先要把握好动态规划的解题思路。
首先,来回顾一下动态规划算法的设计:
(1)首先把握问题的解是否可以分解的,可分解则考虑是否可用动态规划算法。
(2)描述最优解的结构。
(3)递归定义最优解的值。
(4)自底向上的方式计算最优解的值。
(5)有计算出的结果构造出一个最优解。

然后,我们来看一看状态压缩类的动态规划算法的设计和普通动态规划算法的区别:
(1)和普通的dp一样,要把握问题的解是否可以分解。同时,是一些状态的集合一些特定的状态。如:n个车放在棋盘上的某些位置上,就是一个状态;在一张图上,选择走某条线路,通过某些点,
        也称作一种“状态”;在牧场上,某些点放牧某些点不放,也是一种状态。当然,状态需能分解成几个“小”状态。
        如:当把n个车摆放的位置称作总状态的话,某一个车的位置可以称作一个“小”状态,某一行上的所有车的位置也可以称作一个“小”状态。具体根据题目的问题而定。
(2)把握如何描述一个状态。首先,用二进制的0和1来描述最小的状态。如:在棋盘上的一个点,放祺和不放祺可以表示为1和0;图上,走某条线某个点或不走可表示为1和0。
        简而言之,To be or not to be, that is just one or zero。而后,一个比最小状态稍大的状态,就可以用一连串的0和1来表示。如:棋盘的某一行(1~5)在1、3、5号位置摆放棋子可以表示为 10101。
        然后,把状态压缩,降低存储成本(空间开销)。状态压缩在动态规划里较为特殊的原因之一就是因为一个“状态压缩技术”,或者叫做压缩小技巧,就是将二进制数表示的状态用整形量来存储
        比如,二进制数10101可以用整型量21来表示。
(3)写出状态转移方程。这个要根据具体问题具体分析。
(4)确定边界条件,然后自底向上计算最优状态解的值(特定状态的属性?,etc.)所有状态解的和的值(状态集合的状态总和,etc.)。
(5)对于状态压缩类的动态规划问题,一般不会要求求出最优解。所以不需要构造最优解,只需求出最优状态解的值或所有状态解的和。

由此可见,状态压缩类的动态规划算法仅仅是动态规划的一种特例,一种对特殊问题的特殊求解方式。

接下来,我会分析buct_oj上动态规划(一)的ProblemE-ProblemI:
(以下代码并非完整的AC代码,而是我对AC代码的抽象出来的半伪代码。所以边界条件从0还是从1之类的并不强调,只为说明我个人对状态压缩算法的看法)


(一)问题 E: 状态压缩DP----车的摆放1
题目来源:http://coder.buct.edu.cn/JudgeOnline/problem.php?cid=1032&pid=4

时间限制: 3Sec  内存限制: 128 MB

题目描述
在n*n(n≤20)的方格棋盘上放置n 个车(可以攻击所在行、列),求使它们不能互相攻击的方案总数。(状态压缩DP的入门题,希望大家不要使用组合数学公式求解)

个人分析
(1)解可否分解,解是否“状态化”的:显然,解是“状态的集合”。某一总状态为棋盘上满足题目条件下n个车放在棋盘上的状态。该总状态可以分解为“小状态”。  
(2)对于这个问题,我将第i行的“小状态”定义为:第一行到第i行的所有棋子状态用一串二进制数表示,某列的1表示从第一行到第i行,该列上有一棋子。0表示,无棋子。
        即如,r*c = 4*4的棋盘上(如图一),i = 3时,状态(state)为1101。然后将1101压缩成整型数13。
        num[i,State]表示第i行在state的“小状态”下的方案数。
(3)状态转移方程:显然,第i行State状态下的num[i,State]与第i-1行的某些状态数有关。
        不难推出:num[i,State|PreviousState] = sum{num[i-1,PreviousState] | State&PreviousState == 0}
        这里用到了简单的对二进制数的处理。即取与为0即表示列上没有棋子冲突。还有一点,因为一行只能有一个棋子,这里的state只需取特殊的一些串二进制数。这串二进制数里只有一个1,其余全为0。
        即0001,0010,0100,1000。可以再将效率提升,用一个一维长度的数组把这几个数存起来,每次只需取出其中一个进行计算即可。抑或可以用左移运算,1<<j(j从0~r-1)来表示。(这里效率不变)
(4)自底向上运算,第一行num[1,State]显然为1。只需将第一行特殊处理,再把num从1~r行的值都算出来即可。所有状态解的和的值即第r行状态为1111...r个1时的num[r,(1<<r)-1]的值。
        以下为最简单的计算代码:
       (我的最初的代码还有一个函数用作判断某二进制数中1的个数是否符合该行的要求。如:第r行的状态必有且只有r个1,其余为0。
          在此我将其删去,因为我个人觉得这么运算下来状态不符合r行有r个1的num为0。所以不影响运算。)
  1. memset(num,0,sizeof(num));        //        Initialize array.
  2. for (int State = 1; State <= r; State = State << 1) {
  3.         //        Represent 00..01, 00..10, ..., 01..00, 10..00.
  4.         num[1,State] = 1;
  5. }
  6. for (int i = 1; i <= r; i++) {
  7.         //        i is the index of row.
  8.         for (int State = 1; State <= r; State = State << 1) {
  9.                 //        Represent 00..01, 00..10, ..., 01..00, 10..00.
  10.                 for (int PreviousState = 1; PreviousState <= (1<<r); PreviousState++) {
  11.                         //        Represent all combination of zero and one.
  12.                         if ((State&PreviousState) == 0)
  13.                                 num[i,State|PreviousState] += num[i-1,PreviousState];
  14.                 }
  15.         }
  16. }
复制代码
此外,还可以优化一下效率,用一个数组StateArray保存State的所有可能状态。不过这道题的作用不大。用于后面的题作用较大。
  1. memset(num,0,sizeof(num));        //        Initialize array.
  2. for (int State = 1,int s = 1; State <= r; State = State << 1) {
  3.         //        Represent 00..01, 00..10, ..., 01..00, 10..00.
  4.         num[1,State] = 1;
  5.         //        Storage for valid combination of 0 and 1 to state.
  6.         StateArray[s++] = State;
  7. }
  8. for (int i = 1; i <= r; i++) {
  9.         //        i is the index of row.
  10.         for (int s = 1; s <= r; s++) {
  11.                 //        StateArray[s] stores State.
  12.                 for (int PreviousState = 1; PreviousState <= r; PreviousState++) {
  13.                         //        Represent all combination of zero and one.
  14.                         if ((StateArray[s]&PreviousState) == 0)
  15.                                 num[i,StateArray[s]|PreviousState] += num[i-1,PreviousState];
  16.                 }
  17.         }
  18. }
复制代码
这里还可以用滚动数组优化,进一步压缩存储空间。这里只给出部分伪代码,具体请读者自行理解。
  1. memset(num,0,sizeof(num));        //        Initialize array.
  2. for (int State = 1,int s = 1; State <= r; State = State << 1) {
  3.         //        Represent 00..01, 00..10, ..., 01..00, 10..00.
  4.         num[State] = 1;
  5.         //        Storage for valid combination of 0 and 1 to state.
  6.         StateArray[s++] = State;
  7. }
  8. for (int i = 1; i <= r; i++) {
  9.         //        i is the index of row.
  10.         for (int s = 1; s <= r; s++) {
  11.                 //        StateArray[s] stores State.
  12.                 for (Previous is a state which just has i of the one) {
  13.                         if ((StateArray[s]&PreviousState) == 0)
  14.                                 num[StateArray[s]|PreviousState] += num[PreviousState];
  15.                 }
  16.         }
  17. }
复制代码
最后将r个1的num输出即可。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

 
  
chdh14b 当前离线
注册时间
2012-12-10
最后登录
2013-5-21
阅读权限
10
积分
39
精华
1
帖子
6
查看详细资料

1

主题

2

听众

39

积分

新手上路

Rank: 1

新手上路, 积分 39, 距离下一级还需 11 积分
沙发
发表于 2013-5-19 17:47:01 | 只看该作者
本帖最后由 chdh14b 于 2013-5-19 17:55 编辑

(二)问题 F: 状态压缩DP----车的摆放2
题目来源:http://coder.buct.edu.cn/JudgeOnline/problem.php?cid=1032&pid=5
时间限制: 5Sec  内存限制: 128 MB

题目描述
在n*n(n≤20)的方格棋盘上放置n 个车(可以攻击所在行、列),但是有些位置不能摆放任何棋子,求使它们不能互相攻击的方案总数。

个人分析
(1)解和ProblemE一样,是“状态的集合”
(2)对于这个问题,与ProblemE一样
        用num[i,State]表示第i行在state的“小状态”下的方案数。
(3)状态转移方程:num[i,State|PreviousState] = sum{num[i-1,PreviousState] | State&PreviousState == 0 &&Set[i ]&State== 0}
        这里的 Set[i ] 描述的是该行预置的不可摆放的状态。不可放为1,可放为0。
(4)自底向上运算,第一行num[1,State]显然为1。只需将第一行特殊处理,再把num从1~r行的值都算出来即可。所有状态解的和的值即第r行状态为1111...r个1时的num[r,(1<<r)-1]的值。
        以下为计算代码,只是problemE代码稍作修改(部分伪代码):
  1. memset(Set,0,sizeof(Set));
  2. for(int m = 1; m <= M; m++) {
  3.         cin >> s1 >> s2;
  4.         //        For convenience, ignore the indication of index in problem info. Just indicate indexes from 1~r.
  5.         Set[s1] = Set[s1]&(1<<(r-s2));
  6. }
  7. memset(num,0,sizeof(num));
  8. for (int State = 1,int s = 1; State <= r; State = State << 1) {
  9.         //        Represent 00..01, 00..10, ..., 01..00, 10..00.
  10.         num[State] = 1;
  11.         //        Storage for valid combination of 0 and 1 to state.
  12.         StateArray[s++] = State;
  13. }
  14. for (int i = 1; i <= r; i++) {
  15.         //        i is the index of row.
  16.         for (int s = 1; s <= r; s++) {
  17.                 //        StateArray[s] stores State.
  18.                 for (Previous is a state which just has i of the one) {
  19.                         if ((StateArray[s]&PreviousState) == 0 && (StateArray[s]&Set[i]) == 0)
  20.                                 num[i,StateArray[s]|PreviousState] += num[i-1,PreviousState];
  21.                 }
  22.         }
  23. }
复制代码
 
 
  
chdh14b 当前离线
注册时间
2012-12-10
最后登录
2013-5-21
阅读权限
10
积分
39
精华
1
帖子
6
查看详细资料

1

主题

2

听众

39

积分

新手上路

Rank: 1

新手上路, 积分 39, 距离下一级还需 11 积分
板凳
发表于 2013-5-19 17:51:13 | 只看该作者
本帖最后由 chdh14b 于 2013-5-19 23:25 编辑

(三)问题 G: 状态压缩DP----车的摆放3
题目来源:http://coder.buct.edu.cn/JudgeOnline/problem.php?cid=1032&pid=6
时间限制: 5Sec  内存限制: 128 MB

题目描述
在n*m(n,m≤80并且m*n≤80)的方格棋盘上放置k(k不大于20)个车(这是一种特殊的车,它只能攻击前后左右四个位置的车),求使它们不能互相攻击的方案总数。

个人分析
ps:这个题我感觉我在处理k个棋子这里弄复杂了。求指正~
(1)解和ProblemF一样,是“状态的集合”
(2)这个问题的“小状态”与前两个问题的有些区别。这里的“小状态”定于为:第i行的棋子的状态
        这里用到了上两题总结的空间压缩的技巧,将一行的符合该行要求的状态保存下来,存储在StateArray里。对于该题,既是每个1之间必须有一个以上的0才满足。
        StateArray的长度为StateNum,在运算出求得。
        用num[i,StateArray[s ],j]表示第i行在stateArray的“小状态”下此时一共放了j个棋子的方案数。
        这代码为求StateArray(这里有个小技巧,就是在输入时保证m(列数)为m与n中较小值。这样可以大大提高运行效率。由题目条件易得m <= √80 = 9,总状态数少于2^9 = 512):
  1. StateNum = 0;
  2. for (int i = 0; i <= 512; i++) {
  3.         if (((i<<1)&i) == 0)
  4.                 StateArray[StateNum++] = i;
  5. }
复制代码
(3)状态转移方程:num[i,StateArray[s ],j] = sum{num[i-1,StateArray[s' ],j'] |StateArray[s ]&StateArray[s' ] == 0 && j = j' + NumOfChessman(StateArray[s ])  && j <= k}
        这里的StateArray[s' ]是第i-1行的状态。StateArray[s ]为第i行的状态。num[i-1,StateArray[s' ],j']为第i-1行状态为StateArray[s' ]时放棋子数为j'时的方案数。
        同样,num[i,StateArray[s ],j]为第i行状态为StateArray[s ]时放棋子数为j时的方案数。这里,j等于j’加上状态为StateArray[s' ]的该行中的1的数量,即放置的棋子数。
        这里,NumOfChessman(StateArray[s ])为:
  1. int NumOfChessman(int state)
  2. {
  3.         int count = 0;
  4.         for (int b = 1; b <= max; b <<= 1) {
  5.                 if ((a&b) != 0) {
  6.                         count++;
  7.                 }
  8.         }
  9.         return count;
  10. }
复制代码
(4)自底向上运算,用第一行num[1,StateArray[s ],j]显然为1。这里j=NumOfChessman(StateArray[s ])。只需将第一行特殊处理,再把num从1~r行的值都算出来即可。
        所有状态解的和的值是所有满足j==k时的解的值的和,用sum表示
        以下为第一行的计算代码:
  1. //        Calculate first row
  2. for (s = 0; s < StateNum; s++) {
  3.         j= NumOfChessman(StateArray[s]);
  4.         num[0][StateArray[s]][j] = 1;
  5. }
复制代码
以下为余下行的计算代码(伪代码,s' 和j'在c++里非法):
  1. //        Calcuate other rows
  2. for (i = 1; i < n; i++) {
  3.         for (s = 0; s < StateNum; s++) {
  4.                 b = NumOfChessman(StateArray[s]);
  5.                 for (s' = 0; s' < StateNum; s'++) {
  6.                         if (StateArray[s]&StateArray[s'] == 0) {
  7.                                 for (j' = 0; j' + b <= k; j'++) {
  8.                                         num[i][StateArray[s]][j'+b] = (num[i][StateArray[s]][j'+b] + num[i-1][StateArray[s']][j'])%ModNum;
  9.                                 }
  10.                         }
  11.                 }
  12.                 if (b != 0) {
  13.                         sum = (sum + num[i][StateArray[s]][k])%ModNum;
  14.                 }
  15.         }
  16. }
复制代码
 
 
  
chdh14b 当前离线
注册时间
2012-12-10
最后登录
2013-5-21
阅读权限
10
积分
39
精华
1
帖子
6
查看详细资料

1

主题

2

听众

39

积分

新手上路

Rank: 1

新手上路, 积分 39, 距离下一级还需 11 积分
地板
发表于 2013-5-19 17:51:20 | 只看该作者
本帖最后由 chdh14b 于 2013-5-19 23:35 编辑

(四)问题 H: 状态压缩DP----车的摆放4
题目来源:http://coder.buct.edu.cn/JudgeOnline/problem.php?cid=1032&pid=7
时间限制: 5Sec  内存限制: 128 MB

题目描述
在n*m(n,m≤80并且m*n≤80)的方格棋盘上放置k(k不大于20)个车(这是一种特殊的车,它只能攻击相邻8个位置的车),求使它们不能互相攻击的方案总数。

个人分析
这里基本分析与ProblemG相似,仅仅是判断两行的棋子是否可放有些许区别。
将问题G的判断
  1. if (StateArray[s]&StateArray[s'] == 0) {
  2.         ...
  3. }
复制代码
改变成:
  1. if (StateArray[s]&StateArray[s'] == 0 && (StateArray[s]<<1)&StateArray[s'] == 0 && (StateArray[s]>>1)&StateArray[s']) {
  2.         ...
  3. }
复制代码
即可。
 
 
  
chdh14b 当前离线
注册时间
2012-12-10
最后登录
2013-5-21
阅读权限
10
积分
39
精华
1
帖子
6
查看详细资料

1

主题

2

听众

39

积分

新手上路

Rank: 1

新手上路, 积分 39, 距离下一级还需 11 积分
5#
发表于 2013-5-19 17:51:25 | 只看该作者
本帖最后由 chdh14b 于 2013-5-20 00:19 编辑

(五)问题 I: 状态压缩DP----车的摆放5
题目来源:http://coder.buct.edu.cn/JudgeOnline/problem.php?cid=1032&pid=8
时间限制: 5Sec  内存限制: 128 MB

题目描述
在n*m(n≤100,m≤10)的方格棋盘上放置n个车(这是一种特殊的车,它可能攻击水平或垂直方向两个格子内的车,上下左右各两个,共8个),求使它们不能相互攻击能放入的最多的车的数目。

个人分析
(1)这个问题与前四个问题有区别,解是“最优状态的值”,也可以说成是一些特定的状态的值
(2)因为该问题较为特殊,某行的状态与其前两行状态都有关,所以“小状态”定义为:第i-1行状态为StateArray[q ]时第i行的状态。
        用num[i ][p][q]表示第i行在StateArray[p]的状态下且第i-1行在StateArray[q]的状态的方案数。
        这里的StateArray不仅仅是整型数组,是一个pair<int,int>数组。前者用于存储状态,后者用于存储该状态下棋子数。
        因为某行自身合适的状态的棋子数会多次调用,所以记录下来增加运行效率。
        还有这里的数组num后两维的大小只需要是“所有符合自身合适状态的总和”即可。只有60来个,具体不太记得了,我程序里用的是62,即num[105][62][62]。所以数组空间很小
        以下为计算该行自身合适的状态:
  1. for (i = 0; i < 2^10; i++) {
  2.         if (((i<<1)&i) != 0 || ((i<<2)&i) != 0) {
  3.                 StatesArray[StateNum].first = i;
  4.                 StatesArray[StateNum].second = NumOfChessman(i);
  5.                 StateNum++;
  6.         }
  7. }
复制代码
这里函数NumOfChessman()函数与上问题类似。不赘述。
(3)状态转移方程:P = StateArray[p ].first          Q = StateArray[q ].first         T = StateArray[t].first         
                                  即P、Q、T为状态。

                            num[i ][p][q] = max{num[i-1][p][t]+StateArray[p ].second | P&Q == 0 && P&T == 0}
(4)运算,第一行num[1][p][0] = StateArray[p].second。这里的q=0是为了方便运算,仅表示第一行没有前一行
  1. //        Calculate the first row
  2. for (p = 0; p < StateNum; p++) {
  3.         num[1][p][0] = states[p].second;
  4. }
复制代码
第二行,num[2][p][q] = max{num[i-1][q][0]+StateArray[p ].second | P&Q == 0}
  1. //        Calculate the second row
  2. for (p = 0; p < StateNum; p++) {
  3.         //        StateArray[p] is the state of this row
  4.         for (q = 0; q < StateNum; q++) {
  5.                 //        StateArray[q] is the state of the previous row
  6.                 if ((states[p].first)&(states[q].first) == 0) {
  7.                         if (num[2][p][q] < (num[1][q][0] + states[p].second))
  8.                                 num[2][p][q] = num[1][q][0] + states[p].second;
  9.                 }
  10.         }
  11. }
复制代码
再把num从3~r行的值都算出来即可。
  1. //        Other rows
  2. for (i = 2; i < n; i++) {
  3.         //        i is the index of this row
  4.         for (p = 0; p < StateNum; p++) {
  5.                 //        StateArray[p] is the state of this row
  6.                 for (q = 0; q < StateNum; q++) {
  7.                         //        StateArray[q] is the state of the previous row
  8.                         for (t = 0; t < stateNum; t++) {
  9.                                 //        StateArray[t] is the state of second previous row
  10.                                 if ((states[p].first)&(states[q].first) == 0 && (states[p].first)&(states[t].first) == 0) {
  11.                                         if (num[i][p][q] < (num[i-1][q][t] + states[p].second))
  12.                                                 num[i][p][q] = num[i-1][q][t] + states[p].second;
  13.                                 }
  14.                         }
  15.                 }
  16.         }
  17. }
复制代码
最后,把解的值求出来就好。
  1. ans = 0;
  2. //        Calculate the max
  3. for (p = 0; p < StateNum; q++) {
  4.         for (q = 0; q < StateNum; q++) {
  5.                 if (num[r][p][q] > ans) {
  6.                         ans = num[r][p][q];
  7.                 }
  8.         }
  9. }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值