java数独回溯法求解(巧用位运算)

数独的基本规则我想大家也都知道,不知道的可以先去百度百科看一下数独的规则。

位运算对于学过离散数学的人应该很熟悉,但是对于没有学过位运算的人可能就会觉得很头疼,而我就属于后者。但是位运算其实也没有很复杂,我在这里也就不再多赘述了,不懂的朋友还是去百度百科上看一下。

对于如下的数独,0表示需要你填的数。

  6 1 0 2 7 0 5 0 9

  8 9 5 6 3 1 7 2 4

  0 4 0 5 8 0 0 1 0

  9 0 8 3 4 0 1 0 0

  0 3 6 0 9 7 2 4 0

  0 7 2 1 5 6 0 3 8

  3 0 4 7 1 5 0 9 0

  5 2 1 9 6 0 4 7 0

  7 6 9 4 2 0 8 5 0

填好之后应该是这样子的

  6 1 3 2 7 4 5 8 9

  8 9 5 6 3 1 7 2 4

  2 4 7 5 8 9 3 1 6

  9 5 8 3 4 2 1 6 7

  1 3 6 8 9 7 2 4 5

  4 7 2 1 5 6 9 3 8

  3 8 4 7 1 5 6 9 2

  5 2 1 9 6 8 4 7 3

  7 6 9 4 2 3 8 5 1

数独的设计一般是先回溯生成一个完整的数独,然后再挖空某一些数字。一般情况下,解是唯一的,当然也不排除挖空太多导致有多种解(极端情况下是挖空所有数字)。

那如何求解呢?暴力枚举可以吗?肯定可以,数字都不大,是可以在有限的时间完成的,但有没有更快的方法呢?当然是回溯法。其实回溯法也是一种枚举,不过是有返回的枚举。回溯法最核心的就在于回溯这两个字上,如何回溯?下面我们在实际代码中来看。加粗和斜体的部分就是有关位运算的操作,位运算只是辅助工具,核心还是回溯法。

import java.util.Scanner;

public class shudu {

    static int count = 0;//记录需要填写的数独的个数

    static int[][] r = new int[81][2];//需要填写的数独的坐标集合

    static int upperlim = (int)Math.pow(2, 9) - 1;//本算法中二进制的上界即0b111111111(0b表示二进制,后面有9个1)

    public static void main(String[] args) {

    // TODO Auto-generated method stub

        Scanner scan = new Scanner(System.in);

        int[][] a = new int[9][9];//数独

        for (int i = 0; i < 9; i++) {

            for (int j = 0; j < 9; j++) {

                a[i][j] = scan.nextInt();

            }

        }

        scan.close();

        int[] row = new int[9];//对于行已经存在的数的二进制

        int[] col = new int[9];//对于已经存在的数的二进制

        int[][] p = new int[3][3];//对于9宫格已经存在的数的二进制

        for (int i = 0; i < 9; i++) {

        for (int j = 0; j < 9; j++) {

            if (a[i][j] == 0) {

                r[count][0] = i;//需要填的数字的横坐标

                r[count][1] = j;//需要填的数字的纵坐标

                count++;//个数

                continue;

            }

            row[i] += (int)Math.pow(2, a[i][j]-1);//第i行中存在数字j的话就让row【i】自增2^(a[i][j]-1),比如上例中第0行就是row[i] = 0b101110011,表示已经存在1,2,5,6,7,9了。下同

            col[j] += (int)Math.pow(2, a[i][j]-1);

            p[i/3][j/3] += (int)Math.pow(2, a[i][j]-1);

        }

        }

        backtrack(a, row, col, p, 0);

    }

public static void backtrack(int[][] a, int[] row, int[] col, int[][] p, int b) {

    if (b == count) {

        display(a);

        return;

    }

    int pos = upperlim & ~(row[r[b][0]] | col[r[b][1]] | p[r[b][0]/3][r[b][1]/3]);

    //r[b][0]和r[b][1]分别表示第b个需要填的数字的横坐标和纵坐标。row|col|p表示该空格处所处的行,列,与9宫格已经存在的数字的并,也就是说该空格不可以填的数字,那么upperlim  & ~(row|col|p)就表示该空格可以填的数字集合。即同一层级的所有可行解。

    while (pos != 0) {  //这个循环表示同一层级的多种选择

        int u = pos & (-pos);//这个位运算的操作有点复杂,其意义是取出一个可行的选择。

        pos -= u;//从所有可行选择中减掉取出的选择

        a[r[b][0]][r[b][1]] = (int)(Math.log(u)/Math.log(2)) + 1;//这行和下面4行表示将取出的可行解作为当前解,并改变相关量

        row[r[b][0]] += u;

        col[r[b][1]] += u;

        p[r[b][0]/3][r[b][1]/3] += u;

        backtrack(a, row, col, p, b+1);//进入下一层递归

        a[r[b][0]][r[b][1]] = 0;//这行和下面4行就是在回溯,即回到原状态,便于同一层级的下一种选择进行操作

        row[r[b][0]] -= u;

        col[r[b][1]] -= u;

        p[r[b][0]/3][r[b][1]/3] -= u;

    }

}

public static void display(int[][] a) {

    //打印二维数组的方法

    for (int i = 0; i < a.length; i++) {

        for (int j = 0; j < a[0].length; j++) {

            System.out.print(a[i][j]+" ");

        }

        System.out.println();

    }

}

}

好啦,自动数独程序就完成啦,是不是发现数独,回溯法以及位运算都没有那么神秘了呢?

你可能感兴趣的:(java数独回溯法求解(巧用位运算))