算法的基本思想和应用要点
递归与分治算法是一种很经典的问题求解策略,其基本思想是:将一个规模较大的大问题,划分为多个相同相同类型规模较小的小问题,进而一次或多次递归调用自身求解小问题的解,综合所有小问题的解得到大问题的解。就这样通过不断反复的分割和综合,总可以得到能直接得到小问题解的情况,也就是我们所说的递归出口。简单的说就是:将大问题转换成相同的小问题,并且存在一个递归出口。
其应用要点可以用“分治是思想,递归是手段,二者相辅相成”来概括。运用递归与分治策略要求在每一次进行分割递归时,应遵循三个步骤:
分解(Divide):将原问题分解成一系列规模较小的子问题;
解决(Conquer):递归的解决各子问题。若子问题足够小,则直接求解;
合并(Combine):将子问题的结果合并成原问题的解。
另外,在分解时,问题的规模越小越容易解决,且各个子问题间是相互独立的,不存在重叠共线的问题。
问题描述
棋盘覆盖问题(Chess Cover Problem):在一个2k*2k个方格组成的棋盘中,若恰有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一特殊棋盘。要求用下图所示的四种不同形态的L形骨牌覆盖给定的特殊棋盘上除特殊方格外的所有方格,且任何两个L行骨牌不得重复覆盖。
理论分析
棋盘无论是从行还是列来看都是对称的,并且行和列长度相同,准确的说行和列的长度都可以被2整除,因此2k2k的棋盘可以采用分治法进行降维可以划分成4个2(k-1)2(k-1)大小相同的小棋盘。因为原棋盘只有一个特殊方格,所以划分后的4个小棋盘只有一个小棋盘拥有该特殊方格,另外三个没有。
采用递归与分治策略解决棋盘覆盖问题时,可以用一个L型骨牌覆盖这3个小棋盘的交汇点。从而达到将大规模问题转化为四个规模相对较小的问题。然后再递归的使用这种划分策略,一直到将棋盘划分为2020也就是11的小棋盘。
递归填充策略:
如果特殊方块在左上角的子棋盘,则递归填充左上角子棋盘;否则递归填充左上角自棋盘的右下角,将右下角看做特殊方块,然后递归填充左上角子棋盘。
如果特殊方块在右上角的子棋盘,则递归填充右上角子棋盘;否则递归填充右上角子棋盘的左下角,将左下角看做特殊方格,然后递归填充右上角子棋盘。
如果特殊方块在左下角的子棋盘,则递归填充左下角子棋盘;否则递归填充左下角子棋盘的右上角,将右上角看做特殊方块,然后递归填充左下角子棋盘。
如果特殊方块在右下角的子棋盘,则递归填充右下角子棋盘;否则递归填充右下角子棋盘的左上角,将左上角看做特殊方块,然后递归填充右下角子棋盘。
已知条件是一个特殊棋盘上有一个特殊方格,所以算法的输入可以用来表示棋盘的大小,以原始棋盘的左上角为原点建立而为直角坐标系,用(dr,dc)表示特殊方格在棋盘中的位置。
递归方程的建立与复杂度分析:设T(k)是算法ChessBoard覆盖一个2k*2k棋盘所需时间,应用递归与分治策略可得出以下递归方程:
解此递归方程可得T(k)=O(4k)
算法实现
4.1 完整的代码实现
//Board.java棋盘类(棋盘初始化、算法定义以及结果的输出)
package com.company;
/**
* @author 阿飞
* @date 2021/9/18
*/
public class Board {
int dr,dc,size;
int[][] Board;
int tile = 1;//骨牌编号从1开始
public Board() {
}
public Board( int dr, int dc, int size,int[][] Board) {
this.dr = dr;
this.dc = dc;
this.size = size;
Board = new int[size][size];
}
public void ChessBoard(int tr, int tc, int dr, int dc, int size)throws Exception{
if(size == 1){
return;//size=1时,递归结束,即size=1为递归出口
}
int s = size/2;//分割棋盘
int t = tile++;//L型骨牌号
if (dr<tr+s && dc<tc+s){//特殊方格在左上方
ChessBoard(tr,tc,dr,dc,s);//特殊方格在此棋盘上
}else{ //此棋盘无特殊方格
Board[tr+s-1][tc+s-1] =t;//用t号L型骨牌覆盖右下角
ChessBoard(tr,tc,tr+s-1,tc+s-1,s);//覆盖其余方格
}
if(dr<tr+s && dc>=tc+s){//特殊方格在右上方
ChessBoard(tr,tc+s,dr,dc,s);//特殊方格在此棋盘上
}else { //此棋盘无特殊方格
Board[tr+s-1][tc+s] = t;//用t好L型挂牌覆盖左下角
ChessBoard(tr,tc+s,tr+s-1,tc+s,s);//覆盖其余方格
}
if(dr>=tr+s&&dc<tc+s){//特殊方格在左下方
ChessBoard(tr+s,tc,dr,dc,s);//特殊方格在此棋盘上
}else{ //此棋盘无特殊方格
Board[tr+s][tc+s-1] = t;//用t好L型骨牌填充右上角
ChessBoard(tr+s,tc,tr+s,tc+s-1,s);//覆盖其余方格
}
if(dr>=tr+s&&dc>=tc+s){//如果特殊点在右下方
ChessBoard(tr+s,tc+s,dr,dc,s);//特殊方格在此棋盘上
}else{ //次棋盘无特殊方格
Board[tr+s][tc+s] = t;//用t号L型骨牌覆盖左上角
ChessBoard(tr+s,tc+s,tr+s,tc+s,s);//覆盖其余方格
}
}
//打印棋盘
public void printBoard(int dr,int dc,int size)throws Exception{
try {
ChessBoard(0,0,dr,dc,size);
} catch (Exception e) {
e.printStackTrace();
}
for(int i=0;i<size;i++){
for(int j=0;j<size;j++){
System.out.print(Board[i][j]+" ");
}
System.out.println();
} }}
//主函数:
package com.company;
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("请输入棋盘的大小(值的要求为2的幂)");
Scanner s = new Scanner(System.in);//输入棋盘大小
int size = s.nextInt();
if(size%2==0){//检验输入是否符合要求,不符合则退出,符合则继续
System.out.println("请输入特殊方格的横坐标(从1开始)");
Scanner X =new Scanner(System.in);
int x = X.nextInt();
System.out.println("请输入特殊方格的纵坐标(从1开始)");
Scanner Y =new Scanner(System.in);
int y = Y.nextInt();
Board chesssBoard = new Board(x-1,y-1,size);
chesssBoard.printBoard(x-1,y-1,size);
}else{
System.out.println("输入有误!请退出!");
}
}
运行结果截图:(结果说明:标号为0的位置就是特殊方格的位置)
图5.棋盘大小为8,特殊方格坐标为(3,3)的执行结果
图6.棋盘大小为4,特殊方格坐标为(1,1)的执行结果
4.2 关键代码说明
说明一:
if(size == 1){
return;//size=1时,递归结束,即size=1为递归出口
}
int s = size/2;//分割棋盘
int t = tile++;//L型骨牌号
if (dr<tr+s && dc<tc+s){//特殊方格在左上方
ChessBoard(tr,tc,dr,dc,s);//特殊方格在此棋盘上
}else{ //此棋盘无特殊方格
Board[tr+s-1][tc+s-1] =t;//用t号L型骨牌覆盖右下角
ChessBoard(tr,tc,tr+s-1,tc+s-1,s);//覆盖其余方格
}
if(dr<tr+s && dc>=tc+s){//特殊方格在右上方
ChessBoard(tr,tc+s,dr,dc,s);//特殊方格在此棋盘上
}else { //此棋盘无特殊方格
Board[tr+s-1][tc+s] = t;//用t好L型挂牌覆盖左下角
ChessBoard(tr,tc+s,tr+s-1,tc+s,s);//覆盖其余方格
}
if(dr>=tr+s&&dc<tc+s){//特殊方格在左下方
ChessBoard(tr+s,tc,dr,dc,s);//特殊方格在此棋盘上
}else{ //此棋盘无特殊方格
Board[tr+s][tc+s-1] = t;//用t好L型骨牌填充右上角
ChessBoard(tr+s,tc,tr+s,tc+s-1,s);//覆盖其余方格
}
if(dr>=tr+s&&dc>=tc+s){//如果特殊点在右下方
ChessBoard(tr+s,tc+s,dr,dc,s);//特殊方格在此棋盘上
}else{ //次棋盘无特殊方格
Board[tr+s][tc+s] = t;//用t好L型骨牌覆盖左上角
ChessBoard(tr+s,tc+s,tr+s,tc+s,s);//覆盖其余方格
}
此段代码是分别在四种情况下的不同填充策略(理论分析部分已阐述)的实现。
说明二:
Board chesssBoard = new Board(x-1,y-1,size);
chesssBoard.printBoard(x-1,y-1,size);
Board[][]数组是从下标为0开始存储和输出的,所以当使用者输入特殊点的
坐标为(x,y)时,需要进行减1操作,才可以对应到该从1开始的坐标
说明三:
if(size%2==0){//检验输入是否符合要求,不符合则退出,符合则继续
System.out.println("请输入特殊方格的横坐标(从1开始)");
Scanner X =new Scanner(System.in);
int x = X.nextInt();
System.out.println("请输入特殊方格的纵坐标(从1开始)");
Scanner Y =new Scanner(System.in);
int y = Y.nextInt();
Board chesssBoard = new Board(x-1,y-1,size);
chesssBoard.printBoard(x-1,y-1,size);
}else{
System.out.println("输入有误!请退出!");
}
}
在进行代码设计时应将棋盘大小为2k*2k的条件考虑进去,所以需要对输入进行合法性检测,保证用户输入值为2的整数次幂。
实验的大部分都如此这般,后面的代码调试、实验总结均各有不同。