棋盘覆盖问题——分治法——代码清晰易懂

一、运行环境

DevC++

二、题目描述

棋盘覆盖问题。有一个2^k × 2^k个方格的棋盘,其中恰有一个方格残缺。用下面四种三格板覆盖更大的棋盘。
在这里插入图片描述

要求:
①两个三格板不能重叠。
②三格板不能覆盖残缺方格,但必须覆盖其他所有的方格。
用以下形式进行结果输出
在这里插入图片描述

三、问题分析

采用分治的方法解决该问题。把2k×2k的棋盘分为4个2k-1×2k-1的子棋盘,使问题规模变小,直至将棋盘分割为1×1的子棋盘。

该问题属于二分法不相似的情况:分割后的4个子棋盘只有一个带有残缺。分治的技巧在于如何划分棋盘,使划分后的子棋盘的大小相同,并且每个子棋盘均包含一个残缺方格,从而将原问题分解为规模较小的棋盘覆盖问题。k>0时,可将2k×2k的棋盘划分为4个2k-1×2k-1的子棋盘。这样划分后,由于原棋盘只有一个残缺方格,所以,这4个子棋盘中只有一个子棋盘包含该残缺方格,其余3个子棋盘中没有残缺方格。为了将这3个没有特殊方格的子棋盘转化为残缺棋盘,以便采用递归方法求解,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种划分策略,直至将棋盘分割为1×1的子棋盘。

四、算法设计

1. 怎样表示一个棋盘

三要素:棋盘左上角的横坐标、纵坐标、棋盘边长

2.存储结构设计

(1)需要有一个二维数组来标记棋盘,设为全局变量,从i=1,j=1开始储存标记
(2)L型的位置,需要用“111”、“222”…“nnn”来标记,这就需要设置一个计数的变量,每次递归让这个计数变量自加1

3. 递归设计

整体来说,是进行一个深度优先的遍历:
在这里插入图片描述

4. 一次递归的设计

于一整个棋盘来说,残缺的位置分为4种情况:
①残缺在左上角子棋盘中
②残缺在右上角子棋盘中
③残缺在左下角子棋盘中
④残缺在右下角子棋盘中

对于每一个子棋盘,分为两种情况:
①残缺在子棋盘内,不需要用L型覆盖一个格子
②残缺不在子棋盘内,需要用L型覆盖一个格子

对于一整个棋盘来说,残缺的位置分为4种情况,这就需要用到4个if语句
对于每一个子棋盘,分为两种情况,这就需要上述的4个if语句中都具备一个else分支
在这里插入图片描述

五、代码


#include <stdio.h>

int chess[65][65]={0};
//从chess[1][1]开始存放 
static int t=0;
int m;
void cover(int a,int b,int dr,int dl,int length);

int main(){
    int n,a,b,dr,dl,length;
    //a,b是子棋盘左上角的行号和列号
    //dr,dl是特殊点的行号和列号
    //length是棋盘宽度 
    printf("请输入棋盘大小(1~64之间的整数):");
    scanf("%d",&length);
    printf("请输入残缺行号dr:");
    scanf("%d",&dr); 
    printf("请输入残缺列号dl:");
    scanf("%d",&dl);
    a=b=1;
    m=length;
    
    cover(a,b,dr,dl,length);
    
    for(int i=1;i<=m;i++){ //输出结果,都从1号开始存放的 
        for(int j=1;j<=m;j++){
            printf("%4d",chess[i][j]); 
            if(j==m){
                printf("\n");
            }
        }
    }
}

void cover(int a,int b,int dr,int dl,int length){
	for(int i=1;i<=m;i++){ //输出结果,都从1号开始存放的 
        for(int j=1;j<=m;j++){
            printf("%4d",chess[i][j]); 
            if(j==m){
                printf("\n");
            }
        }
    }
    printf("------------\n");
    if(length==1){
        return;
    }
    t++;
    int L =t;
    int l=length/2;
    
    if(dr<a+l && dl<b+l){ //残缺在左上角 
        cover(a,b,dr,dl,l);
    }
    else{                 //残缺不在左上角,填充一个方格 
        chess[a+l-1][b+l-1]=L;
        cover(a,b,a+l-1,b+l-1,l);
    }
    
    if(dr>=a+l && dl<b+l){//残缺在左下角 
        cover(a+l,b,dr,dl,l);
    }
    else{                  //残缺不在左下角,填充一个方格 
        chess[a+l][b+l-1]=L;
        cover(a+l,b,a+l,b+l-1,l);
    }
    
    if(dr<a+l && dl>=b+l){//残缺在右上角
        cover(a,b+l,dr,dl,l);
    }
    else{                //残缺不在右上角,填充一个方格 
        chess[a+l-1][b+l]=L;
        cover(a,b+l,a+l-1,b+l,l);
    }
    
    if(dr>=a+l && dl>=b+l){//残缺在右下角
        cover(a+l,b+l,dr,dl,l);
    }
    else{                  //残缺不在右下角,填充一个方格 
        chess[a+l][b+l]=L;
        cover(a+l,b+l,a+l,b+l,l);
    }
}




下面的版本二是写给我自己看的,和上面的原理一模一样

#include<bits/stdc++.h>
using namespace std;
/*
为了方便逻辑判断,棋盘的行和列都从1开始
不需要将原始的残缺块位置设为全局变量,因为后来的填充都要看作残差块 
*/
int board[65][65];  //int型全局变量默认为0 
int N; //棋盘的边长
int cnt=0;  //记录该摆放第几块骨牌了
 
void cover(int dr, int dl, int a, int b, int length){ 
	//每一次递归都需要知道五个参数:残缺的行,残缺的列,子棋盘左上角的行,左上角的列,子棋盘边长
	int l=length/2; 
	cnt++;
	int L=cnt;
	if(length==1){  //递归出口
		return; 
	}
	//如果残差块在左上角的子棋盘中
	if(dr<=a+l-1&&dl<=b+l-1){  
		cover(dr,dl,a,b,length/2);	
	}
	else{ //不在的话,把右下角格子的填充上 
		board[a+l-1][b+l-1]=L;
		cover(a+l-1,b+l-1,a,b,length/2);
	}
	
	//如果残差块在右上角的子棋盘中
	if(dr<=a+l-1&&dl>=b+l){  
		cover(dr,dl,a,b+l,length/2);	
	}
	else{ //不在的话,把左下角格子的填充上 
		board[a+l-1][b+l]=L;
		cover(a+l-1,b+l,a,b+l,length/2);
	}
	
	//如果残差块在左下角的子棋盘中
	if(dr>=a+l&&dl<=b+l-1){  
		cover(dr,dl,a+l,b,length/2);	
	}
	else{ //不在的话,把右上角格子的填充上 
		board[a+l][b+l-1]=L;
		cover(a+l,b+l-1,a+l,b,length/2);
	}
	
	//如果残差块在右下角的子棋盘中
	if(dr>=a+l&&dl>=b+l){  
		cover(dr,dl,a+l,b+l,length/2);	
	}
	else{ //不在的话,把左上角格子的填充上 
		board[a+l][b+l]=L;
		cover(a+l,b+l,a+l,b+l,length/2);
	}
	
} 
int main(){
	int dr; //残缺所在的行数
	int dl; //残缺所在的列数 
	printf("请输入棋盘大小(1~64之间的整数):");
    scanf("%d",&N);
    printf("请输入残缺行号dr:");
    scanf("%d",&dr); 
    printf("请输入残缺列号dl:");
    scanf("%d",&dl);
	cover(dr,dl,1,1,N); 
	/*---打印输出-----*/
	for(int i=1;i<=N;i++){ //输出结果,都从1号开始存放的 
        for(int j=1;j<=N;j++){
            printf("%4d",board[i][j]); 
            if(j==N){
                printf("\n");
            }
        }
    }
	return 0;
}

六、常见错误分析

在写版本二时,出现了各种错误。列举如下
(1)没有3个相同的序号连在一起
在这里插入图片描述
错误代码(局部)如下:

	int l=length/2; 
	cnt++;//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
	if(length==1){  
		return; 
	}
	
	if(dr<=a+l-1&&dl<=b+l-1){  
		cover(dr,dl,a,b,length/2);	
	}
	else{
		board[a+l-1][b+l-1]=cnt;//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		cnt++;
		cover(a+l-1,b+l-1,a,b,length/2);
	}

原因在于,计数器cnt是全局变量,随着递归的深入,cnt的值不断加1,回溯时也没有减1,这就导致递归树上同一层次的结点的cnt不同
解决方法是,在递归函数内新定义一个局部计数器L,用L去给每一个分支赋值。

	cnt++;
	int L=cnt; //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
	if(length==1){  
		return; 
	}
	
	if(dr<=a+l-1&&dl<=b+l-1){  
		cover(dr,dl,a,b,length/2);	
	}
	else{  
		board[a+l-1][b+l-1]=L; //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		cover(a+l-1,b+l-1,a,b,length/2);
	}

(2)上面的问题解决了(存在3个相同的序号连在一起了),但序号不连续
在这里插入图片描述

这个错误很难发现!原因在于下面两行代码的顺序。

	cnt++;
	int L=cnt;
	if(length==1){  //递归出口
		return; 
	}

按照上面这样写,进入递归出口前会使得计数器加1,但是进入递归出口根本不需要加1,这也就使得序号出现间断。
解决方法很简单,就是把计数器加一的代码移到递归出口的下面

	if(length==1){  //递归出口
		return; 
	}
	cnt++;
	int L=cnt;
二级残缺棋盘覆盖问题是指在一个大小为2^n x 2^n的棋盘中,去掉其中一个格子后,如何使用L型骨牌(一种覆盖三个方格的L形图案)将剩下的格子完全覆盖,且每个L型骨牌不重叠、不遗漏地覆盖棋盘中的三个方格。 该问题可以使用分治算法解决。具体思路为: 1. 对于当前的残缺棋盘,找到其中任意一个空白格子,并以该空白格子为中心,将整个棋盘分成四个大小相等的子棋盘,其中包含三个L型骨牌覆盖的方案。 2. 在上述四个子棋盘中,选取其中一个仍然是残缺的子棋盘,重复步骤1,直到所有的空白格子都被L型骨牌覆盖。 以下是C语言的分治算法代码实现,其中board数组代表棋盘,size表示当前棋盘大小,x和y表示当前空白格子的坐标。 ``` #include <stdio.h> #define MAXN 1024 int board[MAXN][MAXN]; int cnt = 0; void chessboard(int tr, int tc, int dr, int dc, int size, int x, int y) { if (size == 1) return; // 递归结束条件 int t = ++cnt; int s = size / 2; // 当前空白格子在左上角子棋盘中 if (x < tr + s && y < tc + s) { chessboard(tr, tc, tr + s - 1, tc + s - 1, s, x, y); } else { board[tr + s - 1][tc + s - 1] = t; // 其他三个子棋盘 chessboard(tr, tc, tr + s - 1, tc + s - 1, s, tr + s - 1, tc + s - 1); chessboard(tr, tc + s, tr + s - 1, tc + s, s, tr + s - 1, tc + s); chessboard(tr + s, tc, tr + s, tc + s - 1, s, tr + s, tc + s - 1); chessboard(tr + s, tc + s, dr, dc, s, tr + s, tc + s); } } int main() { int n = 8; // 棋盘大小为2^n x 2^n int x = 6; // 空白格子坐标 int y = 5; board[x][y] = -1; chessboard(0, 0, n-1, n-1, n, x, y); // 分治算法 for (int i=0; i<n; i++) { for (int j=0; j<n; j++) { printf("%3d", board[i][j]); } printf("\n"); } return 0; } ```
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值