小猫爬山问题---贪心算法失效,深度优先搜索优化

提示

  小猫爬山问题正解在文章最后部分,如果对求解的曲折过程不感兴趣,可直接跳到文末。

问题描述

  小猫爬山问题是这样的:有n只小猫,每只小猫的质量分别为cat1, cat2, … ,cat n,现在它们已经爬到山顶,筋疲力尽,需要乘坐缆车下山(缆车只有一辆)。缆车最大载重量为w。问总共最少需要坐几趟多少缆车。

  这题需要我们编程求解,

输入格式为:

  第一行包含两个用空格隔开的整数,n和w。
  接下来n行每行一个整数,其中第i+1行的整数表示第i只小猫的质量​。

输出格式为:

  输出一个整数,表示最小需要的趟数。

贪心算法失效的论证

初次接触这类问题,我们一般倾向于利用贪心算法的思想寻找解决思路。一种粗暴的想法是,把这些猫的质量相加,再看看需要多少趟车才能运完。这样想的话:

 

 

这里“|”表示整除。但是,一只猫不应被杀死而分块运走。如缆车载重量为6,猫有三只,质量为5,4,3,按照公式,只要2趟即可完成。但是,仔细一想,便觉恐怖。实际上,这个情况需要运3趟。这个暴力方法不能用。

   当然,更好的贪心思路解法是,我们先根据质量大小对猫进行排序,按照从小到大或从大到小的顺序装车运走。但这个想法并不符合题意。我们仅看一个反例:缆车载重量为9,猫有9只,质量分别为1,2,3,4,5,6,7,8,9,按照这次“不残暴”的贪心思路(对于本反例,无论从大到小还是从小到大,均能得到下面的结果),应该分为6趟车运走:(1+2+3), (4+5), (6), (7), (8), (9) 。但实际上,5趟足矣:(1+8), (2+7), (3+6), (4+5), (9) 。因此,这个方法也不能用。

利用深搜解决及解法优化

    如果我们能把所有的分配情况都列举出来,一定能找出用车最少得情况,最终求出最少需要的趟数。这时,我们需要考虑深度优先搜索算法。深度优先搜索算法,简单地说就是一种能穷举所有可能结果的算法:这个算法可以通过函数递归实现,先通过不断的迭代获得一种结果,接着回溯,再迭代得到另一种结果;不断进行上述操作,直到获得所有结果。在取得每种结果时,可以获得解决问题所需要的信息,当然,我们也可以通过一些条件判断终止某类结果的求解过程,以节省时间与空间,这被称为“剪枝”。深度优先搜索是一种图论算法,我们可以在讲授算法的书中找到它的更严格,更一般的定义。

    我们可以通过从小到大枚举所用缆车的数量,并逐个验证缆车是否够用,来求出所需要的最少的缆车数量。因为,缆车够用即意味着每一只猫都能装在缆车中,我们只需要将每只猫在哪个缆车的所有情况列出,就可以直到缆车是否够用。

    于是,我们可以写出判断缆车是否够用的函数的代码:

 1 bool check(int num_cat, int num_car) { //判断缆车数是否够用,num_cat猫的编号,num_car车的数量
 2     if (num_cat >= n)    return true; //如果猫的编号等于猫的数量,说明已经把0~n-1共n只猫全部放入缆车,缆车够用
 3     else {
 4         bool ava = false; //用于找到可行方案后终止搜索
 5         for (int i = 0; i < num_car; ++i) { //将猫放在不同缆车的情况进行枚举
 6             if (car[i] + cat[num_cat] <= w) { //如果某个缆车能放下这只猫
 7                 car[i] += cat[num_cat]; //放下去
 8                 ava = check(num_cat + 1, num_car); //开始放下一只猫
 9                 if (ava) break; //如果这次放猫能导出可行方案,就终止搜索
10                 else car[i] -= cat[num_cat]; //否则,这只猫不能放这个缆车
11             }
12         }
13         return ava; //所有情况都列出后,返回是否可行
14     }
15 }

    一般来说,对于输入的猫的重量,我们可以直接使用。但是,恰当的顺序有利于减小程序的运行时间。

对于极端的输入数据:“18 100000000 18381246 29249683 12495474 24844134 96242521 67846996 945213 27675252 58653213 12062801 4830609 83790642 10682393 27267295 60527976 8881456 3916444 32450339”,在笔者的电脑上,测试数据如下:

 

情形

最终答案

耗时

不对猫的质量进行排序

6

0.308s

猫的质量从大到小

6

0.001s

猫的质量从小到大

6

860s

但是,按照这个思路写的代码提交到nowcoderoj上测试时,超时了。

   超时,很大一部分是枚举导致的,因为在缆车数从1n枚举的过程中,不可避免地出现重复的验证过程,造成时间上的额外开销。我们换个思路:直接求解所需的最小缆车数。

   为此,我们仍然从列举每只猫放在哪个缆车的所有情况出发,只不过,这回,缆车的数量不是个定值。为了减小求解过程,我们将猫的质量从大到小排序,相比于不排序,这样做可以让我们用更少的时间求出所用缆车的最小结果,并“剪枝”掉大量的搜索过程。完整的AC代码如下:

 1 #include <iostream>
 2 #include <algorithm>
 3 
 4 int n, w;
 5 int cat[25], car[25];
 6 
 7 int solve(int num_cat, int car_used) { //num_cat猫的编号 car_used放了猫的车的数量
 8     //这里,car_used处值为1,即一开始一定有一个缆车被放猫,我们也把这个缆车的编号看作1
 9     static int ans = n; //静态变量,用于存储、更新最佳答案,并及时终止多余的搜索
10     //由于缆车数不可能超过猫的总数n,因此取ans初值为n
11     if (car_used < ans) { //在用车数少于ans时,才可能求得更优的解
12         if (num_cat == n) ans = car_used; //若此时猫已放完,最佳答案更新为此时所用车数
13         else {
14             for (int i = 1; i <= car_used; ++i) {//对于当前的猫,我们可以放在已经放过猫的缆车上
15                 if (car[i] + cat[num_cat] <= w) {
16                     car[i] += cat[num_cat];
17                     solve(num_cat + 1, car_used);
18                     car[i] -= cat[num_cat]; //注意回溯
19                 }
20             }
21             car[car_used + 1] = cat[num_cat]; //也可以把当前的猫放在新的缆车里
22             solve(num_cat + 1, car_used + 1);
23             car[car_used + 1] = 0; //注意回溯
24         }
25     }
26     return ans; //永远返回最佳答案
27 }
28 
29 int main() {
30     std::cin >> n >> w;
31     for (int i = 0; i < n; ++i)
32         std::cin >> cat[i];
33     
34     std::sort(cat, &cat[n], std::greater<int>());
35     std::cout << solve(0, 1);
36 
37     return 0;
38 }

  以深度优先搜索为中心的两种思路都能解决小猫爬山问题,但是显然后者对大数据和极端输入更具优势。在直接求解最小缆车数的过程中,由于我们事先已经对猫的质量进行从大到小的排序,因此,对于要被放入缆车的猫而言,依然有可能进入之前已经放了猫的缆车中,所以不能简单地将放置情形简化为:要么放在当前的缆车,要么放在下一个缆车。

  如果我们将猫的质量从小到大排序,则可以作上面的简化。但是,这样写出的程序不能列出大猫小猫一起放的情形。也就是说,这么写的程序与第二种贪心思路写出的程序会出现一样的错误。故不可取。

 

posted @ 2021-04-27 20:40  Mr_Blug  阅读(743)  评论(0编辑  收藏  举报