0.问题描述
什么是棋盘覆盖呢?在一个 2 k × 2 k 2^{k} \times 2^{k} 2k×2k个方格组成的棋盘中,若有一个方格与其他方格不同,则称该方格为特殊方格,且称该棋盘为一个特殊棋盘.显然特殊方格在棋盘上出现的位置有 4 k 4^{k} 4k种情形.因而对任何 k ≥ 0 k\ge 0 k≥0,有 4 k 4^{k} 4k种不同的特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格(红色框为特殊方格)以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
1.问题解决
1.1分治法
采用分治法解决棋盘覆盖问题是一个比较常见的解决方案,我们如何使用分治法来解决这个问题呢?我们需要找到比当前问题更小的子问题,看一看能否变成解决思路相同但是规模变小的问题。我们可以将棋盘进行划分,划分成规模比当前小的棋盘,对于上图来说我们可以将 4 × 4 4\times 4 4×4的棋盘转化成4个 2 × 2 2\times 2 2×2的 棋盘。
这样对于 4 × 4 4\times 4 4×4的棋盘我们就可以转化成4个 2 × 2 2\times 2 2×2的棋盘,这个就是将一个较大规模的问题转化成4个小问题来解决,再来我们发现只有左上角的棋盘有特殊方格,其他三个没有特殊方格,这样这四个问题中有三个问题与原问题解决逻辑不同,那么我们可以将这三个棋盘变成带有特殊方格的棋盘,我们就可以在三个棋盘的交汇处,放上一个L型骨牌,让其成为带有特殊方格的棋盘。
这样我们就可以递归的解决这四个问题。
当然对于递归的方法我们还需要考虑一个比较重要的问题,就是递归问题的出口是什么?其实很显然,当棋盘规模为1的时候,就是只有一个特殊方格了,那么就可以找到解了。
根据上面所说的,递归的算法实现其实也不难理解了。
/**
* 棋盘覆盖 递归解法
* @param tr 棋盘入口的行号(左上角格子的行号)
* @param tc 棋盘入口的列号(左上角格子的列号)
* @param dr 特殊棋子的行号
* @param dc 特殊棋子的列号
* @param size 棋盘的行数或者列数
*/
public void chessBoard(int tr,int tc,int dr,int dc,int size){
if (size==1) return; //棋盘大小为1的时候返回
int t = tile++;
int s = size/2;//每一次将大棋盘化为小棋盘
//处理左上角棋盘
if (dr < tr+s && dc < tc+ s){ //左上角子棋盘有特殊棋子
chessBoard(tr, tc, dr, dc, s);
}else{ //左上角子棋盘无特殊棋子
board[tr+s-1][tc+s-1] = t; //将中间的方格补上特殊方格
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; //将中间的方格补上特殊方格
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; //将中间的方格补上特殊方格
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; //将中间的方格补上特殊方格
chessBoard(tr+s,tc+s,tr+s,tc+s,s);
}
}
1.2 非递归方法
我们根据递归方法来想一下,我们是将大的棋盘分割成4份然后进行分别处理的,那我们可以不可以这样,也是将棋盘划分为4部分,我们记录下来这个小棋盘的状态(也就是起点位置,特殊方格和棋盘规模)让这个对象进队列或栈中,我们依次出队或者出栈进行处理(实际程序中的处理也就是在标记数字),那么我们就是利用栈和队列来解决系统栈要完成的事情。
1.2.1队列实现非递归
我们先看一下使用队列实现的非递归算法:
我们先定义一个棋盘类,用于封装我们要进队的对象
public class Board {
int tr = 0; //棋盘入口的行号(左上角格子的行号)
int tc = 0; //棋盘入口的列号(左上角格子的列号)
int dr; //特殊棋子的行号
int dc; //特殊棋子的列号
int size = 4; // 棋盘的大小
public int getTr() {
return tr;
}
public void setTr(int tr) {
this.tr = tr;
}
public int getTc() {
return tc;
}
public void setTc(int tc) {
this.tc = tc;
}
public int getDr() {
return dr;
}
public void setDr(int dr) {
this.dr = dr;
}
public int getDc() {
return dc;
}
public void setDc(int dc) {
this.dc = dc;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
@Override
public String toString() {
return "Board{" +
"tr=" + tr +
", tc=" + tc +
", dr=" + dr +
", dc=" + dc +
", size=" + size +
'}';
}
}
我们定义一个入队的函数,这个入队的时候要记录这个棋盘的信息
/**
* 入队函数
* @param queue 队列
* @param tr 棋盘入口的行号(左上角格子的行号)
* @param tc 棋盘入口的列号(左上角格子的列号)
* @param dr 特殊棋子的行号
* @param dc 特殊棋子的列号
* @param size 棋盘的行数或者列数
*/
public void insertQueue(Queue<Board> queue,int tr,int tc,int dr,int dc,int size){
Board b = new Board();
b.setTr(tr);
b.setTc(tc);
b.setDr(dr);
b.setDc(dc);
b.setSize(size);
queue.add(b);
}
下面就是根据递归的规则进行入队,依次出队进行操作即可
/**
* 使用队列非递归解决棋盘覆盖问题
* @param x 特殊棋子的行号
* @param y 特殊棋子的列号
* @param size 棋盘的行数或者列数
*/
public void chessBoardQueue(int x,int y,int size){
Queue<Board> queue = new LinkedList<Board>();
int s,t = 0;
int tr,tc,dr,dc;
//初始化,棋盘进队
insertQueue(queue,0,0,x,y,size);
Board b = new Board();
while (!queue.isEmpty()){
b = queue.peek(); //取的对头元素进行处理
if (b.getSize()==1){ //当棋盘大小为1的时候返回
return;
}else{
s = b.getSize()/2;
t++;
tr = b.getTr();
tc = b.getTc();
dr = b.getDr();
dc = b.getDc();
if (dr < tr+s && dc < tc+ s){ //左上角子棋盘有特殊棋子
insertQueue(queue,tr,tc,dr,dc,s);
}else {//左上角子棋盘无特殊棋子
board[tr+s-1][tc+s-1] = t; //将中间的方格补成特殊方格
insertQueue(queue,tr,tc,tr+s-1,tc+s-1,s); //入队
}
//同理,处理右上角棋盘
if (dr < tr+s && dc >= tc+s){ //右上角子棋盘有特殊棋子
insertQueue(queue,tr,tc+s,dr,dc,s);
}else { //右上角子棋盘无特殊棋子
board[tr + s - 1][tc + s] = t; //将中间的方格补成特殊方格
insertQueue(queue,tr,tc+s,tr+s-1,tc+s,s ); //入队
}
//同理,处理左下角棋盘
if (dr >= tr+s && dc < tc+s){
insertQueue(queue,tr+s,tc,dr,dc,s);
}else {
board[tr+s][tc+s-1] = t;
insertQueue(queue,tr+s,tc,tr+s,+tc+s-1,s);
}
//同理,处理右下角棋盘
if (dr >= tr+s && dc >= tc+s){
insertQueue(queue,tr+s,tc+s,dr,dc,s);
}else{
board[tr+s][tc+s] = t;
insertQueue(queue,tr+s,tc+s,tr+s,tc+s,s);
}
}
//处理完,出队
queue.poll();
}
}
1.2.2 栈实现非递归
栈与队列不同,栈是先进后出的,所以在处理出栈的时候次序时不同的,仅此而已,其他的跟队列的思路是一样的。
先定义一个入栈的函数,记录棋盘的规模
/**
* 入栈函数
* @param stack 栈
* @param tr 棋盘入口的行号(左上角格子的行号)
* @param tc 棋盘入口的列号(左上角格子的列号)
* @param dr 特殊棋子的行号
* @param dc 特殊棋子的列号
* @param size 棋盘的行数或者列数
*/
public void pushStack(Stack<Board> stack,int tr,int tc,int dr,int dc,int size){
Board b = new Board();
b.setTr(tr);
b.setTc(tc);
b.setDr(dr);
b.setDc(dc);
b.setSize(size);
stack.push(b);
}
因为栈的先进后出,所以我们应该在处理这个棋盘之前就要先出栈,并且当棋盘size=1的时候,不是返回,而是出栈。
/**
*
* @param x 特殊方格的行号
* @param y 特殊方格的列号
* @param size 棋盘的大小
*/
public void chessBoardStack(int x,int y,int size){
Stack<Board> stack = new Stack<Board>();
int s,t = 0;
int tr,tc,dr,dc;
//初始化,入栈
pushStack(stack,0,0,x,y,size);
Board b = new Board();
while (!stack.empty()){
//取出栈顶的元素
b = stack.peek();
if (b.getSize() ==1){
stack.pop(); //出栈
}else{
s = b.getSize()/2;
t++;
tr = b.getTr();
tc = b.getTc();
dr = b.getDr();
dc = b.getDc();
//出栈
stack.pop();
if (dr < tr+s && dc < tc+ s){ //左上角子棋盘有特殊棋子
pushStack(stack,tr,tc,dr,dc,s);
}else{ //左上角子棋盘无特殊棋子
board[tr+s-1][tc+s-1] = t; //将中间的方格补成特殊方格
pushStack(stack,tr,tc,tr+s-1,tc+s-1,s); //入栈
}
//同理,处理右上角棋盘
if (dr < tr+s && dc >= tc+s){
pushStack(stack,tr,tc+s,dr,dc,s);
}else {
board[tr + s - 1][tc + s] = t;
pushStack(stack,tr,tc+s,tr+s-1,tc+s,s );
}
//同理,处理左下角棋盘
if (dr >= tr+s && dc < tc+s){
pushStack(stack,tr+s,tc,dr,dc,s);
}else {
board[tr+s][tc+s-1] = t;
pushStack(stack,tr+s,tc,tr+s,+tc+s-1,s);
}
//同理处理右下角棋盘
if (dr >= tr+s && dc >= tc+s){
pushStack(stack,tr+s,tc+s,dr,dc,s);
}else{
board[tr+s][tc+s] = t;
pushStack(stack,tr+s,tc+s,tr+s,tc+s,s);
}
}
}
}
2.问题分析
棋盘覆盖问题,刚刚我们采用了递归和非递归的方法分析解决了,那么我们来分析一下他们的时间复杂度
对于递归解决来说,我们可以写出他的递归方程,这是对于一个
2
k
×
2
k
2^{k} \times 2^{k}
2k×2k的棋盘来说:
T
(
k
)
=
{
O
(
1
)
n
=
0
4
T
(
k
−
1
)
+
O
(
1
)
n
>
0
T(k) = \left\{\begin{matrix} O(1) \qquad n=0\\ 4T(k-1)+O(1) \qquad n>0 \end{matrix}\right.
T(k)={O(1)n=04T(k−1)+O(1)n>0
对于递归方法的时间复杂度分析方法有很多,我们采用递推的方法来分析这个递归的时间复杂度
T
(
k
)
=
4
T
(
k
−
1
)
+
O
(
1
)
=
4
(
4
T
(
k
−
2
)
+
O
(
1
)
)
+
O
(
1
)
=
4
2
T
(
k
−
2
)
+
(
4
+
1
)
O
(
1
)
=
4
2
(
4
T
(
k
−
3
)
+
O
(
1
)
)
+
(
4
+
1
)
O
(
1
)
=
4
3
T
(
k
−
3
)
+
(
4
2
+
4
+
1
)
O
(
1
)
=
.
.
.
=
4
k
T
(
0
)
+
(
4
k
−
1
+
.
.
.
+
4
2
+
4
+
1
)
O
(
1
)
=
4
k
O
(
1
)
+
(
4
k
−
1
3
)
O
(
1
)
\begin{aligned} T(k) &= 4T(k-1) + O(1) \\ &= 4(4T(k-2)+O(1)) + O(1) \\ &= 4^{2} T(k-2) + (4+1) O(1) \\ &= 4^{2} (4T(k-3)+ O(1)) + (4+1) O(1) \\ &= 4^{3} T(k-3) + (4^{2}+4+1) O(1) \\ &= ... \\ &= 4^{k} T(0) + (4^{k-1}+...+4^{2}+4+1) O(1) \\ &= 4^{k} O(1) + ( \frac{4^{k}- 1}{3} ) O(1) \end{aligned}
T(k)=4T(k−1)+O(1)=4(4T(k−2)+O(1))+O(1)=42T(k−2)+(4+1)O(1)=42(4T(k−3)+O(1))+(4+1)O(1)=43T(k−3)+(42+4+1)O(1)=...=4kT(0)+(4k−1+...+42+4+1)O(1)=4kO(1)+(34k−1)O(1)
很显然时间复杂度为
O
(
4
k
)
O(4^{k} )
O(4k) ,要是转化成我们习惯的其中
2
k
=
n
2^{k} = n
2k=n,也就是一个
n
×
n
n \times n
n×n 的棋盘,那么他的时间复杂度可以表示为
O
(
n
2
)
O(n^{2} )
O(n2)
那对于非递归算法来说时间复杂度又是如何呢?
对于非递归时间复杂的分析会稍微简单一点,我们只需要找到运行次数最多的那行代码,来计算一下他的执行次数即可,不管是对于队列还是栈来说,运行次数最多的就是入队和入栈的操作,也就是说,入队入栈的次数就是非递归的时间复杂度。
下面我们分析一下,假设是一个
2
k
×
2
k
2^{k} \times 2^{k}
2k×2k的棋盘 也就是
n
×
n
n \times n
n×n 的棋盘,当棋盘size = n的时候 入队的个数为1,size = n/2的时候 入队次数为
4
4
4,size = n/4的时候 入队次数为
4
2
4^{2}
42,根据这个规律,当size= 1的时候,入队的次数为
4
k
4^{k}
4k,所以我们将size从n到1入队的次数加起来
1
+
4
1
+
4
2
+
4
3
+
.
.
.
+
4
k
=
4
k
+
1
−
1
3
=
4
⋅
(
2
k
)
2
−
1
3
=
4
⋅
n
2
−
1
3
1+4^{1} +4^{2} + 4^{3}+...+4^{k} = \frac{4^{k+1}-1 }{3} = \frac{4\cdot (2^{k})^{2} -1 }{3}= \frac{4\cdot n^{2} -1 }{3}
1+41+42+43+...+4k=34k+1−1=34⋅(2k)2−1=34⋅n2−1
这样就显而易见了,非递归算法的时间复杂度为
O
(
n
2
)
O(n^{2} )
O(n2)