贪心算法是常见的基础算法,它在求解问题时总想用当前看来最好的方法去实现,而到了下一步,再用下一步时最好的方法来解决,因此有了贪心的名字。此方法不从整体去考虑,仅是在某种意义上的局部最优求解。虽然贪心算法不是对所有的问题都能得到整体最优解,但是面对范围相当广泛的很多问题时,能产生整体最优解或是整体最优解的高度近似解,因此可见贪心算法只是追去一定范围内的最优。
1. 贪心算法的基础
贪心算法在解决一个问题时,通常会从某一个初始解出发,逐步逼近给定的目标,以便尽快求出更好的解。
当达到算法中的某一步不能在继续进行下去的时候,就停止算法,给出一个近似解。由贪心算法的特点和思路可以看出,贪心算法存在以下3个问题。
(1).不保证最后得解是最优的(实际上它从不考虑最后得结果)
(2).不能用来求最大或最小解的问题
(3).只能满足某些约束条件的可行解的范围
而通常情况下,贪心算法的思路如下:
(1).建立数学模型来描述问题
(2).将求解的问题分解为若干范围较小的子问题
(3).对每一子问题求解,得到子问题的局部最优解
(4).把子问题的局部最优合并成原问题的局部最优解
下面我们通过两个例子来描述贪心算法
1. 找零方案
在现实生活中,我们买东西经常需要找零,而商店找零的方法通常是优先找面额大的,再找小面额的。因为我们永远都不是商店的最后一个顾客,因此商店为了保证下一个顾客依然有零钱可以找,会尽可能的把面额大的找给我们,而把零钱留给自己。
人民币有100、50、20、10、5、1、0.5、0.2、0.1(单位:元)等多种面额,设计一个找零算法,使得面值大的尽可能多的被找。
#include <iostream>
using namespace std ;
int main(void) {
int i ;
float money ;
float x[9] = {100 , 50 , 20 , 10 , 5 , 1 , 0.5 , 0.2 , 0.1} ;
cin >> money ;
for(i = 0 ; i < 9 ; ++ i) {
while(money >= x[i]) {
money -= x[i] ;
cout << x[i] << " " ;
}
}
return 0 ;
}
上面只是一个简单的例子,下面我们来详细的讨论一下装箱问题
假设有编号为0,1,2.....n-1的n中物品,体积分别为v0,v1,v2......v(n-1),现需要将这些物品装进容积为V的若干箱子中,且箱子的体积不会小于任一物品。不同的装法可能。需要数量不同的箱子,现在我们需要用尽可能少的箱子装下所有的物品。
例:箱子的容量为10,有四个物品,体积分别为6,7,4,3,如果按照6->7->4->3的顺序,需要3个箱子,而如果按照6->4->7->3的顺序,只需要两个箱子。
选择数据结构
数据结构的选择关系到算法的高效与否。
存储链式结构,不涉及元素的进出,排除了栈和队列。有数组和链表可供选择,而我们在装箱问题中选择链表的原因有一下两点:
1.建立数据结构时,我们事先并不知道需要多少箱子,即无法确定元素的个数。
2.在算法的运行中,我们一旦确定了物品装进哪个箱子,就需要将物品和箱子建立某中内存上的联系,使得通过箱子可以找到其中的物品。
而数组显然不具备以上功能,因此我们选择链表。
typedef struct gNode { //structs array
int num ;
int gV ;
}gNode ;
typedef struct ElemNode { //物品节点
int gV ;
int gNum ;
struct ElemNode * next ; //节点中指向下一个物品的指针
}ElemNode ;
typedef struct gBox { //箱子节点
int V ; //箱子的容积
ElemNode * nextNode ; //箱子节点中指向其中物品的指针
struct gBox * nextBox ; //箱子节点中指向下一个箱子的节点
}gBox ;
最终程序运行时的数据结构应该是这样
实际代码如下:
#include <stdio.h>
#include <stdlib.h>
const int maxV = 10 ;
typedef struct gNode { //structs array
int num ;
int gV ;
}gNode ;
typedef struct ElemNode { //物品节点
int gV ;
int gNum ;
struct ElemNode * next ; //节点中指向下一个物品的指针
}ElemNode ;
typedef struct gBox { //箱子节点
int V ; //箱子的容积
ElemNode * nextNode ; //箱子节点中指向其中物品的指针
struct gBox * nextBox ; //箱子节点中指向下一个箱子的节点
}gBox ;
gBox * Pack(gBox * hB , gNode * hG , int num) {
int i ;
gBox * t , * iBox , * newBox;
ElemNode * newNode = NULL , * findEnd = NULL ;
hB = t = (gBox *)malloc(sizeof(gBox)) ; //拿出第一个物品,必定打开一个箱子
t -> V = maxV ; //
t -> nextNode = NULL ; //初始化箱子
t -> nextBox = NULL ; //
for(i = 0 ; i < num ; ++ i) {
newNode = (ElemNode *)malloc(sizeof(ElemNode)) ; //每一个物品,都需要分配一块内存
newNode ->gV = (hG+i)->gV ;
newNode ->gNum = (hG+i)->num ;
newNode ->next = NULL ;//从第一个物品开始装,等到所有的物品都遍历完,说明装箱结束。
for(iBox = hB ; iBox ; iBox = iBox ->nextBox) {
//每拿到一个物品,都要从第一个箱子开始遍历,直到找到一个箱子或者现有的箱子都没法装下
if((hG + i) -> gV < iBox ->V) { //如果当前箱子的剩余容积大于物品的体积,可以放
findEnd = iBox ->nextNode ; //使用findEnd指针来找到物品链的末尾
while(findEnd ->next || findEnd) { //使用或运算符可以兼容箱子中有物品或没物品
findEnd = findEnd -> next ; //两种情况
}
findEnd ->next = newNode ;
iBox ->V -= newNode ->gV ;
}
}
newBox = (gBox *)malloc(sizeof(gBox)) ; //如果当前所有箱子都不符合条件,创建一个新的箱子节点
newBox ->V = maxV ; //并初始化
newBox ->nextNode = NULL ;
newBox ->nextBox = NULL ;
t = t ->nextBox = newBox ; //然后将新的箱子挂在箱子链的末尾
t ->nextNode = newNode ; //最后将新的物品挂在新的箱子上
}
}
void Display(gBox * hBox) {
int i = 1 ;
ElemNode * travelNode = NULL ;
gBox * travelBox = NULL ;
for(travelBox = hBox ; travelBox ; travelBox = travelBox ->nextBox) {
printf("第%d个箱子中装有物品:" , i ++) ;
travelNode = travelBox ->nextNode ;
while(travelNode) {
printf("%4d" , travelNode ->gNum) ;
}
printf("\n") ;
}
}
int main(void) {
int num , i ;
printf("please input the number of boxes:") ;
scanf("%d" , num) ;
gNode * hG = (gNode *)malloc(num * sizeof(gNode)) ; //创建结构体数组,用来存储物品信息
for(i = 0 ; i < num ; ++ i) {
(hG+i)->num = i ;
printf("Input No.%d node:" , i) ;
scanf("%d" , &(hG+i)->gV ) ;
}
gBox * hBox = NULL ;
hBox = Pack(hBox , hG , num) ;
Display(hBox) ;
return 0 ;
}