数据结构与算法之分治法(棋盘覆盖算法&循环赛事日程表)

1 分治法基本思想

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。

2 解题步骤

分治法解题的一般步骤:
(1)分解,将要解决的问题划分成若干规模较小的同类问题;
(2)求解,当子问题划分得足够小时,用较简单的方法解决;
(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

3 棋盘覆盖问题

3.1 问题描述

在一个2^k×2^k 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。

图(1):
这里写图片描述

题目要求在棋盘覆盖问题中,要用下图-图(2)所示的4种不同形态的L型骨牌覆盖一个给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖.

图(2):
这里写图片描述

3.2 解题思路

当k>0时,将2k×2k棋盘分割为4个2^k-1×2^k-1 子棋盘(a)所示。特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,如 (b)所示,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为棋盘1×1。(出自算法设计与分析-王晓东)
这里写图片描述

实现:每次都对分割后的四个小方块进行判断,判断特殊方格是否在里面。这里的判断的方法是每次先记录下整个大方块的左上角(top left coner)方格的行列坐标,然后再与特殊方格坐标进行比较,就可以知道特殊方格是否在该块中。如果特殊方块在里面,这直接递归下去求即可,如果不在,这根据分割的四个方块的不同位置,把右下角、左下角、右上角或者左上角的方格标记为特殊方块,然后继续递归。在递归函数里,还要有一个变量s来记录边的方格数,每次对方块进行划分时,边的方格数都会减半,这个变量是为了方便判断特殊方格的位置。其次还要有一个变nCount来记录L型骨牌的数量。

3.3 参考链接

棋盘覆盖问题

分治算法–棋盘覆盖

棋盘覆盖问题

3.4 演示

//N==2
1 0 
1 1

//N==4
2 0 3 3 
2 2 1 3 
4 1 1 1 
4 4 1 1 

//N==8
3 0 4 4 4 4 1 1 
3 3 2 4 4 3 3 1 
1 2 2 2 2 2 3 3 
1 1 2 2 1 2 3 3 
1 1 2 1 1 2 3 3 
1 4 2 2 2 2 1 3 
3 4 4 4 4 1 1 1 
3 3 4 4 4 4 1 1 

3.5 源码

package com.dn.chess;

/**
 * 棋盘覆盖算法
 */
public class ChessBoradProblem {

    private int[][] board;// 棋盘
    private int sr;// 特殊点的行下标
    private int sc;// 特殊点的列下标
    private int size;
    private int type = 0;

    public ChessBoradProblem(int sr, int sc, int size) {
        super();
        this.sr = sr;
        this.sc = sc;
        this.size = size;
        board = new int[size][size];
    }

    /**
     * (sr,sc)是特殊方格所在的坐标
     * (lr,lc)是棋盘左上角的方格坐标 

     * @param sr     特殊点的行下标
     * @param sc     特殊点的列下标
     * @param lr     矩阵的左边起点行下标
     * @param lc     矩阵的左边起点列下标
     * @param size
     *               矩阵的宽或者高
     */
    private void ChessBoard(int sr, int sc, int lr, int lc, int size) {
//      printResult();

        if (size == 1) {
            return;
        }
        int s = size / 2;// 分割棋盘一半
        type = type % 4 + 1;// 修改type值
        int t = type;// L型骨牌号

        // 覆盖左上角子棋盘
        if (sr < lr + s && sc < lc + s) {//特殊方格在此棋盘中  
            ChessBoard(sr, sc, lr, lc, s);
        } else {// 此棋盘中无特殊方格
            board[lr + s - 1][lc + s - 1] = t;// 用t号L型骨牌覆盖右下角
            ChessBoard(lr + s - 1, lc + s - 1, lr, lc, s);// 覆盖其余方格
        }
        // 覆盖右上角子棋盘
        if (sr < lr + s && sc >= lc + s) {//特殊方格在此棋盘中  
            ChessBoard(sr, sc, lr, lc + s, s);
        } else {// 此棋盘中无特殊方格
            board[lr + s - 1][lc + s] = t;// 用t号L型骨牌覆盖左下角
            ChessBoard(lr + s - 1, lc + s, lr, lc + s, s);// 覆盖其余方格
        }

//      (sr,sc)是特殊方格所在的坐标
//      (lr,lc)是棋盘左上角的方格坐标 
//       * @param sr     特殊点的行下标
//       * @param sc     特殊点的列下标
//       * @param lr     矩阵的左边起点行下标
//       * @param lc     矩阵的左边起点列下标

        // 覆盖左下角子棋盘
        if (sr >= lr + s && sc < lc + s) {
            ChessBoard(sr, sc, lr + s, lc, s);
        } else {
            board[lr + s][lc + s - 1] = t;
            ChessBoard(lr + s, lc + s - 1, lr + s, lc, s);
        }
        // 覆盖右下角子棋盘   
        if (sr >= lr + s && sc >= lc + s) {
            ChessBoard(sr, sc, lr + s, lc + s, s);
        } else {
            board[lr + s][lc + s] = t;
            ChessBoard(lr + s, lc + s, lr + s, lc + s, s);
        }
    }

    public void printBoard(int sr, int sc, int size) {
        ChessBoard(sr, sc, 0, 0, size);
        printResult();
    }

    private void printResult() {
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                System.out.print(board[i][j] + " ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        int N = 8;
        int sr = 0;
        int sc = 1;
        ChessBoradProblem boradProblem = new ChessBoradProblem(sr, sc, N);
        boradProblem.printBoard(sr, sc, N);
    }
}

4 循环赛事日程表

4.1 问题描述

设有n=2^k个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
(1)每个选手必须与其他n-1个选手各赛一次;
(2)每个选手一天只能参赛一次;
(3)循环赛在n-1天内结束。

请按此要求将比赛日程表设计成有n行和n-1列的一个表。在表中的第i行,第j列处填入第i个选手在第j天所遇到的选手。其中1≤i≤n,1≤j≤n-1。8个选手的比赛日程表如下图:
这里写图片描述

4.2 核心思想

按分治策略,我们可以将所有的选手分为两半,则n个选手的比赛日程表可以通过n/2个选手的比赛日程表来决定。递归地用这种一分为二的策略对选手进行划分,直到只剩下两个选手时,比赛日程表的制定就变得很简单。这时只要让这两个选手进行比赛就可以了。如上图,所列出的正方形表是8个选手的比赛日程表。其中左上角与左下角的两小块分别为选手1至选手4和选手5至选手8前3天的比赛日程。据此,将左上角小块中的所有数字按其相对位置抄到右下角,又将左下角小块中的所有数字按其相对位置抄到右上角,这样我们就分别安排好了选手1至选手4和选手5至选手8在后4天的比赛日程。依此思想容易将这个比赛日程表推广到具有任意多个选手的情形。

4.3 过程演示

(1)结果打印

1 2 3 4 5 6 7 8 
2 1 4 3 6 5 8 7 
3 4 1 2 7 8 5 6 
4 3 2 1 8 7 6 5 
5 6 7 8 1 2 3 4 
6 5 8 7 2 1 4 3 
7 8 5 6 3 4 1 2 
8 7 6 5 4 3 2 1 

(2)图例
根据第一行分块,填充第二行,分4块:
这里写图片描述

根据前两行,填充第三四行,分2块:
这里写图片描述

根据填好的前四行,填充剩下的,分一块:
这里写图片描述

4.4 参考链接

循环赛事日程表

【分治法】循环赛事日程表

4.5 源码

package com.dn.dispatch;

/**
 * 【分治法】循环赛事日程表 
 */
public class SportsSchedule {

    public void scheduleTable(int[][] table, int n) {
        if (n == 1) {
            table[0][0] = 1;
        } else {
            // 填充左上区域矩阵
            int m = n / 2;
            scheduleTable(table, m);
            // 填充右上区域矩阵
            for (int i = 0; i < m; i++) {
                for (int j = m; j < n; j++) {
                    table[i][j] = table[i][j - m] + m;
                }
            }

            // 填充左下区域矩阵
            for (int i = m; i < n; i++) {
                for (int j = 0; j < m; j++) {
                    table[i][j] = table[i - m][j] + m;
                }
            }
            // 填充右下区域矩阵
            for (int i = m; i < n; i++) {
                for (int j = m; j < n; j++) {
                    table[i][j] = table[i - m][j - m];
                }
            }
        }
    }

    public static void main(String[] args) {
        int[][] table = new int[8][8];//行是球队、列是日期
        int n = 8;
        SportsSchedule schedule = new SportsSchedule();
        schedule.scheduleTable(table, n);
        int c = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                System.out.print(table[i][j] + " ");
                c++;
                if (c % n == 0) {
                    System.out.println();
                }
            }
        }

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值