1-2、算法设计常用思想之贪婪法
文章内容来自王晓华老师
贪心算法,是寻找最优解问题的常用方法
这种方法模式一般将求解过程分成若干个步骤,但每个步骤都应用贪心原则,选取当前状态下最好的或最优的选择(局部最有利的选择),并以此希望最后堆叠出的结果也是最好或最优的解。
因为不进行回溯处理,贪婪法只在很少的情况下可以得到真正的最优解,比如最短路径问题、图的最小生成树问题。
在大多数情况下,由于选择策略的“短视”,贪婪法会错过真正的最优解,而得不到问题的真正答案。但是贪婪法简单、高效,省去了为找最优解可能需要的穷举操作,可以得到与最优解比较接近的近似最优解,通常作为其他算法的辅助算法来使用。
贪婪法的基本设计思想有以下三个步骤:
• 建立对问题精确描述的数学模型,包括定义最优解的模型;
• 将问题分解为一系列的子问题,同时定义子问题的最优解结构;
• 应用贪心原则确定每个子问题的局部最优解,并根据最优解的模型,用子问题的局部最优解堆叠出全局最优解。
背包问题:有 N 件物品和一个承重为 C 的背包(也可定义为体积),每件物品的重量是 wi,价值是 pi,求解将哪几件物品装入背包可使这些物品在重量总和不超过 C 的情况下价值总和最大。
背包问题(Knapsack Problem)是此类组合优化的NP完全问题的统称,如货箱装载问题、货船载物问题等,因问题最初来源于如何选择最合适的物品装在背包中而得名,这个问题隐含了一个条件,每个物品只有一件,也就是限定每件物品只能选择 0 个或 1 个,因此又被称为 0-1 背包问题
实例
有一个背包,最多能承载重量为 C=150 的物品,现在有 7 个物品(物品不能分割成任意大小),编号为 1~7,重量分别是 wi=[35,30,60,50,40,10,25],价值分别是 pi=[10,40,30,50,35,40,30],现在从这 7 个物品中选择一个或多个装入背包,要求在物品总重量不超过 C 的前提下,所装入的物品总价值最高。
思路
常见的贪婪策略有三种:第一种策略是根据物品价值选择,每次都选价值最高的物品,此时包中物品总重量是 130,总价值是 165。第二种策略是根据物品重量选择,每次都选择重量最轻的物品,此时包中物品总重量是 140,总价值是 155。第三种策略是定义一个价值密度的概念,每次选择都选价值密度最高的物品,物品的价值密度 si 定义为 pi/wi,此时包中物品的总重量是 150,总价值是 170,我们可以选用合适的策略,得到较优解
通用的解题思路
/**GreedyAlgo() 函数是贪婪算法的主体结构,包括子问题的分解和选择策略的选择都在这个函数中。
能够明显看出来这个算法使用了迭代法的算法模式,当然,这个算法主体的实现还可以使用递归法,
正如函数所展示的那样,它可以作为此类问题的一个通用解决思路: */ void GreedyAlgo(KNAPSACK_PROBLEM *problem, SELECT_POLICY spFunc) { int idx; int ntc = 0; //spFunc 每次选最符合策略的那个物品,选后再检查 while((idx = spFunc(problem->objs, problem->totalC - ntc)) != -1) { //所选物品是否满足背包承重要求? if((ntc + problem->objs[idx].weight) <= problem->totalC) { problem->objs[idx].status = 1; ntc += problem->objs[idx].weight; } else { //不能选这个物品了,做个标记后重新选 problem->objs[idx].status = 2; } } PrintResult(problem->objs); } /**spFunc 参数是选择策略函数的接口,通过替换这个参数,可以实现上文提到的三种贪婪策略,
分别得到各种贪婪策略下得到的解。以第一种策略为例,每次总是选择 price 最大的物品,可以这样实现: */ int Choosefunc1(std::vector<OBJECT>& objs, int c) { int index = -1; //-1表示背包容量已满 int mp = 0; for(int i = 0; i < static_cast<int>(objs.size()); i++) { if((objs[i].status == 0) && (objs[i].price > mp)) { mp = objs[i].price; index = i; } } return index; }
lua代码实现
-- 物品 -- obj = { -- weight = 0, -- price = 0, -- status = 0 -- 0未选中 1已选中 2已不可选 -- } -- 背包 -- bag = { -- obj_list = {}, -- totalC = 0 -- } local function choose_func1(obj_bag, use_c) local index = -1 ---1表示背包容量已满 local mp = 0 for i = 1, #obj_bag.obj_list do if((obj_bag.obj_list[i].status == 0) and obj_bag.obj_list[i].price > mp) then mp = obj_bag.obj_list[i].price index = i end end return index end local function greedy_algo(bag, spFunc) local idx = nil local ntc = 0 -- 已用重量 idx = spFunc(bag, ntc) while(idx ~= -1) do if(ntc + bag.obj_list[idx].weight) <= bag.totalC then bag.obj_list[idx].status = 1 ntc = ntc + bag.obj_list[idx].weight else bag.obj_list[idx].status = 2 end idx = spFunc(bag, ntc) end print("==============11111") dump(bag.obj_list) end local bag_1 = { obj_list = { {weight = 10, price = 10, status = 0}, {weight = 20, price = 20, status = 0}, {weight = 30, price = 40, status = 0}, {weight = 40, price = 30, status = 0}, {weight = 50, price = 50, status = 0}, {weight = 60, price = 70, status = 0}, }, totalC = 150 } greedy_algo(bag_1, choose_func1)
总结
贪婪法只能得到比较接近最优解的近似最优解,但是作为一种启发式辅助方法在很多算法中都得到了广泛的应用,很多常用的算法在解决局部最优决策时,都会应用到贪婪法。比如 Dijkstra 的单源最短路径算法在从 dist 中选择当前最短距离的节点时,就是采用的贪婪法策略。事实上,在任何算法中,只要在某个阶段使用了只考虑局部最优情况的选择策略,都可以理解为使用了贪婪算法。