枚举摆放的这一步可以采用dfs递归枚举列,递归出口为列数col == N时。dfs函数的原型可以写成如下的形式:
void dfs( int col, int nextrow, int nowstate, int nextstate, LL cnt);
// col 表示当前枚举到的列编号
// nextrow 表示下一行的行编号
// nowstate 表示当前枚举骨牌摆放时第i 行的状态(随着放置骨后会更新)
// nextstate 表示当前枚举骨牌摆放时第i+1行的状态(随着放置骨后会更新)
// cnt 状态转移前的方案数,即第i行在放置骨牌前的方案数
然后再来看如何将骨牌摆上去,这里对骨牌进行归类,旋转之后得到如下六种情况:
图二-4-6
图二-4-7
为了方便叙述,分别给每个类型的骨牌强加了一个奇怪的名字,都是按照它自身的形状来命名的,o(╯□╰)o。然后我们发现它们都被圈定在一个2X2的格子里,所以每个骨牌都可以用2个2位的2进制整数来表示,编码方式类似上面的状态表示法(参照图6,如果骨牌对应格子为蓝色则累加格子上的权值),定义如下:
int blockMask[6][2] = {
{1, 1}, // 竖向2X1
{3, 0}, // 横向1X2
{3, 1}, // 枪
{3, 2}, // 7
{1, 3}, // L
{2, 3}, // J
};
blockMask[k][0]表示骨牌第一行的状态,blockMask[k][1]表示骨牌第二行的状态。这样一来就可以通过简单的位运算来判断第k块骨牌是否可以放在(i, col)这个格子上,这里的i表示行编号,col则表示列编号。接下来需要用到位运算进行状态转移,所以先介绍几种常用的位运算:
a. x & (1<<i) 值如果非0,表示x这个数字的二进制表示的第i(i >= 0)位为1,否则为0;
b. x & (y<<i) 值如果非0,表示存在一个p(i <= p < i+k),使得x这个数字的二进制表示的第p位和y的p-i位均为1(k为y的二进制表示的位数);
c. x | (1<<i) 将x的第i位变成1(当然,有可能原本就为1,这个不影响);
d. x | (y<<i) 将x的第i~i+k-1位和y进行位或运算(k为y的二进制表示的位数),这一步就是模拟了骨牌摆放;
那么这个格子可以放置第k个骨牌的条件有如下几个:
1、当前骨牌横向长度记为w,那么w必须满足 col + w <= N,否则就超出右边界了。
2、 nowstate & (blockMask[k][0]<<col) == 0,即第i行,骨牌放入前对应的格子为空(对应的格子表示骨牌占据的格子,下同)
3、nextstate & (blockMask[k][1]<<col) == 0,即第i+1行,骨牌放入前对应的格子为空
4、最容易忽略的一点是,“J”骨牌放置时,它的缺口部分之前必须要被骨牌铺满,否则就无法满足第i行全铺满这个条件了,如图二-4-8所示的情况。
图二-4-8
当四个条件都满足就可以递归进入下一层了,递归的时候也是采用位运算,实现如下:
dfs( col+1, nextrow, nowstate|(blockMask[k][0]<<col), nextstate|(blockMask[k][1]<<col), cnt );
这里的位或运算(|)就是模拟将一个骨牌摆放到指定位置的操作(参见位运算d)。
当然,在枚举到第col列的时候,有可能(i, col)这个格子已经被上一行的骨牌给“占据”了(是否被占据可以通过 (1<<col) & nowstate 得到),这时候我们只需要继续递归下一层,只递增col,其它量都不变即可,这表示了这个格子什么都不放的情况。