棋盘覆盖问题递归与非递归解法(java版)

0.问题描述

什么是棋盘覆盖呢?在一个 2 k × 2 k 2^{k} \times 2^{k} 2k×2k个方格组成的棋盘中,若有一个方格与其他方格不同,则称该方格为特殊方格,且称该棋盘为一个特殊棋盘.显然特殊方格在棋盘上出现的位置有 4 k 4^{k} 4k种情形.因而对任何 k ≥ 0 k\ge 0 k0,有 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(k1)+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(k1)+O(1)=4(4T(k2)+O(1))+O(1)=42T(k2)+(4+1)O(1)=42(4T(k3)+O(1))+(4+1)O(1)=43T(k3)+(42+4+1)O(1)=...=4kT(0)+(4k1+...+42+4+1)O(1)=4kO(1)+(34k1)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+11=34(2k)21=34n21
这样就显而易见了,非递归算法的时间复杂度为 O ( n 2 ) O(n^{2} ) O(n2)

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值