常用算法之——贪心算法
算法简介
贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
贪婪算法所得到的结果往往不是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
贪婪算法并没有固定的算法解决框架,算法的关键是贪婪策略的选择,根据不同的问题选择不同的策略。
必须注意的是策略的选择必须具备无后效性,即某个状态的选择不会影响到之前的状态,只与当前状态有关,所以对采用的贪婪的策略一定要仔细分析其是否满足无后效性。
比如前边介绍的最短路径问题(广度优先、狄克斯特拉)都属于贪婪算法,只是在其问题策略的选择上,刚好可以得到最优解。
基本思路
其基本的解题思路为:
1.建立数学模型来描述问题
2.把求解的问题分成若干个子问题
3.对每一子问题求解,得到子问题的局部最优解
4.把子问题对应的局部最优解合成原来整个问题的一个近似最优解
案例详解
1.最佳支付方法
有1元、5元、10元、20元、100元、200元的钞票无穷多张。现使用这些钞票支付X元,最少需要多少张。
例如,X=628,最佳支付方法:3张200的,1张20的,3张1块的,共需要3+1+1+3=8张。
这里给出的钱金额:1,5,10,20,100是成倍数关系的,所以当使用一张较大面额的钞票时,若用较小面额钞票替换,一定需要更多的其他面额的钞票,所以尽可能使用面额较大的钞票,所以可以用贪心算法。
代码实现:
public class TanXin {
public static void main(String[] args)
{
int[] a = {200,100,20,10,5,1};
int X = 628;
int count = 0;
for(int i=0;i<a.length;i++){
if(a[i]<=X){
count+=X/a[i];
X = X%a[i];
}
}
System.out.println("count: "+count);
}
}
说明:这里能使用贪心的条件是,给出的面额成倍数,如果给出的面额不是成倍数,就不一定能使用贪心算法,举例如果给出钱金额:1,10,求组合达到金额为14的最少组合方法,这时候使用贪心算法就无能为力了,因为14=10+1+1+1+1,e而显然最优解是14=7+7。
2.分糖果问题
已知一些小孩和一些糖果,每个孩子有需求因子g,每个糖果有大小s,当某个糖果的大小s>=某个孩子的需求因子g时,代表该糖果可以满足该孩子,求使用这些糖果,最多能满足多少孩子?(注意某个孩子只能用一个糖果满足)
例如,需求因子数组g=[5,10,2,9,15,9];糖果大小数组s=[6,1,20,3,8],最多可以满足3个孩子。
思路:典型的贪心思想,最大的糖果满足需求因子最大的学生,次大的糖果满足次大需求的学生。先将需求因子数组和糖果大小数组都排序,得到s=[1,3,6,8,20],需求因子g=[2,5,9,910,15],如果最小的糖果能满足最小的需求因子,就将最小的糖果给最小的需求因子,例中的1无法满足需求因子2,所以试着将次小的糖果给最小的需求因子,仍然无法满足,然后将第三小的糖果给最小的需求因子,此时可以满足,然后将第四小的糖果给第二小的需求因子,依此下去。
代码实现:
class TanXin
{
public static void main(String[] args)
{
int[] g = {5,10,2,9,15,9};
int[] s = {6,1,20,3,8};
Arrays.sort(g);
Arrays.sort(s);
int i = 0;
int j = 0;
int count = 0;
while(i<g.length&&j<s.length)
{
if(s[j]>=g[i])
{
j++;
i++;
count++;
}
else
{
j++;
}
}
System.out.println("满足的小朋友个数: "+count);
}
}
3.摇摆序列
一个整数序列,如果两个相邻元素的查恰好正负(负正)交替出现,则该序列被称为摇摆序列。
例如:序列[1,7,4,9,2,5],相邻元素的差(6,-3,5,-7,3),该序列为摇摆序列。
理解思路,写并不好写,这里使用状态机做,但是基本思想是简单的,如果一组数字是递增的,那么就选取这组数字中最大的一个,如果一组数字是单调递减的,那么就选取这组数字中最小的那个,这里体现了贪心的思想。
4.移除k个数字
已知一个字符串表示的非负整数num,num中的k个数字移除,求移除k个数字后,可以获得的最小的可能的新数字。(num不会以0开头,num长度小于10002)。
举例:输入num="1432219",k=3,去掉3个数字后,得到的最小的数字是1219。
思考:让得到的数字最小,需要尽可能让得到的新数字优先最高位最小,其次次高位最小,再一次高位最小。例如输入num是1432219,此时可以使用栈存储最终的结果或删除工作,从高位向低位遍历num,如果遍历的数字大于栈顶元素,则将该数字push入栈,如果小于栈顶元素则进行pop弹栈,直到栈为空,或者不能再删除数字(k==0)或栈顶小于当前元素为止。
第一种特殊情况:一种特殊情况是在栈为空的时候,进去的为0,例如数字是num=100200,此时先取出1,1进入栈,但是第二个0的时候,此时1出栈,但0不能进栈,因为此时栈内为空。
第二种特殊情况:数字是单调递增的时候,例如12345678,此时在入栈完成后,检查栈的大小,如果栈的size()大于原先的字符串中的数字的长度减去2,此时要做处理,直接删除最后的两个数字。
代码实现:
class TanXin
{
public static void main(String[] args)
{
//测试用例
//String num = "1432219";
//String num = "12345678";
//String num = "1002345";
//String num = "100200";
String num = "123411";
int k = 2;
Stack<Integer> al = new Stack<>();
for(int i=0;i<num.length();i++)
{
int number = num.charAt(i) - '0';
while(al.size()!=0&&al.peek()>number&&k>0)//如果进入一个小于栈顶数的数字
{
al.pop();
k--;
}
if(number!=0||al.size()!=0)//栈为空的时候不能进去值为0
{
al.push(number);
}
}
//如果是顺序的那么把最后的k位数字删除
while(al.size()>0&&k>0)
{
al.pop();
k--;
}
//打印最终结果,因为栈实现了Iterator接口,所以可以使用for each语句
System.out.println(al.toString());
for(int c:al)
{
System.out.print(c+" ");
}
}
}
说明:最终的栈的输出要从栈底输出,因为栈实现了Iterator接口,所以这里用for each语句最为方便,否则从栈顶出栈后,还要颠倒一次顺序。
5.跳跃游戏
一个数组存储了非负整型数据,数组中第i个元素nums[i],代表了可以从数组第i个位置最多向前跳跃nums[i]步,已知数组各元素的情况下,求是否可以从数组第0个位置跳跃到数组的最后一个元素的位置。
跳跃游戏有两种问题:一种问能不能从第0个位置跳跃到数组的最后一个元素的位置,另一个中是从第0个位置跳跃到数组最后一个位置,最少需要跳跃多少步。
思路:例如:nums = [2,3,1,1,4],从第0个位置(2)可以跳至第一个位置(3)和第二个位置(1),从第一个位置(3)又可以跳至第二(1)、第三(1)、第四个位置(4),从第二个位置(1)又可以跳至第三个位置,所以从第0个位置是跳到第一个位置还是跳到第二个位置呢?此时选择跳到该位置后能够继续跳到的更远的位置。例如跳到第一个位置后最远能跳到第4个位置,而跳到第二个位置后最远能跳到第三个位置,所以我们就选择从第0个位置,跳到第1个位置,这也是典型的一种贪心思想。
如果不能到达的情况,例如nums=[3,2,1,0,4],此时不能达到,那么这该怎么判断呢?可以继续使用上面的办法,这时会出现在第3个位置(值为0)的地方停止,此时发现走一步后在原地,且该位置不是最终位置,说明停滞在该位置,说明无法到达最后位置了,代码描述:nums[i]+i==i&&i!=nums.length-1。
class TanXin
{
public static void main(String[] args)
{
//int[] nums = {2,3,1,1,4};
int[] nums = {3,2,1,0,4};
//int[] nums = {3,2,1,0};
int count = 0;
int i = 0;
boolean flag = true;
while(count<nums.length&&(i+nums[i]<=nums.length-1)){//防止到达不到停死在那,和已经走过两种情况
if(nums[i]+i==i&&i!=nums.length-1){//如果停滞在那边,就说明无法到达
flag = false;
break;
}
if(nums[i]+i==nums.length-1){ //如果下一步能到达,那么加一后退出
count++;
break;
}
int k = i + nums[i];
int maxReach=0;
int j;
int save = 0; //这一个保存变量很重要,记录上次到达的位置
for(j=i+1;j<=k;j++){
if(maxReach<=(nums[j]+j)) //这边的等于号必须加,{3,2,1,0,4}
{
System.out.println("maxReach: "+maxReach+"nums[j]+j: "+(nums[j]+j));
save = j;
maxReach = nums[j]+j;
}
}
i = save;
System.out.println("i: "+i);
count++;
}
System.out.println("count: "+count);
if(flag==false){
System.out.println("无法到达");
}
else
System.out.println("经过"+count+"步后到达");
}
}
6.射气球问题
已知在一个平面上有一定数量的气球,平面可以看作是一个坐标系,在平面的x轴的不同位置安排弓箭手向y轴方向射箭,弓箭可以向y轴走无穷远,给定气球的宽度xstart<=x<=xend,问至少需要多少弓箭手,将全部气球 打爆?
例如:四个气球:[[10,16],[2,8],[1,6],[7,12]],至少需要2个弓箭手。
分析:对于某个气球,至少需要使用1只弓箭将它击穿,在这只气球将其击穿的同时,尽可能击穿其他更多的气球。(这里体现了贪心的思想)。做法:使用一个类Node代表一个气球,类中的first代表的气球的左坐标,类中的second代表气球的右坐标,对各个气球(即Node类)进行排序,按照气球的左端点从小到大排序(即按照Node类的first属性排序),遍历气球数组,同时维护一个射击区间(通过两个变量[shoot_begin,shoot_end]),在满足就将当前气球射穿的情况下,尽可能击穿更多的气球,每击穿一个新的气球,更新一次射击区间。具体的更新方法:如果下一个first属性大于上一个的first属性,那么shoot_beign更新,如果下一个second属性下雨上一个的second属性,那么更新。
代码实现:
class Node implements Comparable<Node>
{
int first;
int second;
public Node(int first,int second)
{
this.first = first;
this.second = second;
}
@Override
public int compareTo(Node o) {
// TODO Auto-generated method stub
return first-o.first;
}
}
class TanXin
{
public static void main(String[] args)
{
Node n1 = new Node(10,16);
Node n2 = new Node(2,8);
Node n3 = new Node(1,6);
Node n4 = new Node(7,12);
ArrayList<Node> al = new ArrayList<>();
al.add(n1);
al.add(n2);
al.add(n3);
al.add(n4);
Collections.sort(al);//排序,等会总结一下排序
//for(Node n:al)
//{
//System.out.println(n.first+" "+n.second);
//}
int shoot_num = 1;
int shoot_begin = n1.first;
int shoot_end = n1.second;
for(int i=1;i<al.size();i++)
{
if(al.get(i).first<=shoot_end)
{
shoot_begin = al.get(i).first; //因为已经排序过了,所以直接赋值
if(shoot_end>al.get(i).second) //跟新设计区间的右坐标
{
shoot_end = al.get(i).second;
}
}
else
{
shoot_num++;
shoot_begin = al.get(i).first; //更新射击区间
shoot_end = al.get(i).second;
}
}
System.out.println("shootNum: "+shoot_num);
}
}