1.贪心算法练习


贪心算法的核心思想是,每一小步都是当前状态的最优解。多个最优解最后汇聚成最优的结果。

分配问题

1.分饼干–455

题目:假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >=> g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 示例 1:
输入: g = [1,2,3], s = [1,1] 输出: 1 解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。

示例 2: 输入: g = [1,2], s = [1,2,3] 输出: 2 解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 你拥有的饼干数量和尺寸都足以让所有孩子满足。 所以你应该输出2.
提示:
1 <= g.length <= 3 * 104
0 <= s.length <= 3 * 104
1 <= g[i], s[j] <= 231 - 1

分析
当糖果数大于小孩的食量时,小孩可以吃饱。要想让最多的小孩吃饱,可以将小孩的食量和糖果的数量分别升序排列,依次从食量小的小孩开始比较,直到小孩全部吃饱或者糖果已经比较完为止。此时有最多的小孩可以吃到糖果。按照价值最大化的思维进行比较。

var findContentChildren = function(g, s) {
    let child =  g.sort((a,b)=>a-b)
    let foods = s.sort((a,b)=>a-b)
    if(foods.length==0) return 0;
    let index=0;
    for(let j=0;j<foods.length; j++){
        if(foods[j]>=child[index]){
            // 可以吃饱
            index++;
            //提前吃饱,提前退出循环
            if(index>=child.length) return index 
        }
    }
    return index;
};

2.分糖果–135

题目:n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
提示:
n == ratings.length
1 <= n <= 2 * 104
0 <= ratings[i] <= 2 * 104

分析
这里就是看升降的排序,右边分数比左边小孩多,右边小孩糖果比左边多一个,左边小孩分数比右边分数多,右边小孩糖果比左边小孩多一个。官方是多使用了一个数组来记录位置,初始每一个位置上都以一个糖果,左边分数比右边高,左边糖果是右边糖果数+1,右边同理。他是用了两次遍历,从左到右查一遍,从右到左查一遍,最终获得的数组就是每个位置上应得的最小糖果数了。
我写的时候用了个比较麻烦的方法,在遍历里面套遍历,从左到右,右边得分高的,糖果数为左边的+1,如果左边比较高,那就加一个变量,把降序的部分取出来翻转一下,再次从一开始发糖果,最后比较一下最后一个糖果和反转之前的糖果,做取舍。

var candy = function (ratings) {
    let count = 1;
    let cands = ratings;
    if (cands.length == 1) return 1
    let up = true;
    let nowCandy = 1;
    let arr = [];
    for (let i = 1; i < ratings.length; i++) {
        if (cands[i - 1] >= cands[i]) {
            // 左边的人得分更高
            arr.push(cands[i - 1])
            if (i == cands.length - 1) {
                arr.push(cands[i]);
                arr = arr.reverse();
                count++;
                count = getCount(arr, count, nowCandy)
                arr = []
            }
            up = false;
        }
        else {
            // 右边的人得分高
            if (!up) {
                arr.push(cands[i - 1]);
                arr = arr.reverse();
                count++;
                count = getCount(arr, count, nowCandy)
                arr = [];
                nowCandy = 1;
            }
            up = true;
            nowCandy++;
            count = count + nowCandy;

        }

    }
    return count;
};
function getCount(arr, count, nowCandy) {
    let now2 = 1
    for (let i = 1; i < arr.length; i++) {
        if (arr[i - 1] == arr[i] && i != arr.length - 1) {
            count += 1
            now2 = 1;
        }
        if (arr[i - 1] < arr[i]) {
            now2++;
            if (i == arr.length - 1) {
                if (now2 > nowCandy)
                    count = count - nowCandy + now2;
            } else {
                count += now2
            }
        }
    }
    return count;
}

区间问题

无重叠区间–435

题目 给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠
示例 1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

分析
其实这里我不是很理解,为什么右区间从小到大排,再进行取舍,会使移除区间的数量最小,官方解释说右区间越小,右边就能容纳更多的区间了,能让容纳的区间数最大化。如果是这样的话,只需要对数组进行排序,然后取不重合的区间,最后多出来的就是需要移除的区间了。

var eraseOverlapIntervals = function (intervals) {
    let n = intervals.sort((a, b) => a[1] - b[1]).length
    if (n == 1) return 0
    //不重合的区间数m
    let m = 1
    let right = intervals[0][1]
    for (let i = 1; i < n; i++) {
    	//判断左区间与右区间是否重合,不重合就更新右边的范围
        if (intervals[i][0] >= right) {
            m++;
            right = intervals[i][1]
        }
    }

    return n - m;
};

练习

1.种花问题–605

题目 假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false 。
示例 1:
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true
示例 2:
输入:flowerbed = [1,0,0,0,1], n = 2
输出:false

分析
这个题目分为两部分,两边种花情况和中间种花情况,如果是在两边只要两个空格就能中一朵花,中间空地则需要三个连续的空地才能种一朵花。使用以此遍历,把可以种花的地方,0换成1,方便下一个空地进行计算。有种走一步看一步的感觉。

/**
 * @param {number[]} flowerbed
 * @param {number} n
 * @return {boolean}
 */
var canPlaceFlowers = function (flowerbed, n) {
    let num = 0;
    // 只有一块地
    if (flowerbed.length == 1 && flowerbed[0] == 0) {
        num++;
        return num >= n;
    }
    for (let i = 0; i < flowerbed.length; i++) {
        // 左边能种
        if (i == 0 && flowerbed[0] == 0 && flowerbed.length > 1 && flowerbed[1] == 0) {
            flowerbed[i] = 1;
            num++;

        }
        // 中间能种 
        else if (i > 0 && i != flowerbed.length - 1 && flowerbed[i - 1] == 0 && flowerbed[i] == 0 && flowerbed[i + 1] == 0) {
            flowerbed[i] = 1;
            num++;
        }
        // 右边能种
        else if (i == flowerbed.length - 1 && flowerbed[i] == 0 && flowerbed.length > 1 && flowerbed[i - 1] == 0) {
            flowerbed[i] = 1;
            num++;
        }
    }
    return num >= n;
};

2.用最少数量的箭引爆气球–452

题目 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
提示:
1 <= points.length <= 105
points[i].length == 2
-231 <= xstart < xend <= 231 - 1

分析
这题和上面的找无重叠区间是一样的,只不过上面是返回重叠区间的个数。这里则是返回不重叠区间的个数。区间重叠时,用一支箭可以解决区间内气球,及总有一个x=key地轴,可以贯穿同一区间上所有的气球。所以这个区间必须是尽可能多的独立区间,而不是采用区间合并的大区间。

var findMinArrowShots = function(points) {
// 找出不重叠的最大数量,其他的都是重叠的,可以一次被射破
    if(points.length==1) return 1;
    points = points.sort((a,b)=>a[1]-b[1])
    let ans = points[0][1]
    let n = 1;
    for(let i=1;i<points.length;i++){
        if(ans<points[i][0]){
            n++;
            ans = points[i][1]
        }
    }

    return n

};

3.划分字母区间–763

题目 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”、“defegde”、“hijhklij” 。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = “eccbbbbdec”
输出:[10]
提示:
1 <= s.length <= 500
s 仅由小写英文字母组成

分析
这题的意思是,这个字符串上有很多字母,但是每个字母只能出现在某一个片段中。也就是说根据字母分布的区域,划分独立的区间,因为他想要尽可能分足够的的片段,也就是说,要把重叠的区域进行合并,然后找区域范围数。不同的是,上题直接找区域,这一则需要添加一步合并区间的操作,否则字母会出现在不同片段里。
首先创建一个对象,key是字母,value是字母出现的范围。得到范围后,进行区间合并并记录每次不能合并的位置,可以通过位置,确定每个片段的长度,最后返回。

/**
 * @param {string} s
 * @return {number[]}
 */
var partitionLabels = function (s) {
    // 把每个字母第一次出现和最后一次出现的位置记录下来
    let obj = {}
    for (let i = 0; i < s.length; i++) {
        let c = s[i]
        if (!obj[c]) {
            obj[c] = [i, i]
        } else {
            obj[c][1] = i;
        }
    }
    let values = Object.values(obj)
    // 将起始点从小到大排列,确保合并完全
    values = values.sort((a, b) => a[0] - b[0])
    // 比较参数
    let ans = values[0][1]
    let arr = []
    let last = -1;
    if (values.length == 1) return [ans + 1]
    for (let i = 1; i < values.length; i++) {
        if (ans >= values[i][0]) {
            // 产生重叠
            if (ans < values[i][1]) {
                ans = values[i][1]
            }
            // 最后一个可合并片段,也需要记录,因为写法上忽略了最后一次的计算,要单独判断一下
            if (i == values.length - 1) {
                arr.push(ans - last)
            }
        } else {
            arr.push(ans - last);
            last = ans
            // 最后一个不可合并片段也需要记录
            if (i == values.length - 1) {
                arr.push(values[i][1] - ans)
            }
            ans = values[i][1]
        }
    }
    return arr;
};

4.买股票最佳时期 Ⅱ–122

题目 给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104

分析
这题核心是,股票跌了就卖,谷底就买。有两个规则:没有买的时候不能卖,最后一天不能卖,然后遍历就可以了。

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function (prices) {
    // 股票可能为0
    let cin = -1;
    let money = 0;
    for (let i = 0; i < prices.length - 1; i++) {
        if (prices[i] > prices[i + 1]) {
            // 买入才能卖
            if (cin >= 0) {
                money += prices[i] - cin
                cin = -1;
            }
        }
        // 涨了就买
        if (prices[i] < prices[i + 1]) {
            if (i == 0) cin = prices[i]
            else if (prices[i - 1] >= prices[i] && cin == -1) {
                cin = prices[i]
            }
        }
        // 判断最后一天状况,直涨时会被忽略,最后一天卖出
        if (cin >= 0 && i == prices.length - 2) {
            money += prices[i + 1] - cin;
        }
    }
    return money;
};

5.根据身高重建队列–406

题目:假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
提示:
1 <= people.length <= 2000
0 <= hi <= 106
0 <= ki < people.length
题目数据确保队列可以被重建

分析
这题需要做两次排序,先对等待次数从小到大排,再对身高从大到小排。按照题目来看,等待数为0的先排出来,然后在到数组里遍历数据,一个个地插入到新的数组里去。如果相同的等待数先拍小的再排大的身高,会使小的身高前面的人数错误,所以这里采用了逆序。
先创建二维数组,对数据进行分类、排序再合并。有一点等待数为0的,身高是从小到大排的,因为是直接排序得到的,不是插入拿到的。

/**
 * @param {number[][]} people
 * @return {number[][]}
 */
var reconstructQueue = function (people) {
    let arr = []
    let n = people.length;
    people.sort((a, b) => a[1] - b[1])
    // 二维数组
    let arrSort = new Array(n)
    for (let m = 0; m < n; m++) {
        arrSort[m] = []
    }
    let temp = 0;
    for (let i = 0; i < n; i++) {
        if (people[i][1] == temp) {
            arrSort[temp].push(people[i])
        } else {
            if (arrSort[temp].length > 0) {
                arrSort[temp].sort((a, b) => b[0] - a[0]);
            }
            i--;
            temp++;
        }
        // 最后一个数组进行降序排列
        if (i == n - 1) {
            arrSort[temp].sort((a, b) => b[0] - a[0]);
        }
    }
    people = []
    // 解构
    arrSort.forEach(item => {
        if (item.length > 0) {
            people.push(...item)
        }
    })
    arr = arrSort[0].reverse();
    for (let i = 0; i < n; i++) {
        let step = 0;
        if (people[i][1] > 0) {
            for (let m = 0; m < arr.length; m++) {
                if (arr[m][0] >= people[i][0]) {
                    step++;
                    if (step == people[i][1]) {
                        //  满足条件,插入
                        arr.splice(m + 1, 0, people[i])
                        break;
                    }
                }
            }
        }
    }
    return arr;
};

6.非递减数列–665

题目:给你一个长度为 n 的整数数组 nums ,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。我们是这样定义一个非递减数列的: 对于数组中任意的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。
示例 1:
输入: nums = [4,2,3]
输出: true
解释: 你可以通过把第一个 4 变成 1 来使得它成为一个非递减数列。
示例 2:
输入: nums = [4,2,1]
输出: false
解释: 你不能在只改变一个元素的情况下将其变为非递减数列。
输入: nums = [4,2,1]
输出: false
解释: 你不能在只改变一个元素的情况下将其变为非递减数列。

分析
非递减数列也就是arr[i]<=arr[i+1].这里给了一次修改的机会,但是修改的方向有两种1.arr[i]=arr[i+1],就是把左边的值改为右边的,也可以改其他的,但是相等最便利。2.arr[i+1]=arr[i],右边的改成左边的,原因同上。当出现降序时,我们做一次修改,然后后再去查一遍有没有降序,有的话就是不成功了。没有的话成功了。

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var checkPossibility = function (nums) {
    // 次数
    let count = 0;
    return compare(count, nums);
};
function compare(count, nums) {
    for (let i = 0; i < nums.length - 1; i++) {
        if (nums[i] > nums[i + 1]) {
            // 降序
            count++;
            if (count > 1) return false;
            let left = nums[i];
            let right = nums[i + 1];
            // 两张种情况都不对的话,返回false
            nums[i] = right;
            let re1 = compare(count, nums)
            nums[i] = left;
            nums[i + 1] = left;
            let re2 = compare(count, nums)
            if (!re1 && !re2) return false
            break;
        }
    }
    return true;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值