Solving SUDOKU using PostgreSQL – Part 1

下图的例子,来源于sudoku.com。转换为PG中的记录,0代表数字未确定。

postgres=> create table sudoku(c char(9));   
CREATE TABLE
postgres=> insert into sudoku values('000071042'),('800040000'),('010006905'),('700250000'),('581930064'),('024610573'),('000005090'),('608000407'),('049060000');
INSERT 0 9

postgres=>  select * From (select substr(c,1,3) as c1, substr(c,4,3) as c2, substr(c,7,3) as c3 from sudoku limit 3 offset 0) t union all select '---','---','---' union all select * From (select substr(c,1,3) as c1, substr(c,4,3) as c2, substr(c,7,3) as c3 from sudoku limit 3 offset 3) t union all select '---','---','---' union all select * From (select substr(c,1,3) as c1, substr(c,4,3) as c2, substr(c,7,3) as c3 from sudoku limit 3 offset 6) t ;
 c1  | c2  | c3  
-----+-----+-----
 000 | 071 | 042
 800 | 040 | 000
 010 | 006 | 905
 --- | --- | ---
 700 | 250 | 000
 581 | 930 | 064
 024 | 610 | 573
 --- | --- | ---
 000 | 005 | 090
 608 | 000 | 407
 049 | 060 | 000
(11 rows)

添加grid/row/col属性

原始记录中只有cell中的数字,为了方便后续计算,需要添加grid/row/col属性。grid即9个3*3的方格。

postgres=> -- split to 81 cells with grid/row/col proeperties
postgres=> WITH 
postgres-> -- all numbers as integer array
postgres-> all_arr(x) AS (
postgres(>   SELECT '{1,2,3,4,5,6,7,8,9}' :: integer[]
postgres(> ),
postgres-> 
postgres-> -- all numbers as multiple rows
postgres-> all_num(x) AS (
postgres(>   SELECT unnest(x) from all_arr
postgres(> ),
postgres-> 
postgres-> all_cell(_grid, _row, _col, num) AS (
postgres(>   SELECT ceil(rn / 3.0) * 10 + ceil(t.x / 3.0) as _grid, rn as _row, t.x as _col, substr(s.c, t.x, 1) :: integer as num
postgres(>   FROM (
postgres(>     SELECT c,row_number() over () as rn
postgres(>     FROM sudoku
postgres(>   ) s, all_num t
postgres(> )
postgres-> SELECT * FROM all_cell;
 _grid | _row | _col | num 
-------+------+------+-----
    11 |    1 |    1 |   0
    11 |    2 |    1 |   8
    11 |    3 |    1 |   0

方格找数字

解数独基本有两大方向,一个方向就是找出这个方格中可能的数字,如果经过排除后只剩下一种可能,那么就确定了方格中的数字。比如方格{5, 6},排除掉row的大部分数字以及grid的2,只剩下7可以填写。

    -- find valid numbers in a cell, just filter forbidden numbers of same grid/row/col
    cell_to_num(_row, _col, forbid_arr) AS (
    SELECT y._row, y._col, uniq(array_agg(x.num)) as forbid_arr
    FROM all_cell x
    JOIN all_cell y
    ON x.num != 0 AND y.num = 0
    WHERE x._grid = y._grid
    OR x._row = y._row
    OR x._col = y._col
    GROUP BY y._row, y._col
    )

数字找方格

此外,就是找出数字可能的候选方格,如果一个grid中的候选方格只有一种可能,那么也就确定了数字所在的方格。比如数字9,在二排三列的grid中,只能放在{4, 9}方格中。同理,找出同一个row中的唯一候选,以及同一个column的唯一候选。

    -- find valid cells for a number, only keep records with only one candidate cell for a number
    num_cell(num, _grid, _row, _col) AS (
    SELECT num, _grid, _row, _col
    FROM (
      SELECT all_num.x as num, y._grid, y._row, y._col, case when x._grid = y._grid OR x._row = y._row OR x._col = y._col THEN 0 ELSE 1 end as flag
      FROM all_num
      JOIN all_cell x
      ON all_num.x = x.num
      JOIN all_cell y
      ON y.num = 0
    ) s 
    GROUP BY num, _grid, _row, _col 
    HAVING min(flag) = 1
    ),
    num_to_cell(num, _row, _col) AS (
    SELECT num_cell.num, num_cell._row, num_cell._col 
    FROM num_cell 
    LEFT JOIN (select _grid, num from num_cell group by _grid, num having count(*) = 1) g
    ON num_cell.num = g.num
    AND num_cell._grid = g._grid
    LEFT JOIN (select _row, num from num_cell group by _row, num having count(*) = 1) r
    ON num_cell.num = r.num
    AND num_cell._row = r._row
    LEFT JOIN (select _col, num from num_cell group by _col, num having count(*) = 1) c
    ON num_cell.num = c.num
    AND num_cell._col = c._col
    WHERE g.num is not null OR r.num is not null OR c.num is not null
    )

最终SQL

WITH RECURSIVE sudoku_result(c) AS (
-- original problem
SELECT c :: text
FROM sudoku
 
UNION ALL
 
SELECT string_agg(num :: text, '' order by _col) :: text as c
FROM (
  -- find num using cell_to_num OR num_to_cell
  SELECT _row, _col, case when # valid_arr = 1 then valid_arr[1] when valid_num IS NOT NULL then valid_num else num end as num
  FROM (
 
    -- split to 81 cells with grid/row/col proeperties
    WITH all_cell(_grid, _row, _col, num) AS (
    SELECT ceil(rn / 3.0) * 10 + ceil(t.x / 3.0) as _grid, rn as _row, t.x as _col, substr(s.c, t.x, 1) :: integer as num
    FROM (
      SELECT c,row_number() over () as rn
      FROM sudoku_result
      ) s, all_num t
    ),
 
 
    -- find valid numbers in a cell, just filter forbidden numbers of same grid/row/col
    cell_to_num(_row, _col, forbid_arr) AS (
    SELECT y._row, y._col, uniq(array_agg(x.num)) as forbid_arr
    FROM all_cell x
    JOIN all_cell y
    ON x.num != 0 AND y.num = 0
    WHERE x._grid = y._grid
    OR x._row = y._row
    OR x._col = y._col
    GROUP BY y._row, y._col
    ),
 
 
    -- find valid cells for a number, only keep records with only one candidate cell for a number
    num_cell(num, _grid, _row, _col) AS (
    SELECT num, _grid, _row, _col
    FROM (
      SELECT all_num.x as num, y._grid, y._row, y._col, case when x._grid = y._grid OR x._row = y._row OR x._col = y._col THEN 0 ELSE 1 end as flag
      FROM all_num
      JOIN all_cell x
      ON all_num.x = x.num
      JOIN all_cell y
      ON y.num = 0
    ) s 
    GROUP BY num, _grid, _row, _col 
    HAVING min(flag) = 1
    ),
    num_to_cell(num, _row, _col) AS (
    SELECT num_cell.num, num_cell._row, num_cell._col 
    FROM num_cell 
    LEFT JOIN (select _grid, num from num_cell group by _grid, num having count(*) = 1) g
    ON num_cell.num = g.num
    AND num_cell._grid = g._grid
    LEFT JOIN (select _row, num from num_cell group by _row, num having count(*) = 1) r
    ON num_cell.num = r.num
    AND num_cell._row = r._row
    LEFT JOIN (select _col, num from num_cell group by _col, num having count(*) = 1) c
    ON num_cell.num = c.num
    AND num_cell._col = c._col
    WHERE g.num is not null OR r.num is not null OR c.num is not null
    )
 
 
    -- condition for recursive query terminated. No blank cells.
    SELECT x.*, case when x.num = 0 then all_arr.x - y.forbid_arr end as valid_arr, z.num as valid_num
    FROM all_cell x
    JOIN all_arr
    ON 1=1
    LEFT JOIN cell_to_num y
    ON x._row = y._row
    AND x._col = y._col
    LEFT JOIN num_to_cell z
    ON x._row = z._row
    AND x._col = z._col
    WHERE exists (select * From all_cell where num = 0) 
  ) s1 order by _row
) s2
GROUP BY _row
),
 
-- all numbers as integer array
all_arr(x) AS (
  SELECT '{1,2,3,4,5,6,7,8,9}' :: integer[]
),
 
-- all numbers as multiple rows
all_num(x) AS (
  SELECT unnest(x) from all_arr
)
SELECT * FROM sudoku_result;

最终的结果如下

 c1  | c2  | c3  
-----+-----+-----
 395 | 871 | 642
 862 | 549 | 731
 417 | 326 | 985
 --- | --- | ---
 736 | 254 | 819
 581 | 937 | 264
 924 | 618 | 573
 --- | --- | ---
 273 | 485 | 196
 658 | 193 | 427
 149 | 762 | 358
(11 rows)

发表评论