美文网首页
0-1规划解数独

0-1规划解数独

作者: 章光辉_数据 | 来源:发表于2018-06-09 11:35 被阅读0次

    背景音乐:虚拟 - 陈粒

    背景

    本人一直以来比较喜欢数独,水平自认为还可以,
    常用的app是Sodoku Joy,最难的Maelstrom模式也能在半小时内完成。
    最近因工作需要,在看运筹优化方面的内容,
    突然想到数独问题也可以用0-1规划求解,非常简单!
    虽然变量和约束条件会变得很多,不过这都不是事儿。
    Google一下,发现Python有个线性规划模块(Pulp)正好也提供了这方面的示例。
    于是,简单地做个整理。

    建模思路

    1. 每个单元格有9种可能取值,每种取值要么为1(被选中)要么为0(不被选中),标准的数独是9*9的网格结构,因此构造9*9*9=729个0-1变量。
    2. 目标函数:由于数独问题只要靠约束条件即可求解,因此目标函数可设为常数0。
    3. 约束条件:
      1)每个单元格里,只能有一种取值被选中。
      2)每行不能有重复数字;
      3)每列不能有重复数字;
      4)每个3*3小方格不能有重复数字;

    案例

    拿以前新闻里说的“最难数独”为例:


    Python实现

    1. 首先创建一个记录了初始值的字典
    # 有些单元格上已有指定的数值
    init_choices = {
        ("1", "1"): "5",
        ("2", "1"): "6",
        ("4", "1"): "8",
        ("5", "1"): "4",
        ("6", "1"): "7",
        ("1", "2"): "3",
        ("3", "2"): "9",
        ("7", "2"): "6",
        ("3", "3"): "8",
        ("2", "4"): "1",
        ("5", "4"): "8",
        ("8", "4"): "4",
        ("1", "5"): "7",
        ("2", "5"): "9",
        ("4", "5"): "6",
        ("6", "5"): "2",
        ("8", "5"): "1",
        ("9", "5"): "8",
        ("2", "6"): "5",
        ("5", "6"): "3",
        ("8", "6"): "9",
        ("7", "7"): "2",
        ("3", "8"): "6",
        ("7", "8"): "8",
        ("9", "8"): "7",
        ("4", "9"): "3",
        ("5", "9"): "1",
        ("6", "9"): "6",
        ("8", "9"): "5",
    }
    
    1. 然后创建数独所需的若干常量
    # 创建一个列表,包含了所有出现的数值
    Sequence = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
    
    # 单元格的可能取值
    Vals = Sequence
    
    # 行列的索引
    Rows = Sequence
    Cols = Sequence
    
    # 创建一个boxes列表,记录每个3*3小方格里的,各单元格的行列索引
    Boxes =[]
    for i in range(3):
        for j in range(3):
            Boxes += [[(Rows[3*i+k], Cols[3*j+l]) for k in range(3) for l in range(3)]]
    
    1. 建模
    import pulp
    
    # 创建0-1变量字典,表征指定单元格内的指定数值是否被选中
    choices = pulp.LpVariable.dicts("Choice", (Vals, Rows, Cols), 0, 1, pulp.LpInteger)
    
    # 创建模型
    prob = pulp.LpProblem("Sudoku Problem", pulp.LpMinimize)
    
    # 定义目标函数:由于数独问题只要满足所有约束条件即可求出解,所以目标函数随便指定为0
    prob += 0, "Arbitrary Objective Function"
    
    # 约束条件1:在一个单元格里,只能由一个数值被选中
    for r in Rows:
        for c in Cols:
            prob += pulp.lpSum([choices[v][r][c] for v in Vals]) == 1, ""
    
    # 约束条件2:对于一个数值,只能在一行/一列/一个box里,出现一次
    for v in Vals:
        for r in Rows:
            prob += pulp.lpSum([choices[v][r][c] for c in Cols]) == 1, ""
    
        for c in Cols:
            prob += pulp.lpSum([choices[v][r][c] for r in Rows]) == 1, ""
    
        for b in Boxes:
            prob += pulp.lpSum([choices[v][r][c] for (r,c) in b]) == 1, ""
    
    # 约束条件3:由于有些单元格上已有指定的数值,因此让它们的0-1变量取值恒为1
    for (r, c), v in init_choices.items():
        prob += choices[v][r][c] == 1,""
    
    1. 模型求解
      这里需要说明的是,如果问题出的不好,数独就可能存在多解。
      因此下面用while来循环求解:
      1)每次求得一个新的解,会加上一个新的约束条件,从而保证相同的解不会再出现。
      2)直到pulp.LpStatus[prob.status]不为Optimal,那么说明已经无解了,break。
      这里用termcolor的colored函数,来print不同颜色,区分是否为初始值。
    from termcolor import colored
    
    while True:
        # 模型循环求解
        prob.solve()
        # 打印当前的状态,并判断是否有解
        print("Status:", pulp.LpStatus[prob.status])
        if pulp.LpStatus[prob.status] == "Optimal":
            # 逐行打印结果
            for r in Rows:
                # 若当前为第1、4、7行,先打印分隔符
                if r == "1" or r == "4" or r == "7":
                    print("+-------+-------+-------+")
                # 记录当前行的字符内容
                s = ''
                for c in Cols:
                    for v in Vals:
                        # 如果该单元格的数值被选中,则打印
                        if pulp.value(choices[v][r][c]) == 1:
                            # 若当前为第1、4、7列,先打印分隔符
                            if c == "1" or c == "4" or c =="7":
                                s += "| "
                            # 按照是否为初始值,打印不同的颜色
                            if init_choices.get((r, c)) == v:
                                s += (colored(v, 'white', 'on_magenta') + " ")
                            else:
                                s += (v + " ")
                s += "|"
                print(s)
            print("+-------+-------+-------+")
            # 添加新的约束,保证不会返回相同的解
            prob += pulp.lpSum([choices[v][r][c] for v in Vals
                                                 for r in Rows
                                                 for c in Cols
                                                 if pulp.value(choices[v][r][c]) == 1]) <= 80
        # 如果不能找到新的解,则结束程序
        else:
            break
    
    1. 结果
      简书里设置颜色很麻烦,直接截图吧:


    感兴趣的话,也可以试试注释掉一个初始解,就会返回多个可行解了。

    速度测试

    将上述内容封装到sudoku_problem函数中,运行

    %timeit sudoku_problem(init_choices)
    

    返回结果

    655 ms ± 13.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    

    求解数独的时间在0.655秒左右,非常好~

    后续可行的计划

    1. 找个随机生成数独游戏的方法,完成“发现问题-解决问题”的闭环。
    2. 简单地做个UX设计,用于交互。
    3. 基于Django或Flask做个web端的界面,可以方便地在上面玩数独。
    4. 加入图像识别模块,这样就可以识别截图的数独问题并求解了。

    相关文章

      网友评论

          本文标题:0-1规划解数独

          本文链接:https://www.haomeiwen.com/subject/secteftx.html