跳跃问题
题目
版本1 正确
public boolean canJump(int[] nums) {
// 目前能够到达的最远距离
int farthDis = 0;
for(int i = 0; i < nums.length - 1; i ++) {
farthDis = Math.max(farthDis, i + nums[i]);
if (farthDis <= i) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
正确的原因
(1) 对于每个i计算能够到达的最远距离, 如果当前i到不了下一个i, 就会被farthDis <= i的条件拦截, 返回错误. 正常来说, 因为nums[i]没有负数, 通过farthDis = Math.max(farthDis, i + nums[i]), farthDis应该是大于i的, 但是当nums[i] = 0的时候, 就会存在farthDis = i的可能, 这也是对于本道题目来说, 唯一无法继续跳月的可能, 因此这里判断条件只能是farthDis <= i
(2) 只需要执行到倒数第二个元素, 并且能够到达最后一个元素即可, 因为如果最后元素的值为0, 也应该返回正确, 而不是错误, 因此i < nums.length - 1
跳跃问题2
题目
版本1 正确 dp + 部分贪心
public static int jump(int[] nums) {
// 明确状态
// 状态就是你所处在nums数组的哪个位置
// 明确dp数组的含义
// dp[i] = x; 表示跳到i这个位置, 需要的最少次数为x
int [] dp = new int[nums.length];
// base case
// dp[0] = 0; 起始位置就是数组的第一个位置
// 枚举所有状态
for (int i = 1; i < nums.length; i ++) {
// 每次计算dp[i] 需要计算i之前的每个位置能否跳到当前i
dp[i] = i;
for(int j = 0; j < i; j ++) {
if (nums[j] >= i - j) {
// 表示能够从j一次跳到i
dp[i] = Math.min(dp[i], dp[j] + 1);
break;
}
}
}
return dp[nums.length - 1];
}
正确的思路
(1) 注意关于j的循环, 距离i最远的j, 如果j能够到达i, 此时的dp[j] 一定是最小的, 因此是可以用break截断的, 这个思想也是借鉴了贪心的思想
版本2 错误
public static int jump(int[] nums) {
// 完全考虑贪心
// 每个位置在跳的时候, 选择下个格子最大的那个格子进行跳跃, 得到的跳跃次数最少
// start是当前的位置
int start = 0;
// len是当前位置能够到达的最远距离
int len = nums[start];
int count = 0;
// 判断是否需要跳跃
while (start + len < nums.length - 1) {
// 这里的len一定不可能为0
int tempIndex = start + len;
for (int i = len; i >= 1; i --) {
if (nums[start + i] > nums[tempIndex]) {
tempIndex = start + i;
}
}
start = tempIndex;
len = nums[tempIndex];
count ++;
}
if (start == nums.length - 1) {
// 此时已经位于最后位置了
return count;
} else {
// 当前位置可以直接跳到最后
return count + 1;
}
}
错误的原因
(1) 错误的认为, 贪心算法是在当前的选择中, 选择最大的那个数字, 但是对于下面的情况就出问题了
(2) 注意在当前位置start, 选择下一步跳的时候, 如果范围内最大的元素是多个, 一定要跳到最后一个元素的地方.
例如输入是 {1, 2, 1, 1, 2}的时候, 在从索引1开始跳的时候, 要跳到索引3, 而不是索引2
版本3 正确
public static int jump(int[] nums) {
// 完全考虑贪心
// 每个位置在跳的时候, 选择的应该是下一个格子j + nums[j]最大的情况
// 而不是单纯的nums[j]最大的情况
// start是当前的位置
int start = 0;
// len是当前位置能够到达的最远距离
int len = nums[start];
int count = 0;
// 判断是否需要跳跃
while (start + len < nums.length - 1) {
// 这里的len一定不可能为0
int tempIndex = start + len;
for (int i = len; i >= 1; i --) {
if (start + i + nums[start + i] > nums[tempIndex] + tempIndex) {
tempIndex = start + i;
}
}
start = tempIndex;
len = nums[tempIndex];
count ++;
}
if (start == nums.length - 1) {
// 此时已经位于最后位置了
return count;
} else {
// 当前位置可以直接跳到最后
return count + 1;
}
}
正确的原因
(1) 正确的识别了贪心的策略是每个位置在跳的时候, 选择的应该是下一个格子j + nums[j]最大的情况, 而不是单纯的nums[j]最大的情况
两地调度
题目
版本1 正确
public int twoCitySchedCost(int[][] costs) {
// 先让n个人都飞往B
// 此时的费用 sum1 = (price_b_1 + price_b_2 + ... + price_b_n-1 + price_b_n);
// 如果我让前n - 1人飞往B, 最后一个人飞往A
// 此时的费用 sum2 = (price_b_1 + price_b_2 + ... + price_b_n-1 + price_a_n);
// 可以发现sum1和sum2是有关系的
// sum2 = sum1 + price_a_n - price_b_n
// 那么如果题目要求的是只要一人飞往A, 那么我们在sum1中挑选出来某个人, 使得price_a_n - price_b_n最小, 是不是就满足条件了
// 同样的如果要选出一半的人飞往A, 那么我们就选出price_a_n - price_b_n最小的那一半人飞往A就可以了
Arrays.sort(costs, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[0] - o1[1] > o2[0] - o2[1]) {
return 1;
}
return -1;
}
});
int n = costs.length;
int sum = 0;
for(int i = 0; i < n; i ++) {
if (i < n / 2) {
sum += costs[i][0];
} else {
sum += costs[i][1];
}
}
return sum;
}
正确的思路
(1) 先假设都飞往一个地方, 然后如果挑选其中一个人飞往另一个地方, 怎么挑选最划算, 然后就得到了一开始应该怎么挑选.
用最少数量的箭引爆气球
题目
版本1 正确
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 1;
}
return -1;
}
});
int end = points[0][1];
int count = 1;
for (int i = 1; i < points.length; i ++) {
int start = points[i][0];
if (start <= end) {
continue;
}
count ++;
end = points[i][1];
}
return count;
}
正确的思路
(1) 寻找最大不重叠的区间, 第一步, 将数组元素按照end升序排列
(2) 第一个元素就是end最小的区间, 遍历数组, 如果start <= end, 即区间有交集, 则跳过该区间
(3) 遇见start > end的情况, 就是新的一个不重叠的区间, count++, 然后end更新, 此时的end又是剩下的所有元素中的最小的end
最长有效括号
题目
版本1 正确
public int longestValidParentheses(String s) {
// 利用栈, 栈底一直保留最后一个不被匹配的右括号
// 每当右括号匹配到一次左括号的时候, 都更新一次最大值, 最大值一定是产生在某一次左右匹配的情况
Stack<Integer> stack = new Stack<>();
// 维持栈底一直保留最后一个不被匹配的右括号的索引的原则
stack.push(-1);
int max = 0;
for (int i = 0; i < s.length(); i ++) {
if (s.charAt(i) == '(') {
stack.push(i);
} else {
// 如果遇见右括号, 弹出栈顶的元素, 要么是能够和它匹配的左括号, 要么是不被匹配的右边括号的索引
int index = stack.pop();
if (index != - 1 && s.charAt(index) == '(') {
// 计算一次最大距离, 此时的最大距离是和目前栈顶的元素做比较的, 思路类似接雨水单调栈的问题
max = Math.max(max, i - stack.peek());
} else {
// 更新最后一个不被匹配的右括号的索引为i
stack.push(i);
}
}
}
return max;
}
正确的原因
(1) 注意计算长度的时机, 以及计算长度时, 是用目前栈顶的元素的索引, 而不是弹出的元素的索引.