数字棒球游戏是这样玩的:先由甲在心里想一个4位数,比如3092,不足4位的以0补足,比如12被认为是0012,这个数被称为目标。乙来猜这个数,方法是这样的,他先随机对甲说一个数,比如5382,甲告诉乙5382里有几个ball几个strike。所谓ball是指5382中有几个数出现在目标中,因为数字3和2都出现过,所以这里有ball=2。所谓strike是指在这2个ball中有几个位置是正确的。比如,上例中数字2的位置正确,所以strike=1。乙随后会进行分析,然后再猜一个数,甲再告诉乙ball和strike分别等于几。这个过程不断重复,直到乙猜出目标。注意,strike永远小于等于ball。以下是一个例子:
甲:我想好了一个四位数
乙:0115
甲:ball=1,strike=0
乙:5623
甲:ball=0,strike=0
乙:1798
甲:ball=1,strike=1
乙:8088
甲:ball=3,strike=1
乙:4088
甲:猜对了!
本节的目标是编写一个程序,人类出题,电脑来猜。大家看到这个问题很容易想到这样一个思路,当猜测5382得知ball=2,strikes=1时,意味着目标的形状像5*3*,5**3,*35*,*3*5,58**,5**8,......。把所有这些可能的模式记下来,然后在其中随机挑选一个,比如*35*,再随机拼出一个数据比如1359,让人类告知对比结果,再用结果更新上面的模式,......。这个过程比较复杂,事实上,我们还有一个简洁的思考方式:逆向思维+数据结构。
所谓逆向思维,就是说,不去试图建立目标的模式,而是反过来,排除不可能的模式。比如猜测5382得b2s1,那就把0~9999这一万个数中与5382对比不是b2s1的数排除。这样做的结果是确定的,不像正向思维得到的那么多的模式都是不确定的。
逆向思维的数据结构是一个含有0~9999这10000个数的列表candidates,每个数又用含有4个数字的列表表示。比如123就用[0, 1, 2, 3]表示。电脑首先从candidates中随机挑选一个数A猜测,人类告知ball和strike的值后,程序从candidates中删除一些数,这些数与A对比后的结果与上述ball及strike不等。然后电脑再从精简后的candidates里随机挑选一个数猜测,重复以上过程,直到乙猜出了这个数为止。
可见,只要建立了合适的数据结构——哪怕这个数据结构很简单——问题的困难也能迎刃而解。代码如下:
Python解决数字棒球游戏
# 文件: p04_03_ball_strike.py
import random
def get_candidates(num=4): # 获取所有4位候选数
return [to_list(i, num) for i in range(int(10**num))]
def to_list(n, num): # 把数n转为num位数字列表
result = []
for _ in range(num):
result.insert(0, n % 10)
n //= 10
return result
def get_balls_strikes(goal, guess): # 获取两个数对比后的结果
balls = 0
goal_copy = goal.copy()
for digit in guess:
if digit in goal_copy:
balls += 1
pos = goal_copy.index(digit)
goal_copy[pos] = -1 # 每找到一个数就填上-1
strikes = 0
for d1, d2 in zip(guess, goal): # 按位对比
if d1 == d2:
strikes += 1
return balls, strikes
def guess(candidates): # 从候选数中随机挑选一个数
return candidates[random.randint(0, len(candidates)-1)]
def extract(candidates, guess, balls, strikes):
# 删除不可能的候选数
for i in range(len(candidates)-1, -1, -1):
bs, ss = get_balls_strikes(candidates[i], guess)
if ss != strikes or bs != balls: # 如果对比结果不匹配
del candidates[i]
def to_str(digit_list):
return ''.join([chr(ord('0') + d) for d in digit_list])
if __name__ == '__main__':
num = 4
goal = input('请输入目标,一个最多%d位的正整数:' % num)
goal = to_list(int(goal), num)
candidates = get_candidates()
guesses = 0
while True:
g = guess(candidates)
guesses += 1
print("我猜:", to_str(g))
if g == goal:
break
balls, strikes = get_balls_strikes(goal, g)
print("\tballs =", balls, "strikes =", strikes)
extract(candidates, g, balls, strikes)
print('猜对了,共猜了%d次' % guesses)
程序运行的结果如下:
请输入目标,一个最多4位的正整数:3450
我猜: 5848
balls = 2 strikes = 0
我猜: 4553
balls = 3 strikes = 1
我猜: 4365
balls = 3 strikes = 0
我猜: 3574
balls = 3 strikes = 1
我猜: 3450
猜对了,共猜了5次