java N皇后 回溯算法 简单三次优化

一、问题描述:
       在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于再n×n的棋盘上放置n个皇后,任何2个皇后不妨在同一行或同一列或同一斜线上。

二、解题思路:
       要解决N皇后问题,其实就是要解决好怎么放置这n个皇后,每一个皇后与前面的所有皇后不能在同一行、同一列、同一对角线,在这里我们可以以行优先,就是说皇后的行号按顺序递增,只考虑第i个皇后放置在第i行的哪一列,所以在放置第i个皇后的时候,可以从第1列判断起,如果可以放置在第1个位置,则跳到下一行放置下一个皇后。如果不能,则跳到下一列...直到最后一列,如果最后一列也不能放置,则说明此时放置方法出错,则回到上一个皇后向之前放置的下一列重新放置。此即是回溯法的精髓所在。当第n个皇后放置成功后,即得到一个可行解,此时再回到上一个皇后重新放置寻找下一个可行解...如此后,即可找出一个n皇后问题的所有可行解。

三、代码与解析:

前提你已经懂得递归了

第一阶段:

        分析:懂得解题思路,第一个感觉写出的代码

      空间上:开了两个size*size二维数组,一个用来保存棋盘,一个用来保存其中的一个解法;以及多个标识数组

      时间上:完全递归

     

import java.util.Arrays;
import java.util.Scanner;

public class Main {
	private static int[][] chessBoard;// 原棋盘数组
	private static int[][] resultChessBoard;// 保存一个N皇后解法的数组
	private static int solutionCount = 0;
	private static int size = 0;
	private static boolean[] rowFlag;// 标识某一行是否已经放下了皇后
	private static boolean[] columnFlag;// 标识某一列是否已经放下了皇后
	private static boolean[] leftToRightFlag;// 标识某一斜行(方向:\)是否已经放下了皇后
	private static boolean[] rightToLeftFlag;// 标识某一斜行(方向:/)是否已经放下了皇后
	private static boolean isPrintResult = true;

	public static void main(String args[]) {
		Scanner cin = new Scanner(System.in);
		size = cin.nextInt();

		chessBoard = new int[size][size];
		resultChessBoard = new int[size][size];
		rowFlag = new boolean[size];
		columnFlag = new boolean[size];
		leftToRightFlag = new boolean[2 * size];
		rightToLeftFlag = new boolean[2 * size];
		nQueens();
		printResult();
	}

	private static void nQueens() {
		backtrace(0);
	}

	/**
	 * 回溯搜索到第i行
	 * 
	 * @param i
	 */
	private static void backtrace(int i) {
		if (i >= size) {
			solutionCount++;
			// 找到一个解之后,将棋盘数组拷贝一份,用于最后的输出
			if (isPrintResult) {
				for (int k = 0; k < size; k++) {
					// 使用浅拷贝
					resultChessBoard[k] = Arrays.copyOfRange(chessBoard[k], 0, size);
				}
				isPrintResult = false;
			}
			return;
		}

		for (int j = 0; j < size; j++) {
			if (isValid(i, j)) {
				// 将皇后放到该位置
				chessBoard[i][j] = 1;
				updateFlag(i, j, true);
				// 进入下一行搜索
				backtrace(i + 1);
				// 递归出来后,说明这种放置方法不对,取消 将皇后放到该位置
				updateFlag(i, j, false);
				chessBoard[i][j] = 0;
			}
		}
	}

	/**
	 * 打印解法个数 并输出一个解法的皇后的位置
	 */
	private static void printResult() {
		System.out.println(solutionCount);// 输出解法数量
		for (int i = 0; i < size; i++)
			for (int j = 0; j < size; j++) {
				if (resultChessBoard[i][j] == 1) {
					System.out.println((i + 1) + " " + (j + 1));
				}
			}
	}

	/**
	 * 更新皇后站位标识
	 * 
	 * @param i
	 * @param j
	 * @param value
	 */
	private static void updateFlag(int i, int j, boolean value) {
		rowFlag[i] = value;
		columnFlag[j] = value;
		leftToRightFlag[i - j + size - 1] = value;
		rightToLeftFlag[j + i] = value;
	}

	/**
	 * 行和列的标识比较简单。有一点技巧的是斜行的标识。 <br>
	 * 方向(↙):i+j的值是相等的 <br>
	 * 方向(↘):i-j的值是相等的,由于相减会出现负数,所以这组标识的下标表示为:i-j+size-1.这样就可以了
	 * 
	 * @param i
	 * @param j
	 * @return
	 */
	private static boolean isValid(int i, int j) {
		return !rowFlag[i] && !columnFlag[j] && !leftToRightFlag[i - j + size - 1] && !rightToLeftFlag[j + i];
	}
}


第二阶段:

          分析:你会发现上面的代码其实在空间的开销是挺大的。既然我们已经用了四个数组去标识,而且我们是逐行遍历的,为什么还需要去用一                 个size*size的数组呢?看上面的代码也可以知道,我们根本没有用的二维数组的什么性质。而且保存位置也没必要用一个size*size                 的数组,不就是想保存坐标么,[size][2]就行了;这样空间上就省一些了

          空间上:一个长度为size的数组表示棋盘,一个size*2的数组保存坐标;标识数组同上

          时间上:完全递归

import java.util.Scanner;

/**
 * 
 *         使用一维数组
 */
public class Main{
	private static int[] chessBoard;// 一维数组即可
	private static int[][] queenPosition;// 保存一个N皇后解法的坐标
	private static int solutionCount = 0;
	private static int size = 0;
	private static boolean[] rowFlag;// 标识某一行是否已经放下了皇后
	private static boolean[] columnFlag;// 标识某一列是否已经放下了皇后
	private static boolean[] leftToRightFlag;// 标识某一斜行(方向:\)是否已经放下了皇后
	private static boolean[] rightToLeftFlag;// 标识某一斜行(方向:/)是否已经放下了皇后
	private static boolean isPrintResult = true;// 因为只输出一个答案,所以标识一下

	public static void main(String args[]) {
		Scanner cin = new Scanner(System.in);
		size = cin.nextInt();

		chessBoard = new int[size];
		queenPosition = new int[size][2];
		rowFlag = new boolean[size];
		columnFlag = new boolean[size];
		leftToRightFlag = new boolean[2 * size];
		rightToLeftFlag = new boolean[2 * size];
		nQueens();
		if (solutionCount > 0)
			printResult();
	}

	private static void nQueens() {
		backtrace(0);
	}

	/**
	 * 回溯法
	 * 
	 * @param i
	 */
	private static void backtrace(int i) {
		if (i >= size) {
			solutionCount++;
			isPrintResult = false;
			return;
		}

		for (int j = 0; j < size; j++) {
			if (isValid(i, j)) {
				// 将皇后放到该位置
				chessBoard[j] = 1;
				if (isPrintResult) {
					queenPosition[i][0] = i + 1;
					queenPosition[i][1] = j + 1;
				}
				updateFlag(i, j, true);
				// 进入下一行搜索
				backtrace(i + 1);
				// 递归出来后,说明这种放置方法不对,取消 将皇后放到该位置
				updateFlag(i, j, false);
				if (isPrintResult) {
					queenPosition[i][0] = 0;
					queenPosition[i][1] = 0;
				}
				chessBoard[j] = 0;
			}
		}
	}

	private static void printResult() {
		System.out.println(solutionCount);// 解法个数
		for (int i = 0; i < size; i++) {
			for (int j = 0; j < 2; j++) {
				System.out.print(queenPosition[i][j] + " ");
			}
			System.out.println();
		}
	}

	/**
	 * 更新皇后站位标识
	 * 
	 * @param i
	 * @param j
	 * @param value
	 */
	private static void updateFlag(int i, int j, boolean value) {
		rowFlag[i] = value;
		columnFlag[j] = value;
		leftToRightFlag[i - j + size - 1] = value;
		rightToLeftFlag[j + i] = value;
	}

	/**
	 * 行和列的标识比较简单。有一点技巧的是斜行的标识。 <br>
	 * 方向(↙):i+j的值是相等的 <br>
	 * 方向(↘):i-j的值是相等的,由于相减会出现负数,所以这组标识的下标表示为:i-j+size-1.这样就可以了
	 * 
	 * @param i
	 * @param j
	 * @return
	 */
	private static boolean isValid(int i, int j) {
		return !rowFlag[i] && !columnFlag[j] && !leftToRightFlag[i - j + size - 1] && !rightToLeftFlag[j + i];
	}
}

第三阶段:

        分析:因为棋盘的大小是size*size,可以知道是对称的,所以我们可以考虑对称性,而不用完全递归

         空间上:同第二阶段

         时间上:考虑对称性,不用完全递归,搜索次数减少将近一半

import java.util.Scanner;

/**
 * 
 *         使用一维数组 并且 使用 对称分析(只对第一行的对称性进行分析)
 */
public class Main {
	private static int[] chessBoard;
	private static int[][] queenPosition;
	private static int solutionCount = 0;
	private static int size = 0;
	private static int topIndex = 0;// 标识第一行已经遍历到第几个格子了,用于对称的分析
	private static boolean[] rowFlag;// 标识某一行是否已经放下了皇后
	private static boolean[] columnFlag;// 标识某一列是否已经放下了皇后
	private static boolean[] leftToRightFlag;// 标识某一斜行(方向:\)是否已经放下了皇后
	private static boolean[] rightToLeftFlag;// 标识某一斜行(方向:/)是否已经放下了皇后
	private static boolean isPrintResult = true;// 只要输出一组答案而已

	public static void main(String args[]) {
		Scanner cin = new Scanner(System.in);
		size = cin.nextInt();

		chessBoard = new int[size];
		queenPosition = new int[size][2];
		rowFlag = new boolean[size];
		columnFlag = new boolean[size];
		leftToRightFlag = new boolean[2 * size];
		rightToLeftFlag = new boolean[2 * size];
		nQueens();
		if (solutionCount > 0)
			printResult();
	}

	private static void nQueens() {
		backtrace(0);
	}

	/**
	 * 回溯搜索到第i行
	 * 
	 * @param i
	 */
	private static void backtrace(int i) {
		if (i >= size) {
			// 如果size只有1的话,很显然+1,如果第一行遍历的位置刚好处于中间,则就不应该+2,否则会重复
			if (size == 1 || (size % 2 != 0 && topIndex == ((size + 1) / 2) - 1)) {
				solutionCount++;
			} else {
				solutionCount += 2;// 所有对称情况+2
			}
			isPrintResult = false;
			return;
		}

		int max = (i == 0 ? ((size + 1) / 2) : size);
		for (int j = 0; j < max; j++) {
			if (isValid(i, j)) {
				if (i == 0)
					topIndex = j;// 记录第一行已经遍历到第几列了

				// 将皇后放到该位置
				chessBoard[j] = 1;
				if (isPrintResult) {
					queenPosition[i][0] = i + 1;
					queenPosition[i][1] = j + 1;
				}
				updateFlag(i, j, true);

				// 进入下一行搜索
				backtrace(i + 1);

				// 递归出来后,说明这种放置方法不对,取消 将皇后放到该位置
				updateFlag(i, j, false);
				if (isPrintResult) {
					queenPosition[i][0] = 0;
					queenPosition[i][1] = 0;
				}
				chessBoard[j] = 0;
			}
		}
	}

	/**
	 * 打印解法个数 并输出一个解法的皇后的位置
	 */
	private static void printResult() {
		System.out.println(solutionCount);// 输出解法个数
		for (int i = 0; i < size; i++) {
			for (int j = 0; j < 2; j++) {
				System.out.print(queenPosition[i][j] + " ");
			}
			System.out.println();
		}
	}

	/**
	 * 更新皇后站位标识
	 * 
	 * @param i
	 * @param j
	 * @param value
	 */
	private static void updateFlag(int i, int j, boolean value) {
		rowFlag[i] = value;
		columnFlag[j] = value;
		leftToRightFlag[i - j + size - 1] = value;
		rightToLeftFlag[j + i] = value;
	}

	/**
	 * 行和列的标识比较简单。有一点技巧的是斜行的标识。 <br>
	 * 方向(↙):i+j的值是相等的 <br>
	 * 方向(↘):i-j的值是相等的,由于相减会出现负数,所以这组标识的下标表示为:i-j+size-1.这样就可以了
	 * 
	 * @param i
	 * @param j
	 * @return
	 */
	private static boolean isValid(int i, int j) {
		return !rowFlag[i] && !columnFlag[j] && !leftToRightFlag[i - j + size - 1] && !rightToLeftFlag[j + i];
	}
}

      OK!!我是一个算法菜鸟,今天心血来潮,去做了ACM的题,刚好做到这题,其实以前上课的时候已经做过了,但是今天还是耐下性子来,写了写,写完一个总感觉可以优化优化,于是就写了这三陀代码。其实也谈不上什么突破性的优化,请大牛不要鄙视。如有错误,请大家指正;如有更好的解法,求分享~~


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值