运动会得分谜题:竞赛数目未知时如何求解#
本章的内容#
本章的问题有一处与前面不同:不知道一共举行了几项竞赛。遇到这种情况,就按 2、3、4 的顺序依次试下去,把最先解出的项数作为答案。竞赛的项数决定变量的个数,不同的项数对应不同的约束集合,因此没法把各种情形写进同一组变量里。
把未知的数目放在外层循环中穷举
由名次得到分数的两种写法,即
If与Function用
m[score_func]查看模型中函数的内容
3 个人参加运动会,参赛者只有这 3 人。每项竞赛给第 1 名、第 2 名、第 3 名分别记分,分数在所有竞赛中始终固定,即第 1 名总是 X 分,第 2 名总是 Y 分,第 3 名总是 Z 分。
分数是满足
X > Y > Z > 0的整数。现在所有竞赛都已结束,结果是
A 总共 22 分
B 在标枪中得了第 1 名,总共 9 分
C 总共 9 分
那么,100 米赛跑的第 2 名是谁?
※ 本题参考了 https://sist8.com/3ath。
难点在哪里#
竞赛数目未知:3 人的合计分数是已知的,但一共举行了几项竞赛并没有写出来
X、Y、Z 未知:只知道
X > Y > Z > 0这个大小关系各项竞赛的名次未知:除了 B 在标枪中得第 1 名之外,谁得了第几名并不清楚
这些都是值未知的量,因此可以直接交给 Z3 作为变量,只写出条件即可。只有竞赛数目会改变变量的个数,因此要用循环来试。分数 X、Y、Z 对所有竞赛相同,无论几项竞赛,这三个变量始终只有三个。
改变竞赛数目求解#
try_solve(n) 是假定竞赛举行了 n 次来求解的函数。有解时显示结果并返回 True,无解时返回 False。项数一改,这道题就变成一个全新的约束问题,之前求出的分数与名次都不再沿用。
from z3 import *
def try_solve(n_games):
"""假设项目数为 n_games 来求解。有解就显示并返回 True"""
X, Y, Z = Ints('X Y Z') # 第 1 名、第 2 名、第 3 名的得分
solver = Solver()
solver.add(X > Y, Y > Z, Z > 0)
# games['A', i] 是“A 在第 i 个项目中的名次”
games = {}
for i in range(n_games):
for p in 'ABC':
games[p, i] = Int(f'{p}_rank_{i}')
solver.add(games[p, i] >= 1, games[p, i] <= 3)
solver.add(Distinct(games['A', i], games['B', i], games['C', i]))
def score(p):
"""p 的总得分:按名次加上 X、Y、Z"""
return Sum([If(games[p, i] == 1, X, If(games[p, i] == 2, Y, Z))
for i in range(n_games)])
solver.add(score('A') == 22)
solver.add(score('B') == 9)
solver.add(score('C') == 9)
solver.add(games['B', 0] == 1) # 第 0 个项目(标枪)中 B 是第 1 名
if solver.check() != sat:
print(f"项目数 {n_games}: 无解")
return False
m = solver.model()
print(f"项目数 {n_games}: 第1名={m[X]}分, 第2名={m[Y]}分, 第3名={m[Z]}分")
ranks = {p: [m[games[p, i]].as_long() for i in range(n_games)] for p in 'ABC'}
for i in range(n_games):
name = '标枪' if i == 0 else f'项目{i}'
print(' ' + name + ': ' + ', '.join(f'{p}=第{ranks[p][i]}名' for p in 'ABC'))
return True
for n in range(2, 10):
if try_solve(n):
break
项目数 2: 无解
项目数 3: 无解
项目数 4: 无解
项目数 5: 第1名=5分, 第2名=2分, 第3名=1分
标枪: A=第2名, B=第1名, C=第3名
项目1: A=第1名, B=第3名, C=第2名
项目2: A=第1名, B=第3名, C=第2名
项目3: A=第1名, B=第3名, C=第2名
项目4: A=第1名, B=第3名, C=第2名
竞赛数为 2、3、4 时都没有解,到 5 项竞赛才首次解出,此时的分数为第 1 名 5 分、第 2 名 2 分、第 3 名 1 分,即每项竞赛 8 分,5 项共 40 分,恰好等于 22+9+9。
再看名次,A 在标枪以外的 4 项竞赛中都是第 1 名,B 在这 4 项中都是第 3 名,而 C 都是第 2 名,因此 100 米赛跑的第 2 名是 C。
用 If 可以写成名次为 1 时取 X、为 2 时取 Y、其余取 Z,缺点是嵌套一深就不易读。三个名次只需要两层嵌套;名次再多,嵌套层数就跟着增加,读到某一层对应哪个名次会越来越吃力。
用 Function 改写#
由名次到分数的转换,也可以用 Function() 作为表交给 Z3。嵌套的 If 消失之后,代码也随之变短。两者表达的是同一个映射,区别只在约束的形式:一组等式,还是一串条件。
score_func = Function('score', IntSort(), IntSort())
各个参数的含义如下。
'score':函数的名字,供显示用最后的
IntSort():返回值的类型中间的
IntSort():参数的类型
名次与分数都是整数,因此两者都取 IntSort()。
接着把第 1 名取 X 分、第 2 名取 Y 分、第 3 名取 Z 分这个对应关系作为约束追加进去。
solver.add(
score_func(1) == X,
score_func(2) == Y,
score_func(3) == Z,
)
这样 score_func(名次) 就得到对应的分数。与 If 的写法相比,多的是一条函数声明与三条等式,少的是逐层嵌套。
def try_solve(n_games):
"""用 Function 重写的版本"""
X, Y, Z = Ints('X Y Z')
solver = Solver()
solver.add(X > Y, Y > Z, Z > 0)
# 把名次 → 得分 的对应关系当作函数持有
score_func = Function('score', IntSort(), IntSort())
solver.add(score_func(1) == X, score_func(2) == Y, score_func(3) == Z)
games = {}
for i in range(n_games):
for p in 'ABC':
games[p, i] = Int(f'{p}_rank_{i}')
solver.add(games[p, i] >= 1, games[p, i] <= 3)
solver.add(Distinct(games['A', i], games['B', i], games['C', i]))
def score(p):
return Sum([score_func(games[p, i]) for i in range(n_games)])
solver.add(score('A') == 22)
solver.add(score('B') == 9)
solver.add(score('C') == 9)
solver.add(games['B', 0] == 1)
if solver.check() != sat:
print(f"项目数 {n_games}: 无解")
return False
m = solver.model()
print(f"项目数 {n_games}: 第1名={m[X]}分, 第2名={m[Y]}分, 第3名={m[Z]}分")
ranks = {p: [m[games[p, i]].as_long() for i in range(n_games)] for p in 'ABC'}
for i in range(n_games):
name = '标枪' if i == 0 else f'项目{i}'
print(' ' + name + ': ' + ', '.join(f'{p}=第{ranks[p][i]}名' for p in 'ABC'))
# 从模型里取出函数本身,就能看到定义好的对应关系
print(' score_func:', m[score_func])
return True
for n in range(2, 10):
if try_solve(n):
break
项目数 2: 无解
项目数 3: 无解
项目数 4: 无解
项目数 5: 第1名=5分, 第2名=2分, 第3名=1分
标枪: A=第2名, B=第1名, C=第3名
项目1: A=第1名, B=第3名, C=第2名
项目2: A=第1名, B=第3名, C=第2名
项目3: A=第1名, B=第3名, C=第2名
项目4: A=第1名, B=第3名, C=第2名
score_func: [1 -> 5, 3 -> 1, else -> 2]
用 m[score_func] 可以取出函数的内容,显示出来的 [1 -> 5, 3 -> 1, else -> 2] 表示第 1 名 5 分、第 3 名 1 分、其余即第 2 名 2 分,这份表可以逐项核对。Z3 只列出必须区分的取值,剩下的用 else 概括,因此表比逐项列举要短。
小结#
结果 |
|
|---|---|
竞赛数 |
2、3、4 无解,5 项竞赛时才有解 |
分数 |
第 1 名 5 分、第 2 名 2 分、第 3 名 1 分,满足 X > Y > Z > 0 |
100 米赛跑的第 2 名 |
C,因为 C 在标枪以外的所有竞赛中都是第 2 名 |
未知的个数放在外层循环里由小到大试
由名次到分数的转换,用
If与Function都能写。表一旦变大,Function更易读用
m[函数]可以确认模型所决定的函数内容