汽车加油问题——贪心算法初探

最近开始准备考研复试上机,因为是跨考,毫无编程基础,所以从九度的考研机试教程开始练习。前二十题都是都是顺风顺水,轻松完成。练到贪心算法,就开始有点卡壳,一直做到了浙大2012年的机试真题To Fill or not to Fill,据说当年只有三个人全对,心想书上不是说贪心算法很简单么,这题真有这么难?于是满怀信心地就着手开始整。几个小时过去了,不但编译通过都花了很久,而且始终怀疑自己的算法是否正确。直到无论如何也得不到样例的正确答案,我终于放弃了,开始在网上寻求帮助,然后幸运地发现了这个帖子http://blog.csdn.net/wzy_1988/article/details/8607208。其实这个帖子已经很清楚地解决了这个问题,只是作为初学者的小晓鸟,在看了AC的代码后,又自己独立得尝试了很久,花费了半天左右的时间才最终得以用C++实现该算法,也才真正理解了这个帖子中的思想,所以想详细地记录下自己的犯错和学习过程,此为背景。

所谓贪心算法,按我通俗的理解,就是总是想占最大的便宜。例如买东西的时候,由于钱的总量有限,要想最大化获得的使用价值,就要总是优先买性价比最高的东西,把性价比最高的东西全买完后,如果还有钱,就接着买剩下的商品里性价比最高的,直到把钱花完(九度考研机试教程第21题)。那么在“贪心”的时候,就一定要找到一个“贪心”的对象,例如上例中的性价比,以它为指标衡量是不是够最“贪心”。然而,这个“贪心”对象并不总是那么容易找到。就像我倒腾了好久的这道To Fill or not to Fill汽车加油题。

题目描述:

With highways available, driving a car from Hangzhou to any other city is easy. But since the tank capacity of a car is limited, we have to find gas stations on the way from time to time. Different gas station may give different price. You are asked to carefully design the cheapest route to go.

输入:

For each case, the first line contains 4 positive numbers: Cmax (<= 100), the maximum capacity of the tank; D (<=30000), the distance between Hangzhou and the destination city; Davg (<=20), the average distance per unit gas that the car can run; and N (<= 500), the total number of gas stations. Then N lines follow, each contains a pair of non-negative numbers: Pi, the unit gas price, and Di (<=D), the distance between this station and Hangzhou, for i=1,…N. All the numbers in a line are separated by a space.

输出:

For each test case, print the cheapest price in a line, accurate up to 2 decimal places. It is assumed that the tank is empty at the beginning. If it is impossible to reach the destination, print “The maximum travel distance = X” where X is the maximum possible distance the car can run, accurate up to 2 decimal places.

样例输入:
50 1300 12 8
6.00 1250
7.00 600
7.00 150
7.10 0
7.20 200
7.50 400
7.30 1000
6.85 300
50 1300 12 2
7.10 0
7.00 600
样例输出:
749.17
The maximum travel distance = 1200.00

大意为,在一个固定长度的路线上,有多个加油站,而且各个加油站的油价不总是相同,现让油箱为空的一辆车行驶完这条路线,怎样能使所花费的油钱最少。这虽然实质上也是一个性价比的问题,要得到性价比最优的解,然后这里的性价比的计算并不是像计算商品的性价比一样用简单的除法就能解决。因为至少要考虑到如下几个问题:

1.油箱容量有限,所以汽车不可能随意开到路线上的任一点然后加油,不一定总是能在油价最低的加油站加油然后驶完全程。

2.加油站的位置是固定的,可能两个车站之间太远以至于满油箱的汽车还没开到下一个加油站就没油了,所以这种情况下不能驶完全程。

3.当找到局部最优解的时候,并不是直接将油箱加满然后去找下一个局部最优解就可以了,而是要尽量的省油,尤其是高价油,用得越少越好。

我的第一思路,是将各个收费站按油价排序,然后找目前可以到达的最低价加油站,加够油过去,然后再找下一个可以到达的低价站。显然,这就是忽略了上面的第3个问题,局部最优解不一定是整体最优解。例如这道题中,从起点可以到达的最低价站为(6.85,300),过去需要花7.10*300的油费,而如果先到(7.00,150)再到(6.85,300),只需花7.10*150+7.00*150。原因就在于,目前使用的油的油费在这段路程中并不是最低的,所以,一旦在前方有更低价的站,一定要把油花光过去换低价油。

看了上述帖子的算法,我学习到了按收费站到起点的距离排序以及如何找低价站的方法。然而,还是没有领悟改算法的真谛,沿用了一定要找到最低价站然后过去的方法。事实上,该算法一每一次加油为单位进行考察,决定下一次到哪个加油站以及在去之前要加多少油,同时解决了现在和未来的事情。汽车到一个加油站,可能有以下几种状态:

1.油箱中还有油,这是已经花出去的钱,无法改变,所以要充分利用这些油,看看能不能再油花完之前找到更低价的站。如果有比当前站更低价的,就开过去;否则,只能在当前站加油。

2.如果在当前站加油,加多少才最省钱呢?显然,加得越少越好,如果不用把油箱加满就可以找到比当前站更低价的站,那就是最好情况,即找到满油箱可到达范围之内的最近的一个比当前站低价的站,加了相应的油之后开过去,到达时正好把油花完,这样高价油的用量最省。

3.如果满油箱可到达范围之内没有比当前站低价的站,即在最大范围内,当前站最低价,那么当然是把最低价油加满(相同距离最省钱),下一站应该去该范围内的次低价站,这样,就算下一站要加油,也是那时所以可能里最低价的。

4.如果满油箱可到达范围之内都没有加油站的话,即上述第2个问题,那么无法到达终点,最远行驶距离为当前站加上把满油耗尽所能行驶的距离。

如果到达下一站,就重复上述判断,这样就能保证总是占最大的便宜。总体思想就是,想尽一切办法找最便宜的加油站,并使开到那一站时油正好花完,若当前站最便宜,就把油加满接着找最便宜的站,这就是“贪心”的精髓。

附上AC的代码

#include <stdio.h>
#include <algorithm>
using namespace std;

struct Station
{
       double p;
       double d;
       bool operator <(const Station &a) const
       {
            return d<a.d;
       }
}buf[501];

struct Tank
{
      double left;
      double cost; 
      double d;
}tank;


int main()
{
    double Cmax,D,Davg;
    int n;
    while (scanf("%lf %lf %lf %d", &Cmax, &D, &Davg, &n)!=EOF)
    {
          for (int i=0;i<n;++i)
          {
              scanf("%lf %lf", &buf[i].p, &buf[i].d);
          }
          buf[n].d=D;
          buf[n].p=0;
          double farest=Cmax*Davg;
          sort(buf, buf+n);
          int i=0;
          tank.left=0;
          tank.cost=0;
          tank.d=0;
          if (buf[0].d!=0)
              printf("The maximum travel distance = 0.00\n");
          else
          {
              
              while (i<n)
              {
                  double lowest=buf[i].p;
                  int index=i;
                  for (int j=i+1;j<=n&&buf[j].d-buf[i].d<=tank.left*Davg;++j)  //找出油耗完前比当前站便宜的最低价站
                      if (buf[j].p<lowest)
                      {
                         index=j;
                         lowest=buf[j].p;
                      }
                  if (index!=i)                                               //对应上述状态1,直接开过去加油
                  {
                     tank.left-=(buf[index].d-buf[i].d)/Davg;
                     i=index;
                     tank.d=buf[i].d;             
                  }
                  else                                                       //否则,找出满油箱可到达范围内最近的比当前站低价站
                  {
                      for (int j=i+1;j<=n&&buf[j].d<=buf[i].d+farest;++j)
                      {
                          if (buf[j].p<lowest)
                          {
                             index=j;
                             break;                     
                          }    
                      }
                      if (index!=i)                                         //对应上述状态2,加上刚好够的油开过去把油耗尽
                      {
                         tank.cost+=((buf[index].d-buf[i].d)/Davg-tank.left)*buf[i].p;
                         tank.left=0;
                         i=index;
                         tank.d=buf[i].d;
                      }
                      else                                                 //否则,把油加满,然后找到满油箱可到达范围内次低价站
                      {
                          tank.cost+=(Cmax-tank.left)*buf[i].p;
                          tank.left=Cmax;
                          double low=1000000;
                          for (int j=i+1;j<=n&&buf[j].d<=buf[i].d+farest;++j)
                              if (buf[j].p<low)
                              {
                                 index=j;
                                 low=buf[j].p;                
                              }
                          if (index!=i)                                    //对应上述状态3,开到次低价站
                          {
                            tank.left-=(buf[index].d-buf[i].d)/Davg;
                            i=index;
                            tank.d=buf[i].d;
                          }
                          else break;                                      //对应上述状态4,没有下一站
                      }
                  }
              }
              if (i==n) printf("%.2lf\n", tank.cost);
              else     printf("The maximum travel distance = %.2lf\n", tank.d+tank.left*Davg);
          }
     
    }
    return 0;
}

水平有限,暂时不会证明该算法。以上仅为个人心得,非常希望大神们批评指正,以帮助我快速学习和进步,谢谢!!!

    原文作者:贪心算法
    原文地址: https://blog.csdn.net/u013457107/article/details/18365097
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞