问题:假设有一个3升的杯子和一个5升的杯子以及充足的水源,现在问如何使用这两个杯子精确的取出4升的水。
简单粗暴无脑法
武功秘籍:
第一式:先选择小杯子装满水然后倒在大杯子,直到大杯子装满
第二式:大杯子水倒掉,把小杯子剩下的水倒进大杯子
第三式:无限循环第一式和第二式,至此武功大成
实战:
假设3升的杯子为C1,5升的杯子为C2
第一步:C1装满倒入C2,此时C1=0,C2=3
第二步:C1装满导入C2,此时C1=1, C2=5
第三步:倒掉C2,然后C1倒入C2, 此时C1=0,C2=1
第四步:C1装满导入C2, 此时C1=0,C2=4
完成
解析
当两个杯子的容量数互质时(假设一个3升,一个5升),他们的最大公约数为1(Gcd(a,b)=1)。
第一次小杯子往大杯子倒水其实就等于C1对C2求余(即3 / 5 = 0…3)
第二次小杯子往大杯子倒水相当于2C1对C2求余(即 2*3 / 5=1…1)
第三次小杯子往大杯子倒水相当于3C1对C2求余(即3*3 / 5 =1…4)
注意
余数就是最终想要获取的水的升数,商就代表大杯子会被装满几次
总结
根据解析求该问题的解其实就是在求
C1*X mod C2 = 4 (小的数的X倍对大的数求余等于想要的数)
看到这里就要开始我们的第二种方法了
扩展欧几里得算法及贝祖等式求解
课前扩展
贝祖定理: 如果a、b是整数,那么一定存在整数x、y使得ax+by=gcd(a,b)
贝祖等式定义: ax + by = m 有整数解时,当且仅当m是 a和b的最大公约数的倍数。
贝祖等式逆命题: ax + by = m 当m为a,b最大公约数的倍数时,一定存在整数解使得等式成立【证明自行搜索】
欧几里得算法: 欧几里得算法又称辗转相除法,是指用于计算两个非负整数a,b的最大公约数。计算公式gcd(a,b) = gcd(b,a mod b)
扩展欧几里得算法 已知整数a、b,扩展欧几里得算法可以在求得a、b的最大公约数的同时,能找到整数x、y(其中一个很可能是负数),使它们满足贝祖等式
题目解析
暂且令a=3,b=5,m=4(3,5互质,其她们的最大公约数为1,m为想要获得的水量),来看这道题
首先根据我们简单粗暴法获得结论:ax mod b = m
转换后 ax / b = y…m (注意在这里x,y都为正整数)
在转换后 ax=by+m
此时我们令y为负数的时候可以写成 ax + by = m
此时看样子是不是很像贝祖等式呢?
因为m=4 是gcd(a,b)=1的倍数,所以该题有解。即求出x则方案已出【数学求解后续补上,暂时使用代码】。
答案是:Gcd=1,x=2,y=-1
即a,b的最大公约数是1,当a往b中倒入2次水后会得到4升水,此时b会被倒满过1次
【解释一下:因为在简单组爆法里边x代表小杯子要往大杯子倒的次数,y代表大杯子会被装满几次,所以这里x,y都只能是正整数。当令y为负数的时候by就可以写到左边,也就是说求解的时候y大概率是负数】
扩展欧几里得算法代码实现
python版本
def ext_euclidean(a, b):
if b == 0:
return a, 1, 0
gcd, x1, y1 = ext_euclidean(b, a % b)
x = y1
y = x1 - (a // b) * y1
return gcd, x, y
go版本
func extEuclidean(a, b int) (int, int, int) {
if b == 0 {
return a, 1, 0
}
gcd, x1, y1 := extEuclidean(b, a%b)
x := y1
y := x1 - (a/b)*y1
return gcd, x, y
}
总结
当所需要的水的容量是两个杯子容量的最大公约数的倍数时,改题目有解,否则无解【不信的犟种可以试试2升和4升的杯子获取3升的水】。只需要根据扩展欧几里得算法求出x,y即可。
注意:
虽然该方法能够获取答案,但是不一定是最优答案
接下来进入到我们的最后环节
贝祖等式结合广度优先算法求最优解
解题思路
第一步:定义当前杯子状态
第二步:从该状态开始,考虑所有可能的倒水操作,生成最新的状态
第三步:将最新状态存储,若该状态没有被存储过则将该状态和倒水步骤放进队列
第四步:循环队列,直到找到最终想要的状态。若队列结束都没有想要的状态则无解
思路解释 每一次添加的队列就是一种可能的解法,当一次倒水操作后若发现状态被存储过则说明此次状态以及在更少的步骤到达过了,非最优解。所以放弃本次操作,继续其他操作。当第一次发现满足条件的状态的时候输出步骤就是最优解。【后续会画一个图解释】
代码实现
python版本
from collections import deque
# 广度优先算法
def pour_water(a_capacity, b_capacity, target):
queue = deque([(0, 0, [])])
visited = {(0,0),}
while queue:
current_a, current_b, path = queue.popleft()
if current_a == target or current_b == target:
return path
# 倒满A杯子
if (a_capacity, current_b) not in visited:
queue.append((a_capacity, current_b, path + [(current_a, current_b, 'fill A')]))
visited.add((a_capacity, current_b))
# 倒满B杯子
if (current_a, b_capacity) not in visited:
queue.append((current_a, b_capacity, path + [(current_a, current_b, 'fill B')]))
visited.add((current_a, b_capacity))
# 清空A杯子
if (0, current_b) not in visited:
queue.append((0, current_b, path + [(current_a, current_b, 'empty A')]))
visited.add((0, current_b))
# 清空B杯子
if (current_a, 0) not in visited:
queue.append((current_a, 0, path + [(current_a, current_b, 'empty B')]))
visited.add((current_a, 0))
# A杯子倒入B杯子
pour_amount = min(current_a, b_capacity - current_b)
if (current_a - pour_amount, current_b + pour_amount) not in visited:
queue.append((current_a - pour_amount, current_b + pour_amount, path + [(current_a, current_b, 'pour A to B')]))
visited.add((current_a - pour_amount, current_b + pour_amount))
# B杯子倒入A杯子
pour_amount = min(a_capacity - current_a, current_b)
if (current_a + pour_amount, current_b - pour_amount) not in visited:
queue.append((current_a + pour_amount, current_b - pour_amount, path + [(current_a, current_b, 'pour B to A')]))
visited.add((current_a + pour_amount, current_b - pour_amount))
return None
# 测试
a_capacity = 2
b_capacity = 4
target = 3
solution = pour_water(a_capacity, b_capacity, target)
if solution:
print("最优解的方案如下:")
for step, action in enumerate(solution):
print(format(f"步骤{step + 1}: {action[2]}","<30"),f"当前状态:(A={action[0]}, B={action[1]})")
else:
print(f"无法通过倒水操作得到{target}升水")
go版本
package main
import (
"fmt"
)
type state struct {
a, b int
path []string
}
func pourWater(aCapacity, bCapacity, target int) []string {
queue := []state{{0, 0, nil}}
visited := make(map[string]bool)
for len(queue) > 0 {
currentState := queue[0]
queue = queue[1:]
if currentState.a == target || currentState.b == target {
return currentState.path
}
// Fill A cup
if _, found := visited[fmt.Sprintf("%d_%d", aCapacity, currentState.b)]; !found {
visited[fmt.Sprintf("%d_%d", aCapacity, currentState.b)] = true
queue = append(queue, state{aCapacity, currentState.b, append(currentState.path, "fill A")})
}
// Fill B cup
if _, found := visited[fmt.Sprintf("%d_%d", currentState.a, bCapacity)]; !found {
visited[fmt.Sprintf("%d_%d", currentState.a, bCapacity)] = true
queue = append(queue, state{currentState.a, bCapacity, append(currentState.path, "fill B")})
}
// Empty A cup
if _, found := visited[fmt.Sprintf("%d_%d", 0, currentState.b)]; !found {
visited[fmt.Sprintf("%d_%d", 0, currentState.b)] = true
queue = append(queue, state{0, currentState.b, append(currentState.path, "empty A")})
}
// Empty B cup
if _, found := visited[fmt.Sprintf("%d_%d", currentState.a, 0)]; !found {
visited[fmt.Sprintf("%d_%d", currentState.a, 0)] = true
queue = append(queue, state{currentState.a, 0, append(currentState.path, "empty B")})
}
// Pour A to B cup
pourAmount := min(currentState.a, bCapacity-currentState.b)
if _, found := visited[fmt.Sprintf("%d_%d", currentState.a-pourAmount, currentState.b+pourAmount)]; !found {
visited[fmt.Sprintf("%d_%d", currentState.a-pourAmount, currentState.b+pourAmount)] = true
queue = append(queue, state{currentState.a - pourAmount, currentState.b + pourAmount, append(currentState.path, "pour A to B")})
}
// Pour B to A cup
pourAmount = min(aCapacity-currentState.a, currentState.b)
if _, found := visited[fmt.Sprintf("%d_%d", currentState.a+pourAmount, currentState.b-pourAmount)]; !found {
visited[fmt.Sprintf("%d_%d", currentState.a+pourAmount, currentState.b-pourAmount)] = true
queue = append(queue, state{currentState.a + pourAmount, currentState.b - pourAmount, append(currentState.path, "pour B to A")})
}
}
return nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
aCapacity := 5
bCapacity := 3
target := 4
solution := pourWater(aCapacity, bCapacity, target)
if solution != nil {
fmt.Println("最优解的方案如下:")
for step, action := range solution {
fmt.Printf("步骤%d: %s,当前状态:(A=%d, B=%d)\n", step+1, action, aCapacity, bCapacity)
}
} else {
fmt.Printf("无法通过倒水操作得到%d升水\n", target)
}
}