【算法刷题】解数独

本文为个人解题思路整理,水平有限,有问题欢迎交流

概览

本题已数独问题为背景,要求计算出唯一解,表面是一个暴力深搜和回溯的问题,然而实际上如何优化才是精华所在

难度:中等

核心知识点:DFS(回溯)、状态压缩、位运算

题目来源

力扣:https://leetcode-cn.com/problems/sudoku-solver

题目内容

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法遵循如下规则:

  • 数字 1-9 在每一行只能出现一次。

  • 数字 1-9 在每一列只能出现一次。

  • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

本题满足以下设定:

  • 给定的数独序列只包含数字 1-9 和字符 '.'

  • 你可以假设给定的数独只有唯一解。

  • 给定数独永远是 9x9 形式的。

样例

源数据
img
结果
img

解题思路

  • 基本思路:显然可以使用递归DFS暴力搜索每个可能的结果

    • 若最终所有结果被排除则搜索失败

    • 若所有空白被填充即搜索成功

  • 剪枝:只需要搜索空白点就可以了,那么可以将这些空白点整理出来

  • 剪枝:玩过数独的都知道先从选择少的点下手,这里同理,可以减少搜索的可能路径数量

  • 优化:搜索的时候需要确定当前点可能的数字,而这些数字要求不允许与同行、同列、同块(3*3)相同,那么可以提前将每行、每列、每块已出现的数字存储起来,只需要检查某个点所在的行列块就能知道可选数字,第一想法是存在数组里,

  • 优化:行列快均只允许1-9数字,那么可以用二进制数字表示,第i位为1则代表数字i出现过,为0则代表没出现过,注意二级制高位在右边,比如第一行的二进制是001010100,这样处理带来下面几个好处

    • 每行仅需要一个10位二进制数字表示即可,最大也就2047,总共只需要三个int[9]即可分别存放行、列、块的状态

    • 使用位运算进行数据变更或者检查都极其方便(性能消耗也小)

      如某个目标状态为states,进行以下操作

      • 将第i位设为1state |= 1 << i

      • 将第i位设为0state ^= 1 << i

      • 检查第i位是否为0:(state >> i) % 2 == 0

  • 优化:因为只需要获得唯一解,那么放置一个全部变量用于标记是否找到答案即可。当标记为true的时候即结束所有搜索,不再搜索;若检查了所有可能性,该标记仍未false则证明没有答案

    当然,如果答案不止一个就不能这么处理了

解题思路确定,开始整理解题方案

解题方案

  1. 遍历整个棋盘的每个点,以计算行、列、块的状态,并获取

    • 若该点为.,证明为空白,将这个点存储在列表中

    • 若该点不位.,证明为数字,将相关的行、列、块中标记已出现过这个数字

  2. 开始深度搜索

    1. 获取选择可能性最小的点position,并计算其可能的所有数字

    2. 检查position是否存在,不存在则证明所有空点已填充,修改标记为搜索成功,并结束递归

    3. 用数字i枚举1-9

      1. 检查是否是否允许数字i,不允许则跳过

      2. 修改数据

        1. 将当前搜索的点修改为数字i

        2. 修正与当前相关的行、列、块的状态

        3. 将当前点标记为已填充

      3. 进行下一层搜索

      4. 检查搜索成功的标记,若搜索成功则结束递归

      5. 撤回修改数据

        1. 将当前搜索的点修改为空白.

        2. 修正与当前相关的行、列、块的状态

        3. 将当前点标记为未填充

完整代码

<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="java" cid="n398" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> class DemoBasicApplicationTests {
@Test
void test() {
char[][] board = {
{'5', '3', '.', '.', '7', '.', '.', '.', '.'},
{'6', '.', '.', '1', '9', '5', '.', '.', '.'},
{'.', '9', '8', '.', '.', '.', '.', '6', '.'},
{'8', '.', '.', '.', '6', '.', '.', '.', '3'},
{'4', '.', '.', '8', '.', '.', '.', '.', '1'},
{'7', '.', '.', '.', '2', '.', '.', '.', '6'},
{'.', '6', '.', '.', '.', '.', '2', '8', '.'},
{'.', '.', '.', '4', '1', '9', '.', '.', '5'},
{'.', '.', '.', '.', '8', '.', '.', '7', '9'}
};
solveSudoku(board);
}

public int[] col = new int[9];//行
public int[] row = new int[9];//列
public int[][] block = new int[3][3];//块
List<Integer> list = new ArrayList<>();//空白位列表
boolean flag = false;//标记,用于识别搜索是否成功

/**

  • 解决方案
    /
    public void solveSudoku(char[][] board) {
    //初始化
    init(board);
    //执行dfs搜索
    dfs(board);
    //打印结果
    // System.out.println(flag);
    out(board);
    }

    /
    *
  • 打印board
  • 调试用
  • @param board 目标board
    /
    public void out(char[][] board) {
    for (int i = 0; i < 9; i++) {
    for (int j = 0; j < 9; j++) {
    System.out.print(board[i][j] + " ");
    }
    System.out.println();
    }
    System.out.println();
    }

    /
    *
  • 初始化,填充行、列、块以及空白位列表的值
  • @param board 目标board
    /
    public void init(char[][] board) {
    for (int i = 0; i < 9; i++) {
    for (int j = 0; j < 9; j++) {
    if (board[i][j] != '.') {
    //不为空的时候,更新行、列、块的值
    update(i, j, board);
    } else {
    //为空,则将其添加到空白位列表
    list.add(i * 9 + j);
    }
    }
    }
    }

    /
    *
  • 执行搜索
  • 在排除所有可能后结束搜索,flag标记搜索成功或失败
  • @param board 目标board
    /
    private void dfs(char[][] board) {
    //查询到list中下一个位置
    int nextPosition = getNextPosition();
    //找不到下一个尝试位置,即所有点均填充完成,则结束搜索,并判断为搜索成功
    if (nextPosition < 0) {
    flag = true;
    return;
    }
    //下一个位置的棋盘中的位置
    int next = list.get(nextPosition);
    int state = getState(next);
    //从1开始检索,检索到9
    int i = 0;
    while (++i < 10) {
    //开始尝试
    if ((state >> i) % 2 == 0) {//第i位为0,则证明该位置可能为i
    //更新行列
    board[next / 9][next % 9] = (char) (i + '0');
    list.set(nextPosition, -1);
    update(next / 9, next % 9, board);
    // out();
    // System.out.println("" + next / 9 + " " + next % 9 + " " + i);
    //开始搜索下一个位置
    dfs(board);
    //找到答案,结束搜索
    if (flag) {
    return;
    }
    //未找到答案,撤回修改,继续尝试
    board[next / 9][next % 9] = '.';
    list.set(nextPosition, next);
    update(next / 9, next % 9, i, board);
    }
    }
    }

    /
    *
  • 获取下一个位置
  • @return 下一个位置在list中的位置,若结果为-1则证明没有需要查询的结果
    /
    private int getNextPosition() {
    int position = -1;
    int minNum = -1;
    for (int i = 0; i < list.size(); i++) {
    //忽略被标记为-1的位置
    if (list.get(i) >= 0) {
    //找到可能性最少的位置
    int possibleNum = getPossibleNum(list.get(i));
    if (position < 0 || possibleNum < minNum) {
    minNum = possibleNum;
    position = i;
    }
    }
    }
    return position;
    }

    /
    *
  • 计算可能数字的数量
    /
    private int getPossibleNum(int position) {
    int state = getState(position);
    int num = 0;
    //遍历每个二进制位,若为1则计数器加1
    while (state > 0) {
    num += state & 1;
    state >>= 1;
    }
    return 9 - num;
    }

    /
    *
  • 查询某个位置的状态
    /
    private int getState(int position) {
    int x = position / 9;
    int y = position % 9;
    int state = col[x] | row[y] | block[x / 3][y / 3];
    return state;
    }

    /
    *
  • 更新某个位置的数据
  • @param x 横坐标
  • @param y 纵坐标
  • @param board 棋盘
    /
    private void update(int x, int y, char[][] board) {
    int num = 1 << (board[x][y] - '0');
    col[x] |= num;
    row[y] |= num;
    block[x / 3][y / 3] |= num;
    }

    /
    *
  • 更新某个位置的数据到指定数字
  • @param x 横坐标
  • @param y 纵坐标
  • @param target 目标数字
  • @param board 棋盘
    */
    private void update(int x, int y, int target, char[][] board) {
    int num = 1 << target;
    col[x] ^= num;
    row[y] ^= num;
    block[x / 3][y / 3] ^= num;
    }
    }</pre>

执行结果:

image-20200916163654701

性能:

image-20200916164057937

记得关闭掉打印,否则会影响执行时间

后记

表面深搜,实则优化,提出解决方案并不难,提出优质的解决方案才是我们该追求的


作者:Echo_Ye

WX:Echo_YeZ

Email :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,928评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,748评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,282评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,065评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,101评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,855评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,521评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,414评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,931评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,053评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,191评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,873评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,529评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,074评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,188评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,491评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,173评论 2 357