POJ 2411 Mondriaan's Dream [经典状态压缩dp]
题意:略。
思路:这一题开始做的时候完全没有思路,便去看了别人的题解。
首先,对于这个题目解法想有一个初步的了解,请看这里:http://www.2cto.com/kf/201208/146894.html
根据这篇讲解,写了一篇扭曲的代码,提交之后TLE。
经过排查分析之后发现,算法的复杂度为O(hw*(2^(2w))),这个复杂度肯定超了。后来进行了优化,如果两种状态可以匹配,就将它们用邻接表(vector实现)存储起来,这样只需一遍预处理,以后直接读取就可以了。
此外,还有两个地方的优化:
1. 如果h*w为奇数,则结果必为0。(每个砖块的面积为2,无法用整数块铺满)
2. 如果h < w, 将两者数值交换。
后面还有一种dfs+dp的解法,我觉得很精巧,在下面重点分析。这里先贴下上面方法的代码。
1 #include<stdio.h> 2 #include<iostream> 3 #include<vector> 4 #include<string.h> 5 #include<algorithm> 6 using namespace std; 7 long long dp[1<<12][13]; 8 vector<int> ok[1<<12]; 9 int h, w; 10 bool judge(int up, int down) 11 { 12 int u[13], d[13]; 13 int now = w; 14 while (now) 15 { 16 u[now] = up % 2; 17 d[now--] = down % 2; 18 up /= 2; 19 down /= 2; 20 } 21 for (int i = 1; i <= w;) 22 { 23 if (!d[i])//该行该位为0 24 { 25 if (!u[i]) return 0;//上一行若也为0,则不合法 26 i++; 27 } 28 else if (!u[i])//该行该位为1,且上一行该位为0 29 i++; 30 else//该行该位为1,且上一行该位也为1 31 { 32 if (i + 1 > w || !d[i+1] || !u[i+1]) return 0; 33 i += 2; 34 } 35 } 36 return 1; 37 } 38 long long getdp() 39 { 40 memset(dp, 0, sizeof(dp)); 41 for (int i = 0; i < (1<<w); i++) 42 { 43 ok[i].clear(); 44 for (int j = 0; j < (1<<w); j++) if (judge(j, i)) 45 ok[i].push_back(j); 46 } 47 for (int i = 0; i < (1 << w); i++) 48 for (int j = 0; j < ok[i].size(); j++) if (ok[i][j] == (1<<w) - 1) 49 dp[i][1] = 1; 50 for (int i = 2; i <= h; i++) 51 for (int j = 0; j < (1 << w); j++) 52 for (int k = 0; k < ok[j].size(); k++) 53 dp[j][i] += dp[ ok[j][k] ][i-1]; 54 return dp[(1<<w)-1][h]; 55 } 56 int main() 57 { 58 while (~scanf("%d%d", &h, &w) && h && w) 59 { 60 if ((h * w) % 2) 61 { 62 printf("0\n"); 63 continue; 64 } 65 if (h < w) swap(h, w); 66 printf("%lld\n", getdp()); 67 } 68 return 0; 69 }
===========分割线===============
dfs的方法:
状态压缩的原则与上一种方法是一样的:如果该位为0,则说明该处为一竖放的砖块,且为该砖块的上半部分;其余为1。
核心部分是dfs的方法。首先,dp[state][row]表示铺到第row行,且该行状态为state时的方法总数,这并没有变。每次枚举状态,与上面不同的是,枚举的是上一层的状态。
上一种方法:
for(i = 2; i <= h; i++)//枚举行数
for(j = 0; j < (1<<w); j++)//枚举该行的状态
for(k...)//枚举该行可匹配的上一行状态
dfs版:
for(i = 2; i <= h; i++)//枚举行数
for(j = 0; j < (1<<w); j++)//枚举上一行的状态
if(...) dfs(...)//如果上一行该状态方法数不为0,则dfs遍历该行可行状态
遍历的方法很巧妙:假设所枚举到的上一行状态为s,则将s每一位都取反便是该行的一种可行状态。因为,如果s的某一位是0,说明这是一竖放砖块的上半部分,取反后恰好就是下半部分对应的1;如果s某一位是1,取反后是0,这当然也是可行的。
又由于当s某一位是1时,下一行对应的位可以是0或者1(若是1则必然是横放的,需要连续两位状态都是1)。所以在dfs的过程中需要遍历所有两个0相邻的情况,将他们置为1。当dfs的下标pos到达最后一位(即pos=w)时,说明该状态是可行的,就将该状态的dp值加上上一行状态s的dp值。因此,每次dfs之前都需要暂时存储下上一行状态s的dp值。
这种方法代码既短又巧妙,让人佩服啊。
1 #include<stdio.h> 2 #include<algorithm> 3 #include<string.h> 4 using namespace std; 5 long long dp[1<<12][13], tem, h, w; 6 void dfs(int row,int state,int pos) 7 { 8 if (pos == w) 9 { 10 dp[state][row] += tem; 11 return; 12 } 13 dfs(row, state, pos + 1); 14 if (pos <= w - 2 && !(state & (1<<pos)) && !(state & (1<<(pos + 1)))) 15 dfs(row, state | 1<<pos | 1<<(pos+1), pos + 2); 16 } 17 int main() 18 { 19 while (~scanf("%d%d", &h, &w) && h && w) 20 { 21 if (h * w % 2) 22 { 23 printf("0\n"); 24 continue; 25 } 26 if (h < w) swap(h, w); 27 memset(dp, 0, sizeof(dp)); 28 tem = 1; 29 dfs(1, 0, 0); 30 for (int i = 2; i <= h; i++) 31 for (int j = 0; j < (1<<w); j++) if (dp[j][i-1]) 32 { 33 tem = dp[j][i-1]; 34 dfs(i, ~j & ((1<<w) - 1), 0); 35 } 36 printf("%lld\n", dp[(1<<w)-1][h]); 37 } 38 return 0; 39 }