第16章 贪心算法
16.1 活动选择问题
问题:假设有一个n个活动的集合 S=a1,a2,...,an ,这些活动使用同一个资源,而这个资源在某个时刻只能供一个活动使用。每个活动都有一个开始时间 si 和一个结束时间 fi 。若 si≥fi 或 sj≥fi ,则 ai 和 aj 是兼容的。在活动选择问题中,我们希望选出一个最大兼容活动集。假设活动已按结束时间的单调递增顺序排序。
1、动态规划法
令
Sij
表示在
ai
结束之后开始。且在
aj
开始之前结束的那些活动的集合,用
c[i,j]
表示集合
Sij
的最优解的大小,故有
2、贪心选择
定理:考虑任意非空子问题
Sk
,令
am
是
Sk
中结束时间最早的活动,则
am
在
Sk
的某个最大兼容活动子集中。
证明:令
Ak
是
Sk
的一个最大兼容子集,且
aj
是
Ak
中结束时间最早的活动。
若
aj=am
,即证。
若
aj≠am
,将
Ak
中的
aj
替换为
am
,因为
fm≤fj
,则还是兼容的,数目不变,故替换后的也是
Sk
的一个最大兼容活动子集,且包含
am
。
- 递归贪心算法
为方便初始化,添加一个虚拟活动
a0
,其结束时间
f0=0
public List<Integer> RECURSIV_ACTIVITY_SELECTOR(int[] s,int[] f,int k,int n){
int m = k + 1;
while(m<=n && s[m]<=f[k])
m++;
List<Integer> res = new ArrayList<>();
if(m<=n)
return res.add(m).addAll(RECURSIV_ACTIVITY_SELECTOR(int[] s,int []f,int k,int n))
else
return res;
}
- 迭代贪心算法
public List<Integer> GREED_ACTIVITY_SELECTOR(int[] s,int[] f){
int n = s.length - 1; //去掉虚拟活动
List<Integer> res = new ArrayList<>();
res.add(1);
int k = 1;
for(int i = 2; i<=n; i++){
if(s[m]>=f[k]){
res.add(m);
k = m;
}
}
return res;
}
16.2 贪心算法原理
一般地,我们按如下步骤设计贪心算法:
1. 将最优化问题转化为这样的形式:对其做出一次选择后,只剩下一个子问题需要求解。
2. 证明做出贪心选择后,原问题总是存在最优解,即贪心选择总是安全的。
3. 证明做出贪心选择后,剩余的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解,这样就得到了最优子结构。
贪心对动态规划
0-1背包问题:动态规划
分数背包问题:贪心
/**
* @param m 表示背包的最大容量
* @param n 表示商品个数
* @param w 表示商品重量数组
* @param p 表示商品价值数组
*/
public static int[][] Package1(int m, int n, int[] w, int[] p) {
//c[i][v]表示前i件物品恰放入一个重量为m的背包可以获得的最大价值
int c[][] = new int[n + 1][m + 1];
for (int i = 0; i < n + 1; i++)
c[i][0] = 0;
for (int j = 0; j < m + 1; j++)
c[0][j] = 0;
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
//当物品为i件重量为j时,如果第i件的重量(w[i-1])小于重量j时,c[i][j]为下列两种情况之一:
//(1)物品i不放入背包中,所以c[i][j]为c[i-1][j]的值
//(2)物品i放入背包中,则背包剩余重量为j-w[i-1],所以c[i][j]为c[i-1][j-w[i-1]]的值加上当前物品i的价值
if (w[i - 1] <= j) {
if (c[i - 1][j] < (c[i - 1][j - w[i - 1]] + p[i - 1]))
c[i][j] = c[i - 1][j - w[i - 1]] + p[i - 1];
else
c[i][j] = c[i - 1][j];
} else
c[i][j] = c[i - 1][j];
}
}
return c;
}
//用一维数组存储
public static int Package2(int m, int n, int[] w, int[] p) {
int[] f = new int[m + 1];
for (int i = 1; i < f.length; i++) //必装满则f[0]=0,f[1...m]都初始化为无穷小
f[i] = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
for (int j = f.length - 1; j >= w[i]; j--) {
f[j] = Math.max(f[j], f[j - w[i]] + p[i]);
}
}
return f[m];
}
在具体运用时,可以首先假设全部物品装进背包是否超载,若不超载则直接输出最优解。有一次某公司一道笔试题就是这样,直接上dq就超时gg了=。=
16.3 赫夫曼编码
赫夫曼编码用于压缩数据,根据每个字符的出现频率构造最优二进制表示。
前缀码:没有任何码字是其他码字的前缀。
构造赫夫曼编码,使用一个以属性freq为关键字的最小优先队列Q。
HUFFMAN(C)
n = C.length
Q = C
for i =1 to n-1
allocate a new node z
z.left = x = EXTRACT-MIN(Q)
z.right = y = EXTRACT-MIN(Q)
z.freq = x.freq + y.freq
INSERT(Q,z)
return EXTRACT-MIN(Q) //return the root of the tree
定理:过程HUFFMAN会生成一个最优前缀码。