POJ 2411 - 状压DP详解

poj:http://poj.org/problem?id=2411

这个题属于状态压缩的一个入门题,在讲解该题之间我们先简单的回顾一下状压DP。

注意再看这个题之前,一定要对动态规划有一定的掌握

状压DP:

与传统的DP一样,状压DP也是定义状态,通过前一阶段的状态,推出当前阶段的状态。但是有时候状态特别多,但是每个状态的决策又非常少,我们如果用多维数组来表示状态会造成空间浪费,也可能会空间爆炸。有时候我们就可以考虑用状压DP来做。比如每个状态的决策只有两个,但是状态的维度很多。这道题就是属于这种。

为了让大家更加深刻的理解,这里简单的讲解一下利用状压DP来解决背包问题,背包问题的决策也非常的少,就是放与不放的问题,其决策也只有两个。但是背包问题使用状压DP来解决有点得不偿失,但是可以帮助大家更好的理解背包问题。

背包问题,状压DP解决

现在假设背包容量为10,物品个数5。那么我们就可以使用5的二进制位来表示这5个物品的放置情况。

如果5个物品都不放入背包,那么5个二进制位就为00000

如果5个物品都放入背包,那么5个二进制位就为11111

那么所有物品的放置情况就在00000-11111这个区间之中。我们就用这些二进制数表示完了所有物品的放置情况,如果我们将这些二进制数转换为10进制的话,那么就可以用这个10进制数来表示所有物品的放置情况了,这就是状态压缩的和核心。

上面简单的介绍了状态的表示,那么现在就来介绍状态的转移。

dp[00001]这个状态能由哪个状态转换过来?肯定就是dp[00000]这个状态了,就可以得出dp[00001] = dp[00000]+w1

注意:dp这个数组表示的是某种物品放置状态的价值。dp[00001]就表示放置了第1个物品的最大价值。

dp[01001]可以由dp[00001] + w4 或者 dp[01000] + w1转换过来,没什么区别这两个。这样就得到了状态转移方程,就能得到最终的答案了。可能有人要问了,背包容量限制呢?其实与价值的状态转移是一样的,容量的状态转移也可以用一个数组来进行表示。

例如:我们用dp1这个数组来表示容量的状态转移,dp1[00001]就表示放置了第1个物品所占容量,最后可以这两个数组,取出满足背包容量的,背包中物品价值最大的那一个就为结果。

讲的不明白?看看代码吧

代码实现:

#include<stdio.h>
#define max(x,y) x > y ? x : y
// 记录不同状态的价值,注意根据物品的多少来决定容量
int dp[1000];
// 记录不同状态所占容量
int dp1[1000] ;
int main()
{
	int n,m;
	while(scanf("%d%d",&n,&m) == 2)
	{
		int W[10],C[10];
		for(int i = 0; i < n; i++){
            scanf("%d %d",&W[i],&C[i]) ;
		}
		for(int i = 0; i < (1 << n); i++)
		{
			for(int j = 0; j < n; j++)
			{
				if(!(i&(1 << j)))		//状态转移
				{
					int temp = i | (1<<j);
					dp[temp] = dp[i] + W[j];
					dp1[temp] = dp1[i] + C[j];

				}
			}
		}
		int res = 0;
		for(int i = 0; i < (1 << n); i++)
		{
            if(dp1[i] <= m){
                res = max(res,dp[i]);
            }
		}
		printf("%d",res);
	}
	return 0;
}

我们也可以看出,状压DP解决的问题一般来说规模较小,状态一般只有两种状态

 

好了铺垫的差不多了,接下来进入我们的正题吧!

POJ 2411

简单的翻译一下题目吧

给你一个n*m的方格矩阵,要求用1*2的多米诺骨牌去填充,问有多少种填充方式。

这个题就是一个经典的状压DP题目,题中的状态非常的多,但是状态的决策又很少。进入正题吧

首先如果(n*m)%2!=0,那么填充方式为0种,这个很好分析。

给定状态转移方程:

f(i,j) = (f(i-1,k)之和) k 是与j状态对应的上一行的不同状态的集合。

f(i,j)表示第i行的状态为j时,前i行的总方案数。

现在给定一个矩阵,我们在矩阵上用0或者1来进行填充,当然这个填充不能是随便填充,我们必须要满足题目中的条件

首先我们定义如下这种填充表示方式:如果一个骨牌是横着放的,那么它所在的两个方格都填充1.如果它是竖着放的,那么它所在的两个格子中,上面的那个填0,下面的这个填1.如下图所示:

我们用1来表示矩阵当前位置的骨牌是竖着放置的,用0来表示骨牌横着放置的或者当前位置没有放置骨牌。

我们可以看出,每一行的格式我们可以将其看成一个二进制数,然后就可以使用一个10进制的数来表示其状态了。

我们把给定的矩阵两行两行的来看待,因为两行之间是相互影响的,我们只需要关注两个行之间不同的状态。

例如:图中第1行为110011 通过分析我们知道它的下一行只能是001111或者111111或001100或111100,根据我上面给出的条件这个很好分析。这里是通过上一行分析当前这一行,其实如果我们确定了当前这一行,他的上一行也会有多种状态与之对应

只要我们能得出当前行的所有状态对应的前一行的状态是什么,我们就能使用上面的状态转移方程了。最终就能解决这个题了

怎么得出这个状态关系?

我们知道当前行的状态确定了之后,他的前一行会有多个与之对用的状态。

由当前行来确定其前一行是可以用暴力的方式来解决,暴力枚举出合法的状态,能得到答案。但是其实有更简单的方法。

我们不确定当前行的状态,而是将其看待成对当前行的当前列的操作(操作:即为放置骨牌,对当前位置来说骨牌是横放还是竖放),寻找出与之对应的前一行的当前列的操作。

对于每个两两相邻的两行来说,我们可以使用三个变量,c表示当前所在列,now表示当前行的状态,pre表示前一行的状态。

然后从每一列的开始位置开始进行操作,直到向后模拟到最后一位,模拟到最后一位之后,就确定了一种状态。

我们的目的就是对当前行的当前列实施不同的操作,使其枚举出所有状态,并且找到前一行对应的状态

对于每一种操作我们都能唯一确定一个与之对应的前一行的操作,最终能得到对应的行,具体的操作在代码中进行说明

注意:这种操作只是关注当前行和其前一行的当前操作的位,而不去关注其他的位的,也不去关注其他行。千万不要想的太多

 

代码实现:

#include<stdio.h>
#include<string.h>
#define ll long long
#define N 12

int w,h,t;
ll dp[N][2100];
int path[14000][2];

void dfs(int c,int now,int pre){
    if(c > w){
        return;
    }
    if(c == w){
        // 得到了一种状态。
        path[t][0] = pre;
        path[t++][1] = now;
        return;
   
    // 当前位置横放,那么他的前一行也是横放,因为没有其他操作能一次填充前一行的两个位置
    dfs(c+2,(now<<2)|3,(pre<<2)|3);
    // 当前位置竖放,那么他的前一行肯定就是不放,因为前一行的当前位是由当前行的当前位竖放的骨牌进行填充的
    dfs(c+1,(now<<1)|1,pre<<1);
    // 当前位置不放,那么他的前一行肯定是竖放,因为前一行只有一个位的位置,只有竖放才满足
    dfs(c+1,now<<1,(pre<<1)|1);
}

int main(){
    while(scanf("%d %d",&h,&w) == 2){
        if(h == 0 && w == 0){
            return 0;
        }
        if(h < w){
            int temp = h;
            h = w;
            w = temp;
        }
        t = 0;
        dfs(0,0,0);
        memset(dp,0,sizeof(dp));
        dp[0][(1<<w)-1] = 1;
        for(int i=0;i<h;i++){
            for(int j=0;j<t;j++){
                dp[i+1][path[j][1]] += dp[i][path[j][0]];
            }
        }
        printf("%lld\n",dp[h][(1<<w)-1]);
    }
    return 0;
}

 

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值