15. 回溯(Back Tracking)

1 回溯
  1. 回溯可以理解为:通过选择不同的岔路口来通往目的地(找到想要的结果)
    1. 每一步都选择一条路出发,能进则进,不能进则退回上一步(回溯),换一条路再试
    2. 树、图的深度优先搜索(DFS)、八皇后、走迷宫都是典型的回溯应用
  2. 从图中不难看出来,回溯很适合使用递归,因为4做的事和根节点7做的事完全是一模一样的
    在这里插入图片描述
2 练习 – 八皇后问题(Eight Queens)
  1. 在8x8格的国际象棋上摆放八个皇后,使其不能互相攻击:任意两个皇后都不能处于同一行、同一列、同一斜线上。请问有多少种摆法。
  2. 剪枝
    在这里插入图片描述
  3. 回溯
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  4. Queens:使用int[] cols表示几行几列已经拜访了皇后,数组索引为行号,元素为列号
package com.mj;

public class Queens {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new Queens().placeQueens(4);
	}
	
	/**
	 * 数组索引是行号,数组元素是列号
	 */
	int[] cols;
	/**
	 * 一共有多少种摆法
	 */
	int ways;
	
	void placeQueens(int n) {
		if (n < 1) return;
		cols = new int[n];
		place(0);
		System.out.println(n + "皇后一共有" + ways + "种摆法");
	}
	
	/**
	 * 从第row行开始,摆放皇后
	 * @param row
	 */
	void place(int row) {
		//如果第八行都摆完了(cols[7]=xxx),总摆法+1,并且打印当前摆法
		if (row == cols.length) {
			ways++;
			show();
			return;
		}
		
		for (int col = 0; col < cols.length; col++) {
			//剪枝
			if (isValid(row, col)) {
				// 在第row行第col列摆放皇后
				cols[row] = col;
				//回溯
				place(row + 1);
			}
		}
	}
	
	/**
	 * 判断第row行第col列是否可以摆放皇后
	 */
	boolean isValid(int row, int col) {
		for (int i = 0; i < row; i++) {
			// 如果当前列号,与之前摆放完的皇后所在的列号相同,说明不能摆放
			if (cols[i] == col) {
				//这几行打印是为了查看回溯和剪枝的整个流程
				System.out.println("[" + row + "][" + col + "]=false");
				return false;
			}
			// 如果在同一条斜线上,斜率要么是1要么是-1,两点之间斜率 = (y2 - y1)/(x2-x1)
			// 第i行的皇后跟第row行第col列格子处在同一斜线上
			if (row - i == Math.abs(col - cols[i])) {
				System.out.println("[" + row + "][" + col + "]=false");
				return false;
			}
		}
		System.out.println("[" + row + "][" + col + "]=true");
		return true;
	}
	void show() {
		for (int row = 0; row < cols.length; row++) {
			for (int col = 0; col < cols.length; col++) {
				if (cols[row] == col) {
					System.out.print("1 ");
				} else {
					System.out.print("0 ");
				}
			}
			System.out.println();
		}
		System.out.println("------------------------------");
	}
}

  1. Queens2:改为使用boolean[] cols标记哪列拜访了皇后、boolean[] leftTop与boolean[] rightTop表示哪条斜线保存了皇后
package com.mj;

public class Queens2 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new Queens2().placeQueens(4);
	}
	
	/**
	 * 数组索引是行号,数组元素是列号,只是为了打印才添加的变量
	 */
	int[] queens;
	/**
	 * 标记着某一列是否有皇后
	 */
	boolean[] cols;
	/**
	 * 标记着某一斜线上是否有皇后(左上角 -> 右下角),每个方向2n-1条线
	 * 根据一元一次方程的性质,我们应该知道,对于同处于一条从左向右的斜线上的x和y,应满足如下方程
	 * y=-x、y=-x+1、y=-x+2、y=-x+3,等等。
	 * 我们现在的想法就是根据y和x的相关运算,得到一个数组下标值,来表示这条斜线,所以自然而然考虑到将x和y放到一边
	 * 也就是y+x=0、y+x=1、y+x=2...,现在可以看出,不同的斜线,对于x+y值不同,所以我们可以考虑使用不同x+y的值来对照不同的数组下标值
	 * 此时我们很容易发现想到,可以将y+x的值,作为数组的下标,当下标为0,就表示y+x=0这条斜线,下标为1,就表示y+x=1这条斜线
	 * 因此在减枝判断时,只要查看leftTop数组中,以y+x的值为下标的元素是否为true,就可以判断该条斜线上是否已经摆放了皇后
	 */
	boolean[] leftTop;
	/**
	 * 标记着某一斜线上是否有皇后(右上角 -> 左下角),每个方向2n-1条线
	 * 根据一元一次方程的性质,我们应该知道,对于同处于一条从左向右的斜线上的x和y,应满足如下方程
	 * y=x-7、y=x-6、y=x-5...y=x+7,此时我们还是和之前想法相同,利用y和x的关系,得到数组下标,从而表示不同斜线
	 * 得到y-x=-7、y-x=-6...y-x=7,由于数组下标没有负数,因此考虑大家都加7,这样得到y-x+7=0、y-x+7=1、y-x+7=2等等
	 * 因此可以通过y-x+7就能得到索引值,从而查看数组中该元素是否为true,从而判断该条斜线是否已经摆放了皇后
	 * 其实完全可以在这使用一个map,里面记录着Map<int,boolean>,int值从-7开始,到7,这样,每次map中以新元素的y-x的值,作为key的value是否为true,来判断该斜线上是否摆放了皇后
	 */
	boolean[] rightTop;
	/**
	 * 一共有多少种摆法
	 */
	int ways;
	
	void placeQueens(int n) {
		if (n < 1) return;
		queens = new int[n];
		cols = new boolean[n];
		//有2n-1条斜线
		leftTop = new boolean[(n << 1) - 1];
		rightTop = new boolean[leftTop.length];
		place(0);
		System.out.println(n + "皇后一共有" + ways + "种摆法");
	}
	
	/**
	 * 从第row行开始摆放皇后
	 * @param row
	 */
	void place(int row) {
		if (row == cols.length) {
			ways++;
			show();
			return;
		}
		
		for (int col = 0; col < cols.length; col++) {
			//如果当前列已经摆过了
			if (cols[col]) continue;
			//计算当前位置所在斜线的索引
			int ltIndex = row - col + cols.length - 1;
			//如果斜线上已经摆过了
			if (leftTop[ltIndex]) continue;
			int rtIndex = row +col;
			//如果斜线上已经摆过了
			if (rightTop[rtIndex]) continue;
			//只是为了打印才添加的变量,该变量不需还原
			queens[row] = col;
			cols[col] = true;
			leftTop[ltIndex] = true;
			rightTop[rtIndex] = true;
			place(row + 1);
			
			//需要对之前的更改进行还原,否则回溯时,会影响下一步的剪枝判断
			//注意:之前代码不需要还原,因为会有新的列号覆盖原来的设置,且我们判断是否需要剪枝时,只循环到i<=row,因此不还原也不会影响剪枝判断
			cols[col] = false;
			leftTop[ltIndex] = false;
			rightTop[rtIndex] = false;
		}
	}
	
	void show() {
		for (int row = 0; row < cols.length; row++) {
			for (int col = 0; col < cols.length; col++) {
				if (queens[row] == col) {
					System.out.print("1 ");
				} else {
					System.out.print("0 ");
				}
			}
			System.out.println();
		}
		System.out.println("------------------------------");
	}
}

  1. Queens3:当boolean数组不是很长时,使用byte cols、short leftTop、short rightTop来替代boolean数组,使用位运算来表示某列、斜线上,是否摆放了皇后。如果boolean数组非常长,那么也可以使用long[]来替代,其中一个long元素,代替boolean数组中的32个元素
package com.mj;

public class Queens3 {

	public static void main(String[] args) {
		// 01111101 n
		//&11111011  ~00000100
		// 01111001
		
		
		//&00100000 v
//		int n = 125;
//		for (int i = 0; i < 8; i++) {
//			int result = n & (1 << i);
//			System.out.println(i + "_" + (result != 0));
//		}
//		int col = 7;
//		int result = n & (1 << col);
//		System.out.println(result != 0);
		
//		System.out.println(Integer.toBinaryString(n));
		
		// TODO Auto-generated method stub
		new Queens3().place8Queens();
	}
	
	/**
	 * 数组索引是行号,数组元素是列号,00000001,表示第0列有皇后
	 */
	int[] queens;
	/**
	 * 标记着某一列是否有皇后:byte有八位,替代原来的boolean数组,1表示该列有值,0表示该列无值
	 */
	byte cols;
	/**
	 * 标记着某一斜线上是否有皇后(左上角 -> 右下角):8皇后,15条斜线,所以16个字节的short来存放它
	 */
	short leftTop;
	/**
	 * 标记着某一斜线上是否有皇后(右上角 -> 左下角)
	 */
	short rightTop;
	/**
	 * 一共有多少种摆法
	 */
	int ways;
	
	void place8Queens() {
		queens = new int[8];
		place(0);
		System.out.println("8皇后一共有" + ways + "种摆法");
	}
	
	/**
	 * 从第row行开始摆放皇后
	 * @param row
	 */
	void place(int row) {
		if (row == 8) {
			ways++;
			show();
			return;
		}
		
		for (int col = 0; col < 8; col++) {
			int cv = 1 << col;
			//00001011表示第0、1、3列摆放了皇后,想取到第三列位置是否为1,只需要00001011 & 00001000,看结果是否不为0即可,而00001000其实就是“1<<列号“
			//判断当前列是否有皇后,cols & (1<<col)就表示判断cols对应的二进制码中,倒数第col个数字是否为1,也就是判断是否有皇后
			if ((cols & cv) != 0) continue;
			
			int lv = 1 << (row - col + 7);
			if ((leftTop & lv) != 0) continue;
			
			int rv = 1 << (row + col);
			if ((rightTop & rv) != 0) continue;
			
			queens[row] = col;
			//使用与运算,将第col列设置为1,也就是将cols中,倒数第col-1位,设置为1
			//01000001 | 00000010 = 01000011
			cols |= cv;
			leftTop |= lv;
			rightTop |= rv;
			place(row + 1);
			//将第col列重新改为0
			//01000011 & 11111101 = 01000001
			//11111101就是00000010取反
			cols &= ~cv;
			leftTop &= ~lv;
			rightTop &= ~rv;
		}
	}
	
	void show() {
		for (int row = 0; row < 8; row++) {
			for (int col = 0; col < 8; col++) {
				if (queens[row] == col) {
					System.out.print("1 ");
				} else {
					System.out.print("0 ");
				}
			}
			System.out.println();
		}
		System.out.println("------------------------------");
	}
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值