贪心
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
最好用的策略就是举反例。
做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
455.分发饼干
大饼干优先喂给胃口大的小孩儿。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int count = 0;
int i=g.length-1, j=s.length-1;
for(; i>=0 && j>=0; i--){
if(g[i]>s[j]) continue;
count++;
j--;
}
return count;
}
}
376. 摆动序列 (情况很复杂)
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
本题要考虑三种情况:
- 上下坡中有平坡:连续的相同元素,只计算最后一个 —— 相同数字连续时,
prediff = 0 ,curdiff < 0 或 >0
也记为波谷。 - 数组首尾两端:假设数组最前面还有一个数字
prediff=0
。序列默认序列最右边有一个峰值,初始值为1
。 - 单调坡中有平坡:不要实时更新
prediff
,只需要在 这个坡度 摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。
指针:使用prediff, curdiff
,而非 pre, cur, next
。
// 一开始的思路:是错的
class Solution {
public int wiggleMaxLength(int[] nums) { // 波峰越多,最长摆动序列越长
if(nums.length==1) return 1;
else if(nums.length==2){
if(nums[0] != nums[1]) return 2;
else return 1;
}
int count = 0;
int left=0, cur=1, right=2;
for(; right<nums.length;){
if((nums[cur] - nums[left]) * (nums[right] - nums[cur])<0 ) count++;
right++;
cur++;
left++;
}
// 两边……
if(nums[0] == nums[right-1]) count+=1;
else count+=2;
return count;
}
}
此题考虑条件很多,不太好想……
class Solution {
public int wiggleMaxLength(int[] nums) {
int result = 1; // 默认最右边也是序列
int prediff = 0, curdiff = 0;
for(int i=1; i<nums.length; i++){
curdiff = nums[i] - nums[i-1];
if((prediff<=0 && curdiff>0) || (prediff>=0 && curdiff<0)){
result++;
prediff = curdiff; // 更新pre
}
}
return result;
}
}
动态规划
下标及其含义:right是作波峰还是波谷——总之要算进来
dp及其下标的值,一定与题解有关:对于我们当前考虑的这个数,要么是作为山峰(即 nums[i] > nums[i-1]
),要么是作为山谷(即 nums[i] < nums[i - 1]
)。
- 设 dp 状态
dp[i][0]
,表示考虑前 i 个数,第 i 个数作为山峰的摆动子序列的最长长度 - 设 dp 状态
dp[i][1]
,表示考虑前 i 个数,第 i 个数作为山谷的摆动子序列的最长长度
递推公式:
dp[i][0] = max(dp[i][0], dp[j][1] + 1)
,其中0 < j < i
且nums[j] < nums[i]
,表示将nums[i]
接到前面某个山谷后面,作为山峰。dp[i][1] = max(dp[i][1], dp[j][0] + 1)
,其中0 < j < i
且nums[j] > nums[i]
,表示将nums[i]
接到前面某个山峰后面,作为山谷。
初始状态:
由于一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:dp[0][0] = dp[0][1] = 1
。
还可以这样写:dp[0][0] = dp[0][1] = 1;
// DP
class Solution {
public int wiggleMaxLength(int[] nums) {
// 0 i 作为波峰的最大长度
// 1 i 作为波谷的最大长度
int dp[][] = new int[nums.length][2];
dp[0][0] = dp[0][1] = 1; // 可以以自身为子序列的起点
for (int i = 1; i < nums.length; i++){
//i 自己可以成为波峰或者波谷
dp[i][0] = dp[i][1] = 1; // 可以以自身为子序列的起点
for (int j = 0; j < i; j++){
if (nums[j] > nums[i]){ // 是否可以作为新的波谷:遍历前面所有的波谷
// i 是波谷
dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1);
}
if (nums[j] < nums[i]){ // 是否可以作为新的波峰:遍历前面所有的波峰
// i 是波峰
dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1);
}
// 如果相等,证明什么也做不到,既不是波峰,也不是波谷
}
}
return Math.max(dp[nums.length - 1][0], dp[nums.length - 1][1]);
}
}
53. 最大子序和
只能想到两端一定要是正数——但是如果没有正数……
总之,还是要看随想录的解法:
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
class Solution {
public int maxSubArray(int[] nums) {
int result = Integer.MIN_VALUE;
int left=0, right=1; // 左闭右开
int count = nums[0];
while(right<=nums.length){
result = result>count? result: count;
if(count>0){
if(right<nums.length) count+= nums[right];
}else{
left=right;
if(right<nums.length) count = nums[left];
}
right++;
}
return result;
}
}
代码随想录解法:
- 不需要用到区间:因为一旦count<0时,区间大小就要缩小为0。所以,对right进行遍历即可。
- 使用Math.max,替代三元表达式。
class Solution {
public int maxSubArray(int[] nums) {
if (nums.length == 1){ // 边缘条件
return nums[0];
}
int sum = Integer.MIN_VALUE;
int count = 0;
for (int i = 0; i < nums.length; i++){
count += nums[i];
sum = Math.max(sum, count); // 取区间累计的最大值(相当于不断确定最大子序终止位置)
if (count <= 0){
count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
}
}
return sum;
}
}
动态规划
class Solution {
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
for(int i=1; i<nums.length; i++){
if(dp[i-1]<=0){
dp[i] = nums[i];
}else{
dp[i] = dp[i-1] + nums[i];
}
}
return Arrays.stream(dp).max().getAsInt();
}
}