百马百担货物问题
讲解这个问题的应该挺多的,这里我在整理一次,方便以后自己快速找到吧
题目描述
百马百担问题:有 100 匹马,驮 100 担货,大马驮 3 担,中马驮 2 担,两匹小马驮 1 担,问有大、中、小马各多少?
这里我们用变量small、mid、big分别代表小、中、大马的数量,本文代码均为C/C++代码
方法一:暴力方法(100w次)
这题很容易想到的是,写一个三层循环遍历所有small、mid、big的情况,然后去验证答案。
代码如下:
for(big = 0; big <= 100; big++){
for(mid = 0; mid <= 100; mid++){
for(small = 0; small <= 100; small++){
if(big+mid+small == 100 && small%2 == 0){
if(big*3+mid*2+small/2 == 100){
printf("%d %d %d",big,mid,small);
}
}
}
}
}
显然该方法需要循环101×101×101= 1030301 次,循环百万次!
下面我们将通过 降维 以及 缩小边界、数学技巧 等 剪枝方法 将其优化到7次比较
方法二:降维思想(百万次)
我们知道了马的总数100,当我们知道大马和中马数量时,是不是就能够确定小马数量了,所以没必要去遍历小马个数,直接用100-big-mid表示small即可
代码如下:
for(big = 0; big <= 100; big++){
for(mid = 0; mid <= 100; mid++){
small = 100 - big - mid;
if(big+mid+small == 100 && small%2 == 0){
if(big*3+mid*2+small/2 == 100){
printf("%d %d %d",big,mid,small);
}
}
}
}
上述方法将循环从三层变成两层,循环次数为101×101= 10201 次,循环万次!,可见降维对程序效率的改进是很大的。
方法三:缩小边界(千次)
在方法二的基础上,我们还可以发现big不可能超过33,mid不可能超过50,这里也看出来为什么要降维small,而不是其他的,显然是因为small能缩小的边界大小远不如另外两个。
代码如下:
for(big = 0; big <= 33; big++){
for(mid = 0; mid <= 50; mid++){
small = 100 - big - mid;
if(big+mid+small == 100 && small%2 == 0){
if(big*3+mid*2+small/2 == 100){
printf("%d %d %d",big,mid,small);
}
}
}
}
该方法循环次数为34*51= 1734 次,即千次循环
方法四:进一步降维(34次)
什么还能降维?是的,可以的。
仔细想想,根据货物总数,我们有方程,
b i g ∗ 3 + m i d ∗ 2 + s m a l l / 2 = 100 big*3+mid*2+small/2=100 big∗3+mid∗2+small/2=100
然后根据马总数,进行降维(用100-big-mid代替small),有方程,
b i g ∗ 3 + m i d ∗ 2 + ( 100 − b i g − m i d ) / 2 = 100 big*3+mid*2+(100-big-mid)/2=100 big∗3+mid∗2+(100−big−mid)/2=100
优化一下除法,两边同时乘2,这里要约束small为偶数,然后有方程,
b i g ∗ 6 + m i d ∗ 4 + 100 − b i g − m i d = 200 big*6+mid*4+100-big-mid=200 big∗6+mid∗4+100−big−mid=200
然后发现这不就是个二元一次方程吗?整理下式子,得到mid的表达式(因为big的边界更小),
m i d = ( 100 − b i g ∗ 5 ) / 3 mid=(100-big*5)/3 mid=(100−big∗5)/3
欸!mid可以用big表示,small可以用big和mid表示,那么我们知道big不就相当于知道mid和small了嘛,所以只遍历big即可。
代码如下:
for(big = 0; big <= 33; big++){
mid=(100 - big * 5) / 3;
small = 100 - big - mid;
if(big+mid+small == 100 && small%2 == 0){
if(big*3+mid*2+small/2 == 100){
printf("%d %d %d",big,mid,small);
}
}
}
这里的循环就只有34次了。
方法五:进一步缩小边界(21次)
m
i
d
=
(
100
−
b
i
g
∗
5
)
/
3
mid=(100-big*5)/3
mid=(100−big∗5)/3
m
i
d
>
=
0
mid>=0
mid>=0
可以发现,big的边界可以继续缩小到20
代码如下:
for(big = 0; big <= 20; big++){
mid=(100 - big * 5) / 3;
small = 100 - big - mid;
if(big+mid+small == 100 && small%2 == 0){
if(big*3+mid*2+small/2 == 100){
printf("%d %d %d",big,mid,small);
}
}
}
循环到此就只有21次了
方法六:数学技巧(7次)
考虑到方法五中的式子 m i d = ( 100 − b i g ∗ 5 ) / 3 mid=(100-big*5)/3 mid=(100−big∗5)/3,这里想让这个式子成立,那么 100 − b i g ∗ 3 100-big*3 100−big∗3就必须是3的倍数。而争取答案的第一个值为2,那么我们可以让big=2,然后每次遍历步长为3,让其遍历的上界即可。
代码如下:
for(big = 2; big <= 20; big+=3){
mid=(100 - big * 5) / 3;
small = 100 - big - mid;
if(big+mid+small == 100 && small%2 == 0){
if(big*3+mid*2+small/2 == 100){
printf("%d %d %d",big,mid,small);
}
}
}
这里big的遍历值有2,5,8,11,14,17,20。
显然,仅用7次循环!,至此该问题完成了从百万次循环到七次循环的优化。