本节内容
1005.K次取反后最大化的数组和
加油站
分发糖果
1005.K次取反后最大化的数组和※ 建议 :
题目链接: https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/ 文章讲解: https://programmercarl.com/1005.K%E6%AC%A1%E5%8F%96%E5%8F%8D%E5%90%8E%E6%9C%80%E5%A4%A7%E5%8C%96%E7%9A%84%E6%95%B0%E7%BB%84%E5%92%8C.html 视频讲解:
题目分析
方案一 每次都去找最小的
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public int largestSumAfterKNegations (int [] nums, int k) { Arrays.sort(nums); int index = 0 ; for (int i = 0 ; i < k; i++) { nums[index] = -1 * nums[index]; if (index < nums.length - 1 && nums[index] > nums[index + 1 ]) index++; } return Arrays.stream(nums).sum(); } }
结果 解答成功: 执行耗时:3 ms,击败了61.01% 的Java用户 内存消耗:40.3 MB,击败了40.98% 的Java用户
分析 时间复杂度: O( n log(n) )
空间复杂度: O( n )
代码随想录 https://programmercarl.com/1005.K%E6%AC%A1%E5%8F%96%E5%8F%8D%E5%90%8E%E6%9C%80%E5%A4%A7%E5%8C%96%E7%9A%84%E6%95%B0%E7%BB%84%E5%92%8C.html
思路 本题思路其实比较好想了,如何可以让数组和最大呢?
贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
局部最优可以推出全局最优。
那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。
虽然这道题目大家做的时候,可能都不会去想什么贪心算法,一鼓作气,就AC了。
我这里其实是为了给大家展现出来 经常被大家忽略的贪心思路,这么一道简单题,就用了两次贪心!
那么本题的解题步骤为:
第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
第二步:从前向后遍历,遇到负数将其变为正数,同时K–
第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
第四步:求和
对应C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {static bool cmp (int a, int b) { return abs (a) > abs (b); }public : int largestSumAfterKNegations (vector<int >& A, int K) { sort (A.begin (), A.end (), cmp); for (int i = 0 ; i < A.size (); i++) { if (A[i] < 0 && K > 0 ) { A[i] *= -1 ; K--; } } if (K % 2 == 1 ) A[A.size () - 1 ] *= -1 ; int result = 0 ; for (int a : A) result += a; return result; } };
时间复杂度: O(nlogn)
空间复杂度: O(1)
总结
贪心的题目如果简单起来,会让人简单到开始怀疑:本来不就应该这么做么?这也算是算法?我认为这不是贪心?
本题其实很简单,不会贪心算法的同学都可以做出来,但是我还是全程用贪心的思路来讲解。
因为贪心的思考方式一定要有!
如果没有贪心的思考方式(局部最优,全局最优),很容易陷入贪心简单题凭感觉做,贪心难题直接不会做,其实这样就锻炼不了贪心的思考方式了 。
所以明知道是贪心简单题,也要靠贪心的思考方式来解题,这样对培养解题感觉很有帮助。
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public int largestSumAfterKNegations (int [] nums, int K) { nums = IntStream.of(nums) .boxed() .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1)) .mapToInt(Integer::intValue).toArray(); int len = nums.length; for (int i = 0 ; i < len; i++) { if (nums[i] < 0 && K > 0 ) { nums[i] = -nums[i]; K--; } } if (K % 2 == 1 ) nums[len - 1 ] = -nums[len - 1 ]; return Arrays.stream(nums).sum(); } }
134. 加油站※ 建议 :
题目链接: https://leetcode.cn/problems/gas-station/ 文章讲解: https://programmercarl.com/0134.%E5%8A%A0%E6%B2%B9%E7%AB%99.html 视频讲解:
题目分析
方案一 人傻了,辣模容易都没想起来。
一直想着遍历两圈,没想到可以使用判断总和的大小而省略掉第二圈,真的是让人……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int canCompleteCircuit (int [] gas, int [] cost) { int tempSum = 0 ; int totalSum = 0 ; int index = 0 ; for (int i = 0 ; i < gas.length; i++) { tempSum += gas[i] - cost[i]; totalSum += gas[i] - cost[i]; if (tempSum < 0 ) { tempSum = 0 ; index = i + 1 ; } } if (totalSum < 0 ) return -1 ; return index; } }
结果 解答成功: 执行耗时:2 ms,击败了89.64% 的Java用户 内存消耗:54 MB,击败了46.79% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( 1 )
代码随想录 https://programmercarl.com/0134.%E5%8A%A0%E6%B2%B9%E7%AB%99.html
暴力方法 暴力的方法很明显就是O(n^2)的,遍历每一个加油站为起点的情况,模拟一圈。
如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。
暴力的方法思路比较简单,但代码写起来也不是很容易,关键是要模拟跑一圈的过程。
for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!
C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public : int canCompleteCircuit (vector<int >& gas, vector<int >& cost) { for (int i = 0 ; i < cost.size (); i++) { int rest = gas[i] - cost[i]; int index = (i + 1 ) % cost.size (); while (rest > 0 && index != i) { rest += gas[index] - cost[index]; index = (index + 1 ) % cost.size (); } if (rest >= 0 && index == i) return i; } return -1 ; } };
贪心算法(方法一) 直接从全局进行贪心选择,情况如下:
情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。 (倒车请注意)
C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution {public : int canCompleteCircuit (vector<int >& gas, vector<int >& cost) { int curSum = 0 ; int min = INT_MAX; for (int i = 0 ; i < gas.size (); i++) { int rest = gas[i] - cost[i]; curSum += rest; if (curSum < min) { min = curSum; } } if (curSum < 0 ) return -1 ; if (min >= 0 ) return 0 ; for (int i = gas.size () - 1 ; i >= 0 ; i--) { int rest = gas[i] - cost[i]; min += rest; if (min >= 0 ) { return i; } } return -1 ; } };
其实我不认为这种方式是贪心算法,因为没有找出局部最优,而是直接从全局最优的角度上思考问题 。
但这种解法又说不出是什么方法,这就是一个从全局角度选取最优解的模拟操作。
所以对于本解法是贪心,我持保留意见!
但不管怎么说,解法毕竟还是巧妙的,不用过于执着于其名字称呼。
贪心算法(方法二) 可以换一个思路,首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。
每个加油站的剩余量rest[i]为gas[i] - cost[i]。
i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。
如图:
那么为什么一旦[0,i] 区间和为负数,起始位置就可以是i+1呢,i+1后面就不会出现更大的负数?
如果出现更大的负数,就是更新i,那么起始位置又变成新的i+1了。
那有没有可能 [0,i] 区间 选某一个作为起点,累加到 i这里 curSum是不会小于零呢? 如图:
如果 curSum<0 说明 区间和1 + 区间和2 < 0, 那么 假设从上图中的位置开始计数curSum不会小于0的话,就是 区间和2>0。
区间和1 + 区间和2 < 0 同时 区间和2>0,只能说明区间和1 < 0, 那么就会从假设的箭头初就开始从新选择其实位置了。
那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置 。
局部最优可以推出全局最优,找不出反例,试试贪心!
C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : int canCompleteCircuit (vector<int >& gas, vector<int >& cost) { int curSum = 0 ; int totalSum = 0 ; int start = 0 ; for (int i = 0 ; i < gas.size (); i++) { curSum += gas[i] - cost[i]; totalSum += gas[i] - cost[i]; if (curSum < 0 ) { start = i + 1 ; curSum = 0 ; } } if (totalSum < 0 ) return -1 ; return start; } };
说这种解法为贪心算法,才是有理有据的,因为全局最优解是根据局部最优推导出来的 。
总结
对于本题首先给出了暴力解法,暴力解法模拟跑一圈的过程其实比较考验代码技巧的,要对while使用的很熟练。
然后给出了两种贪心算法,对于第一种贪心方法,其实我认为就是一种直接从全局选取最优的模拟操作,思路还是很巧妙的,值得学习一下。
对于第二种贪心方法,才真正体现出贪心的精髓,用局部最优可以推出全局最优,进而求得起始位置。
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public int canCompleteCircuit (int [] gas, int [] cost) { int sum = 0 ; int min = 0 ; for (int i = 0 ; i < gas.length; i++) { sum += (gas[i] - cost[i]); min = Math.min(sum, min); } if (sum < 0 ) return -1 ; if (min >= 0 ) return 0 ; for (int i = gas.length - 1 ; i > 0 ; i--) { min += (gas[i] - cost[i]); if (min >= 0 ) return i; } return -1 ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public int canCompleteCircuit (int [] gas, int [] cost) { int curSum = 0 ; int totalSum = 0 ; int index = 0 ; for (int i = 0 ; i < gas.length; i++) { curSum += gas[i] - cost[i]; totalSum += gas[i] - cost[i]; if (curSum < 0 ) { index = (i + 1 ) % gas.length ; curSum = 0 ; } } if (totalSum < 0 ) return -1 ; return index; } }
135. 分发糖果※ 建议 :
题目链接: https://leetcode.cn/problems/candy/ 文章讲解: https://programmercarl.com/0135.%E5%88%86%E5%8F%91%E7%B3%96%E6%9E%9C.html 视频讲解:
题目分析
方案一 将数据放入到优先队列,然后一层层弹出优先队列,去对孩子的糖果进行赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Solution { public int candy (int [] ratings) { if (ratings.length == 1 ) return 1 ; PriorityQueue<Integer[]> queue = new PriorityQueue <>((o1, o2) -> o1[0 ].compareTo(o2[0 ])); int [] children = new int [ratings.length]; int sum = 0 ; for (int i = 0 ; i < ratings.length; i++) { queue.offer(new Integer []{ratings[i], i}); } while (!queue.isEmpty()) { int index = queue.poll()[1 ]; if (index == 0 ) { if (ratings[index] > ratings[index + 1 ]) { children[index] = children[index + 1 ] + 1 ; } else { children[index] = 1 ; } sum += children[index]; } else if (index == ratings.length - 1 ) { if (ratings[index] > ratings[index - 1 ]) { children[index] = children[index - 1 ] + 1 ; } else { children[index] = 1 ; } sum += children[index]; } else { if (ratings[index] == ratings[index + 1 ] && ratings[index] == ratings[index - 1 ]) { children[index] = 1 ; } else if (ratings[index] == ratings[index + 1 ]){ children[index] = children[index - 1 ] + 1 ; } else if (ratings[index] == ratings[index - 1 ]){ children[index] = children[index + 1 ] + 1 ; } else { children[index] = Math.max(children[index + 1 ], children[index - 1 ]) + 1 ; } sum += children[index]; } } return sum; } }
结果 解答成功: 执行耗时:33 ms,击败了5.14% 的Java用户 内存消耗:44.2 MB,击败了5.12% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( n )
方案二: 根据官方题解得
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution { public int candy (int [] ratings) { int [] list = new int [ratings.length]; list[0 ] = 1 ; for (int i = 1 ; i < ratings.length; i++) { if (ratings[i] > ratings[i - 1 ]) { list[i] = list[i - 1 ] + 1 ; } else { list[i] = 1 ; } } for (int i = ratings.length - 2 ; i >= 0 ; i--) { if (ratings[i] > ratings[i + 1 ]) { list[i] = Math.max(list[i], list[i + 1 ] + 1 ); } } int result = 0 ; for (int i = 0 ; i < list.length; i++) { result += list[i]; } return result; } }
结果 解答成功: 执行耗时:2 ms,击败了72.64% 的Java用户 内存消耗:42.4 MB,击败了94.08% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( n )
代码随想录 https://programmercarl.com/0135.%E5%88%86%E5%8F%91%E7%B3%96%E6%9E%9C.html
思路 这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼 。
先确定右边评分大于左边的情况(也就是从前向后遍历)
此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
局部最优可以推出全局最优。
如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1
代码如下:
1 2 3 4 for (int i = 1 ; i < ratings.size (); i++) { if (ratings[i] > ratings[i - 1 ]) candyVec[i] = candyVec[i - 1 ] + 1 ; }
如图:
再确定左孩子大于右孩子的情况(从后向前遍历)
遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢?
因为 rating[5]与rating[4]的比较 要利用上 rating[5]与rating[6]的比较结果,所以 要从后向前遍历。
如果从前向后遍历,rating[5]与rating[4]的比较 就不能用上 rating[5]与rating[6]的比较结果了 。如图:
所以确定左孩子大于右孩子的情况一定要从后向前遍历!
如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。
那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
局部最优可以推出全局最优。
所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多 。
如图:
所以该过程代码如下:
1 2 3 4 5 6 for (int i = ratings.size () - 2 ; i >= 0 ; i--) { if (ratings[i] > ratings[i + 1 ] ) { candyVec[i] = max (candyVec[i], candyVec[i + 1 ] + 1 ); } }
整体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : int candy (vector<int >& ratings) { vector<int > candyVec (ratings.size(), 1 ) ; for (int i = 1 ; i < ratings.size (); i++) { if (ratings[i] > ratings[i - 1 ]) candyVec[i] = candyVec[i - 1 ] + 1 ; } for (int i = ratings.size () - 2 ; i >= 0 ; i--) { if (ratings[i] > ratings[i + 1 ] ) { candyVec[i] = max (candyVec[i], candyVec[i + 1 ] + 1 ); } } int result = 0 ; for (int i = 0 ; i < candyVec.size (); i++) result += candyVec[i]; return result; } };
总结
这在leetcode上是一道困难的题目,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。
那么本题我采用了两次贪心的策略:
一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。
这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution { public int candy (int [] ratings) { int len = ratings.length; int [] candyVec = new int [len]; candyVec[0 ] = 1 ; for (int i = 1 ; i < len; i++) { candyVec[i] = (ratings[i] > ratings[i - 1 ]) ? candyVec[i - 1 ] + 1 : 1 ; } for (int i = len - 2 ; i >= 0 ; i--) { if (ratings[i] > ratings[i + 1 ]) { candyVec[i] = Math.max(candyVec[i], candyVec[i + 1 ] + 1 ); } } int ans = 0 ; for (int num : candyVec) { ans += num; } return ans; } }