poj 2411 Mondriaan's Dream (状态dp)

题目

这个题目的题意很容易理解,在一个N*M的格子里,我们现在有两种类型的 砖块,1 * 2 和 2 * 1,问一共有多少种方案,可以将整个N*M的空间都填满。

最简单的例子就是下面的了:

题解:

状态标记 横放和竖放的下一个均为1,竖放的上一个和不放置为,每行可以转化为12进制数。为什么要这样呢,应为这样表示肯定是包括了pre(前一行)和now(后一行)的所有状态的(而且多了很多,后面的主要工作是怎样删掉这些多的),而且可以区分,还容易区分。也就是说从pre转到now状态时要做的事不是很多。

或者还有一种理解方法:pre为1表示now与pre无关,为0就表示有关;也就是:

1. 在横着贴砖的时候,(i, j), (i, j+1) 都是1,这个值其实对下一行如何选择没有影响。

2. 竖着贴砖的第二个,我们也选择了1, 因为这个砖头结束了,对下一行如何选择依然没有影响。

3. 而竖着的第一个砖头,这个砖头是对下面有影响的,如果(i,j)是0,那么(i+1, j)只有是1的情况下才能满足条件。

那好下面介绍3种方法(越来越叼):

一:暴力:就是三重循环,如果pre的状态k与now的状态j匹配的话dp[i][j] += dp[i-1I][k];

然后懒得写了,具体想知道就看代码和注释吧:

//这个代码是直接复制别人的;
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <map>
#include <cmath>
#include <iomanip>
#define INF 99999999
typedef long long LL;
using namespace std;

const int MAX=(1<<11)+10;
int n,m;
LL temp[MAX],dp[MAX],bin[15];
bool mark[MAX];

bool check(int i){
	while(i){
		if(i&1){
			i>>=1;
			if(!(i&1))return false;//第j列是1则第j+1列必须是1
			i>>=1;//继续判断下一列
		}else i>>=1;//继续判断下一列
	}
	return true;
}

void Init(){
	memset(mark,false,sizeof mark);
	memset(temp,0,sizeof temp);
	for(int i=0;i<bin[m];++i){//初始化第一行和可以到达什么状态
		if(check(i))temp[i]=1,mark[i]=true;
	}
}

void DP(){
	for(int k=2;k<=n;++k){
		for(int i=0;i<bin[m];++i)dp[i]=0;
		for(int i=0;i<bin[m];++i){
			for(int j=0;j<bin[m];++j){
				if((i|j) != bin[m]-1)continue;//每一位或之后必须每一位是1(综合前面3种情况和分析可知)
				if(!mark[i&j])continue;//由初始化和前面分析三种情况分析可知i&j必须得到和初始化可以到达的状态一样才行
				dp[i]+=temp[j];//i可以从j到达,则增加j的方案数
			}
		}
		for(int i=0;i<bin[m];++i)temp[i]=dp[i];/*这个滚动有点瓜皮^.^;
	}
	  /*
        那他上面那两个判断就很有灵性了;可能很多人还没理解;我在举例子来说明一下。
        首先我们那样定义就是有两个地方要解决,第一个是你不能在第i-1行放一个竖的(放0)又在第i行放一个竖的;
        也就是双零情况,所以只要满足i|j是满的(每位都为一)就行了;
        第二种就是不能放半个横的;又懒得讲了,自己举个例子就好了;
        */
}

int main(){
	bin[0]=1;
	for(int i=1;i<12;++i)bin[i]=2*bin[i-1];
	while(~scanf("%d%d",&n,&m),n+m){
		if(n<m)swap(n,m);//始终保持m<n,提高效率,少了会超时的,这是个大优化;
		Init();
		DP();
		printf("%lld\n",temp[bin[m]-1]);//输出最后一行到达时的状态必须全部是1
	}
	return 0;
}

好,那现在介绍第二种方法:

上面是先枚举pre和now的状态,再看他们是否匹配;

  你会发现很多状态都是不匹配的;

现在是先枚举now的状态,在去搜索可用的pre状态;或者先枚举pre再搜now也行;

代码:

哦,令外说一点就是求第一行的所有可用状态,可以假设有0行,dp[1][j] = dp[0][1<<m-1],相当于第一行是第零行全为1的状态发展而来(第一行与第0行无关,自由发挥),但是比直接求会慢一点;

//这就是我自己写的;
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <queue>
#include <map>
using namespace std;
const int MAXN = 16;
int sPath, m, v;
long long dp[2][1<<11];

void dfs(int k, int pre, int now) {//dfs自行体会
    if (k == m) {
        dp[v^1][now] += dp[v][pre];
        return;
    }
    if (now>>k&1) {
        dfs(k+1, pre, now);
        if (k+1 < m && now>>(k+1)&1) dfs(k+2, pre|1<<k|1<<(k+1), now);
    }
    else dfs(k+1, pre|1<<k, now);
}

int main() {
    int n;
    //freopen("in.txt", "r", stdin);
    while(scanf("%d%d", &n ,&m)) {
        if (!n && !m) break;
        if (n&1 && m&1) {
            printf("0\n");
            continue;
        }
        if (n < m) {
            n = n^m;
            m = n^m;
            n = n^m;
        }
        v = 0;
        memset(dp[0], 0, sizeof(dp[0]));
        dp[0][(1<<m)-1] = 1;//这就是我注意说的,
        for(int i = 1; i <= n; i++) {
            memset(dp[v^1], 0, sizeof(dp[v^1]));
            //if(i == 1) j = (1<<m)-1; //可以这样优化一下,就差不多了;
            for(int j = 0; j < 1<<m; j++)
                dfs(0, 0, j);
            v ^= 1;//这样滚才有灵性
        }
        printf("%I64d\n", dp[v][(1<<m)-1]);
    }
}
方法三:
你们发现一个东西没,每次从pre转到now时都是一样的东西;比如第一行的i状态可以转到第二行的j;那么第二行的i肯定也可以转到第3行的j;
这样的话,就可以先把所有的状态都先存下来,然后直接取出来用。
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <queue>
#include <map>
using namespace std;
const int MAXN = 16;
int path[1<<16][2], sPath, m; //path小了会蜜汁re哦;
long long dp[2][(1<<11)];

void dfs(int k, int pre, int now) {
    if (k == m) {
        path[sPath][0] = pre;
        path[sPath++][1] = now;
        return ;
    }
    dfs(k+1, pre<<1|1, now<<1);
    dfs(k+1, pre<<1, now<<1|1);
    if (k+2 <= m) dfs(k+2, pre<<2|3, now<<2|3);
}

int main() {
    int n, v;
    while(scanf("%d%d", &n ,&m)) {
        if (!n && !m) break;
        if (n&1 && m&1) {
            printf("0\n");
            continue;
        }
        if (n < m) {
            n = n^m;
            m = n^m;
            n = n^m;
        }
        sPath = 0;
        dfs(0, 0, 0);
        v = 0;
        memset(dp[0], 0, sizeof(dp[0]));
        dp[0][(1<<m)-1] = (long long)1;
        for(int i = 1; i <= n; i++) {
            memset(dp[v^1], 0, sizeof(dp[v^1]));
            for(int j = 0; j < sPath; j++)
                dp[v^1][path[j][1]] += dp[v][path[j][0]];
            v ^= 1;
        }
        printf("%I64d\n", dp[v][(1<<m)-1]);
    }
}



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值