不断更新中
题解
455 分发饼干问题
题目详情:假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
题目分析:该题是一个很经典的,也很简单的贪心问题,可以很舒服的引入贪心的思想。
我们的目标是满足尽可能更多的孩子。即,我们只考虑满足孩子的数量,而孩子本身并没有优先性。那么此时,我们先满足胃口小的孩子,从小尺寸的饼干开始,如此进行
所以这里我们贪心的思路是:从小开始。
代码中是用到了双指针,这样会更快,双指针的使用基于这样一个事实,满足过的孩子或饼干不能再用了,并且没法使用的饼干就不再考虑了。
class Solution {
public int findContentChildren(int[] g,int[] s){
Arrays.sort(g);
Arrays.sort(s);
int result = 0;
int gi=0,si=0;
while (gi < g.length && si < s.length) {
if (g[gi] <= s[si]) {
gi++;
}
si++;
}
return gi;
}
}
435 无重叠区间
题目详情:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
题目分析:该题是一个区域重叠问题,后面的很多问题都是它的一个变体,需要十分十分注意注意,好好理解。当然本题也可以用动态规划做,不过时间效率略低,还是贪心更适合一些,同时它的变体们用贪心也是很好做的。
这里因为是区域,可以说是分布在一维空间上,所以这里我们需要对它们进行排序。这样才便于我们去分析。不然问题会十分复杂。
排序的规则是先按照右边界排序,右边界相等的话按照左边界排序,都是升序。
进行排序之后,我们的区域可以说就是很有序的分布在一维空间上了。有这样一个简单的事实,我们从第一区域开始,它会有几种情况。
第一种情况是,后面的左边界都大于它(右边界大于他是显然,因为我们是这样排序的)
那么很明显,我们需要在于区域1重叠区域保留一个(或者说是删除其他,来保证不重叠),那我们保留谁呢,2,3都与1重叠,删除哪个呢,那么很明显,保留1最好,因为它的右边界最小,它会留给右边的空白区域更多,它让后面的重叠可能更小所以这里保留1,删除2,3个。
在完成这个操作后,我们不去考虑前面的了,直接将不与区域1重叠的区域4当做新区域1 。如此便一直迭代下去,直到最后一个便可以找完所有的。
当然也会遇到一种不一样的情况:
后面的区域中有的左边界比区域1的左边界更左,此时也很简单,还是一样的处理,我们不用在乎左边界,因为我们的目标是从左边的1,2,3中挑选出最靠左的区域。然后让区域4作为新的区域1,如此迭代下去。
讲到这,应该你已经明白了,这里的贪心就是在于,从左往右,在重叠的区域们中每次都保留靠左的区域,使得右边的空白空间尽可能大,这样右边重叠的可能性也就会小。(其实也有点像上面的饼干分配问题,从小开始)
class Solution {
public int eraseOverlapIntervals(int[][] intervals){
if(intervals.length==0){//判断数组是否为空
return 0;
}
//进行排序,按照开始点进行排序
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {//按照右边界排序,如果做边界相等,则按照左边界重复。最终左边界小的,
if(o1[1]==o2[1]){
return o1[0]-o2[0];
}
return o1[1]-o2[1];
}
});
int count = 0;
int temp=0;
//贪心的思想,每次寻找最短的,因为已经排好序
//对于这个贪心的思想,怎么说呢,如果重复,则选择当前情况下结束最早的,那么肯定比其他的要好
for(int i =0;i<intervals.length;i++){
if(i==0){
count++;
}
//进行比较,看看是否重复
else {
if(intervals[i][0]>=intervals[temp][1]){//这么比较的原因在于,按照右边界排好了序,只用考虑左边界
temp = i;
count++;
}
}
}
return intervals.length-count;
}
}
452. 用最少数量的箭引爆气球
题目详情:在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量
题目分析:这题可以说是435题的变体,也是个区域重叠问题。弄懂了435,这题也就很简单了。对于本题,它的目标实际就是,找哪些区域同时重复的最多。
题目思路:对该题,我们也首先进行排序,跟435一样的排序,此处不赘述。
然后从左往右遍历每个区域,我们秉承这样一个思想,因为已经排序好了,区域们都映射到了一维空间上,那么我们当前遍历的区域是肯定要射一箭的,那么能找到与它重复的越多越好,那么可以将它们一起给射了。以这样一个思路,不断的进行,就能最少次数的将所有的气球射玩。
所以这里的贪心在于,从左往右,对于遍历到的每个未破的气球,每次将与它重复的气球都射爆(遍历到该气球时,尽可能射爆更多的气球,或者说射的位置尽可能靠右,这样才能射爆的更多)。
class Solution {
public int findMinArrowShots(int[][] points) {
if (points.length == 0) {
return 0;
}
//对数组进行排序
Arrays.sort(points, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {//按照右边界排序,如果做边界相等,则按照左边界重复。最终左边界小的,
if (o1[1] == o2[1]) {
return o1[0] - o2[0];
}
return o1[1] - o2[1];
}
});
int result = 0;//结果
int area = 0;//当前访问到的区域
while(area<points.length){
//判断和当前区域重复的区域
int i =area+1;
while(i<points.length){
if(points[i][0]<=points[area][1]){
i++;
}
else {//一旦没有了重叠的可能,就停止
break;
}
}
result++;
area=i;
}
return result;
}
}
当然,这里的排序可以用左边界来(对这个问题可以,不过结果不变)。
406. 根据身高重建队列
问题详情:假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。
问题分析:关于这个问题,有个很巧妙的一点就是:高的人对矮的人是视而不见的,矮的人排在它们右边不影响,排在左边也不影响,删掉也不影响,高的人只会在乎比他更高或者一样高的人。
所以这就决定了我们先放置谁的问题–最高的人,最高的人谁也不在乎,自己影响自己,所以从他们开始,然后是次高,一旦最高的确定,次高的只需要考虑最高的和自己,其他矮的也不需要考虑,如此从高到低来放置,从而可以保证一点,放置后面的时,不会导致先前放置的出问题。
所以在贪心问题中,我们时常得考虑一点,哪一类会对哪一类有影响,哪一类是顶多只考虑自己的,被影响的越少,越应该被我们先考虑。
所以,我们将所有的元素按照身高h分成最多h类,在同一类中,明显是k小的在前面,k大的在后面,当然,在同一类中的顺序也就相等于他们在真实队列中的相对顺序。
如此一来,我们便进行好了排序。下面就是进行重建队列了。
明显,这就是n个序列进行组合的问题。
那么很简单的一个思路就是,我先排最高的人,它们的相对顺序是k从小到大,然后再排次高的,次高的需要考虑最高的人,所以需要挨个的比较大小,然后找到属于自己的位置进行插入。然后再是次次高,如此迭代下去。
这确实是没问题的,但是,我们是不是可以简化一下呢?
是的,可以简化。我就以次高来举例,当我要插入次高的人们中第一人(h,k)时,应该插到哪里呢?
按照之前的我需要一个个去比较,但实际上,不用比较,直接插到第k个就行,为什么呢?
很简单,因为之前已经安排好的人都比该人高或者一样高,所以理所应当这个人就放在第k个位置。同样,次高的第二个人要是放在他的第“k”个位置,当然,此时第二人要插入的队列是第一个人已经插入过的队列,是现在时刻的队列。
如此一来,对于每个人来说,它就直接插到第k个位置就好了,因为它需要考虑的所有人都比它高或者和它一样高。
public int[][] reconstructQueue(int[][] people){
//对数组进行排序
Arrays.sort(people, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {//按照右边界排序,如果做边界相等,则按照左边界重复。最终左边界小的,
if (o2[0] == o1[0]) {
return o1[1] - o2[1];
}
return o2[0] - o1[0];
}
});
List<int[]> queue = new ArrayList<>();
for(int[] p:people){
queue.add(p[1],p);
}
return queue.toArray(new int[queue.size()][]);
}
121. 买卖股票的最佳时机
问题详情:给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
问题分析:该题最简单的思路就是两层for循环的O(n^2)解法了,这个肯定很慢,没办法。
更好的思路是O(n)的。
假设第一个是最小的,选中第一个,让后往后遍历,差值越来越大,则利润越来越高,一旦出现负值,则说明遇到了当前遍历情况下的最小值,即最低成本价,选中该点,继续进行。
这个思路能够保证出现负值之前,最大值求出来了已经,就是p[i-1]-p[0]那么,以负值那个元素为最小值,往后继续寻找就行。
这里的贪心在于,差值尽可能大。
public int maxProfit(int[] prices){
//该方法的时间复杂度是O(n)
int max = 0;
int index_min = 0;
int i = 0;
while(i<prices.length){
if(prices[i]-prices[index_min]>max){
max = prices[i]-prices[index_min];
}
if(prices[i]-prices[index_min]<0){//出现负值,出现更小值
index_min = i;
}
i++;
}
return max;
}
122. 买卖股票的最佳时机 II
问题详情:给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
问题分析:本题就是在前一题的基础上进行的,就是多个买卖操作。套用前一题的思路,假设第一个就是最低价,然后往后遍历一旦出现负值就在之前一步卖掉,等等,好像不对的样子。
嗯,这里不是到负值就卖,我们实际的基金股票都希望在哪个点卖呢?最高点,对的,所以需要在最高点卖,什么时候后一个值比前一个小,就说明到最高点了,就可以卖了。然后剩下的就当做一开始那样继续进行。如此便完成了整个求解过程。
其次,之前的代码是比较最大的max,而这里是个累加和,所以需要累加。
总结一下,这里就是低买高卖。
这里的贪心过程就是每一小段都是从最小值开始,从最大值结束,否则就不买。
public int maxProfit(int[] prices){
//这个问题就是一个分段的前一个问题了
//按照之前121的思路,当假设第一个最低价,从第一个开始,往后走,到负值之前,那肯定是一直不卖最好
//然后到达负值之前卖了,负值再买,然后再看,这个是一样的,如果一直跌,就不买,
//把前面的比较max改成max+max
int profit = 0;
int index_min = 0;//可以理解为买入点更合适
int i = 0;
int max = 0;
while(i<prices.length){
if(prices[i]-prices[index_min]>max){
max = prices[i]-prices[index_min];
}
if(prices[i]-prices[index_min]<max){//出现负值,出现更小值,买入
profit += max;
index_min = i;
max = 0;
}
i++;
//会有一个问题,一直没有出现负值,这样需要额外对profit赋值一次
}
profit += max;
return profit;
}