题目描述
题目分析
显而易见的重要事实
首先,需要明白一个很重要的事实:
所有的摆放方案数=所有横着摆放且合理的方案数
这是因为,横着的确定之后,竖着的一定会被唯一确定,举一个例子:
------唯一确定----->
所以使用动态规划进行状态表示的时候,仅仅需要考虑横着的长方形即可
状态表示
随后,我们来看状态表示:
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;
}