Solid Dominoes Tilings
Time Limit: 6000/3000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)Total Submission(s): 131 Accepted Submission(s): 75
The tiling is called solid if it is not possible to split the tiled rectangle by a straight line, not crossing the interior of any tile. For example, on the picture below the tilings (a) and (b) are solid, while the tilings (c) and (d) are not.
![](https://i-blog.csdnimg.cn/blog_migrate/6f05624cff1087e610f9fc32a185e821.jpeg)
Now the managers of the company wonder, how many different solid tilings exist for an m × n rectangle. Help them to find that out.
2 2 5 6 8 7
0 6 13514HintAll solid tilings for the 5×6 rectangle are provided on the picture below:![]()
题意:给出一个n*m大小的棋盘,需要你用1*2的砖铺满它,砖不可重叠,并且要求棋盘上没有行分割线或列分割线。
输出方法种数。需要取模。
AC代码:
#include <cstdio>
int ans[18][18]={
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
,{0,0,0,0,0,0,6,0,108,0,1182,0,10338,0,79818,0,570342}
,{0,0,0,0,0,6,0,124,62,1646,1630,18120,25654,180288,317338,1684956,3416994}
,{0,0,0,0,0,0,124,0,13514,0,765182,0,32046702,0,136189727,0,378354090}
,{0,0,0,0,0,108,62,13514,25506,991186,3103578,57718190,238225406,965022920,388537910,937145938,315565230}
,{0,0,0,0,0,0,1646,0,991186,0,262834138,0,462717719,0,560132342,0,699538539}
,{0,0,0,0,0,1182,1630,765182,3103578,262834138,759280991,264577134,712492587,886997066,577689269,510014880,807555438}
,{0,0,0,0,0,0,18120,0,57718190,0,264577134,0,759141342,0,567660301,0,47051173}
,{0,0,0,0,0,10338,25654,32046702,238225406,462717719,712492587,759141342,398579168,83006813,821419653,942235780,558077885}
,{0,0,0,0,0,0,180288,0,965022920,0,886997066,0,83006813,0,690415372,0,620388364}
,{0,0,0,0,0,79818,317338,136189727,388537910,560132342,577689269,567660301,821419653,690415372,796514774,696587391,175421667}
,{0,0,0,0,0,0,1684956,0,937145938,0,510014880,0,942235780,0,696587391,0,856463275}
,{0,0,0,0,0,570342,3416994,378354090,315565230,699538539,807555438,47051173,558077885,620388364,175421667,856463275,341279366}
};
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m))
{
printf("%d\n",ans[n][m]);
}
return 0;
}
解题步骤:
1.最简单的轮廓线dp,求解没有任何限制情况下的铺砖方案,打表输出到文件。
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
#define all(x) (x).begin(), (x).end()
#define for0(a, n) for (int (a) = 0; (a) < (n); (a)++)
#define for1(a, n) for (int (a) = 1; (a) <= (n); (a)++)
#define LEFT(x) (x<<1)
#define ysk(x) (1<<x)
#define CLEAR(s,x) (s&~(1<<x))
typedef long long ll;
typedef pair<int, int> pii;
const int INF =0x3f3f3f3f;
const int maxn= 16 ;
const int maxm= 16 ;
const int maxS=65536;
int no_res[maxn+5][maxn+5];
int ed,dp[2][maxS+10];
int n,m,cur;
const int mod=1e9+7.5;
void update(int a,int b)//a->b
{
if( b&ysk(m) ) dp[cur][CLEAR(b,m)]=(dp[cur][CLEAR(b,m)]+dp[1^cur][a])%mod;
}
int solve()
{
if(n<m) swap(n,m);
ed=ysk(m)-1;
dp[0][ed]=1;
cur=0;
for(int i=0;i<n;i++) {
for(int j=0;j<m;j++){
cur^=1;
memset(dp[cur],0,(ed+1)*sizeof dp[cur][0]);
for(int s=0;s<=ed;s++)
{
//不铺
update(s,LEFT(s));
//上铺
if(i&& !(s&ysk(m-1)) ) update(s,LEFT(s)|1|ysk(m) );
//左铺
if(j&& !(s&1) ) update(s,LEFT(s)|3 );
}
}
}
return dp[cur][ed];
}
void get_no_res()
{
for(int i=0;i<=maxn;i++){
for(int j=i;j<=maxm;j++){
if(!i||!j) {no_res[i][j]=no_res[j][i]=1;continue;}
n=i,m=j;
no_res[j][i]=no_res[i][j]=solve();
}
}
}
int main()
{
get_no_res();
freopen("no_res.txt","w",stdout);
printf("int no_res[%d][%d]={\n",18,18);
for0(i,maxn+1)
{
if( i ) putchar(',');
putchar('{');
for0(j,maxm+1)
{
if(j) putchar(',');
printf("%d",no_res[i][j]);
}
putchar('}');
putchar('\n');
}
printf("};\n");
return 0;
}
2.容斥原理
对于n*m的棋盘,求解不存在行分割线和列分割线的情况数ans(n,m)。
很容易想到用容斥原理,但确实是不怎么好用。
有个很自然的想法是对列或行进行容斥,加加减减。
先不考虑行分割。
(容斥原理的过程:一开始认为ans(n,m)就是no_res(n,m),但是不对,所求值比实际值大,因为no_res(n,m)是没有限制的情况,可能会有列分割。
这个时候需要减去有列分割的情况,先考虑至少有一列分割的情况,
将m拆成两部分a,b,答案减去至少有一列的所有拆法。之后减多了,再把至少有两列的拆法加上去,...,+,-+,-...)
大体思路:
用状压枚举列分割的位置,然后不同情况做相应的加减计数。
上述的容斥原理正确的条件是,都没有行分割线。
令dp[x]代表求解当前列宽m时,高度为x的矩形(x*m) 限制后的铺法种数(没有行分割线,但是要满足恰好具有当前枚举的列分割线)。
dp[n]是可递推求解的 ,容易求出dp[1],
n行对应n+1个点 0,1,2,...,n。
首先令dp[n]=no_res(n,m),代表可以有行分割线,之后再想办法减去有行分割线的情况。
如果计算的顺序得当就可以很好的解决重复问题,
dp[n]=dp[n]-dp[1]*( 前面1行内不许出现行分割线,剩余n-1行不作限制但要满足当前列分割线的种数 )-dp[2]*(前面2行内不许出现行分割线,剩余n-2行不作限制但要满足当前列分割线的种数)-...
就是说先减去有最上面一条分割线的情况,再减去有它下面一条分割线的情况(因为这时候dp[2]保证了上面行的分割线都不出现,所以不会重复),再减去存在下面的一条行分割线的情况....。
具体见代码
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
#define all(x) (x).begin(), (x).end()
#define for0(a, n) for (int (a) = 0; (a) < (n); (a)++)
#define for1(a, n) for (int (a) = 1; (a) <= (n); (a)++)
#define ysk(x) (1<<x)
typedef long long ll;
typedef pair<int, int> pii;
const int INF =0x3f3f3f3f;
const int maxn= 16 ;
const int maxm= 16 ;
const int mod=1e9+7.5;
int ans[maxn+3][maxm+3];
int n,m;
int no_res[18][18]={
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
,{1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1}
,{1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597}
,{1,0,3,0,11,0,41,0,153,0,571,0,2131,0,7953,0,29681}
,{1,1,5,11,36,95,281,781,2245,6336,18061,51205,145601,413351,1174500,3335651,9475901}
,{1,0,8,0,95,0,1183,0,14824,0,185921,0,2332097,0,29253160,0,366944287}
,{1,1,13,41,281,1183,6728,31529,167089,817991,4213133,21001799,106912793,536948224,720246619,704300462,289288426}
,{1,0,21,0,781,0,31529,0,1292697,0,53175517,0,188978103,0,124166811,0,708175999}
,{1,1,34,153,2245,14824,167089,1292697,12988816,108435745,31151234,940739768,741005255,164248716,498190405,200052235,282756494}
,{1,0,55,0,6336,0,817991,0,108435745,0,479521663,0,528655152,0,764896039,0,416579196}
,{1,1,89,571,18061,185921,4213133,53175517,31151234,479521663,584044562,472546535,732130620,186229290,274787842,732073997,320338127}
,{1,0,144,0,51205,0,21001799,0,940739768,0,472546535,0,177126748,0,513673802,0,881924366}
,{1,1,233,2131,145601,2332097,106912793,188978103,741005255,528655152,732130620,177126748,150536661,389322891,371114062,65334618,119004311}
,{1,0,377,0,413351,0,536948224,0,164248716,0,186229290,0,389322891,0,351258337,0,144590622}
,{1,1,610,7953,1174500,29253160,720246619,124166811,498190405,764896039,274787842,513673802,371114062,351258337,722065660,236847118,451896972}
,{1,0,987,0,3335651,0,704300462,0,200052235,0,732073997,0,65334618,0,236847118,0,974417347}
,{1,1,1597,29681,9475901,366944287,289288426,708175999,282756494,416579196,320338127,881924366,119004311,144590622,451896972,974417347,378503901}
};
int bitsize[20];
int dp[20];
int solve()
{
int tot=0;
int ted=ysk(m-1)-1;
for(int s=0;s<=ted;s++ )//枚举列分割线位置
{
int nbit=0,last=-1;//nbit代表被划分为了多少块
for(int i=0;i<m-1;i++) if(s&ysk(i))
{
bitsize[nbit++]=i-last;//bitsize[i]指的是序号为i的块的宽度
last=i;
}
bitsize[nbit++]=m-1-last;
for(int i=1;i<=n;i++)
{
for(int j=0;j<i;j++)//[0,j]没有行分割线(但要满足列分割条件),[j,n]不限制,但要满足列分割条件
{
ll reg=1;
for(int k=0;k<nbit;k++)
{
reg=reg*no_res[i-j][bitsize[k] ]%mod;//[j,n]区域乘法原理
}
if(!j) dp[i]=reg;
else
{
reg=(reg*dp[j])%mod;//[0,j]区域,利用已经计算的dp值递推。
dp[i]=((dp[i]-reg)%mod+mod)%mod;
}
}
}
if(nbit&1) tot=(tot+dp[n])%mod;//容斥原理的偶加,注意这里nbit是奇数,那么列分割线则为偶数条
else tot=((tot-dp[n])%mod+mod)%mod;//奇减
}
return tot;
}
int main()
{
for(int i=1;i<=maxn;i++) {
for(int j=i;j<=maxn;j++){
n=i,m=j;
ans[i][j]=ans[j][i]=solve();
}
}
freopen("ans.txt","w",stdout);
printf("int ans[%d][%d]={\n",18,18);
for0(i,maxn+1)
{
if( i ) putchar(',');
putchar('{');
for0(j,maxm+1)
{
if(j) putchar(',');
printf("%d",ans[i][j]);
}
putchar('}');
putchar('\n');
}
printf("};\n");
return 0;
}
附上一个很慢的方法,上面的方法是看别人的代码弄懂的,我自己的做法打表跑了40分钟:
那就是对列容斥,就是说dfs哪些位置有列分隔线,然后再进行轮廓线dp,使得既没有行分割线,又满足了枚举的列分割线:
具体做法是对普通的铺砖问题稍作变换,
如果现在正考虑铺设某一行的第一个格子(第一行除外),那么轮廓线表示的正好应该是一整行的格子情况,
如果轮廓线上的格子全满,证明有行分割线,continue;
如果现在正考虑铺设某一行的第一个格子(第二行除外),那么轮廓线表示的正好应该是一整行的格子情况,
如果轮廓线上的格子全空,证明有行分割,continue;
保证哪些位置有列分割线更简单:就是如果这里有列分割线,那么不能有砖跨过这条线(针对于横铺才有可能跨过)。
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
#define all(x) (x).begin(), (x).end()
#define for0(a, n) for (int (a) = 0; (a) < (n); (a)++)
#define for1(a, n) for (int (a) = 1; (a) <= (n); (a)++)
#define LEFT(x) (x<<1)
#define ysk(x) (1<<x)
#define CLEAR(s,x) (s&~(1<<x))
typedef long long ll;
typedef pair<int, int> pii;
const int INF =0x3f3f3f3f;
const int maxn=16 ;
const ll mod=1e9+7.5 ;
const int maxS=65536;
int ans_tmp,d[2][maxS+2],ans[maxn+10][maxn+10];
int ed,now;
bool res[maxn+10];
int n,m;
bool ca[maxn+10][maxn+10];
void update(int a,int b)
{
if(b&ysk(m)) d[now][CLEAR(b,m)]= (d[now][CLEAR(b,m)]+d[1^now][a])%mod;
}
void show(bool *res)
{
for(int i=0;i<m;i++)
{
if(res[i] )putchar('1');
else putchar('0');
}
putchar('\n');
}
ll work()
{
d[0][ed]=1;
now=0;
for0(i,n) {
for0(j,m){
now^=1;
memset(d[now],0,(ed+1)*sizeof d[now][0]);
for(int s=0;s<=ed;s++) {
/*如果发现在计算某一行的开头一个格子时,
轮廓线上全部填满,或者未填,不能转移,这样保证了没有行分割线
但是要注意 全满对于最上面一行(第1行)不成立,全空对于正在计算第2行时不成立
*/
if(i&&j==0&&s==ed) continue;
if(i!=1&&j==0&&s==0) continue;
if(d[now^1][s]==0) continue;//剪枝
update( s ,LEFT(s) ); //不放
if(j>0&& !(s&1) )//左放
{
if( !res[j-1] )//如果左边这条线必须是列分割线,那么不能横放。
update( s ,LEFT(s)|3 );
}
if(i>0&& !(s&ysk(m-1)) )//上
{
update( s ,LEFT(s)|ysk(m)|1 );
}
}
}
}
return d[now][ed];
}
void cal(int neg)
{
for(int j=0;j<m;j++) if(res[j])
{
int x= (j+1)*n;
int y=n*m-x;
if( (x&1) || (y&1) ) return;//如果分割线两端的格子数为奇数,那么一定无解,剪枝。
}
int ret=neg*work();
ans_tmp=ans_tmp+ret;
ans_tmp=(ans_tmp%mod+mod)%mod;
}
void dfs(int col,int neg)//dfs容斥原理,neg代表正负号
{
if(col==m-1){
cal(neg);
return;
}
res[col]=0;
dfs(col+1,neg);
res[col]=1;
dfs(col+1,-neg);
}
void solve()
{
ed=ysk(m)-1;//轮廓线上的最大状态
memset(res,0,sizeof res);
ans_tmp=0;
dfs(0,1);
ans[n][m]=ans[m][n]=ans_tmp;
ca[n][m]=ca[m][n]=1;
printf("ans[%d][%d]=%d\n",n,m,ans[n][m]);
}
void debug()
{
n=8,m=7;
solve();
}
int main()
{
// debug();
freopen("output.txt","w" ,stdout);
memset(ca,0,sizeof ca);//都没有计算过
for1(i,maxn) for1(j,maxn) if(!ca[i][j])
{
n=i,m=j;
if( (i*j)%2 ) {ans[i][j]=ans[j][i]=0;ca[i][j]=ca[j][i]=1;continue;}
if(n<m) swap(n,m);//这个swap()很重要,不然更慢
solve();
}
//打表出奇迹
printf("ans[16][16]={");
bool put=0;
for1(i,maxn){
if(put) putchar(',');
put=1;
printf("{");
for1(j,maxn){
if(j!=1) putchar(',');
printf("%d",ans[i][j]);
}
printf("}");
}
printf("};");
return 0;
}