第一次接触轮廓线dp,在网上找一些资料感觉介绍很不清楚,看了这篇博客后清晰了一些,主要是根据这道题目Mondriaan’s Dream讲解一下我的理解
图1
- 首先什么是轮廓线,从我查阅的资料来看是未确定的状态和已经确定状态的交界处即使轮廓线,比如上图中格子中的数值为1即是已经确定了状态,从上到下,从左到右依次决定放不放1*2的骨牌都不影响到这些数值为1的格子,而对于K4,K3,K2,K1,K0,这些方格状态都还未确定,后续的操作都会影响到这些格子的状态,所以轮廓线以上的状态都是已经确定的,而轮廓线以下包括轮廓线本身的状态都是未确定的,所以我们可以进行状态压缩,使用一个int型变量s保存轮廓线的状态。
- 然后是如何表示状态s,可以使用上图K4K3K2K1K0二进制串表示状态s,也可以使用下面这种从左向右,或者从右到左的表示形式,只要状态转移是对应的转移方式就是。向上图的状态s转移需要左移一位,丢弃高位K4,并补充低位K0,而下面这种方式则不需要移位操作,所以看网上代码的状态转移过程有的进行移位,有的没有,给我整懵逼了。后面就用上图进行讲解了
图2
图3
- 理解上面的就分类讨论了,先判断轮廓线状态s的对应(i,j)位置上面的状态是0还是1,如果是0(情况1)则必须竖放,因为只有当前位置可以影响到上面位置,要满足题目填满的要求只能竖放,如果上面位置是1,则考虑左边的位置是0还是1,如果是0(情况2),则可以向左横着放置,也可以选择不放,因为第i+1行可以竖放填满第i行的空格,然后就是情况3了,只能选择不放。
首先这道题可以使用常规的dp,枚举第i-1行的状态和第i行的状态进行状态转移。
#include<iostream>
#include<cstring>
using namespace std;
const int ms = 1<<12;
long long dp[2][ms];
bool legal[ms];
int n, m;
bool ck(int i,int j){
bool f = true;
for(int k=0;k<m;){
if(!((j>>k)&1)){
if((i>>k)&1){
k+=1;
}else{
f = false;
break;
}
}else{
if(!((i>>k)&1)){
k+=1;
}else{
if((k+1)<m && (i>>(k+1)&1) && (j>>(k+1))&1){
k+=2;
}else{
f = false;
break;
}
}
}
}
return f;
}
int main() {
// for(int i=0;i<)
if(n<m){
swap(n,m);
}
while (true) {
cin >> n >> m;
if (n == 0 && m == 0) {
return 0;
}
if ((n * m) & 1) {
cout << 0 << endl;
continue;
}
memset(dp, 0, sizeof dp);
for(int i=0;i<(1<<m);i++){
bool f = true;
for(int j=0;j<m;){
if((i>>j)&1){
if(!((i>>(j+1))&1 && j+1<m)){
f = false;
break;
}else{
j+=2;
}
}else{
j++;
}
}
dp[0][i] = f;
}
for(int i=1;i<n;i++){
for(int j=0;j<(1<<m);j++){
if(dp[0][j]){
for(int k=0;k<(1<<m);k++){
if(ck(j, k)){
dp[1][k]+=dp[0][j];
}
}
}
}
for(int j=0;j<(1<<m);j++){
dp[0][j] = dp[1][j];
dp[1][j] = 0;
}
}
cout<<dp[0][(1<<m)-1]<<endl;
}
}
然后是使用轮廓线dp,时间复杂度相比常规状压dp更优
使用图1的状态表示方式源程序
#include<iostream>
#include<cstring>
using namespace std;
const int ms = 1 << 12;
long long dp[2][ms];
bool legal[ms];
int n, m;
int main() {
// for(int i=0;i<)
if (n < m) {
swap(n, m);
}
while (true) {
cin >> n >> m;
if (n == 0 && m == 0) {
return 0;
}
if ((n * m) & 1) {
cout << 0 << endl;
continue;
}
memset(dp, 0, sizeof dp);
int now = 0;
dp[0][(1<<m)-1] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
now ^= 1;
memset(dp[now], 0, sizeof dp[now]);
for (int s = 0; s < (1 << m); s++) {
if (dp[now ^ 1][s]) {
if(!((s>>(m-1))&1)) {//上放
dp[now][((s<<1)&((1<<m)-1)) | 1] += dp[now^1][s];
}
else if (!(s & 1) && j!=0) {//左放
dp[now][(s << 1) & ((1 << m) - 1)] += dp[now ^ 1][s];
dp[now][((s << 1) & ((1 << m) - 1)) | 3] += dp[now^1][s];
}
else {
dp[now][(s << 1) & ((1 << m) - 1)] += dp[now ^ 1][s];
}
}
}
}
}
cout << dp[now][(1<<m)-1] << endl;
}
}
使用图3表示状态的方式源程序
#include<iostream>
#include<cstring>
using namespace std;
const int ms = 1 << 12;
long long dp[2][ms];
bool legal[ms];
int n, m;
int main() {
// for(int i=0;i<)
if (n < m) {
swap(n, m);
}
while (true) {
cin >> n >> m;
if (n == 0 && m == 0) {
return 0;
}
if ((n * m) & 1) {
cout << 0 << endl;
continue;
}
memset(dp, 0, sizeof dp);
int now = 0;
dp[0][(1<<m)-1] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
now ^= 1;
memset(dp[now], 0, sizeof dp[now]);
for (int s = 0; s < (1 << m); s++) {
if (dp[now ^ 1][s]) {
int left = j>0?(s>>(j-1))&1:0;
int up = (s>>j)&1;
if (!up) { //不需要判断 i != 0, 因为设置了第-1行的路廓线全为1的方案数为1,其余为0
dp[now][s^(1<<j)] += dp[now^1][s];
}
else if (!left && j != 0) {
dp[now][s^(1<<(j-1))] += dp[now^1][s];
dp[now][s^(1<<j)] += dp[now^1][s];
}
else {
dp[now][s^(1<<j)] += dp[now^1][s];
}
}
// 上述代码可以简化如下
//if (dp[now ^ 1][s]) {
// if (j && (s & (1 << j)) && !(s & (1 << j - 1))) {
// dp[now][s ^ (1 << (j - 1))] += dp[now ^ 1][s];
// }
// dp[now][s ^ (1 << j)] += dp[now ^ 1][s];
//}
}
}
}
cout << dp[now][(1<<m) - 1] << endl;
}
}