返回列表 发帖

[挑战]解数独程序与数独题目生成

本帖最后由 523066680 于 2017-9-24 16:14 编辑

以下是从某网站提取的5种难度的数独游戏(难度从1-5,循着网站是可以找到答案的):
001000008800010532750024090005302060670500004010780900098053406426179803537460219
503092000700000008006007310020600000065000730007043500000706102070000800400009000
700306000000000050060000018000081000000000000900000200000200900050400000080000007
030000001200806000000005000000000000000000650043070000600002080090000000000010003
010000200300800000000504000800090000000070120500000000000000000020130000400000005

写程序对这五个数独题目求解,并计算解每道题的时间消耗。
编程语言:不限

------------------------------------------ 2017-09-18补充 ------------------------------------------
如果解决上面五道题耗时不超过1秒,可以尝试批量解题库中的题,并统计时间消耗。

题库下载:http://523066680.ys168.com/
Perl/数独/数独题库200801-201709_含答案.zip

题库按键值对存储,key 是时间戳,value 中前81个字符是题目,后81个字符是唯一答案。
nd0 - nd4 表示不同的难度。
[url=][/url]

本帖最后由 523066680 于 2017-9-14 16:57 编辑

脚本,每填入一个数字,重新判断并缩小范围,第四道数独花了五百多秒 ……
2,6,1,9,3,5,7,4,8
8,4,9,6,1,7,5,3,2
7,5,3,8,2,4,6,9,1
9,8,5,3,4,2,1,6,7
6,7,2,5,9,1,3,8,4
3,1,4,7,8,6,9,2,5
1,9,8,2,5,3,4,7,6
4,2,6,1,7,9,8,5,3
5,3,7,4,6,8,2,1,9
Game level: 1, time used: 0.000s
8,3,6,9,4,7,5,2,1
2,5,1,8,3,6,4,9,7
4,7,9,1,2,5,8,3,6
9,6,8,2,5,1,3,7,4
1,2,7,4,8,3,6,5,9
5,4,3,6,7,9,2,1,8
6,1,4,3,9,2,7,8,5
3,9,5,7,6,8,1,4,2
7,8,2,5,1,4,9,6,3
Game level: 4, time used: 537.594sCOPY
--------------- 2017-09-14 蜜汁 Trick ----------------
8,3,6,9,4,7,5,2,1
2,5,1,8,3,6,4,9,7
4,7,9,1,2,5,8,3,6
9,6,8,2,5,1,3,7,4
1,2,7,4,8,3,6,5,9
5,4,3,6,7,9,2,1,8
6,1,4,3,9,2,7,8,5
3,9,5,7,6,8,1,4,2
7,8,2,5,1,4,9,6,3
[Finished in 2.2s]COPY
[url=][/url]

TOP

这个用暴力解肯定慢啦...这是我解数独的基本思路,除了第一步,其他的都是嵌套循环的步骤
1、生成草稿表
2、检查纵、横、九宫格内唯一数,填写并从相关单元格的草稿中去除该数
3、根据草稿,排除某九宫格中唯一行或者唯一列对其他行列的影响
4、根据数独“只有唯一解”的规则排除平行对称基本型
5、setlocal,寻找连锁反应最多的点进行暴力解(以只有两种可能性的点为佳)

TOP

本帖最后由 523066680 于 2017-9-14 17:44 编辑

回复 3# CrLf

    也不是完全的暴力跑,前三项规则已经应用了,后两项还没有试过。题目的前两道数独秒出结果,第四道耗时很长。
[url=][/url]

TOP

level 3:3,7,9,6,8,8,26 秒

level 5:52,17,85,64,57,34,82,51,14,35 秒


level 4:503,1402,690 秒

8 3 6  9 4 7  5 2 1
2 5 1  8 3 6  4 9 7
4 7 9  1 2 5  8 3 6

9 6 8  2 5 1  3 7 4
1 2 7  4 8 3  6 5 9
5 4 3  6 7 9  2 1 8

6 1 4  3 9 2  7 8 5
3 9 5  7 6 8  1 4 2
7 8 2  5 1 4  9 6 3


找到了9年前用回溯法解数独的VBS脚本,选择下一个可用时,是随机抽取一个,
所以运算时间不固定,有运气成分在里面.
链接: https://pan.baidu.com/s/1EGOqaIQyPy5yZd0unXLMuA?pwd=sycn

不过,level 5运行10次也在1分钟内,level 4运行了3次,长的达26分钟?
level你是不是写反了.

另外,有个数独的小flash程序,做的挺好的,简单,而且各个格子4个角还可以临时设置候选值.

链接: https://pan.baidu.com/s/1GYg2RMSW7X-1mQQqcw1RHg?pwd=8sjh
1

评分人数

TOP

居然是按字符串存储和处理的,如果用int估计会变快吧.  

TOP

本帖最后由 523066680 于 2017-9-15 09:53 编辑

回复 5# slore

题库在这个网站抓的 免费的在线数独
http://bbs.bathome.net/thread-45401-1-1.html

http://523066680.ys168.com/
Perl/数独/数独题库200801-201709_含答案.zip

难度分为 入门,初级,中级,高级,骨灰级。1楼的题来自每个等级的第一道题目。这个等级划分我也觉得有点问题……  
初级的比如:数独 - 2008年1月6日 - 初级
000600500001007000004000000050002000060000800000001007080900000000500001007000004
比同等级的其他题目明显数字要少,可选数字可能性更多。
[url=][/url]

TOP

Rosettacode 有一段 Perl 代码非常简短,不过这段代码因为简短而牺牲了性能,有很多冗余的循环。
http://rosettacode.org/wiki/Sudoku#Perl
1

评分人数

[url=][/url]

TOP

本帖最后由 523066680 于 2017-9-16 10:13 编辑

初始版本
=info
    解数独 初版
    523066680 2017-09
=cut
use IO::Handle;
use Time::HiRes qw/time/;
STDOUT->autoflush(1);
my @games =
qw/
    001000008800010532750024090005302060670500004010780900098053406426179803537460219
    503092000700000008006007310020600000065000730007043500000706102070000800400009000
    700306000000000050060000018000081000000000000900000200000200900050400000080000007
    030000001200806000000005000000000000000000650043070000600002080090000000000010003
    010000200300800000000504000800090000000070120500000000000000000020130000400000005
/;
our @unsolve;
our $block = [];  #区块引用
my $mat = [[]];
my $level = 0;
my $time_a;
#创建区块引用,以便于操作
make_block_refs( $block, $mat );
while ( $level <= $#games )
{
    #字符串转矩阵
    str_to_mat( $games[$level] , $mat );
    $time_a = time();
    #设置 @unsolve 数组,存储空缺元素的坐标
    set_unsolve_array($mat);
    solve($mat, 0);
   
    print_mat($mat);
    printf "Game level: %d, time used: %.3fs\n\n", $level+1, time() - $time_a;
    $level++;
}
sub solve
{
    my ($mat, $prev, $lv) = @_;
    my @possible;
    my ($row, $col);
    my $current;
    for my $i ( $prev .. $#unsolve )
    {
        ($row, $col) = @{$unsolve[$i]};
        if ( $mat->[$row][$col] == 0 )
        {
            $current = $i;
            @possible = get_possible_num( $mat, $row, $col );
            last;
        }
    }
    if ( not defined $current ) { return 1 }
    else
    {   
        return 0 if ( $#possible < 0 )
    }
    my $res = 0;
    for my $p ( @possible )
    {
        $mat->[ $row ][ $col ] = $p;
        $res = solve($mat, $current+1, $lv+1);
        last if ($res == 1);
    }
    #使对应单元恢复为0 否则影响递归判断
    $mat->[$row][$col] = 0 if ($res == 0) ;
    return $res;
}
sub get_possible_num
{
    our ($block, %hash);
    my ($mat, $row, $col) = @_;
    my %possible = (1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9);
    #排除元素
    for my $n ( 0 .. 8 )
    {
        delete $possible{ $mat->[$n][$col] };
        delete $possible{ $mat->[$row][$n] };
        delete $possible{ ${$block->[$row/3][$col/3][$n]} };
    }
    return sort keys %possible;
}
sub get_possible_num_2
{
    our ($block, %hash);
    my ($mat, $row, $col) = @_;
    my @possible = (0,1,2,3,4,5,6,7,8,9);
    #区块坐标
    my $blockrow = int($row/3);
    my $blockcol = int($col/3);
    #排除元素
    for my $n ( 0 .. 9 )
    {
        $possible[ $mat->[$n][$col] ] = 0;
        $possible[ $mat->[$row][$n] ] = 0;
        $possible[ ${$block->[$blockrow][$blockcol][$n]} ] = 0;
    }
    return grep { $_ } @possible;
}
sub set_unsolve_array
{
    our ( @unsolve );
    my ($mat) = @_;
    @unsolve = ();
    for my $row ( 0..8 )
    {
        for my $col ( 0..8 )
        {
            if ( $mat->[$row][$col] == 0 )
            {
                push @unsolve, [ $row, $col, [get_possible_num( $mat, $row, $col )] ];
            }
        }
    }
    #根据可选数字的数量由少到多排序
    #@unsolve = sort { $#{$a->[2]} <=> $#{$b->[2]} } @unsolve;
}
sub make_block_refs
{
    my ($block, $mat) = @_;
    #将数独的九个宫对应的引用分组保存
    for my $r ( 0..2 )
    {
        for my $c ( 0..2 )
        {
            for my $rr ( $r*3 .. $r*3+2 )
            {
                for my $cc ( $c*3 .. $c*3+2 )
                {
                    push @{ $block->[$r][$c] }, \$mat->[$rr][$cc];
                }
            }
        }
    }
}
sub str_to_mat
{
    my ( $str, $mat ) = @_;
    my $idx = 0;
    for my $row ( 0 .. 8 )
    {
        for my $col ( 0 .. 8 )
        {
            $mat->[$row][$col] = substr( $str, $idx++, 1 );
        }
    }
}
sub print_mat
{
    my ($mat) = @_;
    grep { print join(",", @{$mat->[$_]} ),"\n" } ( 0..8 );
}COPY
Game level: 1, time used: 0.001s
Game level: 2, time used: 0.002s
Game level: 3, time used: 11.373s
Game level: 4, time used: 432.737s
Game level: 5, time used: 24.571s

结果相当难看,有空试试 Dancing Links
[url=][/url]

TOP

本帖最后由 523066680 于 2017-9-18 16:27 编辑

最近换了个方案,遇到难度高的题目时比原来的方案快得多(但还是没有Rubyish @Chinaunix 的代码快)

给定一个题目,判断 1-9 每个数字的全局分布情况(可能性)。
比如1可能分布的情况有6种,测试每一种,把9个1全部填入数独,然后继续测试其他数字的分布。

题目
140968000000000900600057300000000007300000840000196500070000405406010030000000000

数字1的可能分布情况
140968000000001900600057310010000007300000841000196500071000405406010030000000100
140968000000001900600057310000000107310000840000196500071000405406010030000000001
140968000000001900600057301000000107310000840000196500071000405406010030000000010
140968000000001900600057301000000107310000840000196500070000415406010030001000000
140968000000001900600057301000000107301000840000196500070000415406010030010000000
140968000000001900600057301000000017310000840000196500071000405406010030000000100

完整代码:
=info
    方案 - 遍历数字分布情况
    523066680 2017-09
=cut
use File::Slurp;
use IO::Handle;
STDOUT->autoflush(1);
# my $gamedata = eval read_file("../sudoku_nd1.txt");
# grep { push @games, $gamedata->{$_} } sort keys %$gamedata;
@games =
qw/
    001000008800010532750024090005302060670500004010780900098053406426179803537460219
    503092000700000008006007310020600000065000730007043500000706102070000800400009000
    700306000000000050060000018000081000000000000900000200000200900050400000080000007
    030000001200806000000005000000000000000000650043070000600002080090000000000010003
    010000200300800000000504000800090000000070120500000000000000000020130000400000005
/;
our @order;
our $answer_mat = [[]];
my $mat;
my $res;
my $allcase;
my $answer_str;
#坐标对应区块位置的表
our @blk = map { int($_ / 3) } ( 0..8 );
while ( $index <= $#games )
{
    $mat = [[]];
    $time_a = time();
    str_to_mat( $games[$index] , $mat );
    $allcase = [ map { [] } (0..9) ];
    grep { possible_mat($mat, $_, $allcase->[$_], 0) } (1..9);
    @order = sort { $#{$allcase->[$a]} <=> $#{$allcase->[$b]} } ( 0 .. 9 );
    $res = recursive( $mat, 1 ); #起点为1,下标 0占位
    if ($res == 1)
    {
        print_mat_inline( $answer_mat );
    }
    else { die "false\n" }
    printf "Game index: %d, time used: %.3fs\n\n", $index, time() - $time_a;
    $index++;
}
sub recursive
{
    our (@order, $answer_mat);
   
    my ( $mat, $lv ) = @_;
    my @case;
    if ( $lv > 9 )
    {
        $answer_mat = $mat;
        return 1;
    }
    $target = $order[$lv];
    possible_mat($mat, $target, \@case, 0);
    my $t_mat = [[]];
    my $res = 0;
    for my $s ( @case )
    {
        str_to_mat( $s, $t_mat );
        $res = recursive( $t_mat, $lv+1 );
        last if ($res == 1);
    }
    return $res;
}
sub possible_mat
{
    my ( $mat, $target, $aref, $lv ) = @_;
    # level means row
    my $str;
    if ($lv == 9)
    {
        $count++;
        push @$aref, mat_to_str($mat);
        return 1;
    }
    my @cols = get_possible_column( $mat, $lv, $target );
    my $res = 0;
    my $ever;
    for my $c ( @cols )
    {
        $ever = $mat->[$lv][$c];
        $mat->[$lv][$c] = $target;
        $res = possible_mat( $mat, $target, $aref, $lv+1 );
        $mat->[$lv][$c] = $ever;
    }
    return $res;
}
sub get_possible_column
{
    our @blk;
    my ( $mat, $row, $target ) = @_;
    my @cols = ( 0..8 );
    for my $c ( 0..8 )
    {
        #如果当前行已经存在这个数字,则直接返回这个数字的位置。
        if ( $mat->[$row][$c] == $target )
        {
            return ( $c );
        }
        elsif ( $mat->[$row][$c] != 0 )
        {
            $cols[$c] = -1;
        }
        for my $r ( 0..8 )
        {
            if ( $mat->[$r][$c] == $target )
            {
                $cols[$c] = -1;
                if ( $blk[$r] == $blk[$row] )
                {
                    $cols[ $blk[$c] * 3 + 0] = -1;
                    $cols[ $blk[$c] * 3 + 1] = -1;
                    $cols[ $blk[$c] * 3 + 2] = -1;
                }
            }
        }
    }
    return grep { $_ != -1 } @cols;
}
sub str_to_mat
{
    my ( $str, $mat ) = @_;
    my $idx = 0;
    for my $row ( 0 .. 8 )
    {
        for my $col ( 0 .. 8 )
        {
            $mat->[$row][$col] = substr( $str, $idx++, 1 );
        }
    }
}
sub mat_to_str
{
    my ( $mat ) = @_;
    return join("", map { join("", @{$mat->[$_]} ) } (0..8));
}
sub print_mat
{
    my ($mat) = @_;
    grep { print join(",", @{$mat->[$_]} ),"\n" } ( 0..8 );
}
sub print_mat_inline
{
    my ($mat) = @_;
    grep { print join("", @{$mat->[$_]} ),"" } ( 0..8 );
    print "\n";
}COPY
261935748849617532753824691985342167672591384314786925198253476426179853537468219
Game index: 0, time used: 0.000s

513892467749361258286457319324675981165928734897143526958736142672514893431289675
Game index: 1, time used: 0.000s

718356492492817653563924718624781539835692174971543286147238965356479821289165347
Game index: 2, time used: 0.000s

836947521251836497479125836968251374127483659543679218614392785395768142782514963
Game index: 3, time used: 0.000s

718963254354827961296514873872391546943675128561248739187456392625139487439782615
Game index: 4, time used: 1.000s


跑完 sudoku_nd0.txt 三千多道题需要十多秒,跑完 sudoku_nd4.txt 大约六百秒。
CU Rubyish 的代码,跑完 nd0 零点几秒,nd4 一百多秒
[url=][/url]

TOP

旋转90度,180度等,开4个进程并行跑,应该会某个路线快些,只用把初始数据按规律变换即可。

再有,4个线程的某单元格的候选项目共享,更快得出唯一解。

再有,改变搜索前进方向,上面旋转数独盘,左右,上下,4个方向同时了,还有顺时旋转前进,逆时针方向,先斜线,先小格等方向,可以开更多线程。

TOP

回复 11# slore

    线程的方案留到最后,单线程仍然有更快的算法,快到解 sudoku_nd4.txt 所有题只要十多秒。
[url=][/url]

TOP

回复 12# 523066680


单纯考虑算法的话,肯定有更优的,但是 多线程对于现在大内存,多核CPU才是更好的利用,而且很多问题都可以采用这个方法来提高。
至于探索路径,不一定非要0~81这个顺序,可以把每个格子的候选项,从小到大排列,按这个数据填,有大概率先填入正确的数据,可以减少尝试次数吧。那天有时间了ruby写下试试。

TOP

本帖最后由 slore 于 2017-9-20 13:03 编辑
class Soduku
  def initialize(str)
    @matrix = 9.times.map{[0]*9}
    str.reverse!       <-只加了一句,LEVEL4立马难度变低了
    nums = str.split('').map(&:to_i)
    ...
  endCOPY
[3, 0, 0, 0, 1, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 9, 0]
[0, 8, 0, 2, 0, 0, 0, 0, 6]
[0, 0, 0, 0, 7, 0, 3, 4, 0]
[0, 5, 6, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 5, 0, 0, 0, 0, 0]
[0, 0, 0, 6, 0, 8, 0, 0, 2]
[1, 0, 0, 0, 0, 0, 0, 3, 0]
5.069903 秒
[3, 6, 9, 4, 1, 5, 2, 8, 7]
[2, 4, 1, 8, 6, 7, 5, 9, 3]
[5, 8, 7, 2, 9, 3, 4, 1, 6]
[8, 1, 2, 9, 7, 6, 3, 4, 5]
[9, 5, 6, 3, 8, 4, 7, 2, 1]
[4, 7, 3, 1, 5, 2, 8, 6, 9]
[6, 3, 8, 5, 2, 1, 9, 7, 4]
[7, 9, 4, 6, 3, 8, 1, 5, 2]
[1, 2, 5, 7, 4, 9, 6, 3, 8]

TOP

至于探索路径,不一定非要0~81这个顺序,可以把每个格子的候选项,从小到大排列,按这个数据填,有大概率先填入正确的数据,可以减少尝试次数吧。


错了,如果按候选项探索的话,比如, [0,0]点可以用5和4,[8,8]可以用1,3,
都是最小的2个候选,但是填写0,0,在填8,8,变成了散点,如果50%概率选错了的话,
其他格子,无效探索反而变得多了,填写的多,但是排除无效值的情报却少了。

探索改成下面这样。

先选择出有最少候选项的位置,然后对它相同X,Y和Z区域的 格子 权值-1。
(假设最小的这个格子我们填入了一个数字,被影响的其他格子,候选项-1)
然后,被影响的格子里面,权值最低的(候选项-1后,最小的),
是我们接下来优先探索的,同理,整理出,每次填入一个单元格,
受其影响的,候选项最少的格的探索路径。这样填入数据之间有关联,
如果填入了错误值,可以更早地回溯。

VBS代码移植为ruby脚本的,只在处理前,修改这1处,从几百秒到稳定10秒LEVEL4的结果输出,
很大的改善方案。

ans = candidate(x, y)
return false if ans.size <= 0

v = ans[0]  #取出候选的第一个
ans.delete v
@matrix[x][y] = v

改成, v = ans.sample #候选中随机抽1个,LEVEL4的结果,2秒~18秒之间
#魔法CODE
str.reserse!
v = ans.sampleCOPY
    sort_seq = []
    while
      sort_seq.push @solve_seq[min_seq]
      x = @solve_seq[min_seq][0]
      y = @solve_seq[min_seq][1]
      z = ((x / 3) * 3) + (y / 3)
      @solve_seq.delete_at(min_seq)
      break if @solve_seq.size == 0
      min = 10
      min_seq = -1
      @solve_seq.each_with_index do |seq, i|
        zz = ((seq[0] / 3) * 3) + (seq[1] / 3)
        if seq[0] == x || seq[1] == y || zz == z
          seq[2] -= 1
          if seq[2] < min
            min = seq[2]
            min_seq = i
          end
        end
      end
    end
    p sort_seq
    @solve_seq = sort_seqCOPY

TOP

返回列表