百马百担问题优化:降维思想,循环从百万次到七次

百马百担货物问题

讲解这个问题的应该挺多的,这里我在整理一次,方便以后自己快速找到吧

题目描述

百马百担问题:有 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次循环!,至此该问题完成了从百万次循环到七次循环的优化。

上一篇:gym 100958 b 题解


下一篇:排序算法:选择排序