有个运营妹纸心算找不到数独答案,心一横干脆写代码实现

1,396 阅读8分钟

0、代码写不好,心痛吖

家里新购了一个数独,周四午饭后消食便拿起来玩,半小时过去了,一小时过去了,一直没成功……超级不服气的,这道难题想逼我是吗?干脆直接写python代码破解好了!半小时后代码还没写好,我被自己蠢哭了。在写代码这件事上,我显然手生得很呢。值得欣慰的是,我起码知道生物脑无法完成的,电脑可以代劳,只不过这代码依然需要生物脑来完成。

趁着端午小长假有点时间,就来琢磨下这件事好了。

1、不擅长写代码,总擅长需求分析吧?

计算目标:找到数独的答案。是找一些答案,还是找出所有答案呢?后者难度高,且包含前者,我选择后者。

数独规则:在下述六角形棋盘的12个位置中分别放入数字1至12,使得图中每条连线4个位置加和为26。

shudu

2、生物脑和电脑在演算方法上的迥异

我的生物脑是如何尝试解出答案的呢?

从棋盘取下所有数字,先随意选择4个数字放入某条线使之加和为26,然后再选择3个数字放入与此线有交点的另外一条线使之加和为26,然后尝试第3条线……结果是,每次尝试到第3、4条线时开始吃力,偶尔能满足5条线但最后一条线无法满足条件。

我将如何用代码来指挥电脑演算呢?用代码复原生物脑的思路,显然是非常蠢的解法。你们知道我是如何知道自己蠢的吗?因为上述思路拆解出来就是:

  1. 指定前3个位置数值,计算得出第4个数,使得一条线==26
  2. 选择剩余5条线中已得到赋值的位置最多的那条线,计算得出使该条线加和为26的数值
  3. 循环以上,尝试使所有线的加和都满足26;如果不满足,则重新指令最初3个位置的数值

我完全无法快速用代码表达出以上思路,基于此不难自我判断上述算法是愚蠢的。

经过一些摸索后,我把碳基问题抽象为下述硅基问题:

  1. 棋盘12个位置的不同取值,构成了一个列表。
  2. 自然数1至12有多少种排列组合的方式,就有多少个上述列表。
  3. 对于每个列表,计算特定位置的加和(6条线)是否满足条件,是通用的,能定义为函数。
  4. 穷举自然数1至12所有的排列组合方式。

3. 多说无益,秀出代码

3.1 版本1.0:能找到部分答案,但找不到所有答案

import math
import random

def areyouwin(point_list):
    if point_list[0] + point_list[6] + point_list[11] + point_list[4] != 26 :
        return False
    elif point_list[0] + point_list[7] + point_list[8] + point_list[2] != 26 :
        return False
    elif point_list[1] + point_list[7] + point_list[6] + point_list[5] != 26 :
        return False
    elif point_list[1] + point_list[9] + point_list[8] + point_list[3] != 26 :
        return False
    elif point_list[4] + point_list[9] + point_list[10] + point_list[2] != 26 :
        return False
    elif point_list[3] + point_list[10] + point_list[11] + point_list[5] != 26 :
        return False
    else:
        return True

point_list = [1,2,3,4,5,6,7,8,9,10,11,12]
k = 0
while k < 10 :#此处k表示找出10个答案,想要更多答案就改k的值即可
    random.shuffle(point_list)#新知识点:随机打乱列表
    if areyouwin(point_list):
        print(point_list,k+1)
        k = k + 1

3.2 版本2.0:自己动手,穷举所有排列组合

初步搜索,我没有找到如何实现输出列表项全部的排列组合。鉴于非常清楚自己对排列组合的代码实现接近于0经验,于是故意想要自己写代码实现。

计算目标的大框架已经实现,现在只缺排列组合。那我聚焦于此:

子目标:输出自然数1至12的所有排列组合

演算方法是怎样的呢?

  1. 位置1的取值范围为1至12,先取值1,完成下列所有演算后,再取值2,完成下列所有演算,再取值3……直至完成取值范围内的所有数值
  2. 位置2的取值范围为除了位置1当前取值之外的11个数,先取值其中一个数值,完成下列所有演算后,再取值另外一个数值,完成下列所有演算后,再取值第3个数值……直至完成取值范围内的所有数值
  3. 位置3的取值范围为除了位置1和位置2当前取值之外的10个数,先取值其中一个数值,完成下列所有演算后,再取值另外一个数值,完成下列所有演算后,再取值第3个数值……直至完成取值范围内的所有数值
  4. 位置4的取值范围为除了位置1和位置2当前取值之外的9个数,……
  5. ……
  6. 位置12的取值范围为除了位置1至11当前取值之外的11个数,此为唯一取值。输出位置1至位置12的当前取值

这个演算方法可行吗?代码将如何写?似乎有些困难。那我我先试着简化问题:输出自然数1至3的所有排列组合。并写出代码来验证结果是否正确。

point_list = [1,2,3]

#列表A赋值给列表B时需要用切片的方式,否则列表B改变时会引发列表A改变!
point_1_list = point_list[:]
for i in point_1_list:#实现位置1的循环
    point_2_list = point_1_list[:]
    point_2_list.remove(i) #位置2的取值范围是位置1的取值范围移除位置1的当前值
    #print(point_2_list)
    for j in point_2_list:
        #print(j,"j的值")
        point_3_list = point_2_list[:]
        point_3_list.remove(j)#位置3的取值范围是位置2的取值范围移除位置1的当前值
        k = point_3_list[0]#位置3的取值范围只剩1个数值
        print(i,j,k)#打印排列组合结果

在完成上述代码的过程中,刚开始列表赋值并没有采用切片的方式,导致所有列表均发生改变。无他,还是反映我对列表的操作不熟。

先不论代码是否简洁优雅,至少它在功能上实现了。那么现在,先试着用这个思路实现自然数1至12的全部排列组合,并计算得出数独26的解法共有多少个!

下面就是不简洁不优雅版本的数独所有答案的代码:

def areyouwin(point_list):
    if point_list[0] + point_list[6] + point_list[11] + point_list[4] != 26 :
        return False
    elif point_list[0] + point_list[7] + point_list[8] + point_list[2] != 26 :
        return False
    elif point_list[1] + point_list[7] + point_list[6] + point_list[5] != 26 :
        return False
    elif point_list[1] + point_list[9] + point_list[8] + point_list[3] != 26 :
        return False
    elif point_list[4] + point_list[9] + point_list[10] + point_list[2] != 26 :
        return False
    elif point_list[3] + point_list[10] + point_list[11] + point_list[5] != 26 :
        return False
    else:
        return True

point_list = [1,2,3,4,5,6,7,8,9,10,11,12]

#列表A赋值给列表B时需要用切片的方式,否则列表B改变时会引发列表A改变!
point_1_list = point_list[:]
for p_1 in point_1_list:#实现位置1的循环
    point_2_list = point_1_list[:]
    point_2_list.remove(p_1) #位置2的取值范围是位置1的取值范围移除位置1的当前值
    for p_2 in point_2_list:
        point_3_list = point_2_list[:]
        point_3_list.remove(p_2)#位置3的取值范围是位置2的取值范围移除位置1的当前值
        for p_3 in point_3_list:
            point_4_list = point_3_list[:]
            point_4_list.remove(p_3)
            for p_4 in point_4_list:
                point_5_list = point_4_list[:]
                point_5_list.remove(p_4)
                for p_5 in point_5_list:
                    point_6_list = point_5_list[:]
                    point_6_list.remove(p_5)
                    for p_6 in point_6_list:
                        point_7_list = point_6_list[:]
                        point_7_list.remove(p_6)
                        for p_7 in point_7_list:
                            point_8_list = point_7_list[:]
                            point_8_list.remove(p_7)
                            for p_8 in point_8_list:
                                point_9_list = point_8_list[:]
                                point_9_list.remove(p_8)
                                for p_9 in point_9_list:
                                    point_10_list = point_9_list[:]
                                    point_10_list.remove(p_9)
                                    for p_10 in point_10_list:
                                        point_11_list = point_10_list[:]
                                        point_11_list.remove(p_10)
                                        for p_11 in point_11_list:
                                            point_12_list = point_11_list[:]
                                            point_12_list.remove(p_11)
                                            p_12=point_12_list[0]
                                            one_list=[p_1, p_2, p_3, p_4, p_5, p_6, p_7, p_8, p_9, p_10, p_11, p_12]

                                            if areyouwin(one_list):
                                                print(one_list)

3.3 还有什么遗憾?

上述代码for循环嵌套代码部分,不够简洁优雅。如何优化,是我接下来需要再琢磨的。笨办法也是办法,终于把数独的答案全部演算出来了,终于能安心睡个好觉了。

这就完了吗?当然不!这个碳基转硅基的解决方式中,有个内核的问题是,棋盘是个对称的六角星型,于是碳基世界的正确答案,其实只需硅基代码演算出答案的1/6。

以正确答案[1,2,3,6,11,7,5,12,10,8,4,9]为例,棋盘每转动60°就产生一个新答案: [2,3,6,11,7,1,12,10,8,4,9,5] [3,6,11,7,1,2,10,8,4,9,5,12] [6,11,7,1,2,3,8,4,9,5,12,10] [11,7,1,2,3,6,4,9,5,12,10,8] [7,1,2,3,6,11,9,5,12,10,8,4]

咿??如何优化代码,能撇掉那5/6臃肿的答案呢?——难度超纲,我还是暂时放下吧。

一句话小结

代码写得好不好,手熟与否很关键呀!而手熟并非简单的经常写就够了,而是要专门分解出哪些基础功或知识点薄弱,并重点突破之。此乃大咖们常曰的“刻意练习”。