5贪心算法
贪心算法
1 主要内容
- 贪心算法的思想
- 活动安排问题
- 贪心策略的基本要素
- 贪心算法实例
2 贪心算法基本思想
基本思想
- 适用于求解最优化问题的算法往往包含一系列步骤,每一步都有一组选择
- 贪心算法总是作出在当前看来是最好的选择
- 贪心算法并不从整体最优上加以考虑,它所作出的选择只是在某种意义上的局部最优选择
- 贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解
- 在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似
- 与动态规划方法相比,贪心算法更简单,更直接
基本要素
-
贪心算法通过做一系列的选择来给出某一问题的最优解。它所作出的每一个选择当前状态下的最好选择(局部),即贪心选择;这种贪心选择并不总能产生最优解,但对于一些问题,比如活动安排问题,可以给出最优解
-
可以根据下列步骤设计贪心算法
-
- 将最优化问题转化为这样的一个问题,即先做出选择,再解决剩下的一个子问题
- 证明原问题总有一个最优解是做贪心选择得到的,从而说明贪心选择的安全
- 说明在做出贪心选择之后,子问题的最优解与所作出的贪心选择联合起来,可以得出原问题的一个最优解
-
许多可以用贪心算法求解的问题,具备以下两个性质
-
- 贪心选择性质
- 最优子结构性质
-
贪心选择性质
-
- 是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到
- 这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别
- 动态规划算法通常以自底向上的方式解各子问题
- 贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题
-
最优子结构性质
-
- 当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质
- 问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征
3 活动安排问题
问题
- 设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,而在同一时间内只有一个活动能使用这一资源
- 每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi
- 如果选择了活动i,则它在半开时间区间[si,fi)内占用资源,若区间[si,fi)与区间[sj,fj)不相交,则称活动i与活动j是相容的
- 目标:要在所给的活动集合中选出最大的相容活动子集合
思路
- 将活动按照结束时间的增序f1≤f2 ≤…≤fn排列
- 一开始选择活动1,然后依次检查后面的活动i是否与前面已选择的活动相容,若相容,则将活动i加入到已选择的活动集合A中;若不相容,则继续检查下一活动与已选择活动的相容性
- 由于活动已按结束时间排序,设刚加入已选择活动集合的活动是j,因此只需检查活动i的是否与j相容;也就是检查是否满足si≥fj,若满足,则活动i与j相容
- 贪心体现在总是选择具有最早完成时间的相容活动
复杂度分析
- 如果已经排序,算法的时间复杂度为Θ(n)
- 如果事先没有按照结束时间增序排列,排序需O(nlgn)
public class test {
public Activity activity;
private static void GreedySeletor(int n,Activity[] acs,boolean[] a){
a[1]=true;
int j=1;
for (int i=2;i<=n;i++){
if (acs[i].getStartTime()>=acs[j].getEndTime()){
a[i]=true;
j=i;
}
else {
a[i]=false;
}
}
for (int i=1;i<=n;i++){
if (a[i]==true){
System.out.println(acs[i].getName());
}
}
}
public static void main(String[] args) {
boolean[] a = new boolean[7];
a[0]=false;
Activity activity0 = new Activity();
Activity activity1 = new Activity(1, 4, "活动1");
Activity activity2 = new Activity(3, 5, "活动2");
Activity activity3 = new Activity(0, 6, "活动3");
Activity activity4 = new Activity(3, 8, "活动4");
Activity activity5 = new Activity(5, 7, "活动5");
Activity activity6 = new Activity(5, 9, "活动6");
Activity[] acs = new Activity[7];
acs[0]=activity0;
acs[1]=activity1;
acs[2]=activity2;
acs[3]=activity3;
acs[4]=activity4;
acs[5]=activity5;
acs[6]=activity6;
Arrays.sort(acs);
GreedySeletor(6,acs,a);
}
}
class Activity implements Comparable<Activity>{
private int startTime;//开始时间
private int endTime;//结束时间
private String name;//活动名称
public Activity() {
}
public Activity(int startTime, int endTime, String name) {
this.startTime = startTime;
this.endTime = endTime;
this.name = name;
}
public int getStartTime() {
return startTime;
}
public void setStartTime(int startTime) {
this.startTime = startTime;
}
public int getEndTime() {
return endTime;
}
public void setEndTime(int endTime) {
this.endTime = endTime;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int compareTo(Activity o) {
return this.endTime-o.endTime;
}
}
4 0-1背包和背包问题
0-1背包
- 给定n种物品和一个背包。物品i的重量是Wi,其价值为Vi,背包的容量为C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
- 限制:在选择装入背包的物品时,对每种物品i只有2种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i
背包问题
- 与0-1背包问题类似,所不同的是在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包,1≤i≤n
背包问题可以用贪心
- 首先计算每种物品单位重量的价值Vi/Wi
- 然后,依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包
- 若将这种物品全部装入背包后,背包内的物品总重量未超过C,则选择单位重量价值次高的物品并尽可能多地装入背包
- 依此策略一直地进行下去,直到背包装满为止
0-1背包不能用贪心
- 因为在这种情况下,它无法保证最终能将背包装满,部分闲置的背包空间使单位重量背包空间的价值降低
- 事实上,在考虑0-1背包问题时,应比较选择该物品和不选择该物品所导致的最终方案,然后再作出最好选择。由此就导出许多互相重叠的子问题(子问题重叠)
- 这正是该问题可用动态规划算法求解的重要特征
5最优装载问题
问题
- 有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船
思路
- 采用重量最轻者先装的贪心选择策略,可产生最优装载问题的最优解
public class test {
public Xiang xiang;
public static void ZhuangZai(double c,Xiang[] xs){
Arrays.sort(xs);
for (int i=0;i<xs.length;i++){
c=c-xs[i].getWeight();
if (c<=0)
break;
System.out.println(xs[i].getName());
}
}
public static void main(String[] args) {
Xiang x1 = new Xiang(5, "箱子1");
Xiang x2 = new Xiang(14, "箱子2");
Xiang x3 = new Xiang(1, "箱子3");
Xiang x4 = new Xiang(3, "箱子4");
Xiang x5 = new Xiang(2, "箱子5");
Xiang x6 = new Xiang(66, "箱子6");
Xiang[] xs = {x1, x2, x3, x4, x5, x6};
ZhuangZai(16,xs);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Xiang implements Comparable<Xiang>{
private double weight;//集装箱重量
private String name;//集装箱名字
@Override
public int compareTo(Xiang o) {
if(this.weight==o.weight)
return 0;
else if (this.weight<o.weight)
return -1;
else
return 1;
}
}
6单源最短路径Dijkstra算法
问题
- 给定带权有向图G =(V,E),其中每条边的权是非负实数
- 给定V中的一个顶点,称为源,要求计算从源到所有其他各顶点的最短路长度(这里路的长度是指路上各边权之和)
思想
- 设置顶点集合S,初始时,S中仅含有源,此后不断作贪心选择来扩充这个集合
- 一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知
- 设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度
- Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改,检查dist(u)+[u,j]与dist[j]的大小,若dist(u)+[u,j]较小,则更新
- 一旦S包含了所有V中顶点,dist就记录了从源到所有其他顶点之间的最短路径长度

public class test {
//Dijkstra最短路径算法(返回完整路径)
public static int[] dijkstraV2(Graph graph,int startIndex){
//图的顶点数量
int size=graph.vertexes.length;
//创建距离表 存储从起点到每一个顶点的临时距离
int[] distances=new int[size];
//创建前置顶点表,存储从起点到每一个顶点的已知最短路径的前置节点
int[] prevs=new int[size];
//记录顶点的遍历状态
boolean[] access=new boolean[size];
//初始化最短路径表,到达每个顶点的路径代价默认为无穷大
for (int i=0;i<size;i++){
distances[i]=Integer.MAX_VALUE;
}
//遍历起点,刷新距离表
access[0]=true;
LinkedList<Edge> edgesFromStart = graph.adj[startIndex];
for (Edge edge : edgesFromStart) {
distances[edge.index]=edge.weight;
prevs[edge.index]=0;
}
//主循环 重复遍历最短距离顶点和刷新距离表的操作
for (int i=1;i<size;i++){
//寻找最短距离顶点
int minDistanceFromStart=Integer.MAX_VALUE;
int minDistanceIndex=-1;
for (int j=1;j<size;j++){
if (!access[j] && (distances[j]<minDistanceFromStart)){
minDistanceFromStart=distances[j];
minDistanceIndex=j;
}
}
if (minDistanceIndex==-1){
break;
}
//遍历顶点 刷新距离表
access[minDistanceIndex]=true;
for (Edge edge:graph.adj[minDistanceIndex]){
if (access[edge.index]){
continue;
}
int weight=edge.weight;
int preDistance=distances[edge.index];
if ((weight!=Integer.MAX_VALUE) && ((minDistanceFromStart+weight)<preDistance)){
distances[edge.index]=minDistanceFromStart+weight;
prevs[edge.index]=minDistanceIndex;
}
}
}
return prevs;
}
}
//图的顶点
class Vertex{
int data;
Vertex(int data){
this.data=data;
}
}
//图的边
class Edge{
int index;//下一条边 注意是链表形式
int weight;//权重
Edge(int index,int weight){
this.index=index;
this.weight=weight;
}
}
//图邻接表形式
class Graph{
public int size;//顶点数
public Vertex[] vertexes;//顶点
public LinkedList<Edge>[] adj;//边
Graph(int size){
this.size=size;
//初始化顶点和邻接表
vertexes=new Vertex[size];
adj=new LinkedList[size];
//防止空指针
for(int i=0;i<size;i++){
vertexes[i]=new Vertex(i);
adj[i]=new LinkedList();
}
}
}
7 最小生成树
问题
- 设G =(V,E)是无向连通带权图,即一个网络,E中每条边(v,w)的权为c[v] [w],如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树
- 生成树上各边权的总和为该生成树的耗费,在G的所有生成树中,耗费最小的生成树称为G的最小生成树
性质
- 设G=(V,E)是连通带权图,U是V的真子集。如果(u,v)∈E,且u∈U,v∈V-U,且在所有这样的边中,(u,v)的权c[u] [v]最小,那么一定存在G的一棵最小生成树,它以(u,v)为其中一条边
- 用贪心算法设计策略可以设计出构造最小生成树的有效算法,构造最小生成树的Prim算法和Kruskal算法用到了以上性质
Prim算法
- 设G=(V,E)是连通带权图,V={1,2,…,n},首先置S=
- 然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件i∈S,j∈V-S,且c[i][j]最小的边,将顶点j添加到S中
- 这个过程一直进行到S=V时为止
- 在这个过程中选取到的所有边恰好构成G的一棵最小生成树
- Prim算法所需的时间复杂度为O(n2)
Kruskal算法
- 首先将G的n个顶点看成n个孤立的连通分支。将所有的边按权值从小到大排序
- 然后从第一条边开始,依边权递增的顺序查看每一条边,并按下述方法连接2个不同的连通分支:当查看到第k条边(v,w)时,如果端点v和w分别是当前2个不同的连通分支T1和T2中的顶点时,就用边(v,w)将T1和T2连接成一个连通分支,然后继续查看第k+1条边;如果端点v和w在当前的同一个连通分支中,就直接再查看第k+1条边
- 这个过程一直进行到只剩下一个连通分支时为止
8 Huffman编码
前缀码
- 对每一个字符规定一个0,1串作为其代码,并要求任一字符的代码都不是其他字符代码的前缀
- 编码的前缀性质可以使译码方法非常简单
- 使平均码长达到最小的前缀码编码方案称为给定编码字符集C的最优前缀码
构造方式
- Huffman提出构造最优前缀码的贪心算法,由此产生的编码方案称为Huffman编码
- 编码字符集C中每一字符c的频率是f(c)
- 根据给定的|C|个频率,构造n棵二叉树的集合F={T1,T2,...,Tn},其中Ti中只有一个根结点,左右子树为空
- 在F中选取两棵根结点频率最小的树作为左、右子树构造一棵新的二叉树,且置新的二叉树的根结点的频率为左、右子树上根结点的频率之和
- 将新的二叉树加入到F中,删除原两棵根结点频率最小的树
- 重复上面操作,直到F中只含一棵树为止
- 贪心思想体现:每次都选择根节点“频率”值最小的两棵树合并

浙公网安备 33010602011771号