回溯、贪心、动态规划的思路是通过大量做题培养的,不是说几天就能掌握,而且题目不会告诉你使用哪个算法。坚持做题。。。
贪心算法
下面三题在区间问题中已做过,但没印象。所以可以当作新题了。
452. 用最少数量的箭引爆气球(复习)
上次思路是:用最少数量n来引爆区间==最多n个无重叠区间
随想录的思路是:尽量找越多的重叠区间,但重叠区间的右边以最小的为边界
var findMinArrowShots = function(points) {
points.sort((a, b) => a[0]-b[0])
var need = 1, minR = points[0][1]
for(let i=1; i<points.length; i++) {
if(points[i][0] > minR) {
need++
minR = points[i][1]
} else {
minR = Math.min(minR, points[i][1])
}
}
return need
};
435. 无重叠区间 (复习)
需要移除区间的最小数量(==最多无重叠的区间),使剩余区间互不重叠 .
从贪心算法出发,右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。
原来跟经典题——听讲座一样
56. 合并区间 (复习)
只考虑重叠和不相交两种情况
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
var merge = function(intervals) {
intervals.sort((a, b) => a[0] - b[0])
var st = intervals[0][0], en = intervals[0][1], ans = []
for(let i=1; i<intervals.length; i++) {
// 重叠
if(intervals[i][0] <= en) {
en = Math.max(intervals[i][1], en)
} else if(intervals[i][0] > en) { // 不相交
ans.push([st, en])
st = intervals[i][0]
en = intervals[i][1]
}
}
ans.push([st, en])
return ans;
};
763.划分字母区间 (中等)
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
输入:S = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca", "defegde", "hijhklij"。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。
跟我想法一样,先用字典存下字符最远的位置。
但差一步:每次的right要保留最远的。
var partitionLabels = function(s) {
var dic = {}
for(let i=0; i<s.length; i++) {
dic[s[i]] = i
}
var res = [], left = 0, right = 0
for(let i=0; i<s.length; i++) {
if(right < dic[s[i]]) {
right = Math.max(right, dic[s[i]])
} else if(right == i) {
res.push(right-left+1)
left = i + 1
right = left
}
}
return res
};
134. 加油站(中等)
那么局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置。
思想是,第一步肯定是有贡献(>0)才作为起始位置。后面遇到走不下去了,就重新定义第一步。
var canCompleteCircuit = function(gas, cost) {
var curSum = 0, totalSum = 0, start = 0
for(let i=0; i<gas.length; 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
};
而我的想法将差值数组从尾部累加,那么最大值,肯定是到达数组末尾是剩油最多的,如果有结果,那不就是最优的出发点。
135. 分发糖果(困难)fail
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
思路:
var candy = function(ratings) {
var res = new Array(ratings.length).fill(1)
// 只比左边节点
for(let i=1; i<ratings.length; i++) {
if(ratings[i] > ratings[i-1]) {
res[i] = res[i-1] + 1
}
}
// 在只比左边的基础上,比较右边节点
for(let i=ratings.length-2; i>=0; i--) {
if(ratings[i] > ratings[i+1]) {
res[i] = Math.max(res[i], res[i+1] + 1)
}
}
return res.reduce((pre, cur) => {
pre += cur
return pre
}, 0)
};
860. 柠檬水找零 (简单)
我漏了:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
贪心:因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!
考察逻辑
var lemonadeChange = function(bills) {
var five = 0, ten = 0
for(let i=0; i<bills.length; i++) {
let bill = bills[i]
if(bill == 5) {
five++
}else if(bill == 10) {
if(five > 0) {
five--
ten++
} else {
return false
}
}else{
if(ten > 0 && five > 0) {
ten--
five--
}else if(five > 2) {
five -= 3
}else {
return false
}
}
}
return true
};
406. 根据身高重建队列 (中等)fail
如果两个维度一起考虑一定会顾此失彼。
按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。
所以在按照身高从大到小排序后:
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
还有一个细节,原来排序 [5,3],[5,2],那么插入[5,3]时忽略了[5,2]应该在前面。
即身高相同时,k小的排前面
var reconstructQueue = function(people) {
let queue = []
people.sort((a, b ) => {
if(b[0] !== a[0]) {
return b[0] - a[0]
} else {
return a[1] - b[1]
}
})
for(let i = 0; i < people.length; i++) {
queue.splice(people[i][1], 0, people[i])
}
return queue
};
回溯算法
131. 分割回文串(中等)
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
"aaa"
[["a","a","a"],["a","aa"],["aa","a"],["aaa"]]
function judge(st, en, s) {
for(let i=st, j=en; i<j; i++, j--) {
if(s[i] !== s[j]) return false
}
return true
}
var partition = function(s) {
var res = [], temp = []
function backtrack(index) {
if(index == s.length) {
res.push([...temp])
return
}
for(let end=index; end<s.length; end++) {
if(!judge(index, end, s)) continue
temp.push(s.substr(index, end - index + 1))
backtrack(end + 1)
temp.pop()
}
}
backtrack(0)
return res
};
417. 太平洋大西洋水流问题(中等)
无意间做的题目,但也用到之前的想法,从两个方向考虑,但使用的是深度遍历。
93. 复原 IP 地址(中等)
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
也是分割问题,差别在于判断条件。一个是不满足则continue,本题是不满足则break。
var restoreIpAddresses = function(s) {
var res = [], temp = []
function backtrack(start) {
if(temp.length > 4) return
if(temp.length == 4 && start == s.length) {
res.push(temp.join("."))
return
}
for(let end = start; end < s.length; end++) {
const str = s.substr(start, end - start + 1)
if(str.length > 3 || +str > 255) break
if(str.length > 1 && str[0] == 0) break
temp.push(str)
backtrack(end + 1)
temp.pop()
}
}
backtrack(0)
return res
};
78. 子集 (中等)
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
90. 子集 II (中等)
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
那么关于回溯算法中的去重问题,在40.组合总和II 中已经详细讲解过了,和本题是一个套路。
后期要讲解的排列问题里去重也是这个套路,所以理解“树层去重”和“树枝去重”非常重要。
var subsetsWithDup = function(nums) {
var res = [], temp = []
nums.sort((a, b) => a - b )
function backtrack(start) {
res.push([...temp])
// var lastVal = -11
for(let i = start; i < nums.length; i++) {
// if(lastVal === nums[i]) continue
if(i > start && nums[i] === nums[i - 1]) continue
temp.push(nums[i])
lastVal = nums[i]
backtrack(i + 1)
temp.pop()
}
}
backtrack(0)
return res
};
491. 递增子序列 (中等)
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
var findSubsequences = function(nums) {
var res = [], temp = []
function backtrack(start) {
if(temp.length > 1) res.push([...temp])
var used = {}
for(let i=start; i<nums.length; i++) {
if(used[nums[i]] || (temp.length > 0 && nums[i] < temp[temp.length-1])) continue
temp.push(nums[i])
used[nums[i]] = true
backtrack(i + 1)
// used[nums[i]] = false
temp.pop()
}
}
backtrack(0)
return res
};
332. 重新安排行程 (困難)
难点:
使用什么数据结构,对谁遍历。
如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
如果在解题的过程中没有对集合元素处理好,就会死循环。
var findItinerary = function(tickets) {
let map = {}
for(const val of tickets) {
const [from, to] = val
if(!map[from]) {
map[from] = []
}
map[from].push(to)
}
for(const city in map) {
map[city].sort()
}
let temp = ['JFK'], res = [], flag = true
function backtrack(lastCity) {
if(temp.length == tickets.length + 1 && flag) {
res.push([...temp])
flag = !flag
}
if(!map[lastCity] || map[lastCity].length == 0) {
return
}
for(let i = 0; i < map[lastCity].length; i++) {
let city = map[lastCity][i]
map[lastCity].splice(i, 1)
temp.push(city)
if(flag) {
backtrack(city)
}
temp.pop()
map[lastCity].splice(i, 0, city)
}
}
backtrack('JFK')
return res[0]
};