首页 > 代码库 > 初步了解--状态压缩dp---poj1185炮兵布阵结题报告

初步了解--状态压缩dp---poj1185炮兵布阵结题报告

好吧,借助poj1185炮兵布阵这题,仔仔细细的了解了一下状态压缩动态规划

首先,借助题目,我们来看看状态压缩是个虾米东西。。Ok follow me

一,所谓状态压缩

根据题意,我们得在长度为M 的地图上放置一些大炮(后面简称“放炮”,应该不会被和谐吧敲打),那么,首先不考虑山地,我们得把所有的放置方法都找出来,并且注意,这里只对于一行且长度为M(好吧,你可能要问考虑一行,左右互相隔2,互相不在攻击范围,那么上下呢?这里先不急,一步步来)

1,找出所有放炮的方法

假设长度为7,那么看下图了解放炮的方法,0表示不放,1表示放一个:


2,处理方法--引用状态压缩

知道了对应长度为M,一行可以放炮的状态之后,我们就可以用这些情况,来进行解题的下一步,但是先别急,我们知道,对于长度为M,会有很多个状态,但是我们莫非要开x*y的二维数组来保存所有状态吗?那么这里就引入了状态压缩:

我们知道,题目的M<= 10,并且所有的放炮的状态我们用 01 来表示,那么我们就可以把每一个状态,都用一个整数了,而不是一个数组,来表示出来,如下:


那么我们用一个数组 pao_st[ ] 来储存所有的状态:

pao_st[0] = 0 = (二进制) ......0000000 (这里一个整数有32位,我们只要用到后面的即可)

pao_st[1] = 1 = (二进制) ......0000001
pao_st[2] = 2 = (二进制) ......0000010
pao_st[3] = 4 = (二进制) ......0000100 那么这里显然不能有 状态为3的情况,因为3的二进制 .....000011,那么两个炮相互在攻击范围内不符合题意

............

pao_st[cnt] = 73 = (二进制) ......1001001 那么cnt 就表示总的状态数量,最后一个状态肯定是就是73,因为这种状态时最大的啦

那么到此,我们已经把本该cnt*7的空间开销,变成了一个cnt的一维数组空间啦--那么这里就是状态压缩的目的-节约空间大小

那么,对于题意M<=10,据前辈计算,最多会有 61种放置状态

二,状态压缩后如何解题

OK,到上面为止,我们已经了解到了什么是状态压缩,那么我们就来在状态压缩后解题

1,先别急,基本方法还是要了解

那么对于这些整数,我们都要把它当做二进制数来对待,那么就必须要有相关的对应的操作:

|’,‘&’,‘^’,‘~’,‘<<’,  ‘>>‘ 好吧,这些基本的东东就不一一介绍啦

2,看看怎么用

对于上面,长度为M,我们已经得到炮的状态pao_st[ ],那么考虑一个地图的某一行,我们枚举所有的状态看能否用某种状态放在这个有山地平原的地图上呢?

那么首先,我们也要把地图进行二进制处理:如某一行,暂时叫做第 i 行的地图:

PHPPPPP ------> 我们也将它对应二进制,弄一个山的状态 shan_st[i] = (二进制) ....01000000

那么我们只考虑第 i 行能不能放一个状态1的时候,就可以这样来  if(shan_[i] & pao[1])---->就说明放不了,按照上例 0100000 & 0000001 = 0000000 那么上例就是可以放置,举个反例 shan_[i] = 0000001,那么shan_st[i] & pao[1] = 0000001 & 0000001 = 0000001 = 1;意思是炮放山上啦!!!不可以!!!!!

那么这里就可以看看前面忽略的问题:考虑上下的炮是不是能放,假设 第 i 行 放第 j 种状态的炮pao_st[j],第 i+1 行放第 k 种状态的炮pao_st[k], 那么 if(pao_st[j] & pao[k] )--->就说明放不了。。。也和上面是 一样的意思

三,了解了基本的,那么开始dp

首先来看看poj1185的状态转移方程:

dp[i][j][k]表示 当前第 i 行,状态为 j,前一行 i-1 状态为 k

dp[i][j][k] = max(dp[i][j][k] , dp[i-1][k][p] + sum[ j ]);  那么这里sum[j] 就表示 第 j 种状态 放置炮的个数---也就是对应pao_st[ j ] 对应的二进制中 1 的个数

那么直接上马:代码中的注释更为详细

#include <stdio.h>
#include <string.h>

#define MAX 105

int N,M;

char map[MAX][MAX/10];

int pao_st[61],cnt; //炮的状态,也就是放置方法,以及状态数量||||根据前辈计算,M<=10 状态最多61,你也可以试试
int sum[61];//每一种状态能放置的炮的数量
int dp[MAX][61][61];
int shan_st[MAX]; //山的状态

int max(int a,int b) {return a>b ? a:b;}

//-----------------初始化炮的状态,也就是初始放炮的方法-----------------begin
bool can(int x){ //判断 x 的这种状态---也就是二进制状态可不可行
    if(x & (x<<1)) return false;
    if(x & (x<<2)) return false;
    return true;
}

int get_num(int x){ //x状态的二进制状态有多少个1,也就是此状态的放炮数量
    int num = 0;
    while(x){
        if(x & 1) num++;
        x /= 2;
    }
    return num;
}

void init_paoSt(){ // 对于M列,初始化所有能放炮的状态
    cnt = 0;
    for(int i = 0; i < (1<<M); i ++){
        if(can(i)){
            pao_st[cnt] = i;
            sum[cnt++] = get_num(i);
        }
    }
}
//-----------------初始化炮的状态,也就是初始放炮的方法------------------end
int main()
{
    while(~scanf("%d%d",&N,&M))
    {
        init_paoSt();//--1---初始化炮的状态,也就是初始放炮的方法

        for(int i = 0; i < N; i ++) scanf("%s",map[i]);//--2---get Map

        memset(shan_st,0,sizeof(shan_st));//--3---对于获得的map,初始化山的状态
        for(int i = 0; i < N; i ++){
            for(int j = 0; j < M; j ++){
                if(map[i][j] == 'H'){
                    shan_st[i] += (1<<j); //这里+ 可改为| 操作结果一样
                }
            }
        }

        memset(dp,0,sizeof(dp));//--4---初始化第一行
        for(int i = 0; i < cnt; i ++){
            if(shan_st[0] & pao_st[i]) continue; //如果第一行的山和第i种放炮的方法冲突就continue
            dp[0][i][0] = sum[i];
        }
        for(int i = 0; i < cnt; i ++)//--5---初始化第二行
        {
            if(shan_st[1] & pao_st[i]) continue;
            for(int j = 0; j < cnt; j ++)
            {//如果i种放炮方法和第2行山地不冲突,下面就来看看这行和第一行有没有冲突
                if(shan_st[0] & pao_st[j]) continue;//当然也要第一行能放第j种方法的炮
                if(pao_st[i] & pao_st[j]) continue;//还要保证i,j之间不相互在攻击范围内

                dp[1][i][j] = max(dp[1][i][j],dp[0][j][0]+sum[i]);
            }
        }

        for(int i = 2; i < N; i ++){//--6---dp其他的行
            for(int j = 0; j < cnt; j ++){
                if(shan_st[i] & pao_st[j]) continue;
                for(int k = 0; k < cnt; k ++)//考虑上面一行放第k种炮
                {
                    if(shan_st[i-1] & pao_st[k]) continue;
                    if(pao_st[j] & pao_st[k]) continue;//保证这行和上一行的炮不相互干扰
                    for(int p = 0; p < cnt; p ++)//再看看上面两行
                    {
                        if(shan_st[i-2] & pao_st[p]) continue;
                        if((pao_st[p]&pao_st[k]) || (pao_st[p]&pao_st[j])) continue;//上2行的炮不和下面的冲突
                        dp[i][j][k] = max(dp[i][j][k],dp[i-1][k][p]+sum[j]);
                    }
                }
            }
        }

        int ans = 0;//获取最大解
        for(int i = 0; i < cnt; i ++){
            for(int j = 0; j < cnt; j ++){
                if(dp[N-1][i][j] > ans) ans = dp[N-1][i][j];
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}
那么这里还有一个地方要注意的是:

在初始化山的操作中:

memset(shan_st,0,sizeof(shan_st));//--3---对于获得的map,初始化山的状态
for(int i = 0; i < N; i ++)
{
    for(int j = 0; j < M; j ++)
    {
        if(map[i][j] == 'H')
        {
            shan_st[i] += (1<<j); //这里+ 可改为| 操作结果一样
        }
    }
}
如果山是这样的: PHPPH,那么从代码中可知道,实际上我们是把它shan_st[i] 变成了这样----> ....10010,刚好和地图左右相反,因为这里对应int 的32位,我们只需要后面的 10位, << 操作也是从右边开始,对于 j = 2 的时候,1<<j 为...00010, 操作 实际是 ...00000 | ...00010 = 00010 ,但是这样对应所有放炮的状态考虑过程中是没有影响的,因为所有可行的放炮状态 都是对称的


好吧,到现在,终于啰嗦完啦!!!

个人愚昧观点,欢迎指正与讨论!

pao_st[1] = 1 = (二进制) ......0000001
pao_st[1] = 1 = (二进制) ......0000001