贪心算法

Download Report

Transcript 贪心算法

贪心算法
贪心法的设计思想
贪心法的求解过程
贪心法的基本要素
贪心法的应用举例
1 贪心法的设计思想
贪心法在解决问题的策略上目光短浅,只根据当
前已有的信息就做出选择,而且一旦做出了选择,
不管将来有什么结果,这个选择都不会改变。换言
之,贪心法并不是从整体最优考虑,它所做出的选
择只是在某种意义上的局部最优。
这种局部最优选择并不总能获得整体最优解
(Optimal Solution),但通常能获得近似最优解
(Near-Optimal Solution)。
引例 [找零钱]
• 一个小孩买了价值少于1美元的糖,并将1美元
的钱交给售货员。售货员希望用数目最少的硬币
找给小孩。
• 假设提供了数目不限的面值为2 5美分、1 0美
分、5美分、及1美分的硬币。
• 售货员分步骤组成要找的零钱数,每次加入一个
硬币。选择硬币时所采用的贪心准则如下:每一
次选择应使零钱数尽量增大。为保证解法的可行
性(即:所给的零钱等于要找的零钱数),所选
择的硬币不应使零钱总数超过最终所需的数目。
算法思想
• 为使找回的零钱的硬币数最小,不考虑找零钱的所
有各种方案,而是从最大面值的币种开始,按递减
的顺序考虑各币种,先尽量用大面值的币种,只当
不足大面值币种的金额才会去考虑下一种较小面值
的币种。这就是在采用贪婪法。
• 这种方法在这里之所以总是最优,是因为银行对其
发行的硬币种类和硬币面值的巧妙安排。
• 如果只有面值分别为1,5和11单位的硬币,而希望
找回总额为15单位的硬币,按贪婪算法,应找1个11
单位面值的硬币和4个1单位面值的硬币,共找回5个
硬币。但最优的解答应是3个5单位面值的硬币。
贪心法求解的问题的特征:
(1)最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此
问题具有最优子结构性质,也称此问题满足最优性原理。
(2)贪心选择性质
所谓贪心选择性质是指问题的整体最优解可以通过一
系列局部最优的选择,即贪心选择来得到。
 动态规划法通常以自底向上的方式求解各个子问题,而贪
心法则通常以自顶向下的方式做出一系列的贪心选择。
2 贪心法的求解过程
用贪心法求解问题应该考虑如下几个方面:
(1)候选集合C:为了构造问题的解决方案,有一
个候选集合C作为问题的可能解,即问题的最终解
均取自于候选集合C。例如,在付款问题中,各种
面值的货币构成候选集合。
(2)解集合S:随着贪心选择的进行,解集合S不
断扩展,直到构成一个满足问题的完整解。例如,
在付款问题中,已付出的货币构成解集合。
(3)解决函数solution:检查解集合S是否构成问
题的完整解。例如,在付款问题中,解决函数是已
付出的货币金额恰好等于应付款。
(4)选择函数select:即贪心策略,这是贪心法
的关键,它指出哪个候选对象最有希望构成问题的
解,选择函数通常和目标函数有关。例如,在付款
问题中,贪心策略就是在候选集合中选择面值最大
的货币。
(5)可行函数feasible:检查解集合中加入一个候
选对象是否可行,即解集合扩展后是否满足约束条
件。例如,在付款问题中,可行函数是每一步选择
的货币和已付出的货币相加不超过应付款。
贪心法的一般过程
Greedy(C) //C是问题的输入集合即候选集合
{
S={ }; //初始解集合为空集
while (not solution(S)) //集合S没有构成问题的一个解
{
x=select(C); //在候选集合C中做贪心选择
if feasible(S, x) //判断集合S中加入x后的解是否可行
S=S+{x};
C=C-{x};
}
return S;
}
例1、 活动安排问题
活动安排问题就是要在所给的活动集合中选出
最大的相容活动子集合,是可以用贪心算法有效求解
的很好例子。该问题要求高效地安排一系列争用某一
公共资源的活动。贪心算法提供了一个简单、漂亮的
方法使得尽可能多的活动能兼容地使用公共资源。
例1、活动安排问题
设有n个活动的集合E={1,2,…,n},其中每个活动都
要求使用同一资源,如演讲会场等,而在同一时间内只有
一个活动能使用这一资源。每个活动i都有一个要求使用
该资源的起始时间begin[i]和一个结束时间end[i],且begin[i]
<end[i] 。如果选择了活动i,则它在半开时间区间[begin[i],
end[i])内占用资源。若区间[begin[i], end[i])与区间[begin[j],
end[j])不相交,则称活动i与活动j是相容的。也就是说,当
begin[i]≥end[j]或begin[j]≥end[i]时,活动i与活动j相容。
a 和 b 事件的开始时刻小于结束时刻
Begin[a]<End[a]
Begin[b]<End[b]
b 事件的开始时刻大于等于
a 事件的结束时刻,即
Begin[b] >= End[a]
记为 b > a
这时 b 事件与 a 事件不重叠.
例1、活动安排问题
例:设待安排的12个活动的开始时间和结束时间按结
束时间的非减序排列如下:
i
0 1
2
3
4
5
6
7
8
9
10 11
beg 1 3
in[i]
end 3 4
[i]
0
3
2
5
6
4
10
8
15 15
7
8
9
10 12 14 15 18 19 20
找出 时间上不重叠的事件:
2#,9#
2#,8#,10#
2#,8#,11#
0#,3#,8#,10#
0#,3#,8#,11#
0#,1#,5#,8#,10#
0#,1#,5#,8#,11#
0#,1#,6#,10#
0#,1#,6#,11#
每个都选择最早结束的序列---贪心策略
0#-1#-5#-8#-10#
这是最长序列
#include<iostream.h>
const int N=12;
void OtputResult(int Select[N]);
{ cout<<“{ 0”;
for( int i=1; i<N; i++ )
if ( Select[ i ]=1)
cout<<“,”<< i ;
cout<< “ } “ <<endl;
}
void main( )
{ int Begin[N]={1,3,0,3,2,5,6,4,10,8,15,15};
int
End[N]={3,4,7,8,9,10,12,14,15,18,19,20};
int Select[N]={0,0,0,0,0,0,0,0,0,0,0,0};
int i=0;//当前最早结束的事件
//当前可选事件的最早开始时间
int TimeStart=0;
while( i < N )
{
if ( Begin[ i ]>=TimeStart )
{ Select[ i ]=1;
TimeStart=End[ i ]; }
i ++;
}
OutputResult ( Select ) ; }
算法正确性
• 如何证明?
3、贪心算法的基本要素
对于一个具体的问题,怎么知道是否可用贪心算
法解此问题,以及能否得到问题的最优解呢?这个问题
很难给予肯定的回答。
但是,从许多可以用贪心算法求解的问题中看到
这类问题一般具有2个重要的性质:贪心选择性质和
最优子结构性质。
3 贪心算法的基本要素
1.贪心选择性质
所谓贪心选择性质是指所求问题的整体最优解可
以通过一系列局部最优的选择,即贪心选择来达到。
这是贪心算法可行的第一个基本要素,也是贪心算法
与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,
而贪心算法则通常以自顶向下的方式进行,以迭代的
方式作出相继的贪心选择,每作一次贪心选择就将所
求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择
性质,必须证明每一步所作的贪心选择最终导致问题
的整体最优解。
3 贪心算法的基本要素
2.最优子结构性质
当一个问题的最优解包含其子问题的最优
解时,称此问题具有最优子结构性质。问题的
最优子结构性质是该问题可用动态规划算法或
贪心算法求解的关键特征。
贪心与动态规划
• 【例】在一个N×M的方格阵中,每一格子赋予一
个数(即为权)。规定每次移动时只能向上或向
右。现试找出一条路径,使其从左下角至右上角
所经过的权之和最大。
3
4
6
• 贪心:1 →3 →4 → 6
1
2
10
• 动规:1 →2 →10 → 6
• 局部最优解VS全局最优解
例2、 背包问题
给定n种物品和一个容量为C的背包,物
品i的重量是wi,其价值为vi,背包问题是如何
选择装入背包的物品,使得装入背包中物品的
总价值最大?
• 0-1背包问题:
给定n种物品和一个背包。物品i的重量是Wi,
其价值为Vi,背包的容量为C。应如何选择装入背包的
物品,使得装入背包中物品的总价值最大?
在选择装入背包的物品时,对每种物品i只有2种选择,即
装入背包或不装入背包。不能将物品i装入背包多次,也不能只
装入部分的物品i。
• 背包问题:
与0-1背包问题类似,所不同的是在选择物品i装
入背包时,可以选择物品i的一部分,而不一定要全
部装入背包,1≤i≤n。
这2类问题都具有最优子结构性质,极为相似,但
背包问题可以用贪心算法求解,而0-1背包问题却不能
用贪心算法求解。
设xi表示物品i装入背包的情况,根据问题的要求,
有如下约束条件和目标函数:
n

wi xi  C



i 1
0  xi  1 (1  i  n)
n
max vi xi
(式7.1)
(式7.2)
i 1
于是,背包问题归结为寻找一个满足约束条
件式7.1,并使目标函数式7.2达到最大的解向量
X=(x1, x2, …, xn)。
至少有三种看似合理的贪心策略:
(1)选择价值最大的物品,因为这可以尽可能快
地增加背包的总价值。但是,虽然每一步选择获得
了背包价值的极大增长,但背包容量却可能消耗得
太快,使得装入背包的物品个数减少,从而不能保
证目标函数达到最大。
(2)选择重量最轻的物品,因为这可以装入尽可
能多的物品,从而增加背包的总价值。但是,虽然
每一步选择使背包的容量消耗得慢了,但背包的价
值却没能保证迅速增长,从而不能保证目标函数达
到最大。
(3)选择单位重量价值最大的物品,在背包价值
增长和背包容量消耗两者之间寻找平衡。
应用第三种贪心策略,每次从物品集合中选择单
位重量价值最大的物品,如果其重量小于背包容量,
就可以把它装入,并将背包容量减去该物品的重量,
然后我们就面临了一个最优子问题——它同样是背包
问题,只不过背包容量减少了,物品集合减少了。因
此背包问题具有最优子结构性质。
例如,有3个物品,其重量分别是{20, 30, 10},价值分
别为{60, 120, 50},背包的容量为50,应用三种贪心策
略装入背包的物品和获得的价值如图所示。
20
50
20
120
50
背包
(a) 三个物品及背包
180
(b) 贪心策略1
10/20
20
30
10
10
190
200
30
30
10
60
20/30
(c) 贪心策略2 (d) 贪心策略3
设背包容量为C,共有n个物品,物品重量存放
在数组w[n]中,价值存放在数组v[n]中,问题的解存
放在数组x[n]中。
1.改变数组w和v的排列顺序,使其按单位重量价值v[i]/w[i]降序排列;
2.将数组x[n]初始化为0; //初始化解向量
3.i=1;
循环直到(w[i]>C)
1 x[i]=1; //将第i个物品放入背包
2 C=C-w[i];
3 i++;
5. x[i]=C/w[i];
算法的时间主要消耗在将各种物品依其单位重量的价值从
大到小排序。因此,其时间复杂性为O(nlog2n)。
对于0-1背包问题,贪心选择之所以不能得到最优解
是因为在这种情况下,它无法保证最终能将背包装满,部
分闲置的背包空间使每公斤背包空间的价值降低了。事实
上,在考虑0-1背包问题时,应比较选择该物品和不选择
该物品所导致的最终方案,然后再作出最好选择。由此就
导出许多互相重叠的子问题。这正是该问题可用动态规划
算法求解的另一重要特征。
实际上也是如此,动态规划算法的确可以有效地解0-1
背包问题。
例3 应用-- 货箱装船
货箱装船问题:设有编号为0…n-1的n种物品,
重量分别为w0…wn-1,船载重为c,如何装载,使
船可以装载更多的货箱。
算法:采用贪心算法船可以分步装载,每步
装一个货箱,且需要考虑装载哪一个货箱。根据
这种思想可利用如下贪心准则:从剩下的货箱中,
选择重量最小的货箱。这种选择次序可以保证所
选的货箱总重量最小,从而可以装载更多的货箱。
根据这种贪婪策略,首先选择最轻的货箱,然后
选次轻的货箱,如此下去直到所有货箱均装上船
或船上不能再容纳其他任何一个货箱。
货箱装船
#include <iostream.h>
void IndirectSort(int w[], int t[], int n)
{// Cluge to test when weights already in order.
for (int i=1; i <= n; i++) t[i] = i;
}
template<class T>
void ContainerLoading(int x[], T w[], T c, int n)
{// Greedy algorithm for container loading.
// Set x[i] = 1 iff container i, 1<=i<=n is loaded.
// c is ship capacity, w gives container weights.
// do indirect addressing sort of weights
// t is the indirect addressing table
货箱装船
int *t = new int [n+1];
IndirectSort(w, t, n);
// now, w[t[i]] <= w[t[i+1]], 1<=i<n
// initialize x
for (int i = 1; i <= n; i++)
x[i] = 0;
// select objects in order of weight
for (int i = 1; i <= n && w[t[i]] <= c; i++) {
x[t[i]] = 1;
c -= w[t[i]];} // remaining capacity
delete [] t;}
货箱装船
void main(void)
{
int w[13] = {0, 20, 50, 50, 80, 90, 100,
150, 200}, x[13];
ContainerLoading(x, w, 400, 8);
cout << "Loading vector is" << endl;
for (int i = 1; i <= 8; i++)
cout << x[i] << ' ';
cout << endl;
}
例4、 哈夫曼编码
哈夫曼编码是广泛地用于数据文件压缩的十分有
效的编码方法。其压缩率通常在20%~90%之间。哈夫
曼编码算法用字符在文件中出现的频率表来建立一个
用0,1串表示各字符的最优表示方式。
给出现频率高的字符较短的编码,出现频率较低
的字符以较长的编码,可以大大缩短总码长。
1.前缀码
对每一个字符规定一个0,1串作为其代码,并要求
任一字符的代码都不是其他字符代码的前缀。这种编
码称为前缀码。
例4、 哈夫曼编码
编码的前缀性质可以使译码方法非常简单。
表示最优前缀码的二叉树总是一棵完全二叉树,
即树中任一结点都有2个儿子结点。
平均码长定义为:
B(T )   f (c)d T (c)
cC
使平均码长达到最小的前缀码编码方案称为给定
编码字符集C的最优前缀码。
例4、哈夫曼编码
2.构造哈夫曼编码
哈夫曼提出构造最优前缀码的贪心算法,由此产
生的编码方案称为哈夫曼编码。
哈夫曼算法以自底向上的方式构造表示最优前缀
码的二叉树T。
算法以|C|个叶结点开始,执行|C|-1次的“合并”
运算后产生最终所要求的树T。
例4、哈夫曼编码
在书上给出的算法huffmanTree中,编码字符集中
每一字符c的频率是f(c)。以f为键值的优先队列Q用在
贪心选择时有效地确定算法当前要合并的2棵具有最小
频率的树。一旦2棵具有最小频率的树合并后,产生一
棵新的树,其频率为合并的2棵树的频率之和,并将新
树插入优先队列Q。经过n-1次的合并后,优先队列中
只剩下一棵树,即所要求的树T。
算法huffmanTree用最小堆实现优先队列Q。初始
化优先队列需要O(n)计算时间,由于最小堆的
removeMin和put运算均需O(logn)时间,n-1次的合并
总共需要O(nlogn)计算时间。因此,关于n个字符的哈
夫曼算法的计算时间为O(nlogn) 。
例4、哈夫曼编码
3.哈夫曼算法的正确性
要证明哈夫曼算法的正确性,只要证明最优前缀
码问题具有贪心选择性质和最优子结构性质。
(1)贪心选择性质
(2)最优子结构性质
例5、 单源最短路径
给定带权有向图G =(V,E),其中每条边的权是非
负实数。另外,还给定V中的一个顶点,称为源。现在
要计算从源到所有其他各顶点的最短路长度。这里路
的长度是指路上各边权之和。这个问题通常称为单源
最短路径问题。
1.算法基本思想
Dijkstra算法是解单源最短路径问题的贪心算法。
例5、 单源最短路径
其基本思想是,设置顶点集合S并不断地作贪心选
择来扩充这个集合。一个顶点属于集合S当且仅当从源
到该顶点的最短路径长度已知。
初始时,S中仅含有源。设u是G的某一个顶点,把
从源到u且中间只经过S中顶点的路称为从源到u的特殊
路径,并用数组dist记录当前每个顶点所对应的最短
特殊路径长度。Dijkstra算法每次从V-S中取出具有最
短特殊路长度的顶点u,将u添加到S中,同时对数组
dist作必要的修改。一旦S包含了所有V中顶点,dist
就记录了从源到所有其他顶点之间的最短路径长度。
5 单源最短路径
2.算法的正确性和计算复杂性
(1)贪心选择性质
(2)最优子结构性质
(3)计算复杂性
对于具有n个顶点和e条边的带权有向图,如果用
带权邻接矩阵表示这个图,那么Dijkstra算法的主循
环体需要 O(n) 时间。这个循环需要执行n-1次,所以完
成循环需要 O(n 2 ) 时间。算法的其余部分所需要时间不
超过 O(n 2 ) 。
6 最小生成树
设G =(V,E)是无向连通带权图,即一个网络。E中
每条边(v,w)的权为c[v][w]。如果G的子图G’是一棵包
含G的所有顶点的树,则称G’为G的生成树。生成树上
各边权的总和称为该生成树的耗费。在G的所有生成树
中,耗费最小的生成树称为G的最小生成树。
网络的最小生成树在实际中有广泛应用。例如,
在设计通信网络时,用图的顶点表示城市,用边(v,w)
的权c[v][w]表示建立城市v和城市w之间的通信线路所
需的费用,则最小生成树就给出了建立通信网络的最
经济的方案。
6 最小生成树
1.最小生成树性质
用贪心算法设计策略可以设计出构造最小生成树
的有效算法。本节介绍的构造最小生成树的Prim算法
和Kruskal算法都可以看作是应用贪心算法设计策略的
例子。尽管这2个算法做贪心选择的方式不同,它们都
利用了下面的最小生成树性质:
设G=(V,E)是连通带权图,U是V的真子集。如果
(u,v)E,且uU,vV-U,且在所有这样的边中,
(u,v)的权c[u][v]最小,那么一定存在G的一棵最小生
成树,它以(u,v)为其中一条边。这个性质有时也称为
MST性质。
6 最小生成树
2.Prim算法
设G=(V,E)是连通带权图,V={1,2,…,n}。
构造G的最小生成树的Prim算法的基本思想是:首
先置S={1},然后,只要S是V的真子集,就作如下的贪
心选择:选取满足条件iS,jV-S,且c[i][j]最小
的边,将顶点j添加到S中。这个过程一直进行到S=V时
为止。
在这个过程中选取到的所有边恰好构成G的一棵最
小生成树。
6 最小生成树
利用最小生成树性质
和数学归纳法容易证明,
上述算法中的边集合T始终
包含G的某棵最小生成树中
的边。因此,在算法结束
时,T中的所有边构成G的
一棵最小生成树。
例如,对于右图中的
带权图,按Prim算法选取
边的过程如下页图所示。
6 最小生成树
Prim’s Algorithm
Q := V[G];
Complexity:
for each u  Q do
Using binary heaps: O(E lg V).
key[u] := 
Initialization – O(V).
od;
Building initial queue – O(V).
key[r] := 0;
V Extract-Min’s – O(V lgV).
[r] := NIL;
E Decrease-Key’s – O(E lg V).
while Q   do
u := Extract - Min(Q);
Using Fibonacci heaps: O(E + V lg V).
for each v  Adj[u] do
if v  Q and w(u, v) < key[v]
then [v] := u;
key[v] := w(u, v)  decrease-key operation
fi
od
od
Note: A = {(v, [v]) : v  v - {r} - Q}.
6 最小生成树
3.Kruskal算法
Kruskal算法构造G的最小生成树的基本思想是,
首先将G的n个顶点看成n个孤立的连通分支。将所有的
边按权从小到大排序。然后从第一条边开始,依边权
递增的顺序查看每一条边,并按下述方法连接2个不同
的连通分支:当查看到第k条边(v,w)时,如果端点v和
w分别是当前2个不同的连通分支T1和T2中的顶点时,
就用边(v,w)将T1和T2连接成一个连通分支,然后继续
查看第k+1条边;如果端点v和w在当前的同一个连通分
支中,就直接再查看第k+1条边。这个过程一直进行到
只剩下一个连通分支时为止。
6 最小生成树
例如,对前面的连通带权图,按Kruskal算法顺序得到的
最小生成树上的边如下图所示。
6 最小生成树
关于集合的一些基本运算可用于实现Kruskal算法。
按权的递增顺序查看等价于对优先队列执行
removeMin运算。可以用堆实现这个优先队列。
对一个由连通分支组成的集合不断进行修改,需
要用到抽象数据类型并查集UnionFind所支持的基本
运算。
当图的边数为e时,Kruskal算法所需的计算时间
是O(e loge) 。当 e2  (n 2 ) 时,Kruskal算法比Prim算法
差,但当 e  o(n ) 时,Kruskal算法却比Prim算法好得
多。
MST-Kruskal(G, w)
1
2
3
4
5
6
7
8
9
A
// initially A is empty
for each vertex v  V[G]
// line 2-3 takes O(V) time
do Make-Set(v)
// create set for each vertex
sort the edges of E into nondecreasing order by weight w
for each edge (u,v)  E, taken in nondecreasing order by weight
do if Find-Set(u)  Find-Set(v) // u&v on different trees
then A  A  {(u,v)}
Union(u,v)
return A
Total running time is O(E lg E).
例题、The Dragon of
Loowater (UVA 11292)
你的王国有一条n个头的恶龙,你希望雇一些骑士把它杀
死(即砍掉所有的头)。现有m个骑士可以雇佣,一个能力
值为x的骑士可以砍掉恶龙一个直径不超过x的头,且需要支
付x个金币。如何雇佣骑士才行砍掉恶龙的所有头,且需要
支付的金币最少?注意,一个骑士只能砍掉一个头(且不行
被雇佣两次)。
问题分析
能力强的骑士开价高是合理的,但如果被你派去砍一个很弱
的头,就是浪费人才了。因此,可以把雇佣来的骑士按照能力从
小到大排序,所有头按照直径从小到大排序,一个一个砍就可以
了。当然,不能砍掉“当前需要砍的头”的骑士就不要雇佣了。
例题、Assemble
(LA 3971)
你有b块钱,想要组装一台电脑。给出n个配件各自的种类、
品质因子和价格,要求每种类型的配件各买一个,总价格不
超过b,且“品质最差配件”的品质因子应尽量大。
数据:1<=n<=1000, 1<=b<=10^9, 价格不超过10^6,品质
因子不超过10^9.
问题分析
解决“最小值最大”的常用方法是二分答案。
假设答案为x,如何判断这个x是最小还是最大呢?删除品质
因子小于x的所有配件,然后每种类型的配件优先选择最便宜的,
如果可以组装出一台不超过b元的电脑,那么标准答案ans>=x,
否则ans<=x.
例题、Commando War
(UVA 11729)
你有n个部下,每个部下需要完成一项任务。第i个部下需
要你花Bi分钟交代任务,然后他会立刻独立地、无间断地执
行Ji分钟后完成任务。你需要选择交代任务的顺序,使得所
有任务尽早执行完毕(即最后一个执行完的任务应尽早结
束)。注意,不能同时给两个部下交代任务,但部下们可以
同时执行他们各自的任务。
问题分析
直觉告诉我们,执行时间长的任务应该先交代。于是我们
想到一个贪心算法:按照J从大到小的顺序给各个任务排序,然
后依次交代。
这个算法正确吗?如何证明?
例题、Fill the Square
(UVA 11520)
在n*n网格中填了一些大写字母,你的任务是把剩下的格
子中也填满大写字母,使得任意两个相邻格子(即有公共边
的格子)中个字母不同。如果有多种填法,则要求按照从上
到下、从左到右的顺序把所有格子连接起来得到的字符串的
字典序尽量小。
问题分析
直觉告诉我们,执行时间长的任务应该先交代。于是我们
想到一个贪心算法:按照J从大到小的顺序给各个任务排序,然
后依次交代。
这个算法正确吗?如何证明?
例题、Network
(LA 3902)
n台机器连成一个树状网络,其中叶子结点是客户端,其
他结点是服务器。目前有一台服务器正在提供视频服务,虽
然视频质量本身很不错,但对于那些离他很远的客户端来说,
网络延迟却难以忍受。你的任务是在一些其他服务器上也安
装同样的服务,使得每台客户端到最近服务器的距离不超过
给定的整数k。为了节约成本,安装服务的服务器台数应尽
量少。
问题分析
通常,把无根树变成有根树有助于解题。本题中,原始的
服务器就是一个天然的根。对于那些已经满足条件(即到原始服
务器的距离不超过k的客户端,直接当它们不存在就可以了)。
接下来,考虑深度最大的结点u,选择u的k级祖先是最划算
的(父亲是一级祖先,父亲的父亲是2级祖先,以此类推)。
算法实现:每放一个服务器,进行一次DFS,覆盖与它距离
不超过k的所有结点。注意,本题只需要覆盖叶子结点,而且深
度不超过k的叶子已经被原始服务器覆盖了,所有我们只需要处
理深度大于k的叶结点即可。
例题、删数游戏
键盘输入一个高精度的正整数N,去掉其中任意S个数字后
剩下的数字按原左右次序将组成一个新的正整数。编程对给
定的N和S,寻找一种方案使得剩下的数字组成的新数最小。
输入数据均不需判错。输出应包括所去掉的数字的位置
和组成的新的正整数
问题分析
在位数固定的前提下,让高位的数字尽量小其值就较小,依
据此贪婪策略就可以解决这个问题。
怎么样根据贪婪策略删除数字呢?总目标是删除高位较大的
数字,具体地相邻两位比较若高位比低位大则删除高位。我们通
过“枚举归纳”设计算法的细节,看一个实例(s=3) :
n1=“1 2 4 3 5 8 6 3”
4比3大 删除
“1 2
3 5 8 6 3”
8比6大 删除
“1 2
3 5
6 3”
6比3大 删除
“1 2
3 5
3”
问题分析
只看这个实例,有可能“归纳”出不正确的算法,先看下一个实
例,我们再进一步解释:
n2=”2 3 1 1 8 3”
3比1大 删除
“2
1 1 8 3”
2比1大 删除
“
1 1 8 3”
8比3大 删除
“
1 1
3”
再看以下两个实例又可总结出一些需要算法特殊处理的情况。
n3=”1 2 3 4 5
6 7”
s=3
由这个实例看出,经过对n3相邻比较一个数字都没有删除,
这就要考虑将后三位进行删除,当然还有可能,在相邻比较的
过程中删除的位数小于s时,也要进行相似的操作。
n4=”1 2 0 0 8 3”
3比0大 删除
“1
0 0 8 3”
2比0大 删除
“
0 0 8 3”
8比3大 删除
“
0 0
3”
得到的新数数据是3
由这个实例子又能看出,当删除掉一些数字后,结果的高位有
可能出现数字“0”,直接输出这个数据不合理,要将结果中高位
的数字“0”全部删除掉,再输出。特别地还要考虑若结果串是
“0000”时,不能将全部“0”都删除,而要保留一个“0”最后输
出。
由此可以看出进行算法设计时,从具体到抽象的归纳一定要选
取大量不同的实例,充分了解和体会解决问题的过程、规律和各
种不同情况,才能设计出正确的算法。
思考:区间覆盖问题
用i来表示x轴上坐标为[i-1,i]的区间(长度为1),并
给出M(1=<M=<200)个不同的整数,表示M个这样的区间。
现在让你画几条线段覆盖住所有的区间,条件是:每条
线段可以任意长,但是要求所画线段之和最小,并且线
段的数目不超过N(1=<N=<50)。
例如:M=5个整数1、3、4、8和11表示区间,要求所用线
段不超过N=3条
0 1 2 3
4
5
6
7
8
9 10 11
算法分析:
• 如果N>=M,那么显然用M条长度为1的线段
可以覆盖住所有的区间,所求的线段总长为
M。
• 如果N=1,那么显然所需线段总长为:…
• 如果N=2,相当于N=1的情况下从某处断开
(从哪儿断开呢?)。
• 如果N=k呢?
设计一个算法, 把一个真分数表示为埃及分数之和的形式。
所谓埃及分数,是指分子为1的形式。如:7/8=1/2+1/3+1/24。
取数游戏
有2个人轮流取2n个数中的n个数,取数之和大者为胜。请
编写算法,让先取数者胜,模拟取数过程。