【数据结构与算法】贪心思想经典题总结
分发饼干
LeetCode:分发饼干
题目描述:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
注意:
你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。
示例:
输入: [1,2,3], [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
思想:
经典的贪心思想。
对两个数组排序,按顺序分配。若小胃口的孩子都满足不了,则该饼干抛弃,不可能满足胃口更大孩子了。
代码
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int i=0,j=0;
while(i<g.length&&j<s.length){
if(s[j]>=g[i])
++i;
++j;
}
return i;
}
}
无重叠区间
LeetCode:无重叠区间
题目描述:
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
思想:
首先对每个区间排序(按照右边界排序)。假设A区间与B区间相邻(B在A右边),若A与B重叠,两者至少要移除一个才能满足要求,这时一定是移除B区间,因为A与B相比,B对后续区间可能的影响更大。
代码
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
if(intervals.length==0) return 0;
Arrays.sort(intervals,new Comparator<int[]>(){
@Override
public int compare(int[] a, int[] b){
return a[1]-b[1];
}
});
int preEnd=intervals[0][1],res=0;
for(int i=0;i<intervals.length-1;++i){
if(preEnd<=intervals[i+1][0])
preEnd = intervals[i+1][1];
else
++res;
}
return res;
}
}
用最少数量的箭引爆气球
LeetCode:用最少数量的箭引爆气球
题目描述:
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
示例:
输入:
[[10,16], [2,8], [1,6], [7,12]]
输出:
2
解释:
对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。
思想:
首先这题拿来可以想到,大致做法是,先排序,再遍历一遍得到结果。
排序有两种排序方式,按右边界排序,还是左边界排序?假设排序后A区间和B区间相邻。如果按照右边界,A_right只与B_left的大小关系不清晰;如果按照左边界排序,A_right与B_left、B_right关系都不清晰。明显是第一种更好,只需要判断一次。通过判断决定A和B是分别射穿,还是一起射穿。这样,思路就出来了。
贪心思想在于:尽可能地爆更多气球,每次都射边界位置。所以需要每次循环时,用int值记录边界值。
代码
class Solution {
public int findMinArrowShots(int[][] points) {
if(points.length==0) return 0;
Arrays.sort(points,new Comparator<int[]>(){
public int compare(int[] a,int[] b){
return a[1]-b[1];
}
});
int res=1,edge=points[0][1];
for(int i=1;i<points.length;++i){
if(points[i][0]>edge){
++res;
edge = points[i][1];
}
}
return res;
}
}
根据身高重建队列
LeetCode:根据身高重建队列
题目描述:
假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。
注意:
总人数少于1100人。
示例:
输入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
输出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
思想:
贪心算法很重要一个特性:无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
同时本题很重要一个点:高个子的人可以忽略矮个子的人,即每个人的插入位置只与比自己高的人相关。
于是可以想到,先按身高由高到低排序,再依次插入List中,刚好满足贪心的无后效性。每一次插入时,item[1]值刚好是插入序号,这个位置是对当前人来说,最优的决策,即局部最优。然后可以导致全局最优。
代码
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, new Comparator<int[]>(){
public int compare(int[] a,int[] b){
return a[0]!=b[0]?(b[0]-a[0]):(a[1]-b[1]);
}
});
List<int[]> res = new LinkedList<>();
for(int[] item : people){
res.add(item[1],item);
}
return res.toArray(people);
}
}
买卖股票的最佳时机2
LeetCode:买卖股票的最佳时机2
题目描述:
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
思想:
买卖股票的最佳时机是使用dp思想,而本题是升级版,使用贪心思想。
首先要抓住本题特点:每一天都进行买卖不影响结果,即d - a = (d - c) + (c - b) + (b - a)
然后,每一天都进行最大经济效益的买卖,即将前一天的卖出再立即买入今天的(收益为当天与前一天的差价),或者前一天不买今天才来买入(今天收益0)。这样每一天的最大收益加起来就是总收益。典型的局部最优累加得到全局最优。
代码
class Solution {
public int maxProfit(int[] prices) {
if(prices.length<2) return 0;
int num=0,profitNow;
for(int i=1;i<prices.length;++i){
if(prices[i]<=prices[i-1])
profitNow = 0;
else
profitNow = prices[i] - prices[i-1];
num+=profitNow;
}
return num;
}
}
种花问题
LeetCode:种花问题
题目描述:
假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。
注意
- 数组内已种好的花不会违反种植规则。
- 输入的数组长度范围为 [1, 20000]。
- n 是非负整数,且不会超过输入数组的大小。
示例:
输入: flowerbed = [1,0,0,0,1], n = 1
输出: True
思想:
- 通过设置pre和next,来解决前后边界问题
这题我觉得不算严格的贪心问题,因为不满足无后效性。但是推导过程可以稍微借助贪心思想来考虑。假设A和B两个位置相邻,都是0,遍历到A时,种或者不种问题。如果A选择种花,势必会影响到后面,即B不能再种花。但是如果A不种花,为了弥补这一缺憾,B就必须要种(否则就少种了一株花),而这样的话又对B之后的位置产生影响。影响更深远。所以单纯对当前的A而言,选择种花,对后面的影响最小,也就是变相的局部最优。
这是我的理解。
代码
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int len=flowerbed.length,pre,next;
for(int i=0;i<len;++i){
pre = i==0?0:flowerbed[i-1];
next = (i+1)<len?flowerbed[i+1]:0;
if(pre==0&&next==0&&flowerbed[i]==0){
flowerbed[i] = 1;
--n;
}
}
return n<=0;
}
}
判断子序列
LeetCode:判断子序列
题目描述:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例:
示例 1:
s = "abc", t = "ahbgdc"
返回 true.
示例 2:
s = "axc", t = "ahbgdc"
思想:
使用indexOf比使用charAt逐个比较要效率更高。
代码
class Solution {
public boolean isSubsequence(String s, String t) {
char[] arr = s.toCharArray();
int index = -1;
for(int i=0;i<s.length();++i){
index = t.indexOf(arr[i],index+1);
if(index<0)
return false;
}
return true;
}
}
非递减数列
LeetCode:非递减数列
题目描述:
给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。
我们是这样定义一个非递减数列的: 对于数组中所有的 i (1 <= i < n),总满足 array[i] <= array[i + 1]。
示例:
输入: nums = [4,2,3]
输出: true
解释: 你可以通过把第一个4变成1来使得它成为一个非递减数列。
思想:
按顺序遍历,遇到当前位置比前面一位小的情况,有两种修改方式(假设遍历A、B、C,C小于B)
- 当A<=C时,修改B=C,这样对后面影响最小;当B为首元素时,也是这样修改。
- 存在一个特殊情况,当A>C时,只能修改C=B。
代码
class Solution {
public boolean checkPossibility(int[] nums) {
int cnt=0;
for(int i=1;cnt<2&&i<nums.length;++i){
if(nums[i]>=nums[i-1]) continue;
cnt++;
if(i>1&&nums[i-2]>nums[i]){
nums[i] = nums[i-1];
}else
nums[i-1] = nums[i];
}
return cnt<2;
}
}
划分字母区间
LeetCode:划分字母区间
题目描述:
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段。返回一个表示每个字符串片段的长度的列表。
注意:
S的长度在[1, 500]之间。
S只包含小写字母'a'到'z'。
示例:
输入: S = "ababcbacadefegdehijhklij"
输出: [9,7,8]
解释:
划分结果为 "ababcbaca", "defegde", "hijhklij"。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。
思想:
没感觉这题有什么贪心思想。网上说,针对每一个区间,lastIndex能更新就更新,这属于贪心思想。感觉有点牵强。
代码
- 笨方法,时间复杂度O(n^2)
class Solution {
public List<Integer> partitionLabels(String S) {
char[] strList= S.toCharArray();
int lastIndex=0;
ArrayList<Integer> list=new ArrayList<>();
int lastAdd=0;
for(int i=0;i<S.length();++i){
lastIndex=Math.max(lastIndex,S.lastIndexOf(strList[i]));
if(i==lastIndex){
list.add(i+1-lastAdd);
lastAdd=i+1;
}
}
return list;
}
}
- 正确方法。先遍历一次,把每一个字母的lastindex值记入一个数组,这样就不用循环里再套一层循环了。
class Solution {
public List<Integer> partitionLabels(String S) {
int[] charList = new int[26];
for(int i=0;i<S.length();++i){
charList[char2int(S.charAt(i))] = i;
}
List res = new LinkedList();
int lastIndex=0,pos=0;
for(int i=0;i<S.length();++i){
lastIndex = Math.max(lastIndex, charList[char2int(S.charAt(i))]);
if(i==lastIndex){
res.add(lastIndex-pos+1);
pos = lastIndex+1;
}
}
return res;
}
private int char2int(char c){
return c- 'a';
}
}
剪绳子
牛客:剪绳子
题目描述:
给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1,m<=n),每段绳子的长度记为k[1],...,k[m]。请问k[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
示例:
# 输入
8
# 返回
18
思想:
方法一(贪心):找规律可知,每次剪出长度为3的绳子,直到长度小于5,停止减绳,这样乘积一定是最大的。
方法二(动态规划):状态转移方程:dp[i] = Math.max(dp[i],dp[j] * dp[i-j])
,状态为i时(即总长度为i)遍历每种分割情况,取最大乘积,即为dp[i]。
代码:
方法一:
public int cutRope(int target) {
int res = 1;
while(target>4){
res*=3;
target-=3;
}
return target*res;
}
方法二:
public int cutRope(int target) {
if(target<5) return target;
int[] dp = new int[target+1];
dp[1]=1;dp[2]=2;dp[3]=3;dp[4]=4;
for(int i=5;i<=target;++i){
for(int j=2;j<=i/2;++j){
dp[i] = Math.max(dp[i],dp[j] * dp[i-j]);
}
}
return dp[target];
}