算法导论学习——16章 贪心算法

应用范围

求解最优化问题中,每个步骤面临多种选择。对于许多最优化问题来说,使用动态规划过于繁琐。对于某些特定的问题,可以通过不断取局部最优化得到全局最优化的方法,这种方法就是贪心算法。
贪心算法不保证得到最优解,但有些时候可以得到最优解。

一个例子

活动选择问题

n n n个活动,这些活动争用同一个资源。每个活动有一个开始时间 s i s_i si和结束时间 f i f_i fi,假如两个活动 a i a_i ai a j a_j aj满足 [ s i , f i ) [s_i,f_i) [si,fi) [ s j , f j ) [s_j,f_j) [sj,fj)不重叠,则称这两个活动兼容。给定一个活动集合 A = { a 1 , a 2 , . . . , a n } A=\{a_1,a_2,...,a_n\} A={a1,a2,...,an},求最长的兼容活动集合的长度。

i1234567891011
s i s_i si130535688213
f i f_i fi4567991011121416

使用动态规划解决

  1. 验证该问题具有最优子结构性质
    S i j S_{ij} Sij表示在 a i a_i ai结束之后开始,在 a j a_j aj开始之前结束的活动集合,假定 A i j A_{ij} Aij S i j S_{ij} Sij的最大兼容子集,包含活动 a k a_k ak。由此得到两个子问题,寻找 S i k S_{ik} Sik的兼容活动和 S k j S_{kj} Skj的兼容活动。令 A i k = A i j ∩ S i k A_{ik}=A_{ij} \cap S_{ik} Aik=AijSik, A k j = A i j ∩ S k j A_{kj}=A_{ij}\cap S_{kj} Akj=AijSkj,则 ∣ A i j ∣ = ∣ A i k ∣ + ∣ A k j ∣ + 1 |A_{ij}|=|A_{ik}|+|A_{kj}|+1 Aij=Aik+Akj+1
    反证法证明必然包含子问题最优解:假设 S i k S_{ik} Sik有兼容子集 A i k ′ A_{ik}' Aik使得 ∣ A i k ′ ∣ > ∣ A i k ∣ |A_{ik}'|>|A_{ik}| Aik>Aik,则将 A i k ′ A_{ik}' Aik作为子问题的解,使得 ∣ A i j ′ ∣ > ∣ A i j ∣ |A_{ij}'|>|A_{ij}| Aij>Aij,与 A i j A_{ij} Aij是最优解的假设相悖。
  2. 刻画最优解结构
    c [ i ] [ j ] c[i][j] c[i][j]表示 S i j S_{ij} Sij最优解的大小,则 c [ i ] [ j ] = c [ i ] [ k ] + c [ k ] [ j ] + 1 c[i][j]=c[i][k]+c[k][j]+1 c[i][j]=c[i][k]+c[k][j]+1
    • c [ i ] [ j ] = 0 c[i][j]=0 c[i][j]=0, ∣ S i j ∣ = 0 |S_{ij}|=0 Sij=0
    • c [ i ] [ j ] = max ⁡ a k ∈ S i j { c [ i ] [ k ] + c [ k ] [ j ] + 1 } c[i][j]=\max_{a_k\in S_{ij}}\{c[i][k]+c[k][j]+1\} c[i][j]=maxakSij{c[i][k]+c[k][j]+1}

通过自底向上或带备忘的自顶向下的求解策略,可以通过动态规划的算法解决问题。

复杂度分析

变化的参数有 i , j , k i,j,k i,j,k,因此有三层循环,时间复杂度为 O ( n 3 ) O(n^3) O(n3)

贪心法解决

  • 贪心法的策略是选择当前最优的策略,而不考虑后续的问题。对于活动选择问题来说,局部最优的策略是选择结束最早的活动,这样资源有更多的时间被结束较晚的活动所使用。
  • 由于本问题具有最优子结构性质,选择一个活动后,子问题为在剩下的时间段内寻找最优,而最优解的定义为选择最早结束的活动,因此贪心法不断选择可选活动内结束时间最早的活动,加入集合中。选择过程结束后,即可得到最优解。
  • 现在需要证明的问题变为,证明最早结束的活动总是最优解的一部分。
    假设 A i j A_{ij} Aij S i j S_{ij} Sij的一个最优解, a m a_m am S i j S_{ij} Sij中最早结束的活动, a k a_{k} ak A i j A_{ij} Aij中最早结束的活动。若 a m = a k a_m=a_k am=ak则已经是一部分,否则将 a k a_k ak替换成 a m a_m am,由于 A i j A_{ij} Aij中活动都兼容, f m < f k f_m<f_k fm<fk,新得到的集合中活动也兼容,新集合也是一个最优解,得证。
贪心算法实现
vector<int> select(int *s,int *f,int n) // f为排好序的结束时间数组 
{
	int cur=0;
	vector<int> tmp;
	for (int i=0;i<n;i++)
	{
		if (s[i]>=cur) //当前最早结束的活动与已选择的活动兼容
		{
			tmp.push_back(i);// 选择
			cur=f[i]; //更新结束时间
		}
	}
	return tmp;
}

贪心算法原理

在设计贪心算法的过程中,通常采取下述步骤:

  1. 将最优化问题转化成这样的形式: 对其做出一种选择后,只剩下一个子问题需要求解
  2. 证明做出贪心选择后,原问题总是存在最优解,即贪心选择总是安全的
  3. 证明做出贪心选择后,剩余的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解,这就得到了最优子结构

如何证明一个问题可以使用贪心算法解决?贪心选择性质和最优子结构是两个重要因素。

贪心选择性质

我们可以通过不断选择局部最优得到全局最优,这种性质称为贪心选择性质。选择时只需要考虑当前的最优情况,而不需要考虑子问题的解。
为证明具有贪心选择性质,通常首先考察某个子问题的最优解,用贪心选择替换某个选择,证明修改后的解仍然是最优解。

最优子结构性质

一个问题的最优解包含其子问题的最优解,称为最优子结构性质。这是动态规划和贪心算法的关键要素。

贪心对动态规划

一般来说,贪心算法解决的问题都有一个更复杂的动态规划解法,而有些动态规划问题解决的问题不能使用贪心算法。

  1. 0-1背包问题可以动态规划解决而不能贪心解决
  2. 分数背包问题可以动态规划解决也可以贪心解决

贪心算法应用——哈夫曼编码

为解决最优前缀码问题,哈夫曼提出哈夫曼编码。设前缀编码树 T T T,其中有编码节点 v i v_i vi,对应出现频率 f i f_i fi,在树中深度 d i d_i di ∑ f i ⋅ d i \sum{f_i\cdot d_i} fidi最小时, T T T为最优前缀编码树。现构造最优前缀编码树。
使用贪心算法解决问题:
直观的来看,使用频率最高的字符应该在树中深度最小,频率最低的字符在树中深度最大。使用贪心算法的思想来解题,可知从最低叶节点开始构建,每次选择频率最低的字符。现证明最优子结构性质:假设从最低叶节点开始,最优字符的选择顺序为 V = { v 1 , v 2 , . . . , v n } V=\{v_1,v_2,...,v_n\} V={v1,v2,...,vn},按照贪心算法选择的顺序为 V ′ = { v 1 ′ , v 2 ′ , . . . v n ′ } V'=\{v_1',v_2',...v_n'\} V={v1,v2,...vn},其中存在 k k k,使得 v i = v i ′ , i ≤ k v_i=v_i', i\leq k vi=vi,ik v k + 1 ≠ v k + 1 ′ v_{k+1}\neq v_{k+1}' vk+1=vk+1。由于构造性质可知,选择顺序在前的字符深度不小于选择顺序在后的字符,因此 d k + 1 ≥ d k + 1 ′ d_{k+1} \geq d_{k+1}' dk+1dk+1 f k + 1 ≥ f k + 1 f_{k+1}\geq f_{k+1} fk+1fk+1,因此将 v k + 1 v_{k+1} vk+1替换为 v k + 1 v_{k+1} vk+1后,编码树总代价不会增加(不变或减少)。由此可得,贪心算法的解对每个子问题都是最优解,且每个问题的最优解都包含其子问题的最优解。

编码实现

struct Node
{
	char c;
	int pos;
	Node *left,*right;
	Node(char ch='\0',int p=0,Node *l=NULL,Node *r=NULL)
	{
		c=ch;
		pos=p;
		left=l;
		right=r;
	}
};
bool cmp(Node *a, Node *b) //用来栈排序
{
	return a->pos>b->pos;
}
Node* huffman(vector<Node*> v)
{
	vector<Node*> p=v;
	sort(p.begin(),p.end(),cmp);
	while (!p.empty())
	{
		Node *l=p[p.size()-1]; //选择概率最小的
		p.pop_back();
		if (p.empty())
		{
			return l;
		}
		Node *r=p[p.size()-1];//选择概率次小的
		p.pop_back();
		Node *tmp=new Node;
		tmp->left=l;
		tmp->right=r;
		tmp->pos=l->pos+r->pos; //合并
		p.push_back(tmp);
		sort(p.begin(),p.end(),cmp);//排序
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值