You are on page 1of 100

算法分析与设计

——以大学生程序设计竞赛为例

芦旭
lxuu306@sdau.edu.cn

1
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

2
第5章 贪心算法
面值分别为2角5分,1角,5分,1分的硬币,请给出
找6角8分钱的最佳方案(硬币枚数最少)

➢分析:
68-25=43
43-25=18
18-10=8
8-5=3
3-1*3=0
3
第5章 贪心算法
➢ 贪心算法总是作出在当前看来最好的选择;并且每次贪
心选择都能将问题化简为一个更小的与原问题具有相同
形式的子问题。
➢ 贪心算法并不从整体最优考虑,它所作出的选择只是在
某种意义上的局部最优选择。当然,希望贪心算法得到
的最终结果也是整体最优的。

4
第5章 贪心算法
➢ 虽然贪心算法不能对所有问题都得到整体最优解,但对
许多问题(具有最优子结构和贪心性质的问题)它能产
生整体最优解。如单源最短路径问题,最小生成树问题
等。
➢ 在一些情况下,即使贪心算法不能得到整体最优解,其
最终结果却是最优解的一个很好近似。
➢ 贪心算法具有一定的速度优势,如果一个问题可以同时
5
使用几种方法解决,贪心算法应该是最好的选择之一。
第5章 贪心算法
贪心算法和动态规划算法的比较:
共同点:
1. 都是求最优解的选择性算法
2. 所解决的问题都具有最优子结构性质,即全优一定包含
局优

6
第5章 贪心算法
不同点之一:
(1)贪心算法的选择策略即贪心选择策略,通过对候选解
按照一定的规则进行排序,然后就可以按照这个排好的顺序
进行选择了,选择过程中仅需确定当前元素是否要选取,与
后面的元素是什么没有关系。
(2)动态规划的选择策略是试探性的,每一步要试探所有
的可行解并将结果保存起来,最后通过回溯的方法确定最优
解,其试探策略称为决策过程。
7
tri[i ][ j ] = tri[i ][ j ] + max{tri[i + 1][ j ],tri[i + 1,j + 1}
第5章 贪心算法
不同点之二:
贪心算法具有贪心选择特性。贪心算法求得局部最优解(局
部最优,不一定是全局最优);动态规划算法从全局最优考
虑问题。

9
第5章 贪心算法
整体最优与局部最优:
现有面值分别为1角1分,5分,1分的硬币,请给出找1角5
分钱的最佳方案。
➢ 贪心(5枚):
15-11=4
4-4*1=0
➢ 最佳(3枚)
3*5
10
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

11
5.1 活动安排问题
活动安排问题就是要在所给的活动集合中选出最大的相容
活动子集合,是可以用贪心算法有效求解的很好例子。
该问题要求高效地安排一系列争用某一公共资源的活动。
贪心算法提供了一个简单、漂亮的方法使得尽可能多的活
动能兼容地使用公共资源。

12
5.1 活动安排问题
 设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同
一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资
源。
 每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,
且si<fi。如果选择了活动i,则它在半开时间区间[si ,fi )内占用资
源。若区间[si ,fi )与区间[sj,fj )不相交,则称活动i与活动j是相容
的。当 si ≥ fj 或 sj ≥ fi 时,活动i与活动j相容。
 活动安排问题就是在所给的活动集合中选出最大的相容活动子集合。

sj fj si fi si fi sj fj

si ≥ fj sj ≥ fi

13
活动安排问题
➢分析:
◆活动安排问题的贪心特性(贪心的体现:最多的
活动)。
◆如何在有限的时间内安排更多的活动(贪心策略
)?
先安排结束时间早的活动(剩余时间多)
◆因此,需要根据活动的结束时间对活动进行排序
◆在排序的基础上,依次来寻找相容的活动
5.1 活动安排问题
数据结构
struct action{
int s; //起始时间
int f; //结束时间
int index; //活动的编号
};
活动的集合E记为数组:
action a[1000];

15
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

1
2
3
4
5
6
7
8
9
10
11

作业1 作业4 作业8 作业11

i 1 2 3 4 5 6 7 8 9 10 11
a[i].s 1 3 0 5 3 5 6 8 8 2 12
a[i].f 4 5 6 7 8 9 10 11 12 13 14
b 1 0
3 0 1
5 0
3 0
5 0
6 1
8 0
8 0 12
2 1

Page 16
5.1 活动安排问题
按活动的结束时间升序排序
排序比较因子:
bool cmp(const action &a, const action &b)
{
if (a.f<=b.f) return true;
return false;
}
使用标准模板库函数排序(下标0未用):
sort(a, a+n+1, cmp);

17
算法5.1 计算活动安排问题的贪心算法
//形参数组b用来记录被选中的活动
void GreedySelector(int n, action a[], bool b[])
{
b[1] = true; //第1个活动是必选的
//记录最近一次加入到集合b中的活动
int preEnd = 1;
for(int i=2; i<=n; i++)
if (a[i].s>=a[preEnd].f)
{
b[i] = true;
preEnd = i;
}
} i 1 2 3 4 5 6 7 8 9 10 11
a[i].s 1 3 0 5 3 5 6 8 8 2 12
a[i].f 4 5 6 7 8 9 10 11 12 13 14
b 1 0 0 1 0 0 0 1 0 0 1
18

preEnd preEnd preEnd preEnd


5.1 活动安排问题
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
1
2
3
4
5
6
7
8
9
10
11

作业1 作业4 作业8 作业11

i 1 2 3 4 5 6 7 8 9 10 11
a[i].s 1 3 0 5 3 5 6 8 8 2 12
a[i].f 4 5 6 7 8 9 10 11 12 13 14 19

b 1 0 0 1 0 0 0 1 0 0 1
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

20
5.2 贪心算法的理论基础

➢ 利用贪心策略解题,需要解决以下两个问题:
① 该题是否适合于用贪心策略求解(贪心选择性质+最
优子结构性质)【是否满足性质】
② 如何选择贪心标准,以得到问题的最优/较优解【如
何选择策略】
5.2.1 贪心选择性质
贪心选择性质是指所求问题的整体最优解可以通过一系
列局部最优的选择,即贪心选择来达到。
⚫ 这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划
算法的主要区别。
(1)在动态规划算法中,每步所做的选择往往依赖于相关子问题
的解,因而只有在解出相关子问题后,才能做出选择。
(2)在贪心算法中,仅在当前状态下做出最好选择,即局部最优
选择,然后再去解出这个选择后产生的相应的子问题。

22
5.2.2 最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问
题具有最优子结构性质。
⚫ 运用贪心策略在每一次转化时都取得了最优解。
⚫ 问题的最优子结构性质是该问题可用贪心算法或动态规划算法求
解的关键特征。
贪心算法的每一次操作都对结果产生直接影响,而动态
规划则不是。
⚫ 贪心算法对每个子问题的解决方案都做出选择,不能回退;动态
规划则会根据以前的选择结果对当前进行选择,有回退功能。
⚫ 动态规划主要运用于二维或三维问题,而贪心一般是一维问题。

23
5.2.3 贪心算法的求解过程
使用贪心算法求解问题应该考虑如下几个方面:
(1)候选集合A:为了构造问题的解决方案,有一个候选集合A作为
问题的可能解,即问题的最终解均取自于候选集合A。
(2)解集合S:随着贪心选择的进行,解集合S不断扩展,直到构成
满足问题的完整解。
(3)解决函数solution:检查解集合S是否构成问题的完整解。
(4)选择函数select:即贪心策略,这是贪心法的关键,它指出哪个
候选对象最有希望构成问题的解,选择函数通常和目标函数有关。
(5)可行函数feasible:检查解集合中加入一个候选对象是否可行,
即解集合扩展后是否满足约束条件。

24
算法5.2贪心算法的一般流程
//A是问题的输入集合即候选集合
Greedy(A)
{
S={ }; //初始解集合为空集
while (not solution(S)) //集合S没有构成问题的一个解
{
x = select(A); //在候选集合A中做贪心选择
if feasible(S, x) //判断集合S中加入x后的解是否可行
S = S+{x};
A = A-{x};
}
return S;

(1)候选集合A:问题的最终解均取自于候选集合A。
(2)解集合S:解集合S不断扩展,直到构成满足问题的完整解。
(3)解决函数solution:检查解集合S是否构成问题的完整解。
(4)选择函数select:贪心策略,这是贪心算法的关键。 25
(5)可行函数feasible:解集合扩展后是否满足约束条件。
5.2 贪心算法的理论基础

➢ 贪心算法和动态规划算法都要求问题具有最优子结构
性质,这是2类算法的一个共同点。但是,对于具有
最优子结构的问题应该选用贪心算法还是动态规划算
法求解?是否能用动态规划算法求解的问题也能用贪
心算法求解?
下面研究2个经典的组合优化问题,并以此说明贪心
算法与动态规划算法的主要差别。
0-1背包问题
背包问题
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

27
5.3 背包问题
给定一个载重量为M的背包,考虑n个物品,其中第i个
物品的重量 ,价值wi (1≤i≤n),要求把物品装满背
包,且使背包内的物品价值最大。
有两类背包问题(根据物品是否可以分割),如果物品
不可以分割,称为0—1背包问题(动态规划);如果物
品可以分割,则称为背包问题(贪心算法)。

28
5.3 背包问题

29
5.3 背包问题
 有3种方法来选取物品:
(1)当作0—1背包问题,用动态规划算法,获得最优值220;
(2)当作0—1背包问题,用贪心算法,按性价比从高到底顺序选取
物品,获得最优值160。由于物品不可分割,剩下的空间白白浪费。
(3)当作背包问题,用贪心算法,按性价比从高到底的顺序选取物
品,获得最优值240。由于物品可以分割,剩下的空间装入物品3的一
部分,而获得了更好的性能。
0—1背包 0—1背包 背包问题
动态规划 贪心算法 贪心算法

20 100 20 20 80
n 1 2 3
+ +
重量 10 20 50 30
20 100 20 100
价值 60 100
30 120 30 120
+ +
性价比 6 20 5 4
10 10 60 10 60
60 100 120 背包 =220 =160 =240 30
数据结构
struct bag{
int w; //物品的重量
int v; //物品的价值
double c; //性价比
}a[1001]; //存放物品的数组
 排序因子(按性价比降序):
bool cmp(bag a, bag b){ if(a.c >b.c) return true;
return a.c >= b.c; return false;
}
 使用标准模板库函数排序(最好使用stable_sort()函数,在性价比
相同时保持输入的顺序):
sort(a, a+n, cmp);
31
算法5.3 计算背包问题的贪心算法
//形参n是物品的数量,c是背包的容量M,数组a是按物品的性价比降序排序
double knapsack(int n, bag a[], double c)
{
double cleft = c; //背包的剩余容量
int i = 0;
double b = 0; //获得的价值
//当背包还能完全装入物品i
while(i<n && a[i].w<cleft)
{
cleft -= a[i].w;
b += a[i].v;
i++;
}
//装满背包的剩余空间
if (i<n) b += 1.0*a[i].v*cleft/a[i].w;
return b;
} 32
5.3 背包问题
 如果要获得解向量 X = {x1,x2,
,xn } ,则需要在数据结构中加入
物品编号:
struct bag{
int w;
int v;
double x; //装入背包的量,0≤x≤1
int index; //物品编号
double c; //单位重量的价值(性价比),v/w
}a[1001];

33
算法5.4 计算背包问题的贪心算法,同时得到解向量
double knapsack(int n, bag a[], double c)
{
double cleft = c;
int i = 0;
double b = 0;
while(i<n && a[i].w<=cleft)
{
cleft -= a[i].w;
b += a[i].v;
//物品原先的序号是a[i].index,全部装入背包
a[a[i].index].x = 1.0;
i++;
}
if (i<n) {
a[a[i].index].x = 1.0*cleft/a[i].w;
b += a[a[i].index].x*a[i].v;
} 34
return b;
}
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

35
5.4 最优装载问题
有一批集装箱要装上一艘载重量为c的轮船,其中集装
箱i的重量为wi。最优装载问题要求确定在装载体积不
受限制的情况下,将尽可能多的集装箱装上轮船(箱数
最多)。
该问题的形式化描述为:
n n
max  xi, s.t.  wi xi  c
i =1 i =1

其中xi∈{0,1},1≤i≤n。

输入样例 输出样例
50 3 2
40 10 40 12
37 5 2
10 30 24 35 40 13 36
25 3 No answer!
30 40 50
5.4 最优装载问题

➢分析:
◆最优装载问题可用贪心算法求解。采用重量最
轻者先装的贪心选择策略,可产生最优装载问
题的最优解。

37
5.4 最优装载问题
最优装载问题可用贪心算法求解。采用重量最轻者先装的
贪心选择策略,可得到装载问题的最优解。表示集装箱的
数据结构如下:
struct load {
int index; //集装箱编号
int w; //集装箱重量
}box[1001];
排序因子(按集装箱的重量升序):
bool cmp (load a, load b) {
if (a.w<b.w) return true;
else return false;
}
使用标准模板库函数排序(box[0]未使用):
38
stable_sort(box, box+n+1, cmp);
//这是稳定排序函数,当重量相同时,保持输入数据原来的顺序。
算法5.4 最优装载问题的贪心算法
while (scanf("%d%d", &c, &n)!=EOF)
{
memset(box, 0, sizeof(box));
memset(x, 0, sizeof(x));
for (int i=1; i<=n; i++)
{
scanf("%d", &box[i].w);
box[i].index = i;
}
//按集装箱的重量升序排序
stable_sort(box, box+n+1, cmp);
if (box[1].w>c) {
printf("No answer!\n");
continue;
}

39
算法5.4 最优装载问题的贪心算法
//贪心算法的实现,重量最轻者先装载
int i;
for (i=1; i<=n && box[i].w<=c; i++) //循环遍历集装箱数组,判断
集装箱的重量是否小于等于背包的剩余容量
{
x[box[i].index] = 1;
c -= box[i].w;
}
//输出装载的集装箱数量
printf("%d\n", i-1);
//输出装载的集装箱编号
for (i=1; i<=n; i++)
if (x[i]) printf("%d ", i);
printf("\n");
40

第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

41
5.5 单源最短路径

输入样例 输出样例
57 10 45 25 55 V5
1 2 10 2<--1 50
1 4 25 3<--4<--1 80 V4
1 5 80 4<--1
V1 25 20
2 3 40 5<--3<--4<--1 10
3 5 10
10 V3
4 3 20 40
4 5 50 V2
00

42
5.5 单源最短路径

迪杰斯特拉(Dijkstra)提出了一个按路径长度递增
的次序产生最短路径的算法——Dijkstra算法。
Dijkstra算法是解单源最短路径问题的一个贪心算法。
按从源点到其余每一顶点的最短路径长度递增顺序求
从源点到其余各顶点的最短路径长度 。
5.5 单源最短路径
Dijkstra算法
贪心策略/基本思想:
设置一个集合S存放已经找到最短路径的顶点,S的初始状态只
包含源点v1 ;对vi∈V-S,最初假设从源点v1到vi的有向边为
最短路径。

以后每求得一条最短路径v1, …, vk,就将vk加入集合S中,并将
路径v1, …, vk , vi与原来的假设相比较,取路径长度较小者为
最短路径。

重复上述过程,直到集合V中全部顶点加入到集合S中。
5.5 单源最短路径
Dijkstra算法的基本思想

可以将图中的顶点分为两组:
S——已求出的最短路径的终点集合(开始为{v1})。
V-S——尚未求出最短路径的顶点集合(开始为V-{v1}的
全部顶点)。
按最短路径长度的递增顺序逐个将第二组的顶点加入到
第一组中。
5.5 单源最短路径
Dijkstra算法
B
10 50 S={A}

A C A→B:(A, B)10
30 10
A→C:(A, C)∞
100 20
A→D: (A, D)30
E D A→E: (A, E)100
60
5.5 单源最短路径 S={A , B}
Dijkstra算法 A→B:(A, B)10
A→C:(A, C)∞
B
10 50 A→D: (A, D)30
A C A→E: (A, E)100
30 10
S={A, B}
100 20
A→B:(A, B)10
E D A→C:(A, B, C)60
60
A→D: (A, D)30
A→E: (A, E)100
S={A, B , D}
5.5 单源最短路径
A→B:(A, B)10
Dijkstra算法
A→C:(A, B, C)60
B A→D: (A, D)30
10 50

A C A→E: (A, E)100


30 10
S={A, B, D}
100 20 A→B:(A, B)10
E D A→C:(A, D, C)50
60
A→D: (A, D)30
A→E: (A, D, E)90
5.5 单源最短路径 S={A, B, D , C}
Dijkstra算法 A→B:(A, B)10
A→C:(A, D, C)50
B
10 50
A→D: (A, D)30
A C A→E: (A, D, E)90
30 10

100
S={A, B, D, C}
20
A→B:(A, B)10
E D
60 A→C:(A, D, C)50
A→D: (A, D)30
A→E: (A, D, C, E)60
5.5 单源最短路径
Dijkstra算法
B S={A, B, D, C, E}
10 50
A→B:(A, B)10
A C
30 10 A→C:(A, D, C)50
100 20 A→D: (A, D)30

E D A→E: (A, D, C, E)60


60
5.5 单源最短路径

迭代 S u dist[2] dist[3] dist[4] dist[5]

初值 {1} — 10 ∞ 25 80
50
i=1 {1, 2} 2 10 25 80
( v1, v 2, v3)
45 75
i=2 {1, 2, 4} 4 10 25
(v1, v4, v3) (v1, v4, v5)
55
i=3 {1, 2, 4, 3} 3 10 45 25
(v1, v4, v3, v5)
i=4 {1, 2, 4, 3, 5} 5 10 45 25 55

V5 50
1 2 3 4 5
80 V4 1 ∞ 10 ∞ 25 80
25 20 2 ∞ ∞ 40 ∞ ∞
V1 10
3 ∞ ∞ ∞ ∞ 10
V3 4 ∞ ∞ 20 ∞ 50
10 40
5 ∞ ∞ ∞ ∞ ∞ 51
V2
5.5 单源最短路径
DIJKSTRA算法是解单源最短路径问题的一个贪心算法。
 设置顶点集合S并不断地做贪心选择来扩充这个集合。一个顶点属于
集合S当且仅当从源到该顶点的最短路径长度已知。
1. 初始时,S中仅含有源。
2. 设u是G(未处理)的某一个顶点,把从源到u且中间只经过S中顶点的
路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应
的最短特殊路径长度。
3. Dijkstra算法每次从V—S中取出具有最短特殊路长度的顶点u,将
u添加到S中,同时对数组dist作必要的修改。
4. 一旦S包含了所有V中的顶点,dist就记录了从源到所有其它顶点之
间的最短路径长度。

52
算法5.5 计算单源最短路径问题的DIJKSTRA算法
#define NUM 100
#define maxint 10000
//顶点个数n,源点v,有向图的邻接矩阵为c
//数组dist保存从源点v到每个顶点的最短特殊路径长度
//数组prev保存每个顶点在最短特殊路径上的前一个结点
void dijkstra(int n, int v, int dist[], int prev[], int c[][NUM])
{
int i,j;
bool s[NUM]; //集合S
//初始化数组dist、s和prev
for(i=1; i<=n; i++)
{
dist[i] = c[v][i];
s[i] = false;
if (dist[i]>maxint) prev[i] = 0;
else prev[i] = v;
}
//初始化源结点
dist[v] = 0;
53
s[v] = true;
算法5.5 计算单源最短路径问题的DIJKSTRA算法
//遍历其余顶点
for(i=1; i<n; i++)
{
int tmp = maxint;
int u = v;
for(j=1; j<=n; j++) //在数组dist中寻找未处理结点的最小值(赋给u)
if( !(s[j]) && (dist[j]<tmp)) //判断顶点j是否不在集合S中,并
且顶点j到源点v的距离是否小于tmp
{
u = j;
tmp = dist[j];
}
s[u] = 1; //结点u加入s中

54
算法5.5 计算单源最短路径问题的DIJKSTRA算法
//利用结点u更新数组dist(数组dist记录当前每个顶点所对应的最短
特殊路径长度)
for(j=1; j<=n; j++)
if(!(s[j]) && c[u][j]<maxint)
{
//newdist为从源点到该点j的最短特殊路径
int newdist = dist[u]+c[u][j];
if (newdist<dist[j])
{
//修正最短距离
dist[j] = newdist;
//记录j的前一个结点
prev[j] = u;
}
}
}
}
邻接矩阵c用于存储有向图的边的权重信息,c[u][j]表示从顶点u到顶点j 55
的边的权重值。在Dijkstra算法中,通过比较顶点u到其他顶点的最短
路径长度,来更新最短路径长度和前一个结点的信息。
5.5 单源最短路径
 根据算法5.5中数组prev保存的信息,可以输出相应的最短路径。
 算法中数组prev[i]记录最短路径上i的前一个顶点。数组prev的初值
为0,表示没有前一个结点。
 在更新最短路径长度时,只要dist[u]+c[u][i]< prev[i]时,就置
prev[i]=u。当Dijkstra算法终止时,根据数组prev找到从源到i的
最短路径上每个顶点的前一个顶点,从而找到从源到i的最短路径。

算法5.6 根据数组prev计算单源最短路径的算法
void traceback(int v, int i, int prev[])
{
cout<<i<<"<--";
i=prev[i];
if(i!=v) traceback(v, i, prev);
V5
if(i==v) cout<<i<<endl; 50
} 80 V4

V1 25 10 20
序号 1 2 3 4 5 2<--1
值 0 1 4 1 3 3<--4<--1 56
10 V3
40
4<--1 V2
5<--3<--4<--1
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

57
5.6 最小生成树

最小生成树不 b
8
c
7
d
是唯一的,用 4 2 9
边ah替代边bc a 11 i 4
14 e
构成另外一棵 8 7
6
10 58
最小生成树 h
1
g
2
f
5.6.1 最小生成树的性质

59
5.6 最小生成树

构造最小生成树的两个方法:
◆ Prime法:加点法
◆ Kruskal方法:加边法
都使用了贪心算法
普里姆(Prim)算法
贪心策略/基本思想:
设G=(V, E)是具有n个顶点的连通网,

T=(U, TE)是G的最小生成树,

T的初始状态为U={u0}(u0∈V),TE={ },

重复执行下述操作:

在所有u∈U,v∈V-U的边中找一条代价最小的边
(u, v)并入集合TE,同时v并入U,直至U=V。
Prim算法

B
34 12
U={A}
A 19 26 E
V-U={B, C, D, E, F}
F
46 38 cost={(A, B)34, (A, C)46,
25 25
(A, D)∞,(A, E)∞,(A, F)19}
C D
17
Prim算法

B
34 12
U={A, F}
A 19 26 E
V-U={B, C, D, E}
F
46 38 cost={(A, B)34, (A, C)46,
25 25
(F, C)25, (F, D)25,(F, E)26}
C D
17
Prim算法

B
34 12
U={A, F, C}
A 19 26 E
F V-U={B, D, E}
46 25 25 38 cost={(A, B)34, (C, D)17,
(F,D)25,(F, E)26}
C D
17
Prim算法

B
34 12
U={A, F, C, D}
A 19 26 E V-U={B, E}
F
46 cost={(A, B)34, (F, E)26,
25 25 38
(D,E)38}
C D
17
Prim算法

B
34 12
U={A, F, C, D, E}
A 19 26 E
V-U={B}
F
46 38 cost={(E, B)12,(A,B)34}
25 25

C D
17
Prim算法

B
34 12
U={A, F, C, D, E, B}
A 19 26 E
V-U={ }
F
46 25 25 38

C D
17
Prim算法
设无向连通带权图的邻接矩阵为数组c,一对顶点(vi, vj)
没有直接相连的边时c[i][j]=∞。

如何有效地找出满足条件vi∈U, vj∈V-U,且c[i][j]是最
小的边(vi, vj)?
采用两个数组closest和lowcost实现这个目标。
⚫ 对于每一个vj∈V-U ,closest[j]是vj在U中的邻接顶点,它vj与
在U中的其它邻接顶点vk比较有:
c[j][closest[j]]≤ c[j][k],lowcost[j]= c[j][closest[j]]
⚫ 在prim算法执行过程中,先找出V-U中使lowcost值最小的顶点
vj ,然后根据数组closest选取边(vj, closest[j]),最后将vj添加到
U中。根据vj对closest和lowcost做必要的修改。

68
算法5.9 构成最小生成树的PRIM算法实现
#define NUM 1000 //表示顶点的最大个数
#define maxint 10000000//表示∞
//形参n为顶点的个数,数组c为无向连通带权图的邻接矩阵
void Prim (int n, int c[][NUM])
{
int lowcost[NUM]; //记录最小权值
int closest[NUM]; //记录最近顶点
bool s[NUM] = {false}; //记录顶点是否已经加入最小生成树
//初始化数组
for (int i=1; i<=n; i++)
{
lowcost[i] = c[1][i];
closest[i] = 1;
s[i] = false;
}
//从顶点开始
s[1] = true; 69
//处理其余n-1个顶点 算法5.9 构成最小生成树的PRIM算法实现
for (int i=1; i<n; i++)
{
//在未处理的节点集中,找最小边权的结点vj
int min = maxint;
int j=1;
for(int k=2; k<=n; k++)
if((lowcost[k]<min) && (!s[k]) )
{
min = lowcost[k];
j = k;
}
//一条边(vj, closest[j])
printf("%d %d\n", closest[j], j);
//加入结点vj
s[j] = true;
//根据结点vj,更新数组lowcost和closest
for(int k=2; k<=n; k++)
if((c[j][k]<lowcost[k]) && (!s[k])) //遍历所有未加入最小生成树的结点,
如果通过vj到达结点k的权值小于当前最小权值,则更新lowcost[k]和closest[k]
{
lowcost[k] = c[j][k];
closest[k] = j;
70
}
}
}
克鲁斯卡尔(Kruskal)算法
贪心策略/基本思想:
1. 设无向连通网为G=(V, E),令G的最小生成树为T=(U, TE)
,其初态为U=V,TE={ };
2. 然后,按照边的权值由小到大的顺序,考察G的边集E中的
各条边。
 若被考察的边的两个顶点属于T的两个不同的连通分量
,则将此边作为最小生成树的边加入到T中,同时把两
个连通分量连接为一个连通分量;
 若被考察边的两个顶点属于同一个连通分量,则舍去此
边,以免造成回路.
3. 如此下去,当T中的连通分量个数为1时,此连通分量便为
G的一棵最小生成树。
克鲁斯卡尔(Kruskal)算法
B B
34 12

A 19 26 E A E
F F
46 25 25 38

C D C D
17
连通分量={A}, {B}, {C}, {D}, {E}, {F}
克鲁斯卡尔(Kruskal)算法
B B
34 12 12

A 19 26 E A E
F F
46 25 25 38

C D C D
17
连通分量={A}, {B}, {C}, {D}, {E}, {F}
连通分量={A}, {B, E}, {C}, {D}, {F}
克鲁斯卡尔(Kruskal)算法
B B
34 12 12

A 19 26 E A E
F F
46 25 25 38

C D C D
17 17
连通分量={A}, {B, E}, {C}, {D},{F}
连通分量={A}, {B, E}, {C, D},{F}
克鲁斯卡尔(Kruskal)算法
B B
34 12 12

A 19 26 E A 19 E
F F
46 25 25 38

C D C D
17 17
连通分量={A}, {B, E}, {C, D}, {F}
连通分量={A, F}, {B, E}, {C, D}
克鲁斯卡尔(Kruskal)算法
B B
34 12 12

A 19 26 E A 19 E
F F
46 25 25 38 25
C D C D
17 17
连通分量={A, F}, {B, E}, {C, D}
连通分量={A, F, C, D}, {B, E}
克鲁斯卡尔(Kruskal)算法
B B
34 12 12

A 19 26 E A 19 26 E
F F
46 25 25 38 25
C D C D
17 17
连通分量={A, F, C, D}, {B, E}
连通分量={A, F, C, D, B, E}
T的连通分量个数为1,即T为G的一棵最小生成树,连通
分量{A, B, C, D, E, F}为最小生成树的连通分量。
5.6.3 KRUSKAL算法
采用抽象数据类型并查集的基本运算,可以很方便地实现
Kruskal算法。
(1)建立边的数据结构:
#define N 1000
struct graph
{
int u, v, cost; //顶点u与顶点v的边权是cost
void set(int a, int b, int w){u=a, v=b, cost=w;}
};
graph d[N*(N+1)/2];
对于有n个顶点的图,其边的数量e≤n(n+1)/2 。

80
5.6.3 KRUSKAL算法
假设有m条边,每条边的描述是{v,w,edge},表示
顶点v与顶点w的边权是edge,则可以按如下方式构造边
集合d:
int i;
int v, w, edge;
for (i=0; i<m; i++)
{
scanf("%d%d%d", &v, &w, &edge);
d[i].set(v, w, edge);
}

81
5.6.3 KRUSKAL算法
(2)按边权COST的递增顺序排序
⚫ 排序因子:
int cmp(graph x, graph y)
{
return x.cost<y.cost;
}
⚫ 使用C++标准模板库函数排序:
sort(d, d+m, cmp);

82
5.6.3 KRUSKAL算法
(3)应用并查集的基本运算,构造G的最小生成树
并查集是一种树型的数据结构,用于处理一些不相交集合
(Disjoint Sets)的合并及查询问题。在使用中以森林表
示。
1、查找一个元素所在的集合
查找一个元素所在的集合,只要找到这个元素所在集合的
祖先。
⚫ 先设置一个数组Father[x],表示x的“父亲”的编号,即可转换为
寻找两个元素的最久远祖先是否相同,可以采用递归实现。
int father[N];
int Find(int x)
{
if (father[x]==-1) return x;
83
return father[x] = Find(father[x]);
}
1 2 3 4 5

① 合并1和2
1

2
1 2 3 4 5
father
1 2
1 3 4 5
1 2 3 4 5

① 合并1和2 ② 合并1和3
1 1

2 2 3

1 2 3 4 5
father
1 2
1 3
1 4 5
1和4之间是否存在关系?

1 2 3 4 5

① 合并1和2 ② 合并1和3 ③ 合并5和4


1 1

2 2 3
④ 合并3和5

1 2 3 4 5
father
1 2
1 3
1 4
5 5
3
5.6.3 KRUSKAL算法
2、合并两个不相交集合
 找到其中一个集合最久远的祖先,将另外一个集合最久远的祖先
指向它。
bool Union(int x, int y)
{
//判断两个元素是否属于同一集合,检查他们所在集合的祖先是否相同
x = Find(x);
V1
y = Find(y);
//祖先相同,是同一个集合 1
V2 V4
if (x==y) return false; 5
V3
//祖先不相同,合并两个集合
if (x>y) father[x] = y; 3 4 2

if (x<y) father[y] = x; V5 V6
return true;
} 结点 1 2 3 4 5 6
87
父结点 -1 1 1 1 2 4

数组father的值
5.6.3 KRUSKAL算法
3、应用并查集,实现Kruskal算法构造最小生成树
 已经按边权的大小升序排序,最先找到满足条件的边就是最小生成树中的
一条边(在main函数中)。 V1

memset(father, -1, sizeof(father)); //将数组father中的所有元素都


设置为-1。这个数组用来记录每个顶点的父节点,初始时所有顶点都没有 1
V2 V4
父节点 5
V3
int sum=0; //最小生成树中边权的和
int count=0; //已经找到的边数 3 4 2
for(i=0; i<n*(n+1)/2; i++) //遍历每一条边
V5 V6
{
//找到了一条满足条件的边(判断边d[i]的两个顶点d[i].u和d[i].v是
否属于不同的连通分量,如果是,则将它们加入最小生成树中)
if (Union(d[i].u, d[i].v)) 13
{ 46
printf("%d %d\n", d[i].u, d[i].v); 25
sum += d[i].cost; 36
++count; 23
} 15
88
if (count==n-1) break; 最后一行是构成的最小生
成树的边权和。
}
printf("%d\n",sum);
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

89
5.7 删数问题
给定n位正整数a,去掉其中任意k≤n个数字后,剩下的
数字按原次序排列组成一个新的正整数。对于给定的n位
正整数a和正整数k,设计一个算法找出剩下数字组成的新
数最小的删数方案。
输入
第1行是1个正整数a,第2行是正整数k。
输出
对于给定的正整数a,编程计算删去k个数字后得到的最小数。

输入样例 输出样例
178543 13
4

90
5.7 删数问题
删数问题
➢分析:
◆大整数的表示的问题
输入:字符串
存储: 整数数组(或字符数组)
5.7 删数问题
删数问题
➢分析:
◆贪心策略:
表面上看,删除最大的数。
但是
对于14385 ,删除一位
按照删除最大数的策略,1435
如果删除4: 1385(<1435)
因此,贪心策略为:
寻找最近下降点
5.7 删数问题
删数问题
➢分析:
◆本问题采用贪心算法求解,采用最近下降点优先的贪
心策略,即:
✓x1<x2<…<xi-1<xi,如果,xi>xi+1,则意味着出现
了下降点,
✓将xi删除。
✓得到一个n-1位的新数,并且这个新数是所有n-1位
数中最小的一个数
✓显然删去1位数后,原问题变为对n-1位数,删除k-
1个数的新问题
新问题与原问题性质相同,只是规模变小
对新问题,依然采用相同的删除策略进行处理
算法5.13删数问题的贪心算法实现
string a; //n位数a
int k; //删除数字的个数
cin>>a>>k;
//如果k≥n,数字被删完了
if (k >= a.size()) a.erase();
else while(k > 0)
{
//寻找最近下降点(即从左到右找到第一个满足a[i] > a[i+1]的位置i)
int i;
for (i=0; (i<a.size()-1) && (a[i] <= a[i+1]); ++i);
a.erase(i, 1); //删除xi
k- -;
}
//删除字符串a中的前导数字0,即当字符串长度大于1且第一个字符为0时,
删除第一个字符
while(a.size() > 1 && a[0] == '0')
a.erase(0, 1); 94
cout<<a<<endl; //输出处理后的字符串a
第5章 贪心算法
5.1 活动安排问题 5.6 最小生成树
5.2 贪心算法的理论基础 5.6.1 最小生成树的性质
5.2.1 贪心选择性质 5.6.2 Prim算法
5.2.2 最优子结构性质 5.6.3 Kruskal算法
5.2.3 贪心算法的求解过程 5.7 删数问题
5.3 背包问题 5.7.1 问题的贪心选择性质
5.7.2 问题的最优子结构性质
5.4 最优装载问题
5.8 多处最优服务次序问题
5.5 单源最短路径
5.8.1 问题的贪心选择性质
5.8.2 问题的最优子结构性质

95
5.8 多处最优服务次序问题
 设有n个顾客同时等待一项服务,顾客i需要的服务时间为ti,
1≤i≤n,共有s处可以提供此项服务。应如何安排n个顾客的服务次
序才能使平均等待时间达到最小?平均等待时间是n个顾客等待服务
时间的总和除以n。
 给定的n个顾客需要的服务时间和s的值,编程计算最优服务次序。
 输入
第一行有2个正整数n和s,表示有n个顾客且有s处可以提供顾客需
要的服务。接下来的1行中,有n个正整数,表示n个顾客需要的服
务时间。
 输出
最小平均等待时间,输出保留3位小数。

输入样例
10 2
56 12 1 99 1000 234 33 55 99 812
96
输出样例
336.000
5.8 多处最优服务次序问题

97
5.8 多处最优服务次序问题

➢分析:
◆假设只有一个资源,2个顾客,需要服务时间分别是
10,1. 要使等待时间(包括服务所需要的时间)最
短,按照什么顺序处理?
◆两个资源,三个客户,需要服务时间分别是1,10
,5. 要使等待时间最短,按照什么顺序处理?

◆由此,任务的解决具有什么特点?
5.8 多处最优服务次序问题

设计贪心策略如下:
⚫ 对服务时间最短的顾客先服务的贪心选择策略。
⚫ 首先对需要服务时间最短的顾客进行服务,即做完第一次选择后,
原问题T变成了需对n—1个顾客服务的新问题T’。
⚫ 新问题和原问题相同,只是问题规模由n减小为n—1。
⚫ 基于此种选择策略,对新问题T’,在n—1个顾客中选择服务时间
最短的先进行服务,如此进行下去,直至所有服务都完成为止。

输入样例
10 2
56 12 1 99 1000 234 33 55 99 812

i 0 1 2 3 4 5 6 7 8 9
x 1 12 33 55 56 99 99 234 812 1000
99
排序后的数组client
5.8 多处最优服务次序问题
多处最优服务次序问题贪心算法的计算过程

排序后的数组client
i 0 1 2 3 4 5 6 7 8 9
x 1 12 33 55 56 99 99 234 812 1000

Service Sum
数组
(单个顾客等待时间) (顾客等待时间的总和)
j(窗口) 0 1 0 1
i=0,1 1 12 1 12
i=2,3 34 67 35 79
i=4,5 90 166 125 245
i=6,7 189 400 314 645
i=8,9 1001 1400 1315 2045

平均等待时间 100

=(1315+2045)/10=336
算法5.14 多处最优服务次序问题的贪心算法实现
//顾客等待的队列为client,提供服务的窗口s个
double greedy(vector<int> client, int s)
{
//服务窗口的单个顾客等待时间
vector<int> service(s+1, 0);
//服务窗口顾客等待时间的总和
vector<int> sum(s+1, 0);
//顾客的数量
int n = client.size();
//按顾客的服务时间升序排序
sort(client.begin(), client.end());

101
算法5.14 多处最优服务次序问题的贪心算法实现
//顾客等待的队列为client,提供服务的窗口s个
double greedy(vector<int> client, int s)
{
……
//贪心算法的实现
int i=0; //顾客的指针
int j=0; //窗口的指针
while(i < n) //将当前顾客的服务时间加到对应窗口的等待时间service[j]
上,再将当前窗口的等待时间累加到总和sum[j]上。
{ 数组 service sum
service[j] += client[i]; j 0 1 0 1
sum[j] += service[j]; i=0,1 1 12 1 12
++i, ++j; i=2,3 34 67 35 79
if(j == s) j = 0; i=4,5 90 166 125 245
} i=6,7 189 400 314 645
//计算所有窗口服务时间的总和 i=8,9 1001 1400 1315 2045
double t=0;
for(i=0; i<s; ++i) t += sum[i];
t /= n; 102
i 0 1 2 3 4 5 6 7 8 9
return t;
x 1 12 33 55 56 99 99 234 812 1000
}

You might also like