炮兵阵地详解

来源:互联网 发布:什么是单例模式 java 编辑:程序博客网 时间:2024/03/29 03:17
NOI2001 炮兵阵地详解

【题目描述】
 
司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队。一个N*M的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用"P"表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
                    

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
 


【输入文件】Input
 
第一行包含两个由空格分割开的正整数,分别表示N和M;
接下来的N行,每一行含有连续的M个字符('P'或者'H'),中间没有空格。按顺序表示地图中每一行的数据。N <= 100;M <= 10。
 


【输出文件】Output
 
仅一行,包含一个整数K,表示最多能摆放的炮兵部队的数量。
 


【输入样例】Sample Input
 
5 4
PHPP
PPHH
PPPP
PHPP
PHHP
 


【输出样例】Sample Output
 
6

【题目解析】

思路一:

    对于一个位置,如果它部署了一支部队,那么会对它的前后左右的2格位置产生影响,如果以行作为状态,则不能单单由上一状态转移,那样的话不能保证在它的2格处不发生矛盾,而在这个题目中,以行做状态又很明显,所以在状态的转移时必须是上一行的状态和上上行的状态一起转移,如果用f[i,j,k]来表示在第i行时,取第j个状态(注意是第j个状态),i-1行取的是第k个状态,那么不难得出状态转移方程:

     f[i,j,k]=max{f[i,j,k],f[i-1,k,l]+ff[i,j]}

    解释一下这个方程i,j,k的含义如上述,l表示在第i-2行取第l个状态,ff[i,j]表示在第i行取第j个状态时在该行放置的多少支部队,由于第二行仅与第一行有关,所以要对第二行进行一下特殊处理,对于第二行,我们有

     f[2,j,k]=max{ff[2,j]+ff[1,k]};

     这样一来,对动态规划的模型基本处理就完成了,同时我们发现,在m=10时,每一行的状态有2^10种,显然时间和空间都不能承受,必须做出优化,仔细阅读题目,发现如果一个点部署了部队,则其前后两格范围内均不能再部署部队,而且在一个格子为H时,它根本不能部署部队,因此,在许多的状态中,有许多状态是自相矛盾的,根本不需要考虑,可以用某种预处理,把状态提取出来,仅仅储存有用的状态,在这里我的方法是用深度优先搜索预处理一下,try(x:longint)为处理某一行的第x个位置,如果这个位置部署部队,那么x+1,x+2这两个位置都不能放,直接try(x+3)即可,而如果这个地方是山地,就不能放,则try(x+1),即使是平原,也可以不去部署部队,因此还原后也要try(x+1),预处理结束。

     对于状态的储存,为了简洁,用一个二进制数来表示,如当m=4时有一种状态为放,不放,不放,放,就用1代表放,0代表不放,这个二进制数为1001,转化为十进制数为9.这样状态的储存问题就迎刃而解了。

     接下来,判断状态的矛盾问题,这用到了位运算的知识,1001and0011=0001 这已经很明了了,判断两行间的状态是否矛盾,只需要用(状态1)and(状态2),如果结果是0,说明两个状态不矛盾。

 


思路二:

    因为听说是经典的状态压缩DP才做的,所以即使听闻有人用搜索过了,我还是决定试一下用DP(不过其实是十分类似与搜索的)。
    解题思路大致是这样的,假设地图存在map数组中,对于map[i][j]如果是P,就可以有放兵与不放兵两种方案,但对于在i和j位置之前的所有P的摆放方案有多种方案,我们将这两种方案分别去尝试与Pij之前所有方案结合,则Pij又会有很多种方案。
    
    即,取status[i]={从左上方开始第i个P的方案集} (ij位置上是P),动态方程可以表示为:
    status[i] = status[i-1] ∪ {x∈status[i-1] | valid(x & Pi)}        x & Pi 是指将Pi放在x方案中,valid是检验方案的合法性。
    
    valid函数可以用采用以下约束条件:
        当前方案中,Pi的位置上无士兵或可能被上下攻击的可能。
        当前方案中,Pi的左边2格无士兵,右边2格也无士兵,即无左右受攻击的可能。

    光有这些是不够的,因为无论从空间还是时间上都会超出题目要求的范围。

    为了解决空间问题,我们可以对行进行压缩状态,我采用类似4进制的表示方式来压缩状态:
        encode(int[] solution, int length){
            int result = solution[0]*1 + solution[1]*(4) ... + solution[radix-1]*(4^(length-1))
        }
    decode类似。

    比如 PHPP,对于第三个P就可以有以下几种状态:
        3000、0030、0000、3003、0003

    在进入下一行的动规时,需要对每个非0数减一,依上例,经过一行后得:
        2000、0020、0000、2002、0002

    该状态的某个位置上有数,表示在纵向上有士兵可以攻击,横向的攻击直接求左边两个是否为3就行了。因为DP顺序是自左向右、自上向下的。所以对于每个士兵的攻击只需考虑两个方向而不是四个方向。

    为避免浪费大量空间,根据动态方程,我们只需要保留status[i-1] 的状态集就行了,然后更新status[i]为status[i-1],继续求解。所以我取BitSet记录status[i]的所有状态集(因为状态已经可以被压缩为一个int值了),另外我用索引数组的方式记录某状态当前的最佳值。
    同样以PHPP为例:3000可以压缩为3(反过来压也可以,只要解压和压缩是对称的就行),其solution[3] =1,0030压缩为48,solution[48]=1,3003压缩为195,solution[195]=2。在对以后每行的状态集的维护中,记得保持solution[x]的最大值。
    对于更新status[i]要注意,求status[i]时使用的BitSet是status[i-1]的,千万不要用正在修改的status[i]的BitSet求,可以使用一个BitSet backup来辅助保存。

    通过以上处理,基本上就可以保证空间上不超时了,但是时间的问题依然存在。由于status[i]的BitSet在不断扩大,所以每个更新status[i+1]都是件越来越麻烦的事情。为了控制BitSet的大小,需要像搜索中剪支一样,剪去一些不可能产生最优解的状态。
    我使用的剪支方案是,单行的最大摆放士兵数是(int)((m+2)/3),所以对于某一状态来说,如果该状态的解值+单行最大摆放士兵数<目前最优解,那么就可以忽略该方案了。因为我们的状态记录的是一行的状态情况,所以对于第i行某一状态而言,加上下行最大的摆放士兵数都无法达到最优解的话,那么就没必要继续保留该状态到下行了,所以可以略去。