题目描述
题目分析
显而易见的重要事实
首先,需要明白一个很重要的事实:
所有的摆放方案数=所有横着摆放且合理的方案数
这是因为,横着的确定之后,竖着的一定会被唯一确定,举一个例子:
------唯一确定----->
所以使用动态规划进行状态表示的时候,仅仅需要考虑横着的长方形即可
状态表示
随后,我们来看状态表示:
f[i,j]表示:前i-1列已经摆好,且从第i-1列伸出到第i列的状态为j的所有方案数
注意:列数的下标是0~m-1
举一个例子:
如上图,假设一个6*6棋盘,前两列已经以如图方式摆好,此时对于第三列的第一、四行有被第二列伸过来的部分,所以此时第二列的状态就是100100,将这个字符串看成二进制数,转为十进制为36,所以这就是f[2,36]所指的“所有方案数”的其中一个。
状态计算
对于同一列i,即使j值(状态数)相同,也会因为前i-1列的摆放方案不同而导致整体的摆放方案不同,也就是说,在前i-1列已经确定的情况之下,第i-1列的每一个状态的每一个方案都能够组合一个第i列的确定的状态j得到一个新的摆放方案:
比如上述这个图,列3的状态为:101010,由于列2可能存在不同的状态(0x0y0z),导致整体的摆放方案不同,同理,列2的某一种状态的方案数也因为列1可能存在不同的状态导致此方案数有可能大于1,所以呈现一个递归的现象,由此可得状态计算:
其中k为第i-1列的状态值,可以取不同的值
何时合理
显然,对于上述状态计算式子,如果不限制k值和j的关系,而一味地让k和j取遍整个0~2^n-1,就会发生一些不合理的现象,比如:
假设现在已经处理到i=3的情况,并且假设i=0~i=2的所有长方形已经摆好,就会发现两个不合理的现象:
1.在第一行的列2和列3地方发生了交叉
2.第二行的列2位置有一个不合理空缺,这个位置无法再填补任何的长方形,因为现在已经假定0~2列是摆好的,不会再去这个位置填补空缺的横着放的长方形,同时这个地方也根本不可能去放置竖长方形。
为什么产生了这个现象?
因为没有约束k和j的关系
我们先来看第一个不合理之处产生的原因:(k&j)!=0
顺便注意一下这里,k&j一定要加括号,因为!=的优先级大于&
因为如果k&j!=0,那么至少存在一行使得i和i-1列中同时有从i-1和i-2列伸过来的长方形,从而导致长方形的重叠
第二个不合理之处的产生原因:i-1列中的连续的空缺位置中存在空缺大小为奇数的情况
如何避免这个不合理之处呢?
保证:st[j|k]==true,其中st[x]为true的条件是x的二进制表示中没有奇数个连续的0
边界问题
开头已经说过了,此题列的表示下标从0开始,所以初始化为f[0][0]=1,意思是“没有从左边的棋盘边界的外边伸向第一列的长方形的方案数为1”,其它f全部初始化为0。
如果列数为m,列号从0开始,在遍历i的时候,最后遍历到的下标是m而不是m-1,因为当遍历到第i列的时候,实际上保证的是第i-1列的合法性,比如,刚才那个例子:
当遍历到i=3的时候,发现的两个不合理之处全在i=2列。
最后的答案为:f[m][0],意思是“在0~m-1列全部摆好的情况之下,棋盘右边界的右边一列没有第m列伸过来的长方形的总方案数”
朴素代码以及短路现象
#include <cstring>
#include <iostream>
#include <algorithm>using namespace std;const int N = 12, M = 1 << N;int n, m;
long long f[N][M];
bool st[M];int main()
{while (cin >> n >> m, n || m){for (int i = 0; i < 1 << n; i ++ ){int cnt = 0;st[i] = true;for (int j = 0; j < n; j ++ )if (i >> j & 1){if (cnt & 1) st[i] = false;cnt = 0;}else cnt ++ ;if (cnt & 1) st[i] = false;}memset(f, 0, sizeof f);f[0][0] = 1;for (int i = 1; i <= m; i ++ )for (int j = 0; j < 1 << n; j ++ )for (int k = 0; k < 1 << n; k ++ )if ((j & k) == 0 && st[j | k])f[i][j] += f[i - 1][k];cout << f[m][0] << endl;}return 0;
}
在上y总的课时,y总说这个代码有一个神奇的现象,就是把(j & k) == 0 && st[j | k]中&&前后的部分进行交换之后,时间会变长许多,此代码从892ms变为1442ms,很显然这与&&的短路性有关,当
(j & k) == 0不满足时,就会跳过st[j | k]的判断,因为(j & k) == 0的“成功率”小于“st[j | k]”的成功率,所以会出现上述现象。
优化之后的代码
此代码已经经过优化:预处理去除无效状态
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
const int N=12,M=1<<N;
typedef long long LL;
LL f[N][M];
bool st[M];
vector<int>state[M];//存放j和k的合法映射的集合
int n,m;
int main()
{while(scanf("%d%d",&n,&m)==2&&(n||m)){for(int i=0;i<1<<n;i++)//st数组的预处理{int cnt=0;st[i]=true;for(int j=0;j<n;j++){if(i>>j&1){if(cnt&1){st[i]=false;break;}cnt=0;}else cnt++;}if(cnt&1)st[i]=false;}for(int j=0;j<1<<n;j++){state[j].clear();//为什么要清空?因为是多样例输入for(int k=0;k<1<<n;k++)if((k&j)==0&&st[k|j])state[j].push_back(k);//满足推出f[i][j]的第i-1行的其中一个状态数k}memset(f,0,sizeof f);//因为是多样例输入f[0][0]=1;for(int i=1;i<=m;i++){for(int j=0;j<1<<n;j++)for(auto k:state[j])//代码优化的体现,大大减少了冗余f[i][j]+=f[i-1][k];}cout<<f[m][0]<<endl;}return 0;
}