算法设计与分析 (5. 回溯算法之 n 后问题与 m 着色问题)

5.1 回溯法的算法框架

  • 回溯法
    通用解题法
  • 问题的解空间
    至少应包含问题的一个 (最优解)
  • 基本思想
    深度优先, 若遇到可行解是当前最优解, 则记录; 若本结点的所有子结点已为死结点, 则回溯到上层结点.
  • 减少计算量的办法: 避免无效搜索
  1. 用约束函数在扩展结点处剪去不满足约束的子树;
  2. 用限界函数剪去得不到最优解的子树.
    其中第 2 种方式本质上与第 1 种相同, 只是规则由自己制定 (如: 比已得到的最优解差)
  • 用回溯法解题的步骤
  1. 针对所给问题, 定义问题的解空间;
  2. 确定易于搜索的解空间结构 (如树);
  3. 以深度优先方式搜索解空间, 并在搜索过程中用剪枝函数避免无效搜索.
图 1. 问题的解空间

5.5 n n n 后问题

  • 问题描述
    n × n n \times n n×n 格的棋盘上放置 n n n 个皇后, 任何 2 个皇后不可放在同一行或同一列或同一斜线上.
图 2. 5 后问题的第一个解

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

代码分析:

  1. 本质上是在一个 n + 1 n + 1 n+1 层的 n n n 叉树上搜索;
  2. place 方法的技巧: 将问题的隐约束化成显约束;
  3. 递归调用的结束条件: 已经成功放置了 n n n 个皇后;
  4. 使用一个向量 x (全局变量) 来存储当前解;
  5. 剪枝使得复杂度的分析变得困难, 只能统计运行次数.

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

代码分析:

  1. 在同一个循环体内既出现 k++ 又出现 k-- 的代码非常少见. 代码设计有难度;
  2. 利用 k-- 进行回溯;
  3. 三个边界
    • 如果已经超过可放置皇后数则表示找到一个解, 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
图 3. 平面图
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

代码分析:

  1. 利用了 n + 1 n + 1 n+1 层的 m m m-树;
  2. 回溯法具有相同的框架;
  3. 注释里面放了另一个测试用例 (图的邻接矩阵).

小结

如果要使用迭代法, 建议直接抄框架.

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值