「Solution」1223330000组成只读一个零的十位数

这道数学题是我偶然间发现的:

1223330000组合成十位数,问:共有多少种组合方式可使组合出的十位数只读一个零.

这题不需要用到什么高级的数学技巧,但是需要大量分类和枚举,于是,我想到了编程解决这一问题。

语言上我选择的是Python,另外,我的编程环境为PyCharm

接下来,我们进入正文部分。


思路

我的思路是这样的:

遍历每一种组合情况,并对每一种情况进行检查,看是否符合题目中只读一个零的条件。

那么,这个程序就可以被分为两个部分:
1.遍历
2.检查
那么,我们开始逐步实现这一程序。

第一部分:遍历

我们需要用十个数字来组成十位数,这种情况下我们就要对每一个数位进行遍历,我们倒是可以用十个for循环嵌套在一起来实现,但是这显然会影响代码的可读性美观,所以我们选择另外一种算法——递归

什么是递归

如果对递归算法有所了解的,可以直接跳过这一部分。
递归算法,用一句话来解释就是:

递归:自己调用自己

专业一点叫做:

在函数的定义中使用函数自身的方法

为了各位读者更容易理解递归,我们来举一个简单的例子:

首先,定义一个数列,\(a_1=0\)\(a_n=a_{n-1}+2(n\geq2)\)

接下来,我们编程来实现,输入\(n\),求\(a_n\)的值。

由于这是一个首项为0,公差为2的等差数列,我们可以这样写:

n = int(input("Enter n:"))
an = 2 * (n-1)
print(an)

当然,如果用到刚才讲到的递归,我们就可以这样写:

def a(term):
    if term == 1:
        return 0
    elif term >= 2:
        a(term - 1)


n = int(input("Enter n:"))
print(a(n))

这样写可能会有些复杂,但是这一程序很好的体现了递归算法,看到这里:

def a(term):
    …
    elif term >= 2:
        a(term - 1)

这就是前面所提到的,”自己调用自己“。

那么,这一程序是如何运行的呢?
手绘递归展示

注意图中还标有两个词:”回推“和”递推“

用我的方式来解释这两个词就是:

回推:一层一层向下调用的过程

递推:一层一层向上返回值的过程

以上,就是我对递归算法一个简单的介绍,如果想对递归有更多、更深入的了解,可以前往知乎上查看其他大佬对递归的解释

用递归来实现遍历

运用递归,我们的实现方式就应该是这样:递归时,每回推一次,通过for循环遍历一个数位的可能情况,当所有数位确定完成时,进行下一部分。

与此同时,我们就也确定出了我们的函数需要的两个参数:已确定的数位(numlist),还未使用的数字(unused)。

当然,由于这个程序没有返回值,也就没有递推的过程了。

结合以上的思路,我们的程序会是这样:

def recu(numlist, unused):
    if len(numlist) < 10:
        # 长度小于十,说明还未组成十位数
        used = []
        for addnum in unused:
            snumlist = numlist[:]
            sunused = unused[:]
            snumlist.append(addnum)
            sunused.remove(addnum)
            if snumlist[0] != 0:
                recu(snumlist, sunused)
    elif len(numlist) == 10:
        # 长度等于十,说明十位数已经组成,那么检查是否能够读零
        …

当然,我们还需要注意一点,在题目给出的可选数字中,有大量的重复数字,如果我们的遍历部分就这样写,对于同一个数字组合方式,程序会计算\(A^2_2\times A^3_3\times A^4_4=288\)次,也就是说,程序给出答案的时长会多出整整288倍

那么,我们要做的就是避免重复计算同一种情况,方法也很简单,每次遍历,检查addnum是否在列表used中,如果返回值为True,将addnum添加到列表used中。

修改后的程序会是这样:

def recu(numlist, unused):
    if len(numlist) < 10:
        # 长度小于十,说明还未组成十位数
        used = []
        for addnum in unused:
            if addnum not in used:
                used.append(addnum)
                snumlist = numlist[:]
                sunused = unused[:]
                snumlist.append(addnum)
                sunused.remove(addnum)
                if snumlist[0] != 0:
                    recu(snumlist, sunused)
    elif len(numlist) == 10:
        # 长度等于十,说明十位数已经组成,那么检查是否能够读零
        …

第二部分:检查

第二部分要做的工作是检查已组成的十位数是否符合”只读一个零“的条件。

并不是每一个数中的零都会被读出,那么,我们来梳理一下读零的规则:

  • 处于每级末尾的零或由零组成的数串不会被读出「如:10 2300 3300」
  • 处于数中部全部由零组成的数级,且前后数级不全是零的,会读一个零「如:12 0000 2333」

我们会发现,以上两条规则都是基于数级的,所以别的先抛开,我们要做的第一件事,就是将组成的十位数分级。

实现方式不难,我们先准备一个二维列表splitnum,用于存放被分级的十位数。

splitnum = [[], [], []]

然后,for循环遍历numlist,将每一个数字存放在最前面一个尚未存满的数级中(存满,即数级中已有四个数字)。

我的代码如下,**需要注意的是,我的这种方式分解出来的数字是倒序的,在之后的程序,不要忽略这一点,当然,你也可以使分解数字正续排列,可以遍历index,甚至可以这样写:splitnum = [[numlist[0], …], …]:

for i in range(0, len(numlist)):
    for grade in splitnum:
        if len(grade) < 4:
            grade.append(numlist.pop())
            break

结合读零规则的第一条,即”处于每级末尾的零或由零组成的数串不会被读出“,我们首先去掉处于每级末尾的零:

for grade in splitnum:
    while True:
        if grade[0] == 0 and len(grade) > 0:
            del grade[0]
       else:
            break

注意这里的if grade[0] == 0 and len(grade) > 0,在检测第一项为0的同时,也保证不把整个数级删空,这可以避免IndexError,同时,也因为整个数级均由零构成,可能会读零。

做完以上工作后,剩下的处于数中的0数串就全部都会被读出了,所以,我们将三个数级连接起来,再检查里面有几个由零组成的数串即可:

for grade in splitnum:
    # 将每一级连接起来
    for num in grade:
        check.append(num)
while True:
    if check[0] == 0:
        del check[0]
    else:
        break
# 将连在一起的零变为单独的一个零
zero = False
idx = 0
oldcheck = check[:]
for num in oldcheck:
    if num == 0:
        if zero:
            del check[idx]
            idx -= 1
        if not zero:
            zero = True
        else:
            zero = False
    idx += 1
    if check.count(0) == 1:
        result += 1

完工

至此,我们程序的主要部分就已全部编写完成了,加以整理,即可编写出完整的程序!

最后附上带有注释的完整代码:

# -*- coding: utf-8 -*-
"""
题目:
1223330000组合成十位数,问:共有多少种组合方式可使组合出的十位数只读一个零.
"""


def recu(numlist, unused):
    if len(numlist) < 10:
        # 长度小于十,说明还未组成十位数
        used = []
        for addnum in unused:
            if addnum not in used:
                used.append(addnum)
                snumlist = numlist[:]
                sunused = unused[:]
                snumlist.append(addnum)
                sunused.remove(addnum)
                if snumlist[0] != 0:
                    recu(snumlist, sunused)
    elif len(numlist) == 10:
        # 长度等于十,说明十位数已经组成,那么检查是否能够读零
        print(numlist, end='    ')
        global result
        # 首先按级进行分割
        splitnum = [[], [], []]
        for i in range(0, len(numlist)):
            for grade in splitnum:
                if len(grade) < 4:
                    grade.append(numlist.pop())
                    break
        # 对每一级进行检查,看是否读零
        check = []
        for grade in splitnum:
            # 末尾的零不会读出,所以去掉末尾的零
            while True:
                if grade[0] == 0 and len(grade) > 1:
                    del grade[0]
                else:
                    break
            # 将每一级连接起来
            for num in grade:
                check.append(num)
        while True:
            if check[0] == 0:
                del check[0]
            else:
                break
        # 将连在一起的零变为单独的一个零
        zero = False
        idx = 0
        oldcheck = check[:]
        for num in oldcheck:
            if num == 0:
                if zero:
                    del check[idx]
                    idx -= 1
                if not zero:
                    zero = True
            else:
                zero = False
            idx += 1
        print("check:", check, end="    ")
        if check.count(0) == 1:
            result += 1
            print("PASS")
        else:
            print("NOT PASS")
    else:
        print("Error")


if __name__ == "__main__":
    alternative = [1, 2, 2, 3, 3, 3, 0, 0, 0, 0]
    result = 0
    recu([], alternative)
    print(result)

运行结果:

…
[3, 0, 0, 0, 0, 3, 2, 3, 1, 2]    check: [2, 1, 3, 2, 3, 0, 3]    PASS
[3, 0, 0, 0, 0, 3, 2, 3, 2, 1]    check: [1, 2, 3, 2, 3, 0, 3]    PASS
[3, 0, 0, 0, 0, 3, 3, 1, 2, 2]    check: [2, 2, 1, 3, 3, 0, 3]    PASS
[3, 0, 0, 0, 0, 3, 3, 2, 1, 2]    check: [2, 1, 2, 3, 3, 0, 3]    PASS
[3, 0, 0, 0, 0, 3, 3, 2, 2, 1]    check: [1, 2, 2, 3, 3, 0, 3]    PASS
2460

Process finished with exit code 0

所以,我们得出了这道题目的答案——2460。

posted @ 2020-10-03 19:55  Liuxizai  阅读(39)  评论(0编辑  收藏