无法用蛮力解决的难题

42 阅读3分钟

我们购买了一张空白的 DVD 来录制喜欢的电视节目,并且随机附赠了 20 张数字贴纸,其中每个数字都有两张。我们灵机一动,打算用贴纸来标记新的 DVD 编号。将数字“1”贴到第一个录制的 DVD 上,然后把剩下的 19 张贴纸放进抽屉里保存好。第二天,我们又购买了一张空白的 DVD,又随赠 20 张新贴纸。在录制节目后,接着给 DVD 贴上数字“2”。现在我们不禁产生了疑问,什么时候贴纸会用完,那时我们将无法再标记 DVD 了?这个问题用几行 Python 代码就可以解决吗?请提供能够合理运行时间内能够解决问题的代码。

2. 解决方案

这个特别是数字“1”和其它数字之间存在差异,数字“1”比其它数字先用完。算法可以如下进行优化:

  • 优化1:设定数字“1”会先用完(很容易证明这一点)

如果优化1成立,那么数字列表将变成:

NUMBERS = [1]

否则,数字列表将是:

NUMBERS = range(10)

下面是函数 how_many_have,用于计算给定数字 n 和贴纸数量 stickers 时,可以使用的贴纸数量。

def how_many_have(dight, n, stickers):
    return stickers * n

接着我们定义一个缓存,用于存储数字 dight 和 n 的键值对,以优化计算。

cache = {}

函数 how_many_used 用于计算给定数字 n 时,已经使用了多少个贴纸。如果 (dight, n) 在缓存中,那么直接返回缓存中的值。否则,开始计算。如果 dight 是“0”,那么根据优化策略,直接返回 0。否则,如果 n 大于或等于 10,那么需要计算 n 的第一位数字是否为 dight,如果为真,则将 n 的第一位数字移除,并计算剩余数字的贴纸使用情况。否则,如果 n 的值大于或等于 dight,则将 n 减去 dight,并计算剩余数字的贴纸使用情况。如果 n 的值小于 dight,那么使用贴纸的数量为 0。

def how_many_used(dight, n):
    if (dight, n) in cache:
        return cache[(dight,n)]
    result = 0
    if dight == "0":
        if OPTIMIZE_1:
            return 0
        else:
            assert(False)
            #TODO
    else:
        if int(n) >= 10:
            if n[0] == dight:
                result += int(n[1:]) + 1
            result += how_many_used(dight, str(int(n[1:])))
            result += how_many_used(dight, str(int(str(int(n[0])-1) + "9"*(len(n) - 1))))
        else:
            result += 1 if n >= dight else 0
    if n.endswith("9" * (len(n)-4)): # '4' constant was pick out based on preformence tests
        cache[(dight, n)] = result
    return result

函数 best_jump 用于计算从数字 i 到数字 i+1 时,需要使用多少张贴纸。

def best_jump(i, stickers_left):
    no_of_dights = len(str(i))
    return max(1, min(
        stickers_left / no_of_dights,
        10 ** no_of_dights - i - 1,
    ))

最后,函数 solve 用于计算贴纸用完时的 DVD 编号。

def solve(stickers):
    i = 0
    stickers_left = 0
    while stickers_left >= 0:
        i += best_jump(i, stickers_left)

        stickers_left = min(map(
            lambda x: how_many_have(x, i, stickers) - how_many_used(str(x), str(i)),
            NUMBERS
        ))
    return i - 1

通过调用 solve 函数,我们可以得到不同数量贴纸时,贴纸用完时的 DVD 编号。

for stickers in range(10):
    print '%d: %d' % (stickers, solve(stickers))

代码例子

完整的代码如下:

OPTIMIZE_1 = True # we assum that '1' will run out first (It's easy to prove anyway)

if OPTIMIZE_1:
    NUMBERS = [1]
else:
    NUMBERS = range(10)

def how_many_have(dight, n, stickers):
    return stickers * n

cache = {}
def how_many_used(dight, n):
    if (dight, n) in cache:
        return cache[(dight,n)]
    result = 0
    if dight == "0":
        if OPTIMIZE_1:
            return 0
        else:
            assert(False)
            #TODO
    else:
        if int(n) >= 10:
            if n[0] == dight:
                result += int(n[1:]) + 1
            result += how_many_used(dight, str(int(n[1:])))
            result += how_many_used(dight, str(int(str(int(n[0])-1) + "9"*(len(n) - 1))))
        else:
            result += 1 if n >= dight else 0
    if n.endswith("9" * (len(n)-4)): # '4' constant was pick out based on preformence tests
        cache[(dight, n)] = result
    return result

def best_jump(i, stickers_left):
    no_of_dights = len(str(i))
    return max(1, min(
        stickers_left / no_of_dights,
        10 ** no_of_dights - i - 1,
    ))

def solve(stickers):
    i = 0
    stickers_left = 0
    while stickers_left >= 0:
        i += best_jump(i, stickers_left)

        stickers_left = min(map(
            lambda x: how_many_have(x, i, stickers) - how_many_used(str(x), str(i)),
            NUMBERS
        ))
    return i - 1

for stickers in range(10):
    print '%d: %d' % (stickers, solve(stickers))