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


    提示

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

    问题描述

      小猫爬山问题是这样的:有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 }

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

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

     

    若无特别说明,文章均为原创。所有随笔现在都可以自由转载,但请务必注明作者信息(Mr.Blug或其他合作作者)或注明出处(随笔原链接或博客主页地址),否则作者将根据行为恶劣程度从重处理。若文章有明显不妥之处,可以直接在评论中指出,感激不尽。
  • 相关阅读:
    tensorflow之tf.meshgrid()
    Python中数据的保存和读取
    透视投影推导
    tensorflow之tf.slice()
    tensorflow的tf.train.Saver()模型保存与恢复
    偶数分割求平均值
    母牛的故事
    统计一行的不重复的单词字符个数
    N个顶点构成多边形的面积
    贪心法基本入门
  • 原文地址:https://www.cnblogs.com/mrblug/p/14710788.html
Copyright © 2020-2023  润新知