1. 接雨水
给出n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水
示例1:
输入:height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]输出:6
解释:
- 上面由数组表示柱子的高度图,在这种情况下,可以接
6个单位的雨水(蓝色部分表示雨水)
示例2:
输入:height = [4, 2, 0, 3, 2, 5]输出:9
1.1 递归解法
首先,我们将高度为0的柱子,也视为有柱子,只是它的高度很矮。这样一来,雨水只会存储在当前柱子的上方
柱子上可以存水,需要满足以下几个条件:
【条件一】当前柱子的左侧和右侧,必须有柱子。也就是说第一根柱子和最后一根柱子的上方,一定不会存水
【条件二】当前柱子的左侧最高柱子与右侧最高柱子,任意一侧的高度,都必须大于当前柱子的高度。否则,当前柱子上一定不会存水
可以存水的情况:
当前柱子不能是第一根或最后一根,即:
size - 1 > index > 0当前柱子的左侧最高柱子与右侧最高柱子,较矮一侧的高度 > 当前柱子的高度。此时它们的高度差值,即为当前柱子上的存水量
- 存水量 = min(左侧最高柱子的高度, 右侧最高柱子的高度) - 前柱子的高度
代码实现:
class Solution {// 表示柱子高度的数组fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}func trap() -> Int {if(_arr.count < 3){// 小于3根柱子,无法存水return 0;}var sum = 0;for i in 1..<(_arr.count - 1) {// 获取当前柱子高度let currHeight = _arr[i];// 获取左侧最高柱子的高度let leftMaxHeight = getLeftMaxHeight(index: i - 1);// 获取右侧最高柱子的高度let rightMaxHeight = getRightMaxHeight(index: i + 1);// 获取左右较矮一侧柱子的高度let minMaxHeight = min(leftMaxHeight, rightMaxHeight);// 如果较矮一侧柱子的高度 <= 当前柱子高度if(minMaxHeight <= currHeight){// 无法存水,跳过循环continue;}// 否则,当前柱子上的存水量 = 较矮一侧柱子的高度 - 当前柱子高度// 将其累加到总存水量中sum += (minMaxHeight - currHeight);}// 返回所有柱子上的总存水量return sum;}// 获取左侧最高柱子的高度fileprivate func getLeftMaxHeight(index : Int) -> Int {if(index < 1){return _arr[index];}let curr = _arr[index];let left = getLeftMaxHeight(index: index - 1);return max(curr, left);}// 获取右侧最高柱子的高度fileprivate func getRightMaxHeight(index : Int) -> Int {if(index >= _arr.count - 1){return _arr[_arr.count - 1];}let curr = _arr[index];let right = getRightMaxHeight(index: index + 1);return max(curr, right);}}
1.2 缓存优化
定义两个数组,将当前柱子的左侧最高柱子的高度,以及右侧最高柱子的高度分别缓存
class Solution {// 表示柱子高度的数组fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}fileprivate var _leftCache : [Int]?;fileprivate var _rightCache : [Int]?;func trap() -> Int {if(_arr.count < 3){// 小于3根柱子,无法存水return 0;}// 初始化左侧缓存_leftCache = [Int].init(repeating: -1, count: _arr.count);// 初始化右侧缓存_rightCache = [Int].init(repeating: -1, count: _arr.count);var sum = 0;for i in 1..<(_arr.count - 1) {// 获取当前柱子高度let currHeight = _arr[i];// 获取左侧最高柱子的高度let leftMaxHeight = getLeftMaxHeight(index: i - 1);// 获取右侧最高柱子的高度let rightMaxHeight = getRightMaxHeight(index: i + 1);// 获取左右较矮一侧柱子的高度let minMaxHeight = min(leftMaxHeight, rightMaxHeight);// 如果较矮一侧柱子的高度 <= 当前柱子高度if(minMaxHeight <= currHeight){// 无法存水,跳过循环continue;}// 否则,当前柱子上的存水量 = 较矮一侧柱子的高度 - 当前柱子高度// 将其累加到总存水量中sum += (minMaxHeight - currHeight);}// 返回所有柱子上的总存水量return sum;}// 获取左侧最高柱子的高度fileprivate func getLeftMaxHeight(index : Int) -> Int {if(_leftCache![index] != -1){// 命中缓存return _leftCache![index];}// 计算并写入缓存if(index < 1) {_leftCache![index] = _arr[index];}else {let curr = _arr[index];let left = getLeftMaxHeight(index: index - 1);_leftCache![index] = max(curr, left);}return _leftCache![index];}// 获取右侧最高柱子的高度fileprivate func getRightMaxHeight(index : Int) -> Int {if(_rightCache![index] != -1) {// 命中缓存return _rightCache![index];}// 计算并写入缓存if(index >= _arr.count - 1) {_rightCache![index] = _arr[_arr.count - 1];}else {let curr = _arr[index];let right = getRightMaxHeight(index: index + 1);_rightCache![index] = max(curr, right);}return _rightCache![index];}}
1.3 递推解法
计算存水量时,我们会从左往右遍历每一根柱子,所以左侧最高柱子的高度,可以跟随遍历一起计算
但右侧最高柱子的高度,如果在计算存水量时通过子循环遍历结果,会产生大量重复子问题的计算,影响效率。所以我们最好提前将计算出每一根柱子的右侧最高柱子的高度,并将其写入缓存
class Solution {// 表示柱子高度的数组fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}// 获取右侧柱子的缓存fileprivate func getRightCache() -> [Int] {// 初始化右侧柱子的缓存var rightCache = [Int].init(repeating: 0, count: _arr.count);// 初始化右侧最高柱子的高度var rightMax = 0;for j in (1..<(_arr.count - 1)).reversed() {// 获取后一根柱子的高度let next = _arr[j + 1];// 如果后一根柱子的高度 > 右侧最高柱子的高度if(next > rightMax){// 更新右侧最高柱子的高度rightMax = next;}// 写入缓存rightCache[j] = rightMax;}return rightCache;}func trap() -> Int {if(_arr.count < 3){// 小于3根柱子,无法存水return 0;}// 获取右侧柱子的缓存let rightCache = getRightCache();// 初始化左侧最高柱子的高度var leftMaxHeight = 0;// 初始化总存水量var sum = 0;for i in 1..<(_arr.count - 1) {// 获取当前柱子高度let currHeight = _arr[i];// 获取前一根柱子的高度let prev = _arr[i - 1];// 如果前一根柱子的高度 > 左侧最高柱子的高度if(prev > leftMaxHeight){// 更新左侧最高柱子的高度leftMaxHeight = prev;}// 从缓存中获取右侧最高柱子的高度let rightMaxHeight = rightCache[i];// 获取左右较矮一侧柱子的高度let minMaxHeight = min(leftMaxHeight, rightMaxHeight);// 如果较矮一侧柱子的高度 <= 当前柱子高度if(minMaxHeight <= currHeight){// 无法存水,跳过循环continue;}// 否则,当前柱子上的存水量 = 较矮一侧柱子的高度 - 当前柱子高度// 将其累加到总存水量中sum += (minMaxHeight - currHeight);}// 返回所有柱子上的总存水量return sum;}}
1.4 单调栈
单调栈:单调递增或单调减的栈,跟单调队列差不多,但是只用到它的一端
使用单调递增栈,根据柱子的高度进行对比,存储柱子的索引值。栈顶元素arr[top]作为当前柱子,栈顶元素的下一个元素arr[left]作为左侧柱子,arr[i]作为右侧柱子
当前栈非空,并且当前柱子的高度 < 右侧柱子的高度,进入栈的遍历
获取当前柱子,并将其出栈。再获取左侧柱子,如果不存在左侧柱子,结束栈的遍历
存在,先使用i - left - 1计算存水区域,再使用min(left, right) - curr计算存水量。通过存水量 * 存水区域获得区域内柱子的存水量,将其加入到总存水量中
代码实现:
class Solution {// 表示柱子高度的数组fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}func trap() -> Int {if(_arr.count < 3){// 小于3根柱子,无法存水return 0;}// 初始化总存水量var sum = 0;// 初始化栈结构let stack = LinkedStack<Int>();// 遍历所有柱子for i in 0..<_arr.count {// 获取当前柱子的高度,作为右侧柱子的高度使用let right = _arr[i];// 非空栈 && 当前柱子的高度 < 右侧柱子的高度,进入栈的遍历while(!stack.isEmpty() && _arr[stack.getTop()!] < right){// 栈顶元素出栈,作为当前柱子的索引值let currIndex = stack.pop()!;// 获得当前柱子的高度let curr = _arr[currIndex];// 再次获取栈顶元素,作为左侧柱子的索引值let leftIndex = stack.getTop();// 判断是否存在左侧柱子if(leftIndex == nil){// 不存在,结束栈的遍历break;}// 存水区域 = 当前索引值 - 左侧柱子的索引值 - 1let width = i - leftIndex! - 1;// 获得左侧柱子的高度let left = arr[leftIndex!];// 当前柱子上的存水量 = 较矮一侧柱子的高度 - 当前柱子高度let difference = min(left, right) - curr;// 总存水量 = 当前柱子上的存水量 * 可存水的柱子总数// 将其累加到总存水量中sum += difference * width;}// 将右侧柱子的索引值入栈,进入下一次栈的遍历,作为当前柱子的索引值使用stack.push(element: i);}// 返回所有柱子上的总存水量return sum;}}
时间复杂度:O(n),因为从
0到n-1的每个索引值最多只会入栈和出栈各一次空间复杂度:O(n),它主要取决于栈空间,栈的大小不会超过
n
1.5 双指针
使用双指针leftIndex和rightIndex,分别表示左右两侧柱子的索引值。leftIndex默认为0,只能向右移动。rightIndex默认为n - 1,只能向左移动
使用leftMaxHeight和rightMaxHeight,分别记录左右两侧最高柱子的高度。leftMaxHeight默认为第一根柱子的高度,rightMaxHeight默认为最后一根柱子的高度
只要左右指针不相遇,就会持续遍历,内部逻辑如下:
如果
当前左侧柱子的高度 < 当前右侧柱子的高度,必然leftMaxHeight < rightMaxHeight,通过左侧最高柱子的高度 - 当前左侧柱子的高度得到存水量。将左指针向右移动,并更新leftMaxHeight如果
当前左侧柱子的高度 >= 当前右侧柱子的高度,必然leftMaxHeight >= rightMaxHeight,通过右侧最高柱子的高度 - 当前右侧柱子的高度得到存水量。将右指针向左移动,并更新rightMaxHeight
解读:左指针右移的终止条件是找到>= rightMaxHeight的leftMaxHeight,也就是说一旦左指针终止右移,此时的arr[leftIndex]一定是leftMaxHeight,并且>= rightMaxHeight。同理,右指针左移的终止条件是找到> leftMaxHeight的rightMaxHeight,而此时的arr[rightIndex]就是rightMaxHeight
代码实现:
class Solution {// 表示柱子高度的数组fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}func trap() -> Int {if(_arr.count < 3){// 小于3根柱子,无法存水return 0;}// 初始化总存水量var sum = 0;// 左侧柱子的索引值,默认为第一根var leftIndex = 0;// 左侧最高柱子的高度,默认为第一根柱子的高度var leftMaxHeight = _arr[leftIndex];// 右侧柱子的索引值,默认为最后一根var rightIndex = _arr.count - 1;// 右侧最高柱子的高度,默认为最后一根柱子的高度var rightMaxHeight = _arr[rightIndex];// 只要左右指针不相遇,就会持续遍历while(leftIndex != rightIndex){// 获得左侧柱子的高度var leftHeight = _arr[leftIndex];// 获得右侧柱子的高度var rightHeight = _arr[rightIndex];// 如果左侧比右侧柱子的高度矮,必然左侧最高比右侧最高矮// 如果不好理解,也可以改成leftMaxHeight < rightMaxHeight// 两种写法目的相同,都是为了找到更矮一侧的柱子,计算它的存水量// 如果找不到>=rightMaxHeight的leftMaxHeight,则移动的永远都是左指针if(leftHeight < rightHeight){// 存水量取决于左右最高柱子中较矮一侧的高度,与当前柱子高度的差值// 因为:左侧高度 < 右侧高度,所以:左侧最高柱子的高度 < 右侧最高柱子的高度// 故此,我们不必求解min(leftMaxHeight, rightMaxHeight),直接使用以下公式即可// 存水量 = 左侧最高柱子的高度 - 当前左侧柱子的高度sum += leftMaxHeight - leftHeight;// 左侧柱子的索引值+1leftIndex += 1;// 获得当前左侧柱子的高度leftHeight = _arr[leftIndex];// 更新左侧最高leftMaxHeight = max(leftHeight, leftMaxHeight);}else{// 同理:左侧高度 >= 右侧高度,所以:左侧最高柱子的高度 >= 右侧最高柱子的高度// 故此,存水量 = 右侧最高柱子的高度 - 当前右侧柱子的高度sum += rightMaxHeight - rightHeight;// 左侧柱子的索引值-1rightIndex -= 1;// 获得当前右侧柱子的高度rightHeight = _arr[rightIndex];// 更新右侧最高rightMaxHeight = max(rightHeight, rightMaxHeight);}}// 返回所有柱子上的总存水量return sum;}}
时间复杂度:O(n),两个指针的移动总次数不超过
n空间复杂度:O(1),只需要使用常数的额外空间
2. 分割等和子集
给出一个只包含正整数的非空数组nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等
示例1:
输入:nums = [1, 5, 11, 5]输出:true
- 数组可以分割成
[1, 5, 5]和[11]
示例2:
输入:nums = [1, 2, 3, 5]输出:false
- 数组不能分割成两个元素和相等的子集
2.1 递归解法
问题可以归结为:假设S表示所有给定数字的总和,则两个相等的⼦集的和必须等于S / 2
⼦问题:找到总和为S / 2的⼦集
状态:数字只能使用⼀次,要么属于S / 2,要么不属于S / 2
如果给出的集合具有以下特征,则一定无法分割出等和的子集
集合中元素小于
2个集合中元素之和为奇数
集合中最大元素 > 元素之和的一半
否则,我们传入目标数(元素之和的一半),尝试用它减去符合条件的元素。如果可以遍历出一种方式,最终可将目标数减为0,则表示集合可分割出等和的子集
集合中元素只有两种可能性:
当前元素 > 剩余目标数,则不可能放入当前子集中
当前元素 < 剩余目标数,则有可能放入当前子集中
- 放入当前子集中的前提,必须满足:目标数 - 其他元素 - 当前元素 = 0
代码实现:
class Solution {fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}// 集合元素总和的一半,作为目标值fileprivate var _target : Int = 0;func canPartition() -> Bool {// 集合中元素个数小于2个if(_arr.count < 2){// 不可能分割return false;}// 元素总和var sum = 0;// 最大元素var maxItem = 0;// 遍历集合中所有元素for i in _arr {// 累加总和sum += i;// 计算出最大元素maxItem = max(i, maxItem);}// 判断总和为奇数if(sum % 2 == 1) {// 奇数,不可能分割出相等的子集return false;}// 目标的结果数,即:总和取半let target = sum / 2;// 判断最大元素是否超过目标数if(maxItem > target){// 超过,不可能分割出相等的子集return false;}// 是否可分割出等和的子集let reslut = getResult(index: 0, target: target);// 返回结果return reslut;}// 是否可分割出等和的子集func getResult(index : Int, target : Int) -> Bool {// 索引超出集合,表示所有元素都尝试过,无法分割出相等的子集if(index >= _arr.count){return false;}// 目标数减到0,可以分割出相等的子集if(target == 0){return true;}// 判断当前元素是否超过剩余的目标数if(_arr[index] > target){// 超过,跳过该元素,继续后面的元素return getResult(index: index + 1, target: target);}// 否则,尝试选择该元素,剩余目标数 = 当前目标数 - 当前元素let residue = target - _arr[index];// 继续后面的元素,并传入剩余目标数,表示选择该元素let reslut = getResult(index: index + 1, target: residue);// 判断选择该元素之后的结果if(!reslut){// 无法分割出相等的子集,证明该元素不符合加入子集的条件。跳过该元素,继续后面的元素return getResult(index: index + 1, target: target);}// 分割出相等的子集,直接返回return reslut;}}
2.2 缓存优化
定义二维数组,空间开辟大小为:(集合大小 +1) * (目标数 + 1)
class Solution {fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}// 集合元素总和的一半,作为目标值fileprivate var _target : Int = 0;// 缓存fileprivate var _cache : [[Int]]?;func canPartition() -> Bool {// 集合中元素个数小于2个if(_arr.count < 2){// 不可能分割出相等的子集return false;}// 元素总和var sum = 0;// 最大元素var maxItem = 0;// 遍历集合中所有元素for i in _arr {// 累加总和sum += i;// 计算出最大元素maxItem = max(i, maxItem);}// 判断总和为奇数if(sum % 2 == 1) {// 奇数,不可能分割出相等的子集return false;}// 目标的结果数,即:总和取半let target = sum / 2;// 判断最大元素是否超过目标数if(maxItem > target){// 超过,不可能分割出相等的子集return false;}// 初始化缓存大小_cache = [[Int]](repeating: [Int](repeating: -1, count: target + 1), count: _arr.count + 1);// 是否可分割出等和的子集let reslut = getResult(index: 0, target: target);// 返回结果return reslut == 1;}// 是否可分割出等和的子集func getResult(index : Int, target : Int) -> Int {if(_cache![index][target] == -1){// 索引超出集合,表示所有元素都尝试过,无法分割出相等的子集if(index >= _arr.count){// 将结果写入缓存_cache![index][target] = 0;// 返回结果return _cache![index][target];}// 目标数减到0,可以分割出相等的子集if(target == 0){// 将结果写入缓存_cache![index][target] = 1;// 返回结果return _cache![index][target];}// 判断当前元素是否超过剩余的目标数if(_arr[index] > target){// 超过,跳过该元素,继续后面的元素// 将结果写入缓存_cache![index][target] = getResult(index: index + 1, target: target);// 返回结果return _cache![index][target];}// 否则,尝试选择该元素,剩余目标数 = 当前目标数 - 当前元素let residue = target - _arr[index];// 继续后面的元素,并传入剩余目标数,表示选择该元素let reslut = getResult(index: index + 1, target: residue);// 判断选择该元素之后的结果if(reslut == 0){// 无法分割出相等的子集,证明该元素不符合加入子集的条件。跳过该元素,继续后面的元素// 将结果写入缓存_cache![index][target] = getResult(index: index + 1, target: target);// 返回结果return _cache![index][target];}// 可以分割出相等的子集// 将结果写入缓存_cache![index][target] = reslut;}// 返回结果return _cache![index][target];}}
2.3 递推解法
使用自底向上的递推法,创建一张集合大小 * (目标数 + 1)的表格。先解决分解后的子问题,从而解决整体问题
class Solution {fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}// 集合元素总和的一半,作为目标值fileprivate var _target : Int = 0;// 创建表格fileprivate var _list : [[Int]]?;func canPartition() -> Bool {// 集合中元素个数小于2个if(_arr.count < 2){// 不可能分割return false;}// 元素总和var sum = 0;// 最大元素var maxItem = 0;// 遍历集合中所有元素for i in _arr {// 累加总和sum += i;// 计算出最大元素maxItem = max(i, maxItem);}// 判断总和为奇数if(sum % 2 == 1) {// 奇数,不可能分割出相等的子集return false;}// 目标的结果数,即:总和取半let target = sum / 2;// 判断最大元素是否超过目标数if(maxItem > target){// 超过,不可能分割出相等的子集return false;}// 初始化表格大小_list = [[Int]](repeating: [Int](repeating: 0, count: target + 1), count: _arr.count);// 是否可分割出等和的子集let reslut = getResult(target: target);// 返回结果return reslut == 1;}// 是否可分割出等和的子集func getResult(target : Int) -> Int {// 设置初始列默认为1,当剩余目标数减到0,说明该元素可选取for i in 0..<_arr.count {_list![i][0] = 1;}// 第0行,只有初始元素可选取_list![0][_arr[0]] = 1;// 遍历元素for i in 1..<_arr.count {// 获取当前元素let item = _arr[i];// 遍历目标数for j in 1...target {// 判断当前元素是否超过剩余的目标数if(item > j){// 超过,证明该元素不符合加入子集的条件。跳过该元素,使用上一个元素的结果覆盖当前位置_list![i][j] = _list![i - 1][j];continue;}// 否则,尝试选择该元素,剩余目标数 = 当前目标数 - 当前元素let residue = j - item;// 如果选择当前元素,在剩余目标数的情况下,获得上一个元素的结果let reslut = _list![i - 1][residue];// 判断上一个元素是否可以将剩余目标数减空if(reslut == 0){// 无法减空,证明该元素不符合加入子集的条件。跳过该元素,使用上一个元素的结果覆盖当前位置_list![i][j] = _list![i - 1][j];continue;}// 可以减空,表示该元素可以选择,它是分割相等子集的必要元素// 将结果写入表格_list![i][j] = reslut;}}// 返回最终的计算结果return _list![_arr.count - 1][target];}}
2.4 空间优化
当前结果,只依赖于上一个元素的结果。我们可以只创建一行空间,让当前结果与上一个元素的结果共用,将空间复杂度优化为O(n)。为了不影响上一个元素的结果,目标数采用倒序遍历
class Solution {fileprivate var _arr : [Int];init(arr : [Int]) {_arr = arr;}// 集合元素总和的一半,作为目标值fileprivate var _target : Int = 0;// 当前行,当前结果与上一个元素的结果共用fileprivate var _row : [Int]?;func canPartition() -> Bool {// 集合中元素个数小于2个if(_arr.count < 2){// 不可能分割return false;}// 元素总和var sum = 0;// 最大元素var maxItem = 0;// 遍历集合中所有元素for i in _arr {// 累加总和sum += i;// 计算出最大元素maxItem = max(i, maxItem);}// 判断总和为奇数if(sum % 2 == 1) {// 奇数,不可能分割出相等的子集return false;}// 目标的结果数,即:总和取半let target = sum / 2;// 判断最大元素是否超过目标数if(maxItem > target){// 超过,不可能分割出相等的子集return false;}// 初始化当前行_row = [Int](repeating: 0, count: target + 1);// 是否可分割出等和的子集let reslut = getResult(target: target);// 返回结果return reslut == 1;}// 是否可分割出等和的子集func getResult(target : Int) -> Int {// 当剩余目标数减到0,说明该元素可选取_row![0] = 1;// 默认只有初始元素可选取_row![_arr[0]] = 1;// 遍历元素for i in 1..<_arr.count {// 获取当前元素let item = _arr[i];// 为了不影响上一个元素的结果,倒序遍历目标数for j in (1...target).reversed() {// 判断当前元素是否超过剩余的目标数if(item > j){// 超过,证明该元素不符合加入子集的条件。跳过该元素,延用上一个元素的结果continue;}// 否则,尝试选择该元素,剩余目标数 = 当前目标数 - 当前元素let residue = j - item;// 如果选择当前元素,在剩余目标数的情况下,获得上一个元素的结果let reslut = _row![residue];// 判断上一个元素是否可以将剩余目标数减空if(reslut == 0){// 无法减空,证明该元素不符合加入子集的条件。跳过该元素,延用上一个元素的结果continue;}// 可以减空,表示该元素可以选择,它是分割相等子集的必要元素// 将结果覆盖_row![j] = reslut;}}// 返回最终的计算结果return _row![target];}}
