贪心算法的基本思想: 贪心总是作出当前看来最好的选择,也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。
贪心算法的基本要素:
1.贪心选择性质: 所谓贪心选择性质是指求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是与dp的主要区别。
a. dp通常以自下而上的方式解各种子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
b. 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
2.最优子结构性质(上篇dp已经介绍)
Activity-Selection Problem(活动选择问题)
Problem Description :Given a set A = {a1, a2, …, an} of n activities with start and finish times (si, fi), 1 ≤ i ≤ n, select maximal set S of “non-overlapping” activities.
通俗: 调度竞争共享资源的多个活动的问题,目标是选出一个最大的互相兼容的活动集合。假定有一个 n 个活动的集合 S={a1,a2,a3…,an},这些活动使用同一个资源(例如同一个阶梯教室),而这个资源在某个时刻只能供一个活动使用。每个活动 ai 都有一个开始时间 si 和一个结束时间 fi,其中 si<fi<无穷。如果被选中,活动 ai 发生在半开时间区[si,fi)期间。如果两个活动 ai 和 aj满足[si,fi)和[sj,fj)不重叠,则称它们是兼容的。也就是说,若 si>=fj,或sj>=fi,则 ai 和 aj 是兼容的。在活动选择问题中,希望选出一个最大兼容活动集。
DP思想:
用S(1, i)表示啊a1~ai的最大兼容活动集合问题,首先假设其子问题空间是一维的, 即 S(1, i)的最优解由 S(1, i-1)的最优解构成。但是发现 S(1, i)的最优解可能是由 S(2, i-1) 或是 S(3,i-1)的最优解构成。因此,会发现子问题空间是一维是不够的, 子问题空间是二维的;其子问题空间是二维的,对于活动 S(i, j), 需要做出 j-i-1 种选择;
选择活动 ak(其中 i<=k<=j), 那么:S(i, j) = S(i, k) + {ak} + S(k, j);
S(i, j) 具有最优化结构, 可以使用动态规划算法得到,时间复杂度是 O(n^3)
假设 c[i, j] 为问题 S(i, j)中最大兼容活动的个数,有递归式:c[i,j] = c[i,k] + c[k,j] + 1;
其边界条件是当 S(i, j)是空集的时候, c[i, j] = 0;
因此,完整的递归式是(i<=k<=j):
子问题空间是二维的, 对每个子问题都要进程 O(n)次选择,因此总的时间复杂
度是 O(n^3);
贪心算法思想: 原本按照动态规划的方法来求,先求子问题:将S(i,j)的一个最大兼容活动集合设为A(i,j),于是A(i,j)至少包含一个活动ak,则:可以将A(i,j)划分为子问题:A(i,j)=A(i,k)+{ak}+A(k,j)。但是我们一开始是不能知道哪一个k能够使得ak一定在最大兼容活动集A(i,j)中,于是一般需要从i~j遍历所有k的可能取值,来找这个ak。
再次分析活动选择问题,有两个发现:对于任何非空子问题S(i,j),设am是S(i,j)中具有最早结束时间的活动,那么:
1)活动am一定是S(i,j)的最大兼容活动的子集;
2)子问题S(i,m)为空,选择了am,那么子问题S(m,j)是唯一可能的非空;
动态规划中,每个子问题有j-i-1种选择;
贪心算法中,通过选择集合中最早结束时间的活动 (默认活动的结束时间从小到大已排好序,所以首先选择的是第一个活动),使得S(i,m)为空,这样就只用考虑子问题S(m,j)。在解决子问题中,贪心算法只考虑了一种选择,并且子问题空间也变为一维的,这样时间复杂度就是O(n) (最优子结构会随着原问题最优解的子问题数目以及确定子问题时的选择数目变化),此外由于子问题空间变为一维,子问题的选择只有一种,贪心算法在这里可以选用自顶向下解决,使得代码更加简单,时间复杂度从O(n^3)变为O(n)。
代码实现:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] s = new int[n+1];//活动起始时间
int[] f = new int[n+1];//活动结束时间
int[] A = new int[n]; //最大兼容活动集合
for (int i = 1; i <= n; i++) {
s[i] = sc.nextInt();
}
for (int i = 1; i <= n; i++) {
f[i] = sc.nextInt();
}
//对fi进行从大到小的排序,且si的下标随fi而动(为贪心选择服务,相当于每次选性价比最高的)
InsertSort(s,f);
int k = 1,cnt = 0;
A[cnt++] = k; //每次都选择结束时间最早的活动
for (int m = 2; m <= n; m++) {
if(s[m] > f[k]){ //贪心选择
A[cnt++] = m;
k = m;
}
}
System.out.println("选择的活动为:");
for (int i = 0; i < cnt; i++) {
System.out.println("起始:"+s[A[i]]+" 结束:"+f[A[i]]);
}
}
//插入排序
public static void InsertSort(int[] s,int[] f){
for (int i = 2; i < f.length; i++) {
for (int j = i-1; j >=1&&f[j+1]<f[j]; j--) {
swap(f,j,j+1);
swap(s,j,j+1); //s随f而变化
}
}
}
public static void swap(int arr[],int i,int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* case:
* 11
* 5 3 5 0 3 1 6 8 8 2 12
* 9 5 7 6 9 4 10 11 12 14 16
*
* result:
* 选择的活动为:
* 起始:1 结束:4
* 起始:5 结束:7
* 起始:8 结束:11
* 起始:12 结束:16
*/
Fraction knapsack problem(部分背包问题)
Problem Description : A thief robs a store and finds n items, with item i being worth $vi and having weight wi pounds, The thief can carry at most W in his knapsack but he wants to take as valuable a load as possible. Which item should he take?
1.最优子结构: 若它的一个最优解包含物品j,则从该最优解X拿走所含物品j的那部分重量wj,则问题变为:给你n-1个物品中(1,2,…,j-1,j+1,…,n),以及容量为W-wj的背包,则X’ = X-{j}是这个问题的最优解。
2.贪心选择性质: 每次选择价值/重量比值最大的物品,就能达到原问题的最优解(可证明)
代码实现:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); //物品个数
double[] c = new double[n+1]; //物品价值表
double[] w = new double[n+1]; //物品重量表
double[] x = new double[n]; //物品所放的比例表
double[] value = new double[n+1]; //价值/重量 表
double W = sc.nextDouble(); //背包容量
for (int i = 1; i <= n; i++) {
c[i] = sc.nextDouble();
}
for (int i = 1; i <= n; i++) {
w[i] = sc.nextDouble();
}
for (int i = 1; i <= n; i++) {
value[i] = c[i] / w[i];
}
InsertSort(value,c,w);//按照 价值/重量 从大到小排序后的价值表、物品表
int cnt = 0,i;
double sum = 0;
for (i = 1; i <= n; i++) {
if(w[i] <= W){
x[cnt++] = 1.0;
sum += c[i];
W -= w[i];
}else break;
}
if(i <= n){
x[cnt++] = W/w[i];
sum += W/w[i] * c[i];
}
for (int ii = 0; ii < cnt; ii++) {
System.out.println("第"+(ii+1)+"个物品的比例为:"+x[ii]+" ");
}
System.out.println("最大价值:"+sum);
/**
* case:
* 5
* 100
* 20 30 65 40 60
* 10 20 30 40 50
* result:
* 第1个物品的比例为:1.0
* 第2个物品的比例为:1.0
* 第3个物品的比例为:1.0
* 第4个物品的比例为:0.8
* 最大价值:163.0
*/
}
//插入排序(从大到小排序)
public static void InsertSort(double[] value,double[] c,double[] w){
for (int i = value.length-2; i >= 1; i--) {
for (int j = i+1; j <= value.length-1&&value[j-1]<value[j]; j++) {
//3个表的下标同步
swap(value,j,j-1);
swap(c,j,j-1);
swap(w,j,j-1);
}
}
}
public static void swap(double arr[],int i,int j){
double tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}