数独 dfs 剪枝 位运算 保姆注释版 java

算法题解专栏


Acwing166. 数独
数独 dfs 剪枝 位运算 保姆注释版 java_第1张图片

输入

4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end

输出

417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936

剪枝

 优化搜索顺序:优先处理方案数少能尽早出现方案的分支  ⭐(本题使用)
 可行性剪枝:所有非法方案直接跳过不搜
 最优化剪枝:当前结果已经比当前求得最优解糟糕了,继续进行搜索也不会使得结果更好,剪掉
 排除等效冗余:组合和排序的问题,同一方案只搜一次

思路

 一看就知道是暴搜,一看就知道会超时,嗯……
 二进制表示状态,每一位上的 0 1 表示 每一位的使用状态
    0 表示该数位的值已使用,1表示未使用
 lowbit():快速求出某数二进制位的最低数位的 1 (刚好可以作为一种搜索方案)

代码复杂度⭐⭐⭐⭐
思维复杂度⭐⭐

AC code

import java.util.*;

public class Main
{
	static int N = 9;
	static int M = 1 << N;//M表示最大的状态值
	static int[] ones = new int[M];// ones[i] 存 i的二进制 上有多少个 1
	// map[i] 表示 i的二进制 上的 1 对应的是第几数位(从右从0开始数) map[4] = 2; (0000 0100)
	static int[] map = new int[M];
//	状态都用二进制表示(1 表示该位置为空,0表示该位置有数)
//	例:000000110:表示 2,3已经使用了
	static int[] row = new int[N];// 行状态
	static int[] col = new int[N];// 列状态
	static int[][] cell = new int[3][3];// 九宫格的状态
	static char[] ss = new char[81];// 存地图

//	返回二进制 x 中的最低位 1
	static int lowbit(int x)
	{
		return x & -x;
	}

//	isSet == true 即在 (x,y) 上填上 t,否则把(x,y)上的数字去掉
	static void draw(int x, int y, int t, boolean isSet)
	{
		if (isSet)
			ss[x * N + y] = (char) ('1' + t);// 整型转换为字符型
		else
			ss[x * N + y] = '.';// 把(x,y)位置置空

		int v = 1 << t;// 二进制表示状态,1表示空,0表示放了数
		if (!isSet)
			v = -v;// -(-v) = +v 相当于把对应二进制位上的数 从 0 变成 1

		row[x] -= v;
		col[y] -= v;
		cell[x / 3][y / 3] -= v;
	}

//	根据输入初始化数独地图
	static int init()
	{
		int cnt = 0;// 记录总共有多少个空格可以放
		int state = (1 << N) - 1;// 初始状态state的9个二进制位全是1
		Arrays.fill(row, state);
		Arrays.fill(col, state);
		for(int i = 0; i < 3; i++)//九宫格每个都要初始化
		    Arrays.fill(cell[i], state);

		int k = 0;
		for (int i = 0; i < N; i++)
			for (int j = 0; j < N; j++, k++)
				if (ss[k] != '.')
					draw(i, j, ss[k] - '1', true);
				else
					cnt++;
		return cnt;
	}

	public static void main(String[] args)
	{
		Scanner sc = new Scanner(System.in);
		for (int i = 0; i < N; i++)
			map[1 << i] = i;// map[i] 即是 存 log(i) 的值

		for (int i = 0; i < M; i++)
			for (int j = i; j != 0; j -= lowbit(j))
				ones[i] += 1;
		while (sc.hasNext())
		{
			String s = sc.nextLine();
			if (s.equals("end"))
				break;
			ss = s.toCharArray();
			int k = init();
//			System.out.println(String.valueOf(ss));
			dfs(k);
			System.out.println(String.valueOf(ss));
		}
	}

//	返回 x行y列和所在的九宫格可以填哪些数字,1表示可以填
	static int get(int x, int y)
	{
//		1011 & 1001 & 1000 == 1000 ,1 所在的位的校标表示可以填的数值
		return row[x] & col[y] & cell[x / 3][y / 3];
	}

	static boolean dfs(int cnt)
	{
		if (cnt == 0)// 递归出口,填完了所有的空位
			return true;
		int min = 10;// 最少填多少个数字
		int x = 0, y = 0;
		for (int i = 0; i < N; i++)
			for (int j = 0; j < N; j++)
				if (ss[i * N + j] == '.')
				{
//					获取当前位置可以填的数字
					int state = get(i, j);
					// 选一个分支数最小的开始递归搜索(搜索顺序优化)
					if (ones[state] < min)
					{
						min = ones[state];
						x = i;
						y = j;
					}
				}
//		System.out.println("bug");
//		对当前最少分支数的 分支 尝试进行 放合法数
		for (int i = get(x, y); i != 0; i -= lowbit(i))// 每次取 低位1,每次减掉低位1
		{
			int t = map[lowbit(i)];// 取出低位1,映射成所对应可填的 数值
			draw(x, y, t, true);// 填数
			if (dfs(cnt - 1))// 看看能不能成功(成功直接返回)
				return true;
			draw(x, y, t, false);// 失败的尝试,回溯
		}
		return false;// 所有方案都跑完了,没一次成功的则返回false
	}
}

‍ 参考题解

你可能感兴趣的:(算法题解,java,深度优先,剪枝)