注:以下题目来自《程序员的算法趣题》– [日]增井敏克著,原书解法主要用Ruby实现,最近在学Python,随便找点东西写写当做练习,准备改成Python3实现,顺便增加一些自己的理解。
26.高效的立体停车场
最近,一些公寓等建筑也都配备了立体停车场。立体停车场可以充分利用窄小的土地,通过上下左右移动来停车、出库,从而尽可能多地停车。现在有一个立体停车场,车出库时是把车往没有车的位置移动,从而把某台车移动到出库位置。假设要把左上角的车移动到右下角,试找出路径最短时的操作步数。举个例子,在 3×2 的停车场用如 图 13 所示的方式移动时,需要移动 13 步。
不过,如果用如 图 14 所示的移动方法,则只需要移动 9 步。
求在 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 种行驶路线。
求 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 所示的表格。在确定活动场地时,也要考虑各个社团的人数。
请选择一些社团活动,社团总人数不能超过 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。
求 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 种插线板插线方法(使用同一个插线板时,不考虑插口位置,只考虑插线板的连接方法。另外,要使插线板上最后没有多余的插口)。
求 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