注:以下题目来自《程序员的算法趣题》– [日]增井敏克著,原书解法主要用Ruby实现,最近在学Python,随便找点东西写写当做练习,准备改成Python3实现,顺便增加一些自己的理解。

26.高效的立体停车场

最近,一些公寓等建筑也都配备了立体停车场。立体停车场可以充分利用窄小的土地,通过上下左右移动来停车、出库,从而尽可能多地停车。现在有一个立体停车场,车出库时是把车往没有车的位置移动,从而把某台车移动到出库位置。假设要把左上角的车移动到右下角,试找出路径最短时的操作步数。举个例子,在 3×2 的停车场用如 图 13 所示的方式移动时,需要移动 13 步。

python 趣味数学代码 python趣味算法题_python3


不过,如果用如 图 14 所示的移动方法,则只需要移动 9 步。

python 趣味数学代码 python趣味算法题_algorithm_02


求在 10×10 的停车场中,把车从左上角移动到右下角时按最短路径移动时需要的最少步数。

思路:BFS
(car_x, car_y, space_x, space_y) 表示一个局面

import collections

def solve1(width, height):
    def in_board(r, c):
        return r >= 0 and c >= 0 and r < height and c < width

    q = collections.deque()
    q.append((0, 0, height-1, width-1))

    log = {}
    log[(0, 0, height-1, width-1)] = 0

    while q:
        car_r, car_c, space_r, space_c = q.popleft()
        depth = log[(car_r, car_c, space_r, space_c)]

        for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
            sr, sc = space_r + dr, space_c + dc
            if not in_board(sr, sc):
                continue

            cr, cc = car_r, car_c
            if car_r == sr and car_c == sc:
                cr, cc = space_r, space_c
                if cr == height-1 and cc == width-1:
                    return depth+1

            if (cr, cc, sr, sc) not in log:
                log[(cr, cc, sr, sc)] = depth+1
                q.append((cr, cc, sr, sc))

ans = solve1(2, 3)
print(ans)

ans = solve1(10, 10)
print(ans)

9
69

扩展:要求得到最短路径

def solve2(width, height):
    def in_board(r, c):
        return r >= 0 and c >= 0 and r < height and c < width

    def get_path(start):
        path = []
        while start:
            path.insert(0, start)
            start = log[start][1]
        return path

    q = collections.deque()
    q.append((0, 0, height-1, width-1))

    log = {}
    log[(0, 0, height-1, width-1)] = [0, None]

    while q:
        car_r, car_c, space_r, space_c = q.popleft()
        depth = log[(car_r, car_c, space_r, space_c)][0]

        for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
            sr, sc = space_r + dr, space_c + dc
            if not in_board(sr, sc):
                continue

            cr, cc = car_r, car_c
            if car_r == sr and car_c == sc:
                cr, cc = space_r, space_c
                if cr == height-1 and cc == width-1:
                    path = get_path((car_r, car_c, space_r, space_c))
                    path.append((cr, cc, sr, sc))
                    return depth+1, path

            st = (cr, cc, sr, sc)
            if st not in log:
                log[st] = [depth+1, (car_r, car_c, space_r, space_c)]
                q.append(st)

ans = solve2(3, 2)
print(ans)

ans = solve2(10, 10)
print(ans)
(9, [(0, 0, 1, 2), (0, 0, 1, 1), (0, 0, 0, 1), (0, 1, 0, 0), (0, 1, 1, 0), (0, 1, 1, 1), (1, 1, 0, 1), (1, 1, 0, 2), (1, 1, 1, 2), (1, 2, 1, 1)]) 
 (69, [(0, 0, 9, 9), (0, 0, 9, 8), (0, 0, 9, 7), (0, 0, 9, 6), (0, 0, 9, 5), (0, 0, 9, 4), (0, 0, 9, 3), (0, 0, 9, 2), (0, 0, 9, 1), (0, 0, 9, 0), (0, 0, 8, 0), (0, 0, 7, 0), (0, 0, 6, 0), (0, 0, 5, 0), (0, 0, 4, 0), (0, 0, 3, 0), (0, 0, 2, 0), (0, 0, 1, 0), (1, 0, 0, 0), (1, 0, 0, 1), (1, 0, 1, 1), (1, 1, 1, 0), (1, 1, 2, 0), (1, 1, 2, 1), (2, 1, 1, 1), (2, 1, 1, 2), (2, 1, 2, 2), (2, 2, 2, 1), (2, 2, 3, 1), (2, 2, 3, 2), (3, 2, 2, 2), (3, 2, 2, 3), (3, 2, 3, 3), (3, 3, 3, 2), (3, 3, 4, 2), (3, 3, 4, 3), (4, 3, 3, 3), (4, 3, 3, 4), (4, 3, 4, 4), (4, 4, 4, 3), (4, 4, 5, 3), (4, 4, 5, 4), (5, 4, 4, 4), (5, 4, 4, 5), (5, 4, 5, 5), (5, 5, 5, 4), (5, 5, 6, 4), (5, 5, 6, 5), (6, 5, 5, 5), (6, 5, 5, 6), (6, 5, 6, 6), (6, 6, 6, 5), (6, 6, 7, 5), (6, 6, 7, 6), (7, 6, 6, 6), (7, 6, 6, 7), (7, 6, 7, 7), (7, 7, 7, 6), (7, 7, 8, 6), (7, 7, 8, 7), (8, 7, 7, 7), (8, 7, 7, 8), (8, 7, 8, 8), (8, 8, 8, 7), (8, 8, 9, 7), (8, 8, 9, 8), (9, 8, 8, 8), (9, 8, 8, 9), (9, 8, 9, 9), (9, 9, 9, 8)])

27.禁止右转也没关系吗

在像日本这样车辆靠左通行的道路上,开车左转比右转要舒服些。因为不用担心对面来车,所以只要一直靠左行驶,就不用思考怎么变道。

那么,现在来想一下如何只靠直行或者左转到达目的地。假设在像图 15 一样的网状道路上,我们只能直行或者左转,并且已经通过的道路就不能再次通过了。此时允许通行道路有交叉。

请思考一下从左下角去右上角时,满足条件的行驶路线共有多少种。举个例子,如果是像 图 15 这样 3×2 的网状道路,则共有 4 种行驶路线。

python 趣味数学代码 python趣味算法题_python 趣味数学代码_03


求 6×4 的情况下,共有多少种行驶路线?

思路:DFS,需要记录各横线,纵线是否已走过。

import time
import itertools
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, 'cost time:', end - start)
        return result
    return wrapper

@timethis
def solve1(width, height):
    directions = [(0, 1), (-1, 0), (0, -1), (1, 0)]
    rows = [ [False for i in range(width)] for j in range(height+1) ]
    cols = [ [False for i in range(height)] for j in range(width+1) ]

    def dfs(direction, r, c):
        rr, cc = r, c
        if direction == 0 or direction == 2: # forward or backward
            rr = r + directions[direction][1]
            if rr < 0 or rr > height:
                return 0
            if cols[c][min(r, rr)]:
                return 0
        else:
            cc = c + directions[direction][0]
            if cc < 0 or cc > width:
                return 0
            if rows[r][min(c, cc)]:
                return 0

        next_row, next_col = r + directions[direction][1], c + directions[direction][0]
        if next_row < 0 or next_row > height or next_col < 0 or next_col > width:
            return 0

        if next_row == height and next_col == width:
            return 1

        if direction == 0 or direction == 2:
            cols[c][min(r, rr)] = True
        else:
            rows[r][min(c, cc)] = True

        count = 0
        count += dfs(direction, next_row, next_col)
        count += dfs((direction + 1) % len(directions), next_row, next_col)

        if direction == 0 or direction == 2:
            cols[c][min(r, rr)] = False
        else:
            rows[r][min(c, cc)] = False

        return count

    return dfs(3, 0, 0)


ans = solve1(6, 4)
print(ans)

solve1 cost time: 0.14783716201782227
2760

简化:

@timethis
def solve2(width, height):
    directions = [(0, 1), (-1, 0), (0, -1), (1, 0)]
    rows = [ [False for i in range(width)] for j in range(height+1) ]
    cols = [ [False for i in range(height)] for j in range(width+1) ]

    def dfs(direction, r, c):
        next_row, next_col = r + directions[direction][1], c + directions[direction][0]

        # reach it
        if next_row == height and next_col == width:
            return 1

        # out of range
        if next_row < 0 or next_row > height or next_col < 0 or next_col > width:
            return 0

        rr, cc = r, c
        if direction == 0 or direction == 2: # forward or backward
            rr = min(r, next_row)
            if cols[c][rr]:
                return 0
            cols[c][rr] = True
        else:
            cc = min(c, next_col)
            if rows[r][cc]:
                return 0
            rows[r][cc] = True

        count = 0
        count += dfs(direction, next_row, next_col)
        count += dfs((direction + 1) % len(directions), next_row, next_col)

        if direction == 0 or direction == 2:
            cols[c][rr] = False
        else:
            rows[r][cc] = False

        return count

    return dfs(3, 0, 0)


ans = solve2(6, 4)
print(ans)

solve2 cost time: 0.11287117004394531
2760

28.社团活动的最优分配方案

对学生而言,社团活动可能比学习还更重要。假设你即将成为某新建学校的校长,学校里有150 名想要运动的学生,请
你考虑要为他们准备哪些社团活动。
你调查各项运动所需的场地面积后得到了如表 7 所示的表格。在确定活动场地时,也要考虑各个社团的人数。

python 趣味数学代码 python趣味算法题_Ruby_04

请选择一些社团活动,社团总人数不能超过 150 人,还要使场地面积最大。求这个最大的面积的值。

思路:暴力搜索各种组合

import time
import itertools
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, 'cost time:', end - start)
        return result
    return wrapper


def solve1(clubs, n):
    ans = 0
    for i in range(1, len(clubs)+1):
        for comb in itertools.combinations(clubs, i):
            size, student = 0, 0
            for c in comb:
                size += c[0]
                student += c[1]
                if student > n:
                    break

            if student <= n and size > ans:
                ans = size
    return ans

@timethis
def test1():
    clubs = [ (11000, 40), (8000, 30), (400, 24), (800, 20), (900, 14),
              (1800, 16), (1000, 15), (7000, 40), (100, 10), (300, 12) ]
    ans = solve1(clubs, 150)
    print(ans)

test1()

28800
test1 cost time: 0.0012810230255126953

思路:带记忆的DFS,记录已搜过的

def solve2(clubs, n):
    memo = {}
    visited = [0] * len(clubs)

    def dfs(left):
        key = str((visited, left))
        if key in memo:
            return memo[key]

        m = 0
        for i in range(len(clubs)):
            if visited[i] == 0:
                visited[i] = 1
                if left >= clubs[i][1]:
                    m = max(m, clubs[i][0] + dfs(left - clubs[i][1]))
                visited[i] = 0

        memo[key] = m
        return m

    return dfs(n)

@timethis
def test2():
    clubs = [ (11000, 40), (8000, 30), (400, 24), (800, 20), (900, 14),
              (1800, 16), (1000, 15), (7000, 40), (100, 10), (300, 12) ]
    ans = solve2(clubs, 150)
    print(ans)

test2()

28800
test2 cost time: 0.010904073715209961

思路:带记忆的DFS,从可选项删除已搜过的

def solve3(clubs, n):
    memo = {}

    def dfs(clubs, left):
        key = str((clubs, left))
        if key in memo:
            return memo[key]

        m = 0
        for c in clubs:
            if left >= c[1]:
                m = max(m, c[0] + dfs(clubs - {c}, left - c[1]))

        memo[key] = m
        return m

    return dfs(clubs, n)

@timethis
def test3():
    clubs = { (11000, 40), (8000, 30), (400, 24), (800, 20), (900, 14),
              (1800, 16), (1000, 15), (7000, 40), (100, 10), (300, 12) }
    ans = solve3(clubs, 150)
    print(ans)

test3()

28800
test3 cost time: 0.01890397071838379

思路:DP

def solve4(clubs, n):
    dp = [ [0 for j in range(n+1) ] for i in range(len(clubs)+1) ]
    for i in range(len(clubs)-1, -1, -1):
        for j in range(n+1):
            if j < clubs[i][1]:
                dp[i][j] = dp[i+1][j]
            else:
                dp[i][j] = max(dp[i+1][j], dp[i+1][j-clubs[i][1]] + clubs[i][0])

    return dp[0][n]

@timethis
def test4():
    clubs = [ (11000, 40), (8000, 30), (400, 24), (800, 20), (900, 14),
              (1800, 16), (1000, 15), (7000, 40), (100, 10), (300, 12) ]
    ans = solve4(clubs, 150)
    print(ans)

test4()

28800
test4 cost time: 0.0006041526794433594

29.合成电阻的黄金分割比

我们在物理课上都学过“电阻”,通过把电阻串联或者并联可以使电阻值变大或者变小。电阻值分别为 R1、 R2、 R3

的 3 个电阻串联后,合成电阻的值为 R1 + R2 + R3。同样 3 个电阻并联时,合成电阻的值则为“倒数之和的倒数”( 图 18 )。

现在假设有 n 个电阻值为1 Ω 的电阻。组合这些电阻,使图 18 计算合成电阻的电阻值总电阻值接近黄金分割比1.6180339887…。举个例子,当 n = 5 时,如果像 图 19 这样组合,则可以使电阻值为 1.6。

python 趣味数学代码 python趣味算法题_最短路径_05


python 趣味数学代码 python趣味算法题_python3_06


求 n = 10 时,在组合电阻后得到的电阻值中,最接近黄金分割的数值,请精确到小数点后 10 位。

思路:n个电阻,可以在各个点拆开,每段可以串连或并联,或串并联的组合,可以用DFS
product用于将类似[1, [2,3], 4]这样的组合拆分成[1, 2, 4], [1, 3, 4],因为[1, [2,3], 4]表示第一组电阻为1, 第二组可以有2,3两种情况,第三组电阻为4,所以可以组合成1,2,3和1,2,4两种组合。
parallel用于求并联电阻阻值。

import itertools
from fractions import Fraction

def product(cand):
    def dfs(cand, i, length, current, result):
        if i == length:
            result.append(current)
        else:
            for x in cand[i]:
                dfs(cand, i+1, length, current + [x], result)

    result = []
    dfs(cand, 0, len(cand), [], result)
    return result


def parallel(arr):
    s = sum([Fraction(1, x) for x in arr])
    return Fraction(1, s)


def solve(n):
    def dfs(n):
        if n in memo:
            return memo[n]

        # series connection
        result = [x + 1 for x in dfs(n-1)]

        # parallel connection
        nums = [ i for i in range(1, n) ]
        for i in range(2, n):
            cuts = {}
            for comb in itertools.combinations(nums, i-1):
                r = sorted([comb[0]] + [ comb[i] - comb[i-1] for i in range(1, len(comb)) ] + [n - comb[-1]])
                cuts[str(r)] = r

            keys = [ [dfs(c) for c in v] for k, v in cuts.items() ]
            for k in keys:
                conns = product(k)
                for c in conns:
                    result.append(parallel(c))

        memo[n] = result
        return result

    memo = {1 : [1]}
    golden = 1.61800339887
    ans = float('INF')

    cands = dfs(n)
    for c in cands:
        if abs(golden - c) < abs(golden - ans):
            ans = c

    return ans


def test():
    result = solve(10)
    print(result, float(result))

test()

89/55 1.6181818181818182

30.用插线板制作章鱼脚状线路

对工程师而言,确保电源是最重要的事情。不仅是 PC,当智能手机、平板电脑、数码相机等电量不足时,我们也肯定要四处寻找插座。不过,多人共用的时候就必须共享插座,这时插线板就会派上用场。一般的插线板除了有延长线,还会有多个插口。
这里假设有双插口和三插口的插线板。墙壁上只有 1 个插座能用,而需要用电的电器有 n 台,试考虑此时应如何分配插线板。举个例子,当 n = 4 时,如 图 21 所示,有 4 种插线板插线方法(使用同一个插线板时,不考虑插口位置,只考虑插线板的连接方法。另外,要使插线板上最后没有多余的插口)。

python 趣味数学代码 python趣味算法题_algorithm_07

求 n = 20 时,插线板的插线方法有多少种(不考虑电源的功率问题)?

思路:DFS
只考虑2个插口的插线板,按题意,不能有多余的口,则n=1时没有满足条件的方法
n=1: 0
n=2: 1
n=3: 1
n=4: 2
上图左边两种,第一个留一个孔,第二个接3个,或者第一个的两个孔各接一个2个
n=5: 3
第一个留一个孔,第二个孔后面接一个4孔的组合,或者两孔分别接2,3的组合
f(5) = 1*f(4) + f(2)*f(3)

f(x) = 1*f(x-1) + f(2)f(x-2) + … f(x/2) f(x-x/2) x为奇数
f(x) = 1*f(x-1) + f(2)f(x-2) + … f(x/2)(1+f(x/2))/2 x为偶数
x为偶数时,两边各x/2的情况,左右对称算一种情况,共有
n + n*(n-1)/2 = n*(n+1)/2种情况。

三孔的类似。

import time
import itertools
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, 'cost time:', end - start)
        return result
    return wrapper


@timethis
def solve1(n):
    def dfs(n):
        if n == 1:
            return 1

        cnt = 0
        for i in range(1, n//2 + 1):
            if n == i*2:
                solve_i = dfs(i)
                cnt += solve_i * (solve_i + 1) // 2
            else:
                cnt += dfs(i) * dfs(n-i)

        for i in range(1, n//3 + 1):
            for j in range(i, (n-i) // 2 + 1):
                if i == j and i == (n - i - j):
                    solve_i = dfs(i)
                    cnt += solve_i * (solve_i + 1) * (solve_i + 2) // 6
                elif i == j:
                    solve_i = dfs(i)
                    cnt += solve_i * (solve_i + 1) // 2 * dfs(n - i*2)
                elif i == (n - i - j):
                    solve_i = dfs(i)
                    cnt += solve_i * (solve_i + 1) // 2 * dfs(n - i*2)
                elif j == (n - i - j):
                    solve_j = dfs(j)
                    cnt += solve_j * (solve_j + 1) // 2 * dfs(n - j*2)
                else:
                    cnt += dfs(i) * dfs(j) * dfs(n - i - j)

        return cnt

    return dfs(n)

solve1 cost time: 9.859107971191406
63877262

思路:带记忆的DFS

@timethis
def solve2(n):
    def dfs(n):
        if n == 1:
            return 1

        if n in memo:
            return memo[n]

        cnt = 0
        for i in range(1, n//2 + 1):
            if (n - i) == i:
                solve_i = dfs(i)
                cnt += solve_i * (solve_i + 1) // 2
            else:
                cnt += dfs(i) * dfs(n-i)

        for i in range(1, n//3 + 1):
            for j in range(i, (n-i) // 2 + 1):
                if i == j and i == (n - i - j):
                    solve_i = dfs(i)
                    cnt += solve_i * (solve_i + 1) * (solve_i + 2) // 6
                elif i == j:
                    solve_i = dfs(i)
                    cnt += solve_i * (solve_i + 1) // 2 * dfs(n - i*2)
                elif i == (n - i - j):
                    solve_i = dfs(i)
                    cnt += solve_i * (solve_i + 1) // 2 * dfs(n - i*2)
                elif j == (n - i - j):
                    solve_j = dfs(j)
                    cnt += solve_j * (solve_j + 1) // 2 * dfs(n - j*2)
                else:
                    cnt += dfs(i) * dfs(j) * dfs(n - i - j)

        memo[n] = cnt
        return cnt

    memo = {}
    return dfs(n)

print(solve2(20))

solve2 cost time: 0.0001990795135498047
63877262