问题
一辆载油500升的汽车从A开往1000公里外的B,已知汽车每公里耗油量为1升,A处有无穷多的油,其他任何地点都没有油,但该车可以在任何地点存放油以备中转,问从A到B最少需要多少油?
分析
这道题目初一看觉得方案太灵活,有点晕,网上所有对此题目的答案千篇一律的是:
题目可归结为求数列 an=500/(2n+1) n=0,1,2,3......的和Sn什么时候大于等于1000,解得n>6
当n=6时,S6=977.57
所以第一个中转点离起始位置距离为1000-977.57=22.43公里
所以第一次中转之前共耗油 22.43*(2*7+1)=336.50升
此后每次中转耗油500升
所以总耗油量为7*500+336.50=3836.50升
看起来很有道理,但是又没有说明为什么?害我抓破脑皮去想:这是为什么?怎么证明的?
不过我们简单推下:3836.50 - 7*(500 - 2*22.43) = 650.52,也就是说第一次中转跑7次会剩下650.52,证明这个答案是错误的!
后来我猜想出一种解释,首先我们需要抓住几个关键点:
1. 运输距离要尽量短,也就是说,我们的方案肯定是先从起点将所有需要的油搬到第一个x公里外的转运点,然后再以此类推,不能来回奔波于多个转运点。
2. 运输效率要尽可能高,也就是说,尽量以最大运输能力去跑——不要因为1、2公升油再跑一趟。
3. 相邻转运点之间跑的次数一定是奇数次。
由此,我们可以倒过来考虑:
1. 车到1000公里外的终点的时候,只需要放0的油在那里,转运次数1,可以求得距离是500;
2. 车到500公里外的转运点的时候,需要放500的油在那里,转运次数3,那么如何保证3次都是运输效率最高的呢?通过保证运运输效率最高,可以求得转运点的距离。上面也是这样求的。
3. 以此类推… 中转次数 1 3 5 7 …
解法一
/// <summary>
///计算在距离x的地方放a的油,跑2n+1次运输效率最高的距离
/// </summary>
double Distance(double a, int n)
{
return (tankLimit * (n + 1) - a) / (2 * n + 1);
}//x为距离,a = k(tankLimit - 2x) + 500 – x
/// <summary>
/// 在距离x的地方,存放amount的油需要跑的总次数
/// </summary>
int Trip(double x, double amount)
{
int n = 0;
while (true)
{
var a1 = 2 * n * (tankLimit - 2 * x);//来回运的油量
var a2 = tankLimit - x;//最后一次运的油量
var diff = a1 + a2 - amount;
if (diff >= 0 || -diff <= 0.0001) return 2 * n + 1;//避免计算误差
n++;
}
}
void Transport()//倒推中转
{
int k = 0;
double a = 0, d = 0;//a是每次中转的总耗油量,d是距离累计
while (d < 1000)
{
var last_a = a;
var x = Distance(a, 2 * k);
if (d + x <= 1000)
{
a += (2 * k + 1) * x;
k++;
}
else
{
x = 1000 - d;
a += Trip(x, a) * x;
}
d += x;
Console.WriteLine("a = {0:F} x = {1:F} Trip = {2}", a, x, Trip(x, last_a));
}
}
运行程序,可以求得:
a = 500.00 x = 500.00 Trip = 1
a = 1100.00 x = 200.00 Trip = 3
a = 1877.78 x = 155.56 Trip = 5
a = 2751.28 x = 124.79 Trip = 7
a = 2888.89 x = 19.66 Trip = 7
可以看出,如果采用这种运输方法,已经比上面网上流传的答案3836.50升少了很多了。但是,这种1 3 5 7…的运输方法就是最优的么?
解法二
或许我们并不需要1 3 5 7…这样递增这么快,1 3 3 3 5 5 5也可能是很好的方法,因此将程序小改动下:
double Transport(int factor)
{
…
var lastA = a;
var x = Distance(a, 2 * k);
if (x == 0) { k++; continue; }
if (d + x <= 1000)
{
a += (2 * k + 1) * x;
if (cnt++ % factor == 0) k++;
}
…
然后去计算factor从1~100的情况下耗油量分别是:
1 2888.88888888889
2 2507.65432098765
3 2376
4 2350.4
5 2340.16
6 2336.064
7 2334.4256
8 2333.77024
9 2333.508096
10 2333.4032384
11 2333.36129536
12 2333.344518144
13 2333.3378072576
14 2333.33512290304
15 2333.33404916122
可以看出,factor >15以后就趋于稳定了。
讨论
由此可以看出,不可以盲目迷信别人的解法(当然包括我现在的解法:)做到这里我们可以猜测,解法二也不能说就是最优了,可能存在一些更加优秀的序列——哈哈,说到这里,大家应该想到,这种问题最适合遗传算法了!我暂时没时间了,如果有兴趣的同学找到更优秀的解法麻烦通知我:)
作者:Silver 原文链接:http://yishan.cc/blogs/gpww/archive/2009/10/28/1-1.aspx
-------------------------------------
《编程之美——微软技术面试心得》源代码下载地址:
http://download.csdn.net/source/1883633或者http://www.broadview.com.cn/06074
另外,请大家帮个忙,来投票吧,看看《编程之美》第1章“游戏之乐——游戏中碰到的题目”大家最喜欢哪个题目,或者大家认为哪个题目最有趣。
在此先谢过大家了^_^ (可以选择以下任何网址参与投票)
CSDN:http://vote.csdn.net/VotePost.aspx?voteid=6419
开心网:http://www.kaixin001.com/~vote/detail.php?vid=6324994&uid=57936198
QQ空间:http://user.qzone.qq.com/80754098/vote/1021116304
其他文章:
连载1:卡特兰数(Catalan)
连载2:序列 ABAB对应字符串集合
连载3:最长公共子序列
连载4:计算字符串的相似度
连载5:寻找符合条件的整数
连载6:数组循环位移
连载7:天平秤球
连载8:动态有序集合——挑战红黑树
连载9:寻找最大的k 个数
连载10:一摞饼的排序
连载11:离散优化问题搜索框架 overview
连载12:时分秒针重合——看似简单的面试题