贪心算法a

贪心算法

贪心算法有很多经典的应用:霍夫曼编码、prim和kruskal最小生成树算法、dijkstra单源最短路径算法。

贪心算法解决问题步骤

很常见的一个问题,比如背包问题。

  1. 第一步,当我们看到这类问题的时候,首先要联想到贪心算法:针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。
  2. 第二步,我们尝试看下这个问题是否可以用贪心算法解决:每次选择当前情况下,对限制值同等贡献量的情况下,对期望值贡献最大的数据。
  3. 第三步,我们举几个例子看下贪心算法产生的结果是否是最优的。

贪心算法不工作的原因:主要需要考虑前面的选择是否会影响到后面的选择(局部最优并不能保证全局最优)

贪心算法实战:分糖果问题;钱币找零;区间覆盖;

上面的"区间覆盖"问题的关键点就是找出贪心算法模型。之所以“覆盖区间”问题比前面几个问题感觉难度大一些,是因为那个我们想尽量大(或小)的变量不容易一眼看出来。背包豆子是单价尽量大;分糖果是用尽量小的糖果优先满足需求小的孩子;找零钱是尽量用大的面额;区间覆盖需要想到让右边未覆盖的区间尽量大。寻找贪心算法模型虽然没有一个通用的方法,而且老师也说了需要多练习才能对贪心问题有感觉,但是我们还是可以总结出一些启发式方法,这里我总结两个:1.通过画图增加对问题的理解。2.寻找那些跟问题有直接或间接关系的"尽量大(或小)"的变量。
找零问题不能用贪婪算法,即使有面值为一元的币值也不行:考虑币值为100,99和1的币种,每种各一百张,找396元。
动态规划可求出四张99元,但贪心算法解出需三张一百和96张一元。
贪心算法和动态规划分别用在什么地方??

实例

1. 分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
思路:需要先都进行排序,需要注意的点是,期望值和限制值都同时++,然后在比较g[i]和s[j]的情况,如果不满足g[i]>s[j],需要j++,找到第一个满足g[i]>=s[j]的情况。

 public int findContentChildren(int[] g, int[] s) {
        //将g和s进行排序
        Arrays.sort(g);
        Arrays.sort(s);
        int result = 0;
        for(int i =0,j=0;i<g.length&&j<s.length;i++,j++){
            while(j<s.length&&g[i]>s[j]){
                j++;
            }
            if(j<s.length){
                result++;
            }
            
        }
        return result;
    }

2. 最长回文串

给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。
在构造过程中,请注意区分大小写。比如 “Aa” 不能当做一个回文字符串。
注意:
假设字符串的长度不会超过 1010。
示例 1:
输入:
“abccccdd”
输出:
7
解释:
我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
思路:先要统计每个字符出现的次数v,然后可以用来回文的数就是v/2*2;除此之外,如果出现了奇数个,只能用一次加入到v中,例如aba,其中b出现了一次可以加入,但是如果还有一个也出现了奇数词,就不可以了,比如abca,只能选用b和c中的一个。

 public int longestPalindrome(String s) {
        //先将每个字母出现的次数放入到响应的数组中去
        int[] count = new int[128];
        //将每个字符出现的次数放入到数组中去
        for(int i=0;i<s.length();i++){
            char c = s.charAt(i);
            count[c]++;
        }
        int result = 0;
        //遍历count数组,找出可以组成回文串的长度
        for(int v :count){
            result += v/2*2;
            if(v%2==1&&result%2==0){
                result++;
            }
        }
        return result;
    }

3. 验证回文字符串II

给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
示例 1:
输入: s = “aba”
输出: true
思路:这个题和上个题的不同点是,这个题的string的顺序不能变化,所以不能再上题目的基础上去做,最直接的思路就是,先判断原始串是否是回文,如果是就直接返回true,否则的化遍历string每次删除一个字符,然后判断是否是回文,只要有一个是的化,就返回true,否则返回false,响应的代码如下所示,但是这个的时间复杂度为O(n^2),会造成超时问题

public boolean validPalindrome(String s) {
        //思路一:最初的思路就是要遍历整个字符串,然后挨个删除,看是否能组成回文,
        //先判断原字符串是否为回文
        if(valid(s)){
            return true;
        }else{
            //删除一个字符后看下是否可以组成回文,只要有一个满足条件就可以。
            for(int i =0;i<s.length();i++){
                String ss = s.substring(0,i)+s.substring(i+1,s.length());
                if(valid(ss)){
                    return true;
                }
            }
            return false;

        }
    }
    public boolean valid(String s){
        int n = s.length();
        int l = 0,r=n-1;
        while(l<r){
            if(s.charAt(l)==s.charAt(r)){
                l++;
                r--;
            }else{
                return false;
            }
        }
        return true;
    }

下面这段代码的时间复杂度为O(n),判断最长的是否是回文,如果不是的化,左右分别减1,看下是否是回文,至少要保证一个成立,否则就是非回文

public boolean validPalindrome(String s) {
        int l = 0,r = s.length()-1;
        while(l<r){
            char lc = s.charAt(l);
            char rc = s.charAt(r);
            if(lc == rc){
                l++;
                r--;
            }else{
                return valid(s,l+1,r)||valid(s,l,r-1);
            }
        }
        return true;
    }
    public boolean valid(String s,int l,int r){
        while(l<r){
            if(s.charAt(l)==s.charAt(r)){
                l++;
                r--;
            }else{
                return false;
            }
        }
        return true;
    }

4. 种花问题

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
思路:

题目要求是否能在不打破规则的情况下插入n朵花,与直接计算不同,采用“跳格子”的解法只需遍历不到一遍数组,处理以下两种不同的情况即可:
【1】当遍历到index遇到1时,说明这个位置有花,那必然从index+2的位置才有可能种花,因此当碰到1时直接跳过下一格。
【2】当遍历到index遇到0时,由于每次碰到1都是跳两格,因此前一格必定是0,此时只需要判断下一格是不是1即可得出index这一格能不能种花,如果能种则令n减一,然后这个位置就按照遇到1时处理,即跳两格;如果index的后一格是1,说明这个位置不能种花且之后两格也不可能种花(参照【1】),直接跳过3格。
当n减为0时,说明可以种入n朵花,则可以直接退出遍历返回true;如果遍历结束n没有减到0,说明最多种入的花的数量小于n,则返回false。

public boolean canPlaceFlowers(int[] flowerbed, int n) {
    //看不懂题目,为啥示例2就是false呢???原来给到的flowerbed数组为1的情况,是表示当前这个地方已经种了花了,所以这个题目是,给到的一块已经种了部分花的地块,问是否还能再种n个花进去。
        //从头到尾遍历一遍即可
        int i =0;
        while(i<flowerbed.length){
            if(flowerbed[i]==1){
                i=i+2;
            }else if(i==flowerbed.length-1||flowerbed[i+1]==0){
                n--;
                i=i+2;

            }else{//i+1种了花了,所以i处不能种花
                i=i+3;
            }
        }
        if(n<=0){
            return true;
        }else{
            return false;
        }

        
    }

5.买卖股票的最佳时机II

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
思路:
自己题解的思路:
选择好买入点和卖出点规则:
买入点:后面的减去前面的大于0就可以买入
卖出点:只要和买入的交易差有增大的情况就不卖,一旦后一天的交易差小于今天的交易差就卖。
买入和卖出的前提条件是:设置flag,买入后不能再买,卖出前先判断是否前面买入了。
同时需要注意边界条件:最后一天不能买入呀balabala。。。

public int maxProfit(int[] prices) {
        //感觉这题不是贪心算法,像动态规划呢~~~????动态规划???no
        //感觉像找相邻的小的数买入,大的数卖出
        //找到买入点规则:剩余数组的排序最小的数的点买入;但是不能再最后一天买否则就没得卖了
        //卖出规则:???只要和买入的交易差有增大的情况就不卖,一旦后一天的交易差小于今天的交易差就卖。
        //买入点选错了~~~[2,4,1],后减前为正就可以买入~~
        int ans = 0;//最后总收入
        boolean flag = false;
        int buy = 0;
        for(int i =0;i<prices.length;i++){
            if(buyTime(prices,i)&&i!=prices.length-1&&flag==false){//买入时间计算
                ans-=prices[i];
                flag = true;
                buy = prices[i];//记录买入的金额
            }else{
                //计算卖出时间
                if(flag){//手里有货才能卖出
                    //最后一天必须卖掉
                    if(i==prices.length-1){
                        ans+=prices[i];
                        flag = false;
                    }else{//不是最后一天
                        int j = sealTime(prices,i,buy);
                        if(prices[j]-buy>0){
                            i=j;
                            ans+=prices[j];
                            flag = false;
                        }
                    }
                }
            }

        }
        return ans;
    }
    //计算买入时间
    public boolean buyTime(int[] prices,int i){
        int min = i,j=i+1;
        if(j<prices.length && prices[j]-prices[i]>0){
            return true;
        }else{
            return false;
        }
    }
    //计算卖出时间,返回卖出时间下标i
    public int sealTime(int[] prices,int i,int buy){//传入卖出时间(不是最后一天)和买入金额
        int res =  prices[i]-buy;
        int j = i+1;
        for(;j<prices.length;j++){
            int shouru = prices[j]-buy;
            if(shouru>res){
                res = shouru;
            }else{
                return j-1;
            }
        }
        if(j==prices.length){
            return j-1;
        }else{
            return j;
        }
    }

官方思路:
由于股票的购买没有限制,因此整个问题等价于寻找x个不相交的区间(l,r] 使得他们的a[r]-a[l]和最大化.
同时我们注意到从(l,r]的和的最大化,等价于相邻数组的和的值所以,我们可以转化成求相邻和的最大值,如果有正向产出,则取正向值,否则我们取0即可。
**需要说明的是,贪心算法只能用于计算最大利润,计算的过程并不是实际的交易过程。**考虑题目中的例子 [1,2,3,4,5],实际上并不是交易了4次,而是1的时候买入,5的时候卖出

public int maxProfit(int[] prices) {
        int ans  = 0;
        for(int i=0;i<prices.length-1;i++){
            ans += Math.max(0,prices[i+1]-prices[i]);
        }
        return ans;
    }

6.蓄水问题

给定 N 个无限容量且初始均空的水缸,每个水缸配有一个水桶用来打水,第 i 个水缸配备的水桶容量记作 bucket[i]。小扣有以下两种操作:
升级水桶:选择任意一个水桶,使其容量增加为 bucket[i]+1
蓄水:将全部水桶接满水,倒入各自对应的水缸
每个水缸对应最低蓄水量记作 vat[i],返回小扣至少需要多少次操作可以完成所有水缸蓄水要求。
注意:实际蓄水量 达到或超过 最低蓄水量,即完成蓄水要求。
提示:
1 <= bucket.length == vat.length <= 100
0 <= bucket[i], vat[i] <= 10^4
思路:需要注意提示中10^4,最多可以选择蓄水10000次,所以我们遍历每一次,从中选择最少的操作次数即可,需要前面注意的是,如果水缸vat是空的0,则不用蓄水,次数为0;

public int storeWater(int[] bucket, int[] vat) {
        //最后提示很重要    0 <= bucket[i], vat[i] <= 10^4,说明最多的蓄水次数为10^4
        //先判断vat水缸的最大容量,如果最大容量为0,则此时操作数为0
        int maxVat = -1;
        for(int va : vat){
            if(va>maxVat){
                maxVat = va;
            }
        }
        if(maxVat == 0){
            return 0;
        }
        int res = Integer.MAX_VALUE;//最终操作次数,
        int n = vat.length;//vat.length == bucket.length所以此处任选其一即可。
        //开始遍历蓄水次数
        for(int i =1;i<=10000;i++){
            int cur = i;//蓄水为i次时的总操作次数,为蓄水次数(cur = 蓄水次数i+升级次数)升级次数后面来计算
            for(int j = 0;j<n;j++){//遍历n个桶和水缸
                //要达到蓄水次数为i,则水桶的容量需要为vat[j]/i向上取整;
                int per = (vat[j]+i-1)/i;//此处达到向上取整的目的
                cur += Math.max(0,per-bucket[j]);//每个桶需要升级的次数
            }
            res = Math.min(res,cur);//取这1000次的最小值
        }
        return res;
    }

7. 卡车上的最大单元数

请你将一些箱子装在 一辆卡车 上。给你一个二维数组 boxTypes ,其中 boxTypes[i] = [numberOfBoxesi, numberOfUnitsPerBoxi] :
numberOfBoxesi 是类型 i 的箱子的数量。
numberOfUnitsPerBoxi 是类型 i 每个箱子可以装载的单元数量。
整数 truckSize 表示卡车上可以装载 箱子 的 最大数量 。只要箱子数量不超过 truckSize ,你就可以选择任意箱子装到卡车上。
返回卡车可以装载 单元 的 最大 总数。
思路:典型的贪心算法,每次选择贡献率最大的值。

public int maximumUnits(int[][] boxTypes, int truckSize) {
        //先需要根据numberOfUnitsPerBoxi也就是boxTypes[i][1]对boxTypes进行排序,
        Arrays.sort(boxTypes, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o2[1]-o1[1];
            }
        });
        int total = 0;
        for(int i = 0;i<boxTypes.length;i++){
            if(boxTypes[i][0]<=truckSize){
                total += boxTypes[i][0]*boxTypes[i][1];
                truckSize -= boxTypes[i][0];
            }else{
                total+= truckSize*boxTypes[i][1];
                break;
            }
        }
        return total;
    }

8.非递增顺序的最小子序列

给你一个数组 nums,请你从中抽取一个子序列,满足该子序列的元素之和 严格 大于未包含在该子序列中的各元素之和。
如果存在多个解决方案,只需返回 长度最小 的子序列。如果仍然有多个解决方案,则返回 元素之和最大 的子序列。
与子数组不同的地方在于,「数组的子序列」不强调元素在原数组中的连续性,也就是说,它可以通过从数组中分离一些(也可能不分离)元素得到。
注意,题目数据保证满足所有约束条件的解决方案是 唯一 的。同时,返回的答案应当按 非递增顺序 排列。
思路:需要注意的是,本题目最终输出的子序列可以是从数组中随机找的,不用非得连续的,所以就排序从大到小找呗~~

public List<Integer> minSubsequence(int[] nums) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        if(nums.length == 1){
            list.add(nums[0]);
            return list;
        }

        int sum = 0;
        for(int i =0;i<nums.length;i++){
            sum += nums[i];
        }

        int res = 0;//(sum+1)/2;//寻找结果大于等于res的最小的子序列
        if(sum % 2==0){
            res = sum / 2 +1;
        }else{
            res = (sum+1)/2;
        }

        //相邻序列求和
        int ans = 0;
        Arrays.sort(nums);
        for(int i =nums.length-1;i>=0;i--){
            ans += nums[i];
            list.add(nums[i]);
            if(ans>=res){
                break;
            }
        }
        return list;
    }

9.移掉 K 位数字

给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
示例 1 :
输入:num = “1432219”, k = 3
输出:“1219”
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。
思路:核心思路就是从左到右进行遍历,直到右边小于左边的数,就把左侧的数删除掉,直到遍历完字符串为止。
需要考虑特殊情况,如果右面一直大于左侧的,则需要从后向前删除
同时如果出现了0200,这种前面的出现的有0则将前面的0删除掉
考虑这些情况,我们可以选择双端队列

public String removeKdigits(String num, int k) {
        //删除k个字符
        Deque<Character> deque = new LinkedList<Character>();
        int n = num.length();
        for(int i =0;i<n;i++){
            char c= num.charAt(i);
            while(!deque.isEmpty() && k>0 && c < deque.peekLast()){
                deque.pollLast();
                k--;
            }
            deque.offer(c);
        }
        for(int i=0;i<k;i++){
            deque.pollLast();
        }
        StringBuilder str = new StringBuilder();
        boolean flag = true;
        while(!deque.isEmpty()){
            char c = deque.pollFirst();
            if(flag && c=='0'){
                continue;
            }
            flag = false;
            str.append(c);
        }
        return str.toString().equals("") ? "0" : str.toString();
    }

10.不同字符的最小子序列

返回 s 字典序最小的子序列,该子序列包含 s 的所有不同字符,且只包含一次。
思路:此题和上一题类似,是上一题的升级版
需要有两个数据结构来存储特定的数据:
1、数组vis[],用于存放此字符是否存在于stack中;
2、map,用于存放此字符在string中出现了几次,每用一次就-1;
然后在从左到右遍历数组,判断是否存入到stack(stack其实就是一个双端队列)中
可以存入stack的条件是:该字符没有使用过;同时stak内的字符从底到上的顺序应该按照字符由小到大的顺序,除非该字符的map值已经为0了,就是后面不能再有该字符了。

public String smallestSubsequence(String s) {
    //需要两个数据结构来存储,一个存储某字符出现的次数,每次用过之后在-1;一个用于记录stak中已经存有该字符
        boolean[] vis = new boolean[26];
        HashMap<Character,Integer> map = new HashMap<Character, Integer>();
        for(int i =0;i<s.length();i++){
            if(map.get(s.charAt(i))!=null){
                map.put(s.charAt(i),map.get(s.charAt(i))+1);
            }else{
                map.put(s.charAt(i),1);
            }
        }
        Deque<Character> stack  = new LinkedList<Character>();//stack中的数据是需要从底向上,从小到大的;
        for(int i =0;i<s.length();i++){
            char c = s.charAt(i);
            if(!vis[c-'a']){//s.charAt(i)没有被访问过
                while(!stack.isEmpty() && stack.peekLast()>c){
                    if(map.get(stack.peekLast())!=0){//后面还有该字符
                        char ch = stack.pollLast();
                        vis[ch -'a']=false;
                    }else{//
                        break;
                    }
                }
                stack.offerLast(c);
                vis[c-'a']=true;
            }
            map.put(c,map.get(c)-1);//每遍历过一次后,c的map值就要变化,无论是否已经放入到stack中,
        }
        String str = "";
        while(!stack.isEmpty()){
            char c = stack.pollFirst();
            str +=c;
        }
        return str;

    }

11.拼接最大数

给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。
求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。
说明: 请尽可能地优化你算法的时间和空间复杂度。
输入:
nums1 = [3, 4, 6, 5]
nums2 = [9, 1, 2, 5, 8, 3]
k = 5
输出:
[9, 8, 6, 5, 3]
https://leetcode-cn.com/problems/create-maximum-number/
运用分治思想,求两个数组分别组成的最大数,然后合并即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值