# 0x56 动态规划-状态压缩DP

0x56 动态规划-状态压缩DP

Mondriaan's Dream

Description

Squares and rectangles fascinated the famous Dutch painter Piet Mondriaan. One night, after producing the drawings in his ‘toilet series’ (where he had to use his toilet paper to draw on, for all of his paper was filled with squares and rectangles), he dreamt of filling a large rectangle with small rectangles of width 2 and height 1 in varying ways.

img

Expert as he was in this material, he saw at a glance that he’ll need a computer to calculate the number of ways to fill the large rectangle whose dimensions were integer values, as well. Help him, so that his dream won’t turn into a nightmare!

Input

The input contains several test cases. Each test case is made up of two integer numbers: the height h and the width w of the large rectangle. Input is terminated by h=w=0. Otherwise, 1<=h,w<=11.

Output

For each test case, output the number of different ways the given rectangle can be filled with small rectangles of size 2 times 1. Assume the given large rectangle is oriented, i.e. count symmetrical tilings multiple times.

img

Sample Input

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
Null

Sample Output

1
0
1
2
3
5
144
51205
Null

题意

给出一个 n×m 的方格,问用 1×2 的小方格来填充总共有多少种方法。

思路

我们定义 dp[i][j] 代表第 i-1 行已经放满,第 i 行状态为 j 时候的方案数。

其中每一行的状态我们可以用一个二进制来表示, 0 代表未填充, 1 代表已填充。

因为方块有两种摆放形式:竖放、横放

所以当第 i 行第 j 列竖放一个方块时,第 i-1 行第 j 列需要留空;而当第 i 行第 j 列与第 j+1 列横放一个方块时,第 i-1 行第 j 列与第 j+1 列则需已填充,因为我们定义的 dp 需要把第 i-1 行全部放满。

状态转移方程: (dp[i][s]=sum(dp[i−1][si])dp[i][s]=sum(dp[i−1][si])) 其中状态 s 与状态 si 必须兼容,也就是状态 s 中竖放的块能够填满状态 si 中的空。

这里有一个优化,也就是当 (n×m)结果为奇数的时候,无论怎样都不可能成功放置,因为每一个块的面积是偶数。

// https://www.dreamwings.cn/poj2411/4615.html 千千dalao的解法
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;

#define MAX ((1<<11)+10)
typedef __int64 LL;
int n,m;

LL dp[15][MAX];
LL ans[15][15];

bool jud(int x) // 判断 x 二进制中是否存在独立的1
{
    bool is1=false;
    for(int i=0; i<m; i++)
    {
        if(x&(1<<i))
            is1=!is1;
        else if(is1)return false;
    }
    return true;
}

bool ok(int s, int ss)  //判断状态s与状态ss是否兼容
{
    for(int j=0; j<m; )
        if(s & (1<<j)) //第i行第j列为1
        {
            if( ss & (1<<j)) //第i-1行第j列也为1,那么第i行必然是横放
            {
                //第i行和第i-1行的第j+1都必须是1,否则是非法的
                if( j==m-1 || !(s&1<<(j+1)) || !(ss&(1<<(j+1))) ) return false;
                else  j+=2;
            }
            else j++; //第i-1行第j列为0,说明第i行第j列是竖放
        }
        else //第i行第j列为0,那么第i-1行的第j列应该是已经填充了的
        {
            if(ss&(1<<j)) j++;//已经填充
            else return false;
        }
    return true;
}

void solve()
{
    int maxs;
    if(n<m)swap(n,m);   // 交换之后可以得到更小的状态数
    maxs=(1<<m)-1;      // 状态全1的情况
    memset(dp,0,sizeof(dp));
    for(int i=0; i<=maxs; i++)  // 初始化第一行
        dp[1][i]=jud(i);
    for(int c=2; c<=n; c++)     // 枚举 [2,n] 行
        for(int i=0; i<=maxs; i++)  // 第i行的状态
            for(int si=0; si<=maxs; si++)   //第i-1行的状态
                if(ok(i,si))
                    dp[c][i]+=dp[c-1][si];
    ans[n][m]=ans[m][n]=dp[n][maxs];
    printf("%I64d
",dp[n][maxs]);
}

int main()
{
    memset(ans,0,sizeof(ans));
    while(cin>>n>>m&&(n||m))
    {
        if(ans[n][m])   //如果之前计算过,则直接给出结果
        {
            printf("%I64d
",ans[n][m]);
            continue;
        }
        if(n&1&&m&1)    // 如果两边长都为奇数,则其面积也是奇数,无法放置
        {
            printf("0
");
            continue;
        }
        solve();
    }
    return 0;
}
//学习算法竞赛提升指南的写法
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[12][1 << 11];
int in_s[1 << 11];
int main() {
    //freopen("in.txt", "r", stdin);
    int n, m;
    while (cin >> n >> m && n) {
        for (int i = 0; i < 1 << m; ++i) {
            bool cnt = 0, has_odd = 0;
            for (int j = 0; j < m; ++j)
                if (i >> j & 1) has_odd |= cnt, cnt = 0;
                else cnt ^= 1;
            in_s[i] = has_odd | cnt ? 0 : 1;
        }
        f[0][0] = 1;
        for (int i = 1; i <= n; ++i)
            for (int j = 0; j < 1 << m; ++j) {
                f[i][j] = 0;
                for (int k = 0; k < 1 << m; ++k)
                    if ((j & k) == 0 && in_s[j | k])
                        f[i][j] += f[i - 1][k];
            }
        cout << f[n][0] << endl;
    }
}

在学习别人题解的时候发现一个DFS解决的,记录一下方法.

本质上和上面这个类似,但效率很高

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 11;
const int M = 10 + 20;

int n, m, r;
ll  dp[2][1 << N];

void dfs(int k, int u, int v) {
    while (k < m && u & 1 << k) k++;
    if (k >= m) {
        dp[1 - r][v] += dp[r][u];
        return;
    }
    dfs(k + 1, u, v | (1 << k));
    if (k + 1 < m && !(u & (1 << k + 1))) {
        dfs(k + 2, u, v);
    }
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);

    while (cin >> n >> m && n && m) {
        memset(dp, 0, sizeof(dp));

        dp[1][0] = 1;
        for (int i = 1; i <= n; i++) {
            r = i & 1;
            for (int s = 0; s < (1 << m); s++) {
                if (dp[r][s]) dfs(0, s, 0);
            }
            memset(dp[r], 0, sizeof(dp[r]));
        }
        cout << dp[(n + 1) & 1][0] << endl;
    }

    return 0;
}

炮兵阵地

思路:

待补

#include <iostream>
#include <cstring>
using namespace std;
#define MST(a, b) memset(a, b, sizeof(a));
#define CLR(a) MST(a, 0);
#define rep(x, y, z) for (int x = y; x < z; ++x)
const int INF = 0x3f3f3f3f;
int dp[101][77][77];
int sg[101];
int n, m, idx;
int s[77]; //合法摆放的集合
int cnt0[77]; //合法摆放方案的具体摆放个数, 即二进制下1的个数
int get_one(int x) {
	int cnt = 0;
	while (x) x &= (x - 1), ++cnt;
	return cnt;
}
bool ok(int x) {
	// 相邻两个P之间要有两个H
	if (x & (x << 1)) return false;
	if (x & (x << 2)) return false;
	return true;
}
void init() {
	idx = 0;
	int end = 1 << m;
	rep(i, 0, end) if (ok(i)) {
		// s保存合法方案的集合
		s[idx] = i;
		// cnt0保存合法方案的摆放个数, 二进制位1的个数
		cnt0[idx++] = get_one(i);
	}
}
bool valid(int i, int x) {
	if (sg[i] & x) return false;
	return true;
}
int solve() {
	int ans = 0;
	MST(dp, -1);
	dp[0][0][0] = 0;
	rep(j, 0, idx) if (valid(1, s[j])) {
		dp[1][j][0] = cnt0[j];
		// 考虑n==1情况
		ans = max(ans, dp[1][j][0]);
	}
	rep(i, 2, n + 1) {
		// valid()函数判断, 第i行, 用方案s[j]是否合法
		rep(j, 0, idx) if (valid(i, s[j])) {
			// i行跟i-1行的方案, 满足, 互相炸不到对方
			rep(k, 0, idx) if (valid(i - 1, s[k]) && (s[j] & s[k]) == 0) {
				int last = 0;
				// i-2行同上
				rep(l, 0, idx) if (dp[i - 1][k][l] != -1 && (s[l] & s[j]) == 0 && valid(i - 2, s[l])) {
					last = max(last, dp[i - 1][k][l]);
				}
				dp[i][j][k] = max(dp[i][j][k], last + cnt0[j]);
				if (i == n) ans = max(ans, dp[i][j][k]);
			}
		}
	}
	return ans;
}
int main(int argc, char const* argv[]) {
	ios::sync_with_stdio(0); cin.tie(0);
	cin >> n >> m;
	rep(i, 1, n + 1) rep(j, 0, m) {
		char tmp; cin >> tmp;
		if (tmp == 'H') sg[i] |= (1 << (m - 1 - j));
	}
	init();
	cout << solve() << endl;
	return 0;
}
原文地址:https://www.cnblogs.com/RioTian/p/13522441.html