算法系列(一):分治策略--棋盘覆盖
一、分析
问题描述:
图1-1 k=2时的一个特殊棋盘
在一个 2^k * 2^k 个方格组成的棋盘中,若恰有一个方格与其它方格不同,则称该方格为一特殊方格,且称该棋盘为一特殊棋盘。显然特殊方格在棋盘上出现的位置有 4^k 种情形。因而对任何 k>=0 ,有 4^k 种不同的特殊棋盘。图1-1中的特殊棋盘为 k=2 时 16 个特殊棋盘中的一个。
在棋盘覆盖问题中,要用图1-2所示的4种不同形态的 L 型骨牌覆盖一个给定的特殊棋牌上除特殊方格以外的所有方格,且任何 2 个 L 型骨牌不得重叠覆盖。易知,在任何一个 2^k * 2^k 的棋盘中,用到的 L 型骨牌个数恰为 (4^k-1)/3 。
(a) (b) (c) (d) 图1-2 4种不同形态的L型骨牌
用分治策略,可以设计解棋盘问题的一个简捷的算法。
在本例分析之前先简单回顾分治策略,“分治”,顾名思义,分而治之:分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同。递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
基本模式:
二、求解
数学归纳法证明:问题有解
(1) k=1时,有解
(2) k=2时,有解
(3) 设k-1时成立(k>=2),则k时?
将 2^k * 2^k 棋盘分割为 4 个 2^(k-1) * 2^(k-1) 子棋盘,如下图所示。
特殊方格必位于 4 个较小子棋盘之一中,其余 3 个子棋盘中无特殊方格。为了将这 3 个无特殊方格的子棋盘转化为特殊盘,我们可以用一个 L 型骨牌覆盖这 3 个较小的棋盘的汇合处,如下图所示,这 3 个子棋盘上被 L 型骨牌覆盖的方格就成为该棋盘上的特殊方格,从而将原问题化为 4 个较小规模的棋盘覆盖问题。递归的使用这种分割,直至棋盘简化为 2^1x2^1 棋盘。
三、程序
例:每一个L型骨牌用相等的数字填充。从1,2,3,......,(0号为特殊方块所在位置),按左上、右上、右下、左下顺序依次填充
示例:当 k=3,即2^3x2^3:
程序分析及代码(Java):
版本(1.0):我们先把窗体绘制出来:
/**
* @author Sherly-Liu
* @version 1.0(绘制窗体)
*/
class WindowsFrame extends JFrame implements ActionListener {
// 菜单
JMenuBar bar = new JMenuBar();
JMenu fileMenu = new JMenu(" 文件 ");
JMenuItem setupItem = new JMenuItem(" 新建 ");// 菜单项
JMenuItem quitItem = new JMenuItem(" 退出 ");
JMenu helpMenu = new JMenu(" 帮助 ");
JMenuItem aboutItem = new JMenuItem("关于ChessboardCover");
JLabel backgroundLabel = new JLabel(); // 背景
Container contentPane = this.getContentPane();
// 新建任务的窗口
JFrame setupFrame;
// 新建任务窗口元素
JPanel jPanel1, jPanel2, jPanel3, jPanel4;
JLabel jLabel1, jLabel2, jLabel3, jLabel4, jLabel5;
JTextField drTextField, dcTextField;// 特殊方块行、列
JButton confirmButton, cancelButton;// 确认、取消按钮
JComboBox k_cmb;// 下拉列表(问题规模k)
/**
* 窗口类构造方法
*/
public WindowsFrame() {
super("ChessboardCover(棋盘覆盖)");
initialize();
}
/**
* 主窗口元素初始化
*/
public void initialize() {
// 将窗口位置放在屏幕中央
Dimension screensize = Toolkit.getDefaultToolkit().getScreenSize();
this.setSize(880, 600);
Dimension framesize = this.getSize();
int x = (int) screensize.getWidth() / 2 - (int) framesize.getWidth()
/ 2;
int y = (int) screensize.getHeight() / 2 - (int) framesize.getHeight()
/ 2;
this.setLocation(x, y);
// 设置背景图片,把Lable加进LayoutPane
try {
backgroundLabel.setIcon(new ImageIcon(ImageIO.read(new File(
"drawable", "background.jpg"))));
} catch (IOException e) {
e.printStackTrace();
}
this.getLayeredPane().add(backgroundLabel,
new Integer(Integer.MIN_VALUE));
backgroundLabel.setBounds(0, 20, 880, 550);
contentPane.setLayout(null);
this.setJMenuBar(bar);
fileMenu.add(setupItem);
fileMenu.addSeparator();// 分割线
fileMenu.add(quitItem);
helpMenu.add(aboutItem);
bar.add(fileMenu);
bar.add(helpMenu);
setupItem.addActionListener(this);
quitItem.addActionListener(this);
aboutItem.addActionListener(this);
((JPanel) contentPane).setOpaque(false);// 内容面板设置为透明,LayoutPane面板背景才显现
this.setResizable(false);
this.setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
/**
* 新建问题窗口初始化
*/
public void setupInit() {
setupFrame = new JFrame("setup");
jPanel1 = new JPanel();
jPanel2 = new JPanel();
jPanel3 = new JPanel();
jPanel4 = new JPanel();
jLabel1 = new JLabel("k= ");
jLabel2 = new JLabel(" 将建立2^k宽度的棋盘");
jLabel3 = new JLabel("特殊方格的位置: ");
jLabel4 = new JLabel("行: ");
jLabel5 = new JLabel(" 列: ");
Font font = new Font("隶书", Font.BOLD, 18);// 字体大小的设置
jLabel1.setFont(font);
jLabel2.setFont(font);
drTextField = new JTextField(8);
dcTextField = new JTextField(8);
String[] item = { "1", "2", "3", "4", "5", "6", "7" };
k_cmb = new JComboBox(item);
confirmButton = new JButton("确定");
confirmButton.addActionListener(new SetupActionListener());
cancelButton = new JButton("取消");
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setupFrame.dispose();
}
});
jPanel1.add(jLabel1);
jPanel1.add(k_cmb);
jPanel1.add(jLabel2);
jPanel2.add(jLabel3, BorderLayout.WEST);
jPanel3.add(jLabel4);
jPanel3.add(drTextField);
jPanel3.add(jLabel5);
jPanel3.add(dcTextField);
jPanel4.add(confirmButton);
jPanel4.add(cancelButton);
setupFrame.setLayout(new GridLayout(4, 1, 5, 5));
setupFrame.add(jPanel1);
setupFrame.add(jPanel2);
setupFrame.add(jPanel3);
setupFrame.add(jPanel4);
setupFrame.setVisible(true);
setupFrame.setSize(400, 220);
Dimension scrs = Toolkit.getDefaultToolkit().getScreenSize();
Dimension fs = setupFrame.getSize();
int x = (int) scrs.getWidth() / 2 - (int) fs.getWidth() / 2;
int y = (int) scrs.getHeight() / 2 - (int) fs.getHeight() / 2;
setupFrame.setLocation(x, y);
}
/**
* 菜单响应事件
*/
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
String command = e.getActionCommand();
// 新建问题
// ********************************************
if (command.equals(" 新建 ")) {
setupInit();
}
// 退出***************
if (command.equals(" 退出 ")) {
int result = JOptionPane.showConfirmDialog(null, "是否真的退出程序?",
"提示信息", JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (result == 0) {
System.exit(0);
}
}
// 关于******************************************
if (command.equals("关于ChessboardCover")) {
JOptionPane.showMessageDialog(null, "ChessboardCover Demo 1.0"
+ "\n" + "Author:刘晓玲" + "\n" + "2015.12",
"关于ChessboardCover", JOptionPane.INFORMATION_MESSAGE, null);
}
}
/**
* 命名内部类,新建问题监听器
*/
private class SetupActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
}
}
}
// ***************************************
public class Main {
public static void main(String[] args) {
new WindowsFrame();
}
}
运行一下程序:
版本(1.1):设计棋盘的数据结构:
(1)重载Label标签,添加行列坐标属性以及颜色;
import javax.swing.JLabel;
/**
* @author Sherly-Liu
* 重载标签,增加行列坐标属性
* @param lable标签
* @param row行坐标
* @param column列坐标
*/
public class MyLabel extends JLabel {
JLabel label;
private int row;// 行坐标
private int column;// 列坐标
public MyLabel(int row, int column) {
super();
label = new JLabel();
this.row = row;
this.column = column;
}
public int getRow() {
return row;
}
public void setRow(int row) {
this.row = row;
}
public int getColumn() {
return column;
}
public void setColumn(int column) {
this.column = column;
}
}
(2)设计L型骨牌的数据结构
import java.awt.Color;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JPanel;
/**
* @author Sherly-Liu L型骨牌类:组成为三个呈L型相连的lable标签
* @param lable1
* @param lable2
* @param lable3
* @param tile骨牌编号
*/
public class L_Chess extends JPanel {
MyLabel label1;
MyLabel label2;
MyLabel label3;
Color color;
private int tile;// 骨牌编号
/**
* 构造方法:参数:三个标签的横纵坐标以及骨牌编号
*/
public L_Chess(int label1_r, int label1_c, int label2_r, int label2_c,
int label3_r, int label3_c, int tile) {
super();
this.tile = tile;
label1 = new MyLabel(label1_r, label1_c);
label2 = new MyLabel(label2_r, label2_c);
label3 = new MyLabel(label3_r, label3_c);
color = getRandomColor();
MyMouseListener listener = new MyMouseListener();
label1.addMouseListener(listener);
label2.addMouseListener(listener);
label3.addMouseListener(listener);
}
public int getTile() {
return tile;
}
public void setTile(int tile) {
this.tile = tile;
}
/**
* 获取一个随机颜色
*
* @return Color
*/
public Color getRandomColor() {
int r1 = (int) (Math.random() * 255); // 通过调用Math类的random产生随机数
int g1 = (int) (Math.random() * 255); // 再通过随机数分别设置三原色,红绿蓝
int b1 = (int) (Math.random() * 255);
Color color = new Color(r1, g1, b1); // 创建一个颜色对象
return color;
}
/**
* 命名内部类,鼠标点击监听器
*/
private class MyMouseListener implements MouseListener {
@Override
public void mouseClicked(MouseEvent e) {
label1.setText(tile + "");
label1.setBackground(color);
label2.setText(tile + "");
label2.setBackground(color);
label3.setText(tile + "");
label3.setBackground(color);
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
label1.setVisible(true);
label2.setVisible(true);
label3.setVisible(true);
}
@Override
public void mouseReleased(MouseEvent e) {
}
}
}
(3)设计棋盘的数据结构
import java.awt.Color;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.LineBorder;
/**
* @author Sherly-Liu 棋盘类
* @param tr表示棋盘左上角行坐标
* @param tc表示棋盘左上角列坐标
* @param dr表示特殊方格的行坐标
* @param dc表示特殊方格的列坐标
* @param SIZE
* =2^k。棋盘的规格为2^k*2^k
*
*/
public class Chessboard extends JPanel {
int k; // 棋盘的规格为2^k*2^k
int SIZE; // SIZE=2^k
int[][] b;// 棋格,棋盘规格为2^k*2^k个棋格
int tile = 1;// 骨牌编号
L_Chess[] chess;// L型骨牌,个数为 (4^k-1)/3
int dr;// 表示特殊方格的行坐标
int dc;// 表示特殊方格的列坐标
// 构造方法
public Chessboard(int k, int dr, int dc) {
this.k = k;
this.dr = dr;
this.dc = dc;
SIZE = (int) Math.pow(2, k);
b = new int[SIZE][SIZE];// 申请n*n数组空间
int temp = (((int) Math.pow(4, k)) - 1) / 3;// L型骨牌,个数为 (4^k-1)/3
chess = new L_Chess[temp];
Fill(0, 0, SIZE, dr, dc);
InitUI();
}
/**
* Fill方法:每一个L型骨牌用相等的数字填充。从1,2,3,......,(0号为特殊方块所在位置),按左上、右上、右下、左下顺序依次填充
*/
public void Fill(int tr, int tc, int size, int dr, int dc) {
if (size == 1)
return;
else {
int t = tile++;
int s = size / 2;// 将棋盘四等分,即将问题分解为四个SIZE=2^(k-1)的子问题
if ((dr < tr + s) && (dc < tc + s)) {// 特殊方格在左上子棋盘
// b[tr + s - 1][tc + s] = b[tr + s][tc + s] = b[tr + s][tc + s
// - 1] = t;// 覆盖第一个L型骨牌:在右上、右下、左下交界处
chess[t - 1] = new L_Chess(tr + s - 1, tc + s, tr + s, tc + s,
tr + s, tc + s - 1, t);// 覆盖第一个L型骨牌:在右上、右下、左下交界处
// 采用递归分别求解每个子问题
Fill(tr, tc, s, dr, dc);
Fill(tr, tc + s, s, tr + s - 1, tc + s);
Fill(tr + s, tc + s, s, tr + s, tc + s);
Fill(tr + s, tc, s, tr + s, tc + s - 1);
} else if ((dr < tr + s) && (dc >= tc + s)) {// 特殊方格在右上子棋盘
// b[tr + s - 1][tc + s - 1] = b[tr + s][tc + s - 1] = b[tr +
// s][tc
// + s] = t;// 覆盖第一个L型骨牌:在左上、左下、右下交界处
chess[t - 1] = new L_Chess(tr + s - 1, tc + s - 1, tr + s, tc
+ s - 1, tr + s, tc + s, t);// 覆盖第一个L型骨牌:在左上、左下、右下交界处
// 采用递归分别求解每个子问题
Fill(tr, tc, s, tr + s - 1, tc + s - 1);
Fill(tr, tc + s, s, dr, dc);
Fill(tr + s, tc + s, s, tr + s, tc + s);
Fill(tr + s, tc, s, tr + s, tc + s - 1);
} else if ((dr >= tr + s) && (dc >= tc + s)) {// 特殊方格在右下子棋盘
// b[tr + s - 1][tc + s - 1] = b[tr + s - 1][tc + s] = b[tr +
// s][tc
// + s - 1] = t;// 覆盖第一个L型骨牌:在右上、左上、左下交界处
chess[t - 1] = new L_Chess(tr + s - 1, tc + s - 1, tr + s - 1,
tc + s, tr + s, tc + s - 1, t);// 覆盖第一个L型骨牌:在右上、左上、左下交界处
// 采用递归分别求解每个子问题
Fill(tr, tc, s, tr + s - 1, tc + s - 1);
Fill(tr, tc + s, s, tr + s - 1, tc + s);
Fill(tr + s, tc + s, s, dr, dc);
Fill(tr + s, tc, s, tr + s, tc + s - 1);
} else if ((dr >= tr + s) && (dc < tc + s)) {// 特殊方格在左下子棋盘
// b[tr + s - 1][tc + s - 1] = b[tr + s - 1][tc + s] = b[tr +
// s][tc
// + s] = t;// 覆盖第一个L型骨牌:在左上、右上、右下交界处
chess[t - 1] = new L_Chess(tr + s - 1, tc + s - 1, tr + s - 1,
tc + s, tr + s, tc + s, t);// 覆盖第一个L型骨牌:在左上、右上、右下交界处
// 采用递归分别求解每个子问题
Fill(tr, tc, s, tr + s - 1, tc + s - 1);
Fill(tr, tc + s, s, tr + s - 1, tc + s);
Fill(tr + s, tc + s, s, tr + s, tc + s);
Fill(tr + s, tc, s, dr, dc);
}
}
}
/**
* 初始化UI
*/
public void InitUI() {
this.setBounds(0, 0, 880, 500);
this.setVisible(true);
this.setBackground(Color.WHITE);
this.setLayout(null);
int width = 880 / SIZE;
int height = 500 / SIZE;
// 特殊方格
JLabel label = new JLabel();
label.setOpaque(true);
label.setBorder(new LineBorder(Color.GRAY));
label.setBackground(Color.WHITE);
label.setText("0");
label.setBounds(dc * width, dr * height, width, height);
this.add(label);
for (int i = 0; i < chess.length; i++) {
chess[i].label1.setOpaque(true);
chess[i].label1.setBorder(new LineBorder(Color.GRAY));
chess[i].label1.setVisible(true);
chess[i].label2.setOpaque(true);
chess[i].label2.setBorder(new LineBorder(Color.GRAY));
chess[i].label2.setVisible(true);
chess[i].label3.setOpaque(true);
chess[i].label3.setBorder(new LineBorder(Color.GRAY));
chess[i].label3.setVisible(true);
chess[i].label1.setBounds(chess[i].label1.getColumn() * width,
chess[i].label1.getRow() * height, width, height);
chess[i].label2.setBounds(chess[i].label2.getColumn() * width,
chess[i].label2.getRow() * height, width, height);
chess[i].label3.setBounds(chess[i].label3.getColumn() * width,
chess[i].label3.getRow() * height, width, height);
this.add(chess[i].label1);// 参数:(Component comp,int index)
this.add(chess[i].label2);
this.add(chess[i].label3);
}
}
public void Show() {
for (int i = 0; i < chess.length; i++) {
chess[i].label1.setText(chess[i].getTile() + "");
chess[i].label1.setBackground(chess[i].color);
chess[i].label2.setText(chess[i].getTile() + "");
chess[i].label2.setBackground(chess[i].color);
chess[i].label3.setText(chess[i].getTile() + "");
chess[i].label3.setBackground(chess[i].color);
}
}
}
版本(1.2):实现新建问题按钮的监听事件:
/**
* 命名内部类,新建问题监听器
*/
private class SetupActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
setupFrame.dispose();
if (drTextField.getText().toString().equals("")
|| dcTextField.getText().toString().equals("")) {
JOptionPane.showMessageDialog(null, "特殊方块的行值、列值不允许为空", "警告",
JOptionPane.WARNING_MESSAGE, null);
} else {
int k = Integer.valueOf(k_cmb.getSelectedItem().toString());
int dc = Integer.valueOf(dcTextField.getText().toString()) - 1;
int dr = Integer.valueOf(drTextField.getText().toString()) - 1;
int n = (int) Math.pow(2, k);
if (dr >= n || dc >= n) {
JOptionPane.showMessageDialog(null, "特殊方块的行值、列值不允许大于 2^k ",
"警告", JOptionPane.WARNING_MESSAGE, null);
} else if (dr < 0 || dc < 0) {
JOptionPane.showMessageDialog(null, "特殊方块的行值、列值不允许小于等于 0 ",
"警告", JOptionPane.WARNING_MESSAGE, null);
} else {
if (chessboardPanel != null) {
contentPane.remove(chessboardPanel);
}
chessboardPanel = new Chessboard(k, dr, dc);
contentPane.add(chessboardPanel);
showButton.setVisible(true);
showButton.setEnabled(true);
}
}
}
}
四、程序运行结果
实现了点击一下棋格自动填充L型骨牌,点击Show All!按钮全部填充:
PS:类图
(PS:之所以这一次把代码分析得那么细是因为有很多同学总是开头难,不知道要怎么开始写代码,所以加上了详细的版本说明,一步一步引导大家写程序,很适合新手看,当然老程序看看就可以过了,也欢迎指出程序设计上的不足一起探讨。)
源码下载地址:http://download.csdn.net/detail/qq_22145801/9687074
参考:计算机算法设计与分析 电子工业出版社 王晓东编著