5.1 回溯法的算法框架
- 回溯法
通用解题法 - 问题的解空间
至少应包含问题的一个 (最优解) - 基本思想
深度优先, 若遇到可行解是当前最优解, 则记录; 若本结点的所有子结点已为死结点, 则回溯到上层结点. - 减少计算量的办法: 避免无效搜索
- 用约束函数在扩展结点处剪去不满足约束的子树;
- 用限界函数剪去得不到最优解的子树.
其中第 2 种方式本质上与第 1 种相同, 只是规则由自己制定 (如: 比已得到的最优解差)
- 用回溯法解题的步骤
- 针对所给问题, 定义问题的解空间;
- 确定易于搜索的解空间结构 (如树);
- 以深度优先方式搜索解空间, 并在搜索过程中用剪枝函数避免无效搜索.
5.5 n n n 后问题
- 问题描述
在 n × n n \times n n×n 格的棋盘上放置 n n n 个皇后, 任何 2 个皇后不可放在同一行或同一列或同一斜线上.
5.5.1 递归调用版本
package algorithm.backtracking;
/**
* Title: n后问题.<br>
* Version: 1.0<br>
* Copyright: Copyright (c) 2003, all rights reserved<br>
* Author: 闵帆<br>
* Company: <a href=http://www.fansmale.com>www.fansmale.com</a><br>
* Written time: 2003/09/02<br>
* Last modify time: 2021/12/12<br>
*/
public class NQueen1 extends Object {
/** 皇后个数 */
int n;
/** 当前解 */
int[] x;
/** 当前找到的可行方案数 */
long sum;
/** 调用backtracking方法的次数 */
long backtrackingTimes = 0;
/** 比较次数 */
long compareTimes = 0;
/** backtracking 中 for 循环体执行次数 */
long backtrackingForLoopTimes = 0;
/**
* 入口方法.
*/
public static void main(String args[]) {
NQueen1 nQueen = new NQueen1();
nQueen.nQueen(8);
System.out.println(nQueen.sum + " solutions;");
System.out.println(nQueen.backtrackingTimes + " backtracking times;");
System.out.println(nQueen.compareTimes + " compare times;");
System.out.println(nQueen.backtrackingForLoopTimes + " backtracking for loop times. \r\nFinish.");
}// Of main
/**
* 获取最优解,即最少乘法次数,本方法还起到初始化的作用.
*
* @param paraN
* 皇后个数.
* @return 有多少个解.
*/
public long nQueen(int paraN) {
n = paraN;
sum = 0;
x = new int[n + 1];
for (int i = 0; i <= n; i++)
x[i] = 0;
backtracking(1);
return sum;
}// Of nQueen
/**
* 把皇后放到一个位置后是否可行.
*
* @param k
* 放置皇后的列数.
* @return 是否可行.
*/
public boolean place(int k) {
for (int j = 1; j < k; j++) {
compareTimes++;
if ((Math.abs(k - j) == Math.abs(x[j] - x[k])) || (x[j] == x[k]))
return false;
} // Of for
return true;
}// Of place
/**
* 回溯.
*
* @param t
* 层数.
* @return 是否可行.
*/
public void backtracking(int t) {
backtrackingTimes++;
if (t > n) {
for (int i = 1; i <= n; i++) {
System.out.print(x[i] + " ");
} // Of for i
System.out.println();
sum++;
} else {
for (int i = 1; i <= n; i++) {
x[t] = i;
backtrackingForLoopTimes++;
if (place(t))
backtracking(t + 1);
} // Of for
} // Of if
}// Of backtracking
}// Of class NQueen1
代码分析:
- 本质上是在一个 n + 1 n + 1 n+1 层的 n n n 叉树上搜索;
- place 方法的技巧: 将问题的隐约束化成显约束;
- 递归调用的结束条件: 已经成功放置了 n n n 个皇后;
- 使用一个向量 x (全局变量) 来存储当前解;
- 剪枝使得复杂度的分析变得困难, 只能统计运行次数.
5.5.2 迭代法
package algorithm.backtracking;
/**
* Title: n后问题. 迭代法<br>
* Version: 1.0<br>
* Copyright: Copyright (c) 2003, all rights reserved<br>
* Author: 闵帆<br>
* Company: <a href=http://www.fansmale.com>www.fansmale.com</a><br>
* Written time: 2003/09/02<br>
* Last modify time: 2021/12/12<br>
*/
public class NQueen2 extends Object {
/** 皇后个数 */
int n;
/** 当前解 */
int[] x;
/** 当前找到的可行方案数 */
long sum;
/** 调用backtracking方法的次数 */
long backtrackingTimes = 0;
/** 比较次数 */
long compareTimes = 0;
/** backtracking 中内 while 循环体执行次数 */
long backtrackingWhileLoopTimes = 0;
/**
* 入口方法.
*/
public static void main(String args[]) {
NQueen2 nQueen = new NQueen2();
nQueen.nQueen(8);
System.out.println(nQueen.sum + " solutions;");
System.out.println(nQueen.compareTimes + " compare times;");
System.out.println(nQueen.backtrackingWhileLoopTimes + " backtracking while loop times. \r\nFinish.");
}// Of main
/**
* 获取最优解,即最少乘法次数,本方法还起到初始化的作用.
*
* @param paraN
* 皇后个数.
* @return 有多少个解.
*/
public long nQueen(int paraN) {
n = paraN;
sum = 0;
x = new int[n + 1];
for (int i = 0; i <= n; i++)
x[i] = 0;
backtracking();
return sum;
}// Of nQueen
/**
* 把皇后放到一个位置后是否可行.
*
* @param k
* 放置皇后的列数.
* @return 是否可行.
*/
public boolean place(int k) {
for (int j = 1; j < k; j++) {
compareTimes++;
if ((Math.abs(k - j) == Math.abs(x[j] - x[k])) || (x[j] == x[k]))
return false;
} // Of for
return true;
}// Of place
/**
* 回溯.
*/
public void backtracking() {
x[1] = 0;
int k = 1;
// k == 0表示已经回溯到第一个结点之前,而这是不可能的.
while (k > 0) {
x[k]++;
while ((x[k] <= n) && !(place(k))) {
x[k]++;
backtrackingWhileLoopTimes++;
} // Of while x[k]
if (x[k] <= n) {
if (k == n) {
sum++;
printCurrentSolution();
} else {
// 本皇后已经放置好,为放置下一皇后做准备.
k++;
x[k] = 0;
} // Of if k
} else {
// 找不到合适位置,回溯.
k--;
} // of if x[k]
} // Of while k
}// Of backtracking
/**
* 打印一个结果.
*/
public void printCurrentSolution() {
// 将本结果打印出来.
for (int i = 1; i <= n; i++) {
System.out.print(x[i] + " ");
} // Of for i
System.out.println();
}// Of printCurrentSolution()
}// Of class NQueen2
代码分析:
- 在同一个循环体内既出现 k++ 又出现 k-- 的代码非常少见. 代码设计有难度;
- 利用 k-- 进行回溯;
- 三个边界
- 如果已经超过可放置皇后数则表示找到一个解, printCurrentSolution();
- 如果当前皇后无法放置则表示需要回溯, k–;
- 如果需要放置第 0 个皇后 (她不存在) 则表示搜索完成, 结束 while 循环.
5.8 图的 m m m 着色问题
- 问题描述
给定无向连通图 G G G 和 m m m 种不同的颜色, 每个顶点着一种颜色, 是否有一种着色法使 G G G 中每条边的两个顶点着不同颜色? - 算法设计
解空间树为 n n n 层, 每个分支结点的子结点数为 m m m. 只需要找一个解. - 四色猜想
在一个平面或球面上的任何地图, m = 4 m = 4 m=4. - 注意: 对于一般的无向连通图, 不能保证用 4 色着色. 实际上, 对于 n n n 完全图, 需要 n n n 色
package algorithm.backtracking;
/**
* Title: m 色问题.<br>
* Version: 1.0<br>
* Copyright: Copyright (c) 2003, all rights reserved<br>
* Author: 闵帆<br>
* Company: <a href=http://www.fansmale.com>www.fansmale.com</a><br>
* Written time: 2003/09/02<br>
* Last modify time: 2003/12/12<br>
*/
public class MColor extends Object {
/** 结点数 */
int n;
/** 可用颜色数 */
int m;
/** 比较次数 */
long compareTimes = 0;
/** 邻接矩阵 */
/*
* int [][] a = {{1, 1, 1, 1, 1, 1}, {1, 0, 1, 1, 1, 1}, {1, 1, 0, 0, 0, 0},
* {1, 1, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 0}, {1, 1, 0, 0, 0, 0}, };
*/
int[][] a = { { 1, 1, 1, 1, 1, 1, 1, 1 }, { 1, 0, 1, 0, 1, 0, 1, 0 }, { 1, 1, 0, 1, 0, 0, 1, 1 },
{ 1, 0, 1, 0, 0, 0, 1, 1 }, { 1, 1, 0, 0, 0, 1, 1, 0 }, { 1, 0, 0, 0, 1, 0, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 1 }, { 1, 0, 1, 1, 0, 1, 1, 0 } };
/** 当前解 */
int[] x = { -1, -1, -1, -1, -1, -1, -1, -1 };
/** 当前找到的可m着色方案数 */
long sum;
/**
* 入口方法.
*/
public static void main(String args[]) {
MColor mColor = new MColor(4);
System.out.println(mColor.sum + " solutions;");
System.out.println(mColor.compareTimes + " compare times.\r\nFinish.");
}// Of main
/**
* 初始化.
*
* @param mm
* 颜色数.
*/
public MColor(int paraM) {
m = paraM;
sum = 0;
n = a.length - 1;
backtracking(1);
}// Of MColor
/**
* 回溯.
*
* @param t
* 层数.
* @return 是否可行.
*/
public void backtracking(int t) {
if (t > n) {
sum++;
if (sum > 20)
return;
for (int i = 1; i <= n; i++) {
System.out.print(x[i] + " ");
} // Of for i
System.out.println();
} else {
for (int i = 1; i <= m; i++) {
x[t] = i;
if (place(t))
backtracking(t + 1);
} // Of for
} // Of if
}// Of backtracking
/**
* 将当前结点染成颜色 k 是否可行.
*
* @param k
* 颜色编号.
* @return 是否可行.
*/
public boolean place(int k) {
for (int j = 1; j < k; j++) {
compareTimes++;
if ((a[k][j] == 1) && (x[j] == x[k]))
return false;
} // Of for
return true;
}// Of place
}// Of class MColor
代码分析:
- 利用了 n + 1 n + 1 n+1 层的 m m m-树;
- 回溯法具有相同的框架;
- 注释里面放了另一个测试用例 (图的邻接矩阵).
小结
如果要使用迭代法, 建议直接抄框架.