贪心算法(一)
思想
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路经问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
基本要素
-
贪心选择性质。所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
-
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
-
如果大家比较了解动态规划,就会发现它们之间的相似之处。最优解问题大部分都可以拆分成一个个的子问题,把解空间的遍历视作对子问题树的遍历,则以某种形式对树整个的遍历一遍就可以求出最优解,大部分情况下这是不可行的。贪心算法和动态规划本质上是对子问题树的一种修剪,相当于DFS的剪枝优化,两种算法要求问题都具有的一个性质就是子问题最优性(组成最优解的每一个子问题的解,对于这个子问题本身肯定也是最优的)。动态规划方法代表了这一类问题的一般解法,我们自底向上构造子问题的解,对每一个子树的根,求出下面每一个叶子的值,并且以其中的最优值作为自身的值,其它的值舍弃。而贪心算法是动态规划方法的一个特例,可以证明每一个子树的根的值不取决于下面叶子的值,而只取决于当前问题的状况。换句话说,不需要知道一个节点所有子树的情况,就可以求出这个节点的值。由于贪心算法的这个特性,它对解空间树的遍历不需要自底向上,而只需要自根开始,选择最优的路,一直走到底就可以了。
基本思路
1.建立数学模型来描述问题;
2.把求解的问题分成若干个子问题;
3.对每一子问题求解,得到子问题的局部最优解;
4.把子问题的局部最优解合成原来问题的一个解。
入门
硬币问题(支付问题)
问题描述:
有1元、5元、10元、50元、100元、500元的硬币各C1,C5,C10,C50,C100,C500枚。现在要用这些硬币来支付A元,最少需要多少枚硬币?假设本题至少存在一种支付方案。
限制条件:
0<=C1,C5,C10,C50,C100,C500<=10的9次方
0<= A <= 10的9次方
输入:
3 2 1 3 0 2
620
输出:
6(500元硬币1枚,50元硬币2枚,10元硬币1枚,5元硬币2枚,合计6枚)
思路: 典型的贪心问题,先从最大面值考虑起,如果当前最大面值的硬币可以交换就交换,不然考虑下一种大小的面值
代码
import java.util.Scanner;
public class 硬币问题 {
static Scanner scanner=new Scanner(System.in);
static int[] count=new int [7]; // 记录每种硬币的数量
static int money; //需要交换的钱
static int[] value= {1,5,10,50,100,500};
static int ans; //记录交换的硬币总数
public static void main(String[] args) {
// 初始化
for(int i=0;i<6;i++) {
count[i]=scanner.nextInt();
}
money=scanner.nextInt();
// 模拟贪心交换过程,总选择当前最大值的硬币
int index=5; // 记录当前交换的钱在count数组中的下标,考虑到使用贪心,所以初始值为 5
while(money!=0) { // 只要还有钱就一直得选择交换
if(count[index]>0) { //当前的最大值货币还没有交换完
if(value[index]<=money) { //当前的面值是可以交换的
money-=value[index];
count[index]--;
ans++;
}else {
index--; //当前面值过大,换小的面值
}
}else { //当前的最大值货币还交换完
index--;
}
}
System.out.println(ans);
}
}
快速渡河(策略问题)
问题描述
一队人(N个人)期望跨河,有一条船,一次只能载2个人,过河之后需要有一个人划回来,所有人才能够跨河,每个人划船速度都不同,两个人一组整体速度是由划船速度较慢的决定的。问题:确定一种策略用最少的时间所有人都能过河。
输入
方案数:T(1<=T<=20)
人数:N<1000
速度:<100s
输出
最少的时间
样例
输入:
1
4
1 2 5 10
输出
17
**思路:**过河考虑两种情况,第一种,每次用速度最快的人去带速度慢的人;第二种,首先将速度最快的两个人过河,用将速度较慢的人把船带回来,然后速度最慢的两个人再乘船过去,然后速度最快的人再把船带回来。用这两种方法,都能每一轮把速度最慢的两人带走,但是具体采用哪种得比较花费的时间。因此具体选择哪种方法就成为了选择性问题,每次都选择最优的,就成为了贪心问题
代码
import java.util.Arrays;
import java.util.Scanner;
public class 快速渡河 {
static Scanner scanner = new Scanner(System.in);
static int T;
static int ans = 0;
public static void main(String[] args) {
T = scanner.nextInt();
while ((T--) != 0) {
int N = scanner.nextInt();
int[] speed = new int[N]; // 构造速度数组
for (int i = 0; i < N; i++) {
speed[i] = scanner.nextInt();
}
// 对速度数组按照从小到大的顺序排序
Arrays.sort(speed);
int left = N; // 假设渡河是从左到右,初始值左边为N个人
while (left > 0) {// 当左边还有人没渡河
if (left == 1) {
ans += speed[0]; // 只有一个人,渡一次
break;
} else if (left == 2) { // 只有两个人,渡一次河
ans += speed[1];
break;
} else if (left == 3) {// 有三个人,渡两次河
ans = ans + speed[0] + speed[1] + speed[2];
break;
} else {
int situation1 = speed[0] + speed[left - 1] + speed[left - 2] + speed[0]; // 用速度最快的分别带走最慢的两个人
int situation2 = speed[1] + speed[1] + speed[left - 1] + speed[0]; // 最快的两个人先过去,speed[1]把船带回,最慢的两人过去,speed[0]再把船带回
ans += (Math.min(situation1, situation2)); // 两种方法都会把最慢的两人带走,取两种方法中花费时间较少的情况
left -= 2; // 最慢的两人已经过河,减去2
}
}
System.out.println(ans);
}
}
}
区间调度(策略问题)
问题描述
有N项工作,每项工作有开始时间si和结束时间ti,让你选择最多的工作,工作之间不存在时间交叉。
输入:
5
1 3
2 5
4 7
6 9
8 10
输出:
3
**思路:**贪心的思想去考虑,每当我做完一项工作以后,如果当前供我选择的方法一直是最多的,那么最终我所做的工作数量也是最多的。所以我总是希望把结束时间最早的工作先做了,因为这样我的选择才多,最终做的总共的工作量也才最多。
代码
import java.util.Arrays;
import java.util.Iterator;
import java.util.Scanner;
public class 区间调度 {
static Scanner scanner=new Scanner(System.in);
static int n;
static int ans=1; //记录答案,不管是哪种情况,第一个任务都能做
static private class job implements Comparable<job>{
private int start,end;
job(int start,int end){
this.start=start;
this.end=end;
}
// 重载比较函数
public int compareTo(job o) {
return this.end>o.end?1:-1; //保持顺序为按照大于顺序排列,当相等或者小于时会出现对象顺序交换的情况
}
}
public static void main(String[] args) {
n=scanner.nextInt();
if(n==0) { //给的任务为 0 个,什么也做不了
System.out.println(0);
return ;
}
job[] jobs=new job[n]; //构造job数组存储每个job对象
for(int i=0;i<n;i++) {
int start=scanner.nextInt();
int end=scanner.nextInt();
jobs[i]=new job(start, end);
}
Arrays.sort(jobs);
int now=jobs[0].end; //第一个任务肯定能做,初始化now工作指针
for(int i=1;i<n;i++) { //当还有任务可以做时
if(jobs[i].start>now) { //不能等于,同一时刻智能做一个工作
ans++;
now=jobs[i].end; //刷星now工作指针
}
}
System.out.println(ans);
}
}
学习参考
https://blog.csdn.net/qq_32400847/article/details/51336300?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161536720716780255265048%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=161536720716780255265048&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~hot_rank-2-51336300.first_rank_v2_pc_rank_v29&utm_term=%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95 贪心算法定义
https://blog.csdn.net/effective_coder/article/details/8736718?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161536720716780255265048%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=161536720716780255265048&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~hot_rank-3-8736718.first_rank_v2_pc_rank_v29&utm_term=%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95 贪心算法理解
https://www.bilibili.com/video/BV1e7411T7FV?p=126 学习网课
具体进阶部分将在下一次博客种公布