当前位置 博文首页 > Janbar:暴力破解数独+舞蹈链算法解数独

    Janbar:暴力破解数独+舞蹈链算法解数独

    作者:[db:作者] 时间:2021-07-05 16:24

    1. 求解数独的思路
      我想通过自己的思路来求解,虽然网上肯定有非常巧妙高效的解法。因此我安装了HoDoKu这个软件,这个软件会分析当前数独每个待填格子可能存在的值,目前我发现Naked Or Hiden Single这2中是最容易找出来的,找出来了该位置就必填那个数。下图是一个例子,表示裸露的单个数字,该位置只有一种可能值。经过仔细研究,我得出了2个原则:
      1.当前位置只有一种可能值,则优先填入
      2.当前位置的可能值在当前行列宫格唯一,那么这个值是隐藏的单个,也是必填的
      该位置必填的数
      有了上述2个原则,那么我必须有一种算法计算每一个待填单元格可能填入的数据。其实很简单,只需要遍历这些代填的位置,然后变量当前行列所在宫格,去掉已经确定的值,剩下的就是代填值。

      经过上面的计算也只能将代填位置确认值填好,但是剩下有可能存在多个值且无法确定。因此我首先想到的就是暴力破解法,假设代填位置为其中一个可能值,由此继续填数字,每次填入数字后再进行一次上面找以确定单个数,如果无法继续,或者得到一个存在某个位置没有可能填入数据则说明假设出错,恢复上一次保存的状态,继续假设下一个可能值。具体流程图如下:

      下面就贴上我的代码,其中保存状态用了栈结构,每次缓存则压栈,恢复则弹栈:

    package main
    
    import (
        "container/list"
        "fmt"
        "log"
    
        "time"
    
        "io/ioutil"
    
        "flag"
    
        "github.com/jan-bar/golibs"
    )
    
    const Length = 9 /* 数独长宽都是9 */
    
    /**
    * 下面这个结构有点复杂
    * num:  当前位置数据,包括初始值,已经填写的值
    * cnt:  标识该位置可能数的个数
    * flag: 初始时和num相同,只是在结果打印时区别初始值和计算得到值颜色
    * may:  该数组记录当前位置可能值,总是从数组头开始
    **/
    type MySudokuData struct {
        num, cnt, flag int         /* 点位具体值,可能值的个数,该位置需要填值 */
        may            [Length]int /* 记录点位可能的值 */
    }
    
    /**
    * 下面结构保存存在多个可能值的位置
    * pos:  记录可能值的坐标(其中i表示多少行,j表示多少列)
    * cnt:  记录这些坐标个数
    **/
    type MyMayPos struct {
        pos [Length * Length]struct {
            i, j int /* 缓存待定位置i,j值 */
        }
        cnt int /* 待定位置个数 */
    }
    
    /**
    * 总体的数据结构
    * data:  记录9*9的81个点位数据
    * pos:   表示可能值的数据
    * dot:   在计算时表示当前假设到哪个可能点
    * may:   在计算时表示dot的点找到哪个可能值
    **/
    type MyCacheData struct {
        data     [Length][Length]MySudokuData /* 缓存整个数独 */
        pos      MyMayPos                     /* 缓存当前可能位置 */
        dot, may int                          /* 缓存第几个可能点,和该点第几个可能值 */
    }
    
    var SudokuData MyCacheData /* 得到数独数据,和每个空位可能值,用于计算 */
    
    func init() {
        fr := flag.String("f", "Sudoku.txt", "input data file!")
        flag.Parse()
    
        byt, err := ioutil.ReadFile(*fr)
        if err != nil {
            log.Fatal(err.Error())
        }
    
        var i, j, cnt, tmp int
        for _, v := range byt {
            if tmp = int(v - '0'); tmp >= 0 && tmp <= 9 { /* 只处理文件中数字0~9 */
                SudokuData.data[i][j].num = tmp
                SudokuData.data[i][j].flag = tmp
    
                if cnt++; j < 8 {
                    j++
                } else {
                    i++
                    j = 0
                }
            }
        }
    
        if cnt != 81 { /* 无论如何必须要有81个输入 */
            log.Fatal("输入文件不正确!")
        }
    }
    
    /**
    * 主程序入口
    * http://aperiodic.net/phil/scala/s-99/
    **/
    func main() {
        var (
            pos, may, x, y, cnt int
            CacheData           = list.New()  /* 缓存数据栈 */
            TmpElement          *list.Element /* 缓存链表元素 */
            tStart              = time.Now()  /* 开始时间 */
        )
    
        FlushMayNum()                 /* 初始刷新一下可能值 */
        for false == GameComplete() { /* 如果没有完成则一直继续计算 */
            for ; pos < SudokuData.pos.cnt; pos++ { /* 遍历可能点 */
                x, y = SudokuData.pos.pos[pos].i, SudokuData.pos.pos[pos].j
                for ; may < SudokuData.data[x][y].cnt; may++ { /* 遍历可能点中可能填写的值 */
                    SudokuData.dot, SudokuData.may = pos, may
                    CacheData.PushFront(SudokuData) /* 保存当前状态到栈中 */
    
                    SudokuData.data[x][y].num = SudokuData.data[x][y].may[may] /* 数据中填写可能值 */
                    cnt++
                    if FlushMayNum() { /* 进行一次寻找,返回true表示还能继续找 */
                        pos, may = 0, 0
                        goto NextGameLoop /* 数据已经重排,所以要重新遍历 */
                    } /* 下面是else部分 */
    
                    /* 如果找到了一个没有可能值的位置,从栈顶取数据,从下一个值开始遍历 */
                    if TmpElement = CacheData.Front(); TmpElement == nil { /* 取栈顶元素,计算下一个可能值 */
                        return /* 栈中没有数据,无解 */
                    }
                    SudokuData = TmpElement.Value.(MyCacheData) /* 恢复上次状态 */
                    CacheData.Remove(TmpElement)                /* 移除栈顶状态 */
                }
            }
    
            /* 下面表示通过上面的计算,把所有可能点的可能值遍历,还是无法得到结果 */
            if TmpElement = CacheData.Front(); TmpElement == nil { /* 取栈顶元素,计算下一个可能值 */
                return /* 栈中没有数据,无解 */
            }
                SudokuData = TmpElement.Value.(MyCacheData) /* 恢复上次状态 */
                CacheData.Remove(TmpElement)                /* 移除栈顶状态 */
                pos, may = SudokuData.dot, SudokuData.may+1 /* may从下一个开始 */
        NextGameLoop: /* 重排的数据继续计算 */
        }
    
        fmt.Println("计算耗时 :", time.Since(tStart))
        PrintSudoku() /* 完成后打印数独 */
        fmt.Scanln()  /* 避免一闪而逝 */
    }
    
    /**
    * x横坐标,向下递增
    * y纵坐标,向右递增
    * 如果运行过程中有空位只有唯一值,那么填好值,再刷新一次
    * 该方法结束后,空位一定存在多个可能值
    * 返回false表示有位置无解,返回true表示所有位置都有多个解
    **/
    func FlushMayNum() bool {
        var i, j, k, t, x, y, tmpMay, flagBreak, xS, xE, yS, yE int
    
    StartLoop: /* 如果结果中有唯一值的位置,则重新计算 */
        SudokuData.pos.cnt = 0 /* 待定位置从0计数 */
        for i = 0; i < Length; i++ {
            for j = 0; j < Length; j++ {
                if 0 == SudokuData.data[i][j].num { /* 空位才需要刷新可能值 */
                    for k = 0; k < Length; k++ {
                        SudokuData.data[i][j].may[k] = k + 1 /* 为可能值赋初值 */
                    } /* 初始i,j位置默认可能存在的数值 */
    
                    for k = 0; k < Length; k++ {
                        if t = SudokuData.data[i][k].num; t > 0 { /* 遍历行 */
                            SudokuData.data[i][j].may[t-1] = 0 /* 从可能中剔除该数字 */
                        }
                        if t = SudokuData.data[k][j].num; t > 0 { /* 遍历列 */
                            SudokuData.data[i][j].may[t-1] = 0 /* 从可能中剔除该数字 */
                        }
                    } /* 上面循环剔除行列的值 */
    
                    xS = i / 3 * 3 /* 所在宫格x起始 */
                    xE = xS + 3    /* 所在宫格x结束 */
                    yS = j / 3 * 3 /* 所在宫格y起始 */
                    yE = yS + 3    /* 所在宫格y结束 */
                    for ; xS < xE; xS++ {
                        for k = yS; k < yE; k++ {
                            if t = SudokuData.data[xS][k].num; t > 0 {
                                SudokuData.data[i][j].may[t-1] = 0 /* 从可能中剔除该数字 */
                            }
                        }
                    } /* 上面双层循环遍历所在宫格 */
    
                    /* 下面将可用值左移,保证有效值从数组头开始 */
                    for k, SudokuData.data[i][j].cnt = 0, 0; k < Length; k++ {
                        if t = SudokuData.data[i][j].may[k]; t > 0 {
                            SudokuData.data[i][j].may[SudokuData.data[i][j].cnt] = t
                            SudokuData.data[i][j].cnt++ /* 将可能的值移动到前面 */
                        }
                    }
    
                    if 0 == SudokuData.data[i][j].cnt {
                        return false /* 该位置没有解 */
                    }
    
                    if 1 == SudokuData.data[i][j].cnt { /* 如果当前位置只有一种可能值 */
                        SudokuData.data[i][j].num = SudokuData.data[i][j].may[0] /* 将该值填入数组中 */
                        goto StartLoop                                           /* 重新刷新可能值数据 */
                    }
    
                    /* 下面用插入排序发将每个点可能的个数从小到大添加到MayPos中 */
                    //for k = 0; k < SudokuData.pos.cnt; k++ {
                    //  if SudokuData.data[i][j].cnt < SudokuData.data[SudokuData.pos.pos[k].i][SudokuData.pos.pos[k].j].cnt {
                    //      break /* 找到位置,由小到达的排序,可以让循环次数减少 */
                    //  }
                    //}
                    //for t = SudokuData.pos.cnt; t > k; t-- { /* 上面找到位置,该位置右边数据集体右移一位 */
                    //  SudokuData.pos.pos[t].i, SudokuData.pos.pos[t].j = SudokuData.pos.pos[t-1].i, SudokuData.pos.pos[t-1].j
                    //}
                    //SudokuData.pos.pos[k].i, SudokuData.pos.pos[k].j = i, j
                    //SudokuData.pos.cnt++ /* 可能点个数加1 */
                    SudokuData.pos.pos[SudokuData.pos.cnt].i, SudokuData.pos.pos[SudokuData.pos.cnt].j = i, j
                    SudokuData.pos.cnt++ /* 可能点个数加1 */
                } /* end if 0 == SudokuData[i][j].num { */
            } /* end j */
        } /* end i */
    
        flagBreak = 0
        /* 上面得到一个局面,及可能点一定有多个值,下面找隐藏的只有一个解的位置 */
        for i = 0; i < SudokuData.pos.cnt; i++ { /* 遍历每个可能点位置 */
            x, y = SudokuData.pos.pos[i].i, SudokuData.pos.pos[i].j /* 得到该点位置 */
            for j = 0; j < SudokuData.data[x][y].cnt; j++ {
                tmpMay = SudokuData.data[x][y].may[j] /* 找这个可能值,看看是否为隐藏单个 */
    
                for k = 0; k < Length; k++ {
                    if t = SudokuData.data[x][k].num; t == 0 { /* 遍历行中不确定格子 */
                        for ; t < SudokuData.data[x][k].cnt; t++ {
                            if tmpMay == SudokuData.data[x][k].may[t] {
                                goto NextFlagX /* 这个可能值和在当前行不唯一 */
                            }
                        }
                    }
                } /* 在行上找相同可能值 */
                SudokuData.data[x][y].num = tmpMay /* 这个值在行上可能值中是唯一,填值并重新填值 */
                flagBreak = 1
                break
    
            NextFlagX:
                for k = 0; k < Length; k++ {
                    if t = SudokuData.data[k][y].num; t == 0 { /* 遍历列中不确定格子 */
                        for ; t < SudokuData.data[k][y].cnt; t++ {
                            if tmpMay == SudokuData.data[k][y].may[t] {
                                goto NextFlagY /* 这个可能值和在当前列不唯一 */
                            }
                        }
                    }
                } /* 在列上找相同可能值 */
                SudokuData.data[x][y].num = tmpMay /* 这个值在行上可能值中是唯一,填值并重新填值 */
                flagBreak = 1
                break
    
            NextFlagY:
                xS = x / 3 * 3 /* 所在宫格x起始 */
                xE = xS + 3    /* 所在宫格x结束 */
                yS = y / 3 * 3 /* 所在宫格y起始 */
                yE = yS + 3    /* 所在宫格y结束 */
                for ; xS < xE; xS++ {
                    for k = yS; k < yE; k++ {
                        if t = SudokuData.data[xS][k].num; t == 0 {
                            for ; t < SudokuData.data[xS][k].cnt; t++ {
                                if tmpMay == SudokuData.data[xS][k].may[t] {
                                    goto NextFlagZ /* 这个可能值和在当前列不唯一 */
                                }
                            }
                        }
                    }
                }
                SudokuData.data[x][y].num = tmpMay /* 这个值在行上可能值中是唯一,填值并重新填值 */
                flagBreak = 1
                break
    
            NextFlagZ:
            }
        }
        if 1 == flagBreak {
            goto StartLoop
        }
    
        for i = 1; i < SudokuData.pos.cnt; i++ {
            x, y = SudokuData.pos.pos[i].i, SudokuData.pos.pos[i].j
            tmpMay = SudokuData.data[x][y].cnt
    
            for j = i - 1; j >= 0 && SudokuData.data[SudokuData.pos.pos[j].i][SudokuData.pos.pos[j].j].cnt > tmpMay; j-- {
                SudokuData.pos.pos[j+1].i = SudokuData.pos.pos[j].i
                SudokuData.pos.pos[j+1].j = SudokuData.pos.pos[j].j
            }
            SudokuData.pos.pos[j+1].i = x
            SudokuData.pos.pos[j+1].j = y
        }
    
        /* 下面打印可能点个数由少到多的排序 */
        //for i = 0; i < SudokuData.pos.cnt; i++ {
        //  fmt.Println(SudokuData.pos.pos[i], SudokuData.data[SudokuData.pos.pos[i].i][SudokuData.pos.pos[i].j])
        //}
        //fmt.Print("\n\n\n")
        //os.Exit(0)
        return true
    }
    
    /**
    * 打印数独
    * 这里需要win32api
    * 将计算得到的数据上不同颜色
    **/
    func PrintSudoku() {
        var (
            i, j, tmp int
            api       = golibs.NewWin32Api()
        )
        fmt.Println(" ---------+---------+---------")
        for i = 0; i < Length; i++ {
            fmt.Print("|")
            for j = 0; j < Length; j++ {
                if tmp = SudokuData.data[i][j].num; tmp > 0 {
                    if 0 == SudokuData.data[i][j].flag { /* 该位置是计算得到的,标红色 */
                        api.TextBackground(golibs.ForegroundRed | golibs.ForegroundIntensity)
                    }
                    fmt.Printf(" %d ", tmp) /* 下面把前景色重置为白色 */
                    api.TextBackground(golibs.ForegroundRed | golibs.ForegroundGreen | golibs.ForegroundBlue)
                } else {
                    fmt.Print(" . ")
                }
                if j == 2 || j == 5 {
                    fmt.Print("|")
                }
            }
    
            switch i {
            case 2, 5:
                fmt.Print("|\n|---------+---------+---------|\n")
            case 0, 1, 3, 4, 6, 7:
                fmt.Println("|\n|         |         |         |")
            }
        }
        fmt.Println("|\n ---------+---------+---------")
    }
    
    /**
    * 判断当前成功没
    * 如果游戏完成则返回true
    * 否则没有完成则返回false
    **/
    func GameComplete() bool {
        var i, j int
        for i = 0; i < Length; i++ {
            for j = 0; j < Length; j++ {
                if 0 == SudokuData.data[i][j].num {
                    return false /* 数独中存在没有完成的位置,则游戏还要继续 */
                }
            }
        }
        return true /* 所有位置都完成 */
    }
    
    /**
    * http://cn.sudokupuzzle.org/
    * https://www.newdoku.com/zh/sudoku.php
    * 上面是2个在线数独网站
    * 技巧:http://www.conceptispuzzles.com/zh/index.aspx?uri=puzzle/sudoku/techniques
    * 规则:http://www.conceptispuzzles.com/zh/index.aspx?uri=puzzle/sudoku/rules
    **/

    上面的方案效率在应对简单级别的也是很快的,基本毫秒级别。这里找了一个在线数独网站在线玩数独,下面是在里面找的骨灰级运算结果:

    但是比较蛋疼的就是暴力求解存在把所有解遍历一遍的情况,那将遍历非常大,虽然我已经保证每次把确定的值填入,但仍然无可避免,下面就是一个例子,用时44分钟:

    好了上面就把我自己的想法写成代码,并能正确得到结果,只是某些情况计算效率比较低,而且没有处理存在多个值的情况。
    1. 舞蹈链求解数独
    求解数独最佳方案当然是舞蹈链了,优点就是不会占用多于空间,缓存和恢复状态非常快。
    http://www.cnblogs.com/grenet/p/3145800.html 讲解舞蹈链
    http://www.cnblogs.com/grenet/p/3163550.html 讲解如何用舞蹈链解数独
    代码灵感主要来源于上面的博客,并且舞蹈链求解比较快,因此我也做了多解数组至少算2种结果
    2. 舞蹈链求解的具体流程就参照上面博客吧,下面把我的代码贴上:

    package main
    
    import (
        "fmt"
        "log"
    
        "time"
    
        "io/ioutil"
    
        "flag"
    
        "github.com/jan-bar/golibs"
    )
    
    const (
        LenGrid    = 9                 /* 数独都有9行9列格子 */
        Length     = LenGrid * LenGrid /* 数独有81个元素 */
        NineDance  = 9 * Length        /* 81*9 创建出9个舞蹈链,分别代表填入的数字 */
        FourDance  = 4 * Length        /* 81*4 约束条件 */
        MinInitial = 1000000000        /* 最小min的初值 */
    )
    
    type Node struct {
        r, c  int /* 标识第r行,第c列 */
        up    *Node
        down  *Node
        left  *Node
        right *Node
    }
    
    var (
        SudokuData [Length + 1]int                /* 保存数独数据 */
        Mem1       [Length + 1]int                /* 保存数独结果1 */
        Mem2       [Length + 1]int                /* 保存数独结果2 */
        Mem        = &Mem1                        /* 用mem操作2个结果内的值 */
        Cnt        [FourDance + 1]int             /* 0-324  用于记录0-324列,这一列有多少个结点 */
        Scnt       = 0                            /* 记录数独结果个数,本程序最多找到2个就退出 */
        Head       Node                           /* 头结点 */
        All        [NineDance*FourDance + 99]Node /* 0-236294  构建729*324+99列的舞蹈链 */
        AllCnt     int                            /* 舞蹈链的游标 */
        Row        [NineDance]Node                /* 0-728  构建729列的舞蹈链,用于1-9的填入,每个数字用81列来表示 */
        Col        [FourDance]Node                /* 0-323  构建324列的舞蹈链,用于满足4个约束条件 */
    )
    
    func init() {
        fr := flag.String("f", "Sudoku.txt", "input data file!")
        flag.Parse()
    
        byt, err := ioutil.ReadFile(*fr)
        if err != nil {
            log.Fatal(err.Error())
        }
    
        var cnt = 0
        for _, v := range byt {
            if v >= '0' && v <= '9' {
                if cnt < Length { /* 数独只有81个元素 */
                    SudokuData[cnt] = int(v - '0')
                }
                cnt++
            }
        }
    
        if cnt != Length { /* 无论如何只有81个数字输入 */
            log.Fatal("输入文件只能有81个数字!")
        }
        SudokuData[cnt] = MinInitial 
    
    下一篇:没有了