力扣加加 - 努力做西湖区最好的算法题解
  • introduction
  • 第一章 - 算法专题
    • 数据结构
    • 链表专题
    • 树专题
    • 堆专题(上)
    • 堆专题(下)
    • 二分专题(上)
    • 二分专题(下)
    • 动态规划(重置版)
    • 大话搜索
    • 二叉树的遍历
    • 哈夫曼编码和游程编码
    • 布隆过滤器
    • 前缀树
    • 回溯
    • 滑动窗口(思路 + 模板)
    • 位运算
    • 小岛问题
    • 最大公约数
    • 并查集
    • 平衡二叉树专题
    • 蓄水池抽样
    • 单调栈
  • 第二章 - 91 天学算法
    • 91 天学算法第三期视频会议总结
    • 第一期讲义-二分法
    • 第一期讲义-双指针
    • 第三期正在火热进行中
  • 第三章 - 精选题解
    • 字典序列删除
    • 西法的刷题秘籍】一次搞定前缀和
    • 字节跳动的算法面试题是什么难度?
    • 字节跳动的算法面试题是什么难度?(第二弹)
    • 《我是你的妈妈呀》 * 第一期
    • 一文带你看懂二叉树的序列化
    • 穿上衣服我就不认识你了?来聊聊最长上升子序列
    • 你的衣服我扒了 * 《最长公共子序列》
    • 一文看懂《最大子序列和问题》
  • 第四章 - 高频考题(简单)
    • 面试题 17.12. BiNode
    • 0001. 两数之和
    • 0020. 有效的括号
    • 0021. 合并两个有序链表
    • 0026. 删除排序数组中的重复项
    • 0053. 最大子序和
    • 0160. 相交链表
    • 0066. 加一
    • 0088. 合并两个有序数组
    • 0101. 对称二叉树
    • 0104. 二叉树的最大深度
    • 0108. 将有序数组转换为二叉搜索树
    • 0121. 买卖股票的最佳时机
    • 0122. 买卖股票的最佳时机 II
    • 0125. 验证回文串
    • 0136. 只出现一次的数字
    • 0155. 最小栈
    • 0167. 两数之和 II 输入有序数组
    • 0169. 多数元素
    • 0172. 阶乘后的零
    • 0190. 颠倒二进制位
    • 0191. 位 1 的个数
    • 0198. 打家劫舍
    • 0203. 移除链表元素
    • 0206. 反转链表
    • 0219. 存在重复元素 II
    • 0226. 翻转二叉树
    • 0232. 用栈实现队列
    • 0263. 丑数
    • 0283. 移动零
    • 0342. 4 的幂
    • 0349. 两个数组的交集
    • 0371. 两整数之和
    • 401. 二进制手表
    • 0437. 路径总和 III
    • 0455. 分发饼干
    • 0504. 七进制数
    • 0575. 分糖果
    • 0665. 非递减数列
    • 0661. 图片平滑器
    • 821. 字符的最短距离
    • 0874. 模拟行走机器人
    • 1128. 等价多米诺骨牌对的数量
    • 1260. 二维网格迁移
    • 1332. 删除回文子序列
    • 2591. 将钱分给最多的儿童
  • 第五章 - 高频考题(中等)
    • 面试题 17.09. 第 k 个数
    • 面试题 17.23. 最大黑方阵
    • 面试题 16.16. 部分排序
    • Increasing Digits
    • Longest Contiguously Strictly Increasing Sublist After Deletion
    • Consecutive Wins
    • Number of Substrings with Single Character Difference
    • Bus Fare
    • Minimum Dropping Path Sum
    • Every Sublist Min Sum
    • Maximize the Number of Equivalent Pairs After Swaps
    • 0002. 两数相加
    • 0003. 无重复字符的最长子串
    • 0005. 最长回文子串
    • 0011. 盛最多水的容器
    • 0015. 三数之和
    • 0017. 电话号码的字母组合
    • 0019. 删除链表的倒数第 N 个节点
    • 0022. 括号生成
    • 0024. 两两交换链表中的节点
    • 0029. 两数相除
    • 0031. 下一个排列
    • 0033. 搜索旋转排序数组
    • 0039. 组合总和
    • 0040. 组合总和 II
    • 0046. 全排列
    • 0047. 全排列 II
    • 0048. 旋转图像
    • 0049. 字母异位词分组
    • 0050. Pow(x, n)
    • 0055. 跳跃游戏
    • 0056. 合并区间
    • 0060. 第 k 个排列
    • 0061. 旋转链表
    • 0062. 不同路径
    • 0073. 矩阵置零
    • 0075. 颜色分类
    • 0078. 子集
    • 0079. 单词搜索
    • 0080. 删除排序数组中的重复项 II
    • 0086. 分隔链表
    • 0090. 子集 II
    • 0091. 解码方法
    • 0092. 反转链表 II
    • 0094. 二叉树的中序遍历
    • 0095. 不同的二叉搜索树 II
    • 0096. 不同的二叉搜索树
    • 0098. 验证二叉搜索树
    • 0102. 二叉树的层序遍历
    • 0103. 二叉树的锯齿形层次遍历
    • 0113. 路径总和 II
    • 0129. 求根到叶子节点数字之和
    • 0130. 被围绕的区域
    • 0131. 分割回文串
    • 0139. 单词拆分
    • 0144. 二叉树的前序遍历
    • 0147. 对链表进行插入排序
    • 0150. 逆波兰表达式求值
    • 0152. 乘积最大子数组
    • 0153. 寻找旋转排序数组中的最小值
    • 0199. 二叉树的右视图
    • 0200. 岛屿数量
    • 0201. 数字范围按位与
    • 0208. 实现 Trie (前缀树)
    • 0209. 长度最小的子数组
    • 0211. 添加与搜索单词 - 数据结构设计
    • 0215. 数组中的第 K 个最大元素
    • 0220. 存在重复元素 III
    • 0221. 最大正方形
    • 0227. 基本计算器 II
    • 0229. 求众数 II
    • 0230. 二叉搜索树中第 K 小的元素
    • 0236. 二叉树的最近公共祖先
    • 0238. 除自身以外数组的乘积
    • 0240. 搜索二维矩阵 II
    • 0279. 完全平方数
    • 0309. 最佳买卖股票时机含冷冻期
    • 0322. 零钱兑换
    • 0324. 摆动排序 II
    • 0328. 奇偶链表
    • 0331. 验证二叉树的前序序列化
    • 0334. 递增的三元子序列
    • 0337. 打家劫舍 III
    • 0343. 整数拆分
    • 0365. 水壶问题
    • 0378. 有序矩阵中第 K 小的元素
    • 0380. 常数时间插入、删除和获取随机元素
    • 0394. 字符串解码
    • 0416. 分割等和子集
    • 0424. 替换后的最长重复字符
    • 0438. 找到字符串中所有字母异位词
    • 0445. 两数相加 II
    • 0454. 四数相加 II
    • 0456. 132 模式
    • 0457.457. 环形数组是否存在循环
    • 0464. 我能赢么
    • 0470. 用 Rand7() 实现 Rand10
    • 0473. 火柴拼正方形
    • 0494. 目标和
    • 0516. 最长回文子序列
    • 0513. 找树左下角的值
    • 0518. 零钱兑换 II
    • 0525. 连续数组
    • 0547. 朋友圈
    • 0560. 和为 K 的子数组
    • 0609. 在系统中查找重复文件
    • 0611. 有效三角形的个数
    • 0673. 最长递增子序列的个数
    • 0686. 重复叠加字符串匹配
    • 0710. 黑名单中的随机数
    • 0714. 买卖股票的最佳时机含手续费
    • 0718. 最长重复子数组
    • 0735. 行星碰撞
    • 0754. 到达终点数字
    • 0785. 判断二分图
    • 0790. 多米诺和托米诺平铺
    • 0799. 香槟塔
    • 0801. 使序列递增的最小交换次数
    • 0816. 模糊坐标
    • 0820. 单词的压缩编码
    • 0838. 推多米诺
    • 0873. 最长的斐波那契子序列的长度
    • 0875. 爱吃香蕉的珂珂
    • 0877. 石子游戏
    • 0886. 可能的二分法
    • 0898. 子数组按位或操作
    • 0900. RLE 迭代器
    • 0911. 在线选举
    • 0912. 排序数组
    • 0932. 漂亮数组
    • 0935. 骑士拨号器
    • 0947. 移除最多的同行或同列石头
    • 0959. 由斜杠划分区域
    • 0978. 最长湍流子数组
    • 0987. 二叉树的垂序遍历
    • 1004. 最大连续 1 的个数 III
    • 1011. 在 D 天内送达包裹的能力
    • 1014. 最佳观光组合
    • 1015. 可被 K 整除的最小整数
    • 1019. 链表中的下一个更大节点
    • 1020. 飞地的数量
    • 1023. 驼峰式匹配
    • 1031. 两个非重叠子数组的最大和
    • 1043. 分隔数组以得到最大和
    • 1053. 交换一次的先前排列)
    • 1104. 二叉树寻路
    • 1129. 颜色交替的最短路径
    • 1131.绝对值表达式的最大值
    • 1138. 字母板上的路径
    • 1186. 删除一次得到子数组最大和
    • 1218. 最长定差子序列
    • 1227. 飞机座位分配概率
    • 1261. 在受污染的二叉树中查找元素
    • 1262. 可被三整除的最大和
    • 1297. 子串的最大出现次数
    • 1310. 子数组异或查询
    • 1334. 阈值距离内邻居最少的城市
    • 1371.每个元音包含偶数次的最长子字符串
    • 1381. 设计一个支持增量操作的栈
    • 1438. 绝对差不超过限制的最长连续子数组
    • 1558. 得到目标数组的最少函数调用次数
    • 1574. 删除最短的子数组使剩余数组有序
    • 1631. 最小体力消耗路径
    • 1638. 统计只差一个字符的子串数目
    • 1658. 将 x 减到 0 的最小操作数
    • 1697. 检查边长度限制的路径是否存在
    • 1737. 满足三条件之一需改变的最少字符数
    • 1770. 执行乘法运算的最大分数
    • 1793. 好子数组的最大分数
    • 1834. 单线程 CPU
    • 1899. 合并若干三元组以形成目标三元组
    • 1904. 你完成的完整对局数
    • 1906. 查询差绝对值的最小值
    • 1906. 查询差绝对值的最小值
    • 2007. 从双倍数组中还原原数组
    • 2008. 出租车的最大盈利
    • 2100. 适合打劫银行的日子
    • 2101. 引爆最多的炸弹
    • 2121. 相同元素的间隔之和
    • 2207. 字符串中最多数目的子字符串
    • 2592. 最大化数组的伟大值
    • 2593. 标记所有元素后数组的分数
    • 2817. 限制条件下元素之间的最小绝对差
    • 2865. 美丽塔 I
    • 2866. 美丽塔 II
    • 2939. 最大异或乘积
    • 3377. 使两个整数相等的数位操作
    • 3404. 统计特殊子序列的数目
    • 3428. 至多 K 个子序列的最大和最小和
  • 第六章 - 高频考题(困难)
    • LCP 20. 快速公交
    • LCP 21. 追逐游戏
    • Number Stream to Intervals
    • Triple-Inversion
    • Kth-Pair-Distance
    • Minimum-Light-Radius
    • Largest Equivalent Set of Pairs
    • Ticket-Order.md
    • Connected-Road-to-Destination
    • 0004. 寻找两个正序数组的中位数
    • 0023. 合并 K 个升序链表
    • 0025. K 个一组翻转链表
    • 0030. 串联所有单词的子串
    • 0032. 最长有效括号
    • 0042. 接雨水
    • 0052. N 皇后 II
    • 0057. 插入区间
    • 0065. 有效数字
    • 0084. 柱状图中最大的矩形
    • 0085. 最大矩形
    • 0087. 扰乱字符串
    • 0124. 二叉树中的最大路径和
    • 0128. 最长连续序列
    • 0132. 分割回文串 II
    • 0140. 单词拆分 II
    • 0145. 二叉树的后序遍历
    • 0146. LRU 缓存机制
    • 0154. 寻找旋转排序数组中的最小值 II
    • 0212. 单词搜索 II
    • 0239. 滑动窗口最大值
    • 0295. 数据流的中位数
    • 0297. 二叉树的序列化与反序列化
    • 0301. 删除无效的括号
    • 0312. 戳气球
    • 330. 按要求补齐数组
    • 0335. 路径交叉
    • 0460. LFU 缓存
    • 0472. 连接词
    • 0480. 滑动窗口中位数
    • 0483. 最小好进制
    • 0488. 祖玛游戏
    • 0493. 翻转对
    • 0664. 奇怪的打印机
    • 0679. 24 点游戏
    • 0715. Range 模块
    • 0726. 原子的数量
    • 0768. 最多能完成排序的块 II
    • 0805. 数组的均值分割
    • 0839. 相似字符串组
    • 0887. 鸡蛋掉落
    • 0895. 最大频率栈
    • 0975. 奇偶跳
    • 0995. K 连续位的最小翻转次数
    • 1032. 字符流
    • 1168. 水资源分配优化
    • 1178. 猜字谜
    • 1203. 项目管理
    • 1255. 得分最高的单词集合
    • 1345. 跳跃游戏 IV
    • 1449. 数位成本和为目标值的最大数字
    • 1494. 并行课程 II
    • 1521. 找到最接近目标值的函数值
    • 1526. 形成目标数组的子数组最少增加次数
    • 1639. 通过给定词典构造目标字符串的方案数
    • 1649. 通过指令创建有序数组
    • 1671. 得到山形数组的最少删除次数
    • 1707. 与数组中元素的最大异或值
    • 1713. 得到子序列的最少操作次数
    • 1723. 完成所有工作的最短时间
    • 1787. 使所有区间的异或结果为零
    • 1835. 所有数对按位与结果的异或和
    • 1871. 跳跃游戏 VII
    • 1872. 石子游戏 VIII
    • 1883. 准时抵达会议现场的最小跳过休息次数
    • 1970. 你能穿过矩阵的最后一天
    • 2009. 使数组连续的最少操作数
    • 2025. 分割数组的最多方案数
    • 2030. 含特定字母的最小子序列
    • 2102. 序列顺序查询
    • 2141. 同时运行 N 台电脑的最长时间
    • 2179. 统计数组中好三元组数目 👍
    • 2209. 用地毯覆盖后的最少白色砖块
    • 2281.sum-of-total-strength-of-wizards
    • 2306. 公司命名
    • 2312. 卖木头块
    • 2842. 统计一个字符串的 k 子序列美丽值最大的数目
    • 2972. 统计移除递增子数组的数目 II
    • 3027. 人员站位的方案数 II
    • 3041. 修改数组后最大化数组中的连续元素数目
    • 3082. 求出所有子序列的能量和
    • 3108. 带权图里旅途的最小代价
    • 3347. 执行操作后元素的最高频率 II
    • 3336. 最大公约数相等的子序列数量
    • 3410. 删除所有值为某个元素后的最大子数组和
  • 后序
由 GitBook 提供支持
在本页
  • 300. 最长上升子序列
  • 题目地址
  • 题目描述
  • 思路
  • 代码
  • 435. 无重叠区间
  • 题目地址
  • 题目描述
  • 思路
  • 代码
  • 646. 最长数对链
  • 题目地址
  • 题目描述
  • 思路
  • 代码
  • 452. 用最少数量的箭引爆气球
  • 题目地址
  • 题目描述
  • 思路
  • 代码
  • 优化
  • More

这有帮助吗?

  1. 第三章 - 精选题解

穿上衣服我就不认识你了?来聊聊最长上升子序列

最长上升子序列是一个很经典的算法题。有的会直接让你求最长上升子序列,有的则会换个说法,但最终考察的还是最长上升子序列。那么问题来了,它穿上衣服你还看得出来是么?

如果你完全看不出来了,说明抽象思维还不到火候。经常看我的题解的同学应该会知道,我经常强调抽象思维。没有抽象思维,所有的题目对你来说都是新题。你无法将之前做题的经验迁移到这道题,那你做的题意义何在?

虽然抽象思维很难练成,但是幸好算法套路是有限的,经常考察的题型更是有限的。从这些入手,或许可以让你轻松一些。本文就从一个经典到不行的题型《最长上升子序列》,来帮你进一步理解抽象思维。

注意。 本文是帮助你识别套路,从横向上理清解题的思维框架,并没有采用最优解,所有的题目给的解法都不是最优的,但是都可以通过所有的测试用例。如果你想看最优解,可以直接去讨论区看。或者期待我的深入剖析系列。

300. 最长上升子序列

题目地址

https://leetcode-cn.com/problems/longest-increasing-subsequence

题目描述

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

思路

美团和华为都考了这个题。

题目的意思是让我们从给定数组中挑选若干数字,这些数字满足: 如果 i < j 则 nums[i] < nums[j]。问:一次可以挑选最多满足条件的数字是多少个。

这种子序列求极值的题目,应该要考虑到贪心或者动态规划。这道题贪心是不可以的,我们考虑动态规划。

按照动态规划定义状态的套路,我们有两种常见的定义状态的方式:

  • dp[i] : 以 i 结尾(一定包括 i)所能形成的最长上升子序列长度, 答案是 max(dp[i]),其中 i = 0,1,2, ..., n - 1

  • dp[i] : 以 i 结尾(可能包括 i)所能形成的最长上升子序列长度,答案是 dp[-1] (-1 表示最后一个元素)

容易看出第二种定义方式由于无需比较不同的 dp[i] 就可以获得答案,因此更加方便。但是想了下,状态转移方程会很不好写,因为 dp[i] 的末尾数字(最大的)可能是 任意 j < i 的位置。

第一种定义方式虽然需要比较不同的 dp[i] 从而获得结果,但是我们可以在循环的时候顺便得出,对复杂度不会有影响,只是代码多了一点而已。因此我们选择第一种建模方式。

由于 dp[j] 中一定会包括 j,且以 j 结尾, 那么 nums[j] 一定是其所形成的序列中最大的元素,那么如果位于其后(意味着 i > j)的 nums[i] > nums[j],那么 nums[i] 一定能够融入 dp[j] 从而形成更大的序列,这个序列的长度是 dp[j] + 1。因此状态转移方程就有了:dp[i] = dp[j] + 1 (其中 i > j, nums[i] > nums[j])

以 [10, 9, 2, 5, 3, 7, 101, 18] 为例,当我们计算到 dp[5]的时候,我们需要往回和 0,1,2,3,4 进行比较。

具体的比较内容是:

最后从三个中选一个最大的 + 1 赋给 dp[5]即可。

记住这个状态转移方程,后面我们还会频繁用到。

代码

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0: return 0
        dp = [1] * n
        ans = 1
        for i in range(n):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
                    ans = max(ans, dp[i])
        return  ans

复杂度分析

  • 时间复杂度:$O(N ^ 2)$

  • 空间复杂度:$O(N)$

435. 无重叠区间

题目地址

https://leetcode-cn.com/problems/non-overlapping-intervals/

题目描述

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意:

可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:

输入: [ [1,2], [2,3], [3,4], [1,3] ]

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:

输入: [ [1,2], [1,2], [1,2] ]

输出: 2

解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:

输入: [ [1,2], [2,3] ]

输出: 0

解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

思路

我们先来看下最终剩下的区间。由于剩下的区间都是不重叠的,因此剩下的相邻区间的后一个区间的开始时间一定是不小于前一个区间的结束时间的。 比如我们剩下的区间是[ [1,2], [2,3], [3,4] ]。就是第一个区间的 2 小于等于 第二个区间的 2,第二个区间的 3 小于等于第三个区间的 3。

不难发现如果我们将前面区间的结束和后面区间的开始结合起来看,其就是一个非严格递增序列。而我们的目标就是删除若干区间,从而剩下最长的非严格递增子序列。这不就是上面的题么?只不过上面是严格递增,这不重要,就是改个符号的事情。 上面的题你可以看成是删除了若干数字,然后剩下最长的严格递增子序列。 这就是抽象的力量,这就是套路。

如果对区间按照起点或者终点进行排序,那么就转化为上面的最长递增子序列问题了。和上面问题不同的是,由于是一个区间。因此实际上,我们是需要拿后面的开始时间和前面的结束时间进行比较。

而由于:

  • 题目求的是需要移除的区间,因此最后 return 的时候需要做一个转化。

  • 题目不是要求严格递增,而是可以相等,因此我们的判断条件要加上等号。

这道题还有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。

代码

你看代码多像

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        n = len(intervals)
        if n == 0: return 0
        dp = [1] * n
        ans = 1
        intervals.sort(key=lambda a: a[0])

        for i in range(len(intervals)):
            for j in range(i - 1, -1, -1):
                if intervals[i][0] >= intervals[j][1]:
                    dp[i] = max(dp[i], dp[j] + 1)
                    break # 由于是按照开始时间排序的, 因此可以剪枝

        return n - max(dp)

复杂度分析

  • 时间复杂度:$O(N ^ 2)$

  • 空间复杂度:$O(N)$

646. 最长数对链

题目地址

https://leetcode-cn.com/problems/maximum-length-of-pair-chain/

题目描述

给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。

给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 :

输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]
注意:

给出数对的个数在 [1, 1000] 范围内。

思路

和上面的435. 无重叠区间是换皮题,唯一的区别这里又变成了严格增加。没关系,我们把等号去掉就行了。并且由于这道题求解的是最长的长度,因此转化也不需要了。

当然,这道题也有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。

代码

这代码更像了!

class Solution:
    def findLongestChain(self, intervals: List[List[int]]) -> int:
        n = len(intervals)
        if n == 0: return 0
        dp = [1] * n
        ans = 1
        intervals.sort(key=lambda a: a[0])

        for i in range(len(intervals)):
            for j in range(i - 1, -1, -1):
                if intervals[i][0] > intervals[j][1]:
                    dp[i] = max(dp[i], dp[j] + 1)
                    break # 由于是按照开始时间排序的, 因此可以剪枝

        return max(dp)

复杂度分析

  • 时间复杂度:$O(N ^ 2)$

  • 空间复杂度:$O(N)$

452. 用最少数量的箭引爆气球

题目地址

https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/

题目描述

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。

一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

Example:

输入:
[[10,16], [2,8], [1,6], [7,12]]

输出:
2

解释:
对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。

思路

把气球看成区间,几个箭可以全部射爆,意思就是有多少不重叠的区间。注意这里重叠的情况也可以射爆。这么一抽象,就和上面的646. 最长数对链一模一样了,不用我多说了吧?

当然,这道题也有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。

代码

代码像不像?

class Solution:
    def findMinArrowShots(self, intervals: List[List[int]]) -> int:
        n = len(intervals)
        if n == 0: return 0
        dp = [1] * n
        ans = 1
        intervals.sort(key=lambda a: a[0])

        for i in range(len(intervals)):
            for j in range(i - 1, -1, -1):
                if intervals[i][0] > intervals[j][1]:
                    dp[i] = max(dp[i], dp[j] + 1)
                    break # 由于是按照开始时间排序的, 因此可以剪枝

        return max(dp)

复杂度分析

  • 时间复杂度:$O(N ^ 2)$

  • 空间复杂度:$O(N)$

优化

大家想看效率高的,其实也不难。 LIS 也可以用 贪心 + 二分 达到不错的效率。代码如下:

代码文字版如下:

class Solution:
    def lengthOfLIS(self, A: List[int]) -> int:
        d = []
        for a in A:
            i = bisect.bisect_left(d, a)
            if i < len(d):
                d[i] = a
            elif not d or d[-1] < a:
                d.append(a)
        return len(d)

如果求最长不递减子序列呢?

我们只需要将最左插入改为最右插入即可。代码:

class Solution:
    def lengthOfLIS(self, A: List[int]) -> int:
        d = []
        for a in A:
            # 这里改为最右
            i = bisect.bisect(d, a)
            if i < len(d):
                d[i] = a
            # 这里改为小于等号
            elif not d or d[-1] <= a:
                d.append(a)
        return len(d)

最左插入和最右插入分不清的可以看看我的二分专题。

也可以这么写,更简单一点:

def LIS(A):
    d = []
    for a in A:
        # 如果求要严格递增就改为最左插入 bisect_left 即可
        i = bisect.bisect(d, a)
        if i == len(d):
            d.append(a)
        elif d[i] != a:
            d[i] = a
    return len(d)

More

其他的我就不一一说了。

最后推荐两道题大家练习一下,别看它们是 hard, 其实掌握了我这篇文章的内容一点都不难。

参考代码:

class Solution:
    def pileBox(self, box: List[List[int]]) -> int:
        box = sorted(box, key=sorted)
        n = len(box)
        dp = [0 if i == 0 else box[i - 1][2] for i in range(n + 1)]
        ans = max(dp)

        for i in range(1, n + 1):
            for j in range(i + 1, n + 1):
                if box[j - 1][0] > box[i - 1][0] and box[j - 1][1] > box[i - 1][1] and box[j - 1][2] > box[i - 1][2]:
                    dp[j] = max(dp[j], dp[i] + box[j - 1][2])
                    ans = max(ans , dp[j])
        return ans

参考代码:

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        if not envelopes: return 0
        n = len(envelopes)
        dp = [1] * n
        envelopes.sort()
        for i in range(n):
            for j in range(i + 1, n):
                if envelopes[i][0] < envelopes[j][0] and envelopes[i][1] < envelopes[j][1]:
                    dp[j] = max(dp[j], dp[i] + 1)
        return max(dp)

参考代码:

class Solution:
    def minDeletionSize(self, A):
        keep = 1
        m, n = len(A), len(A[0])
        dp = [1] * n
        for j in range(n):
            for k in range(j + 1, n):
                if all([A[i][k] >= A[i][j] for i in range(m)]):
                    dp[k] = max(dp[k], dp[j] + 1)
                    keep = max(keep, dp[k])
        return n - keep

小任务:请尝试使用贪心在 NlogN 的时间内完成算法。(参考我上面的代码就行)

由于这道题数据范围是 $10^5$,因此只能使用 $NlogN$ 的贪心才行。

参考代码:

class Solution:
    def minOperations(self, target: List[int], A: List[int]) -> int:
        def LIS(A):
            d = []
            for a in A:
                i = bisect.bisect_left(d, a)
                if d and i < len(d):
                    d[i] = a
                else:
                    d.append(a)
            return len(d)
        B = []
        target = { t:i for i, t in enumerate(target)}
        for a in A:
            if a in target: B.append(target[a])
        return len(target) - LIS(B)

不就是先排下序,然后求 scores 的最长上升子序列么?

参考代码:

class Solution:
    def bestTeamScore(self, scores: List[int], ages: List[int]) -> int:
        n = len(scores)
        persons = list(zip(ages, scores))
        persons.sort(key=lambda x : (x[0], x[1]))
        dp = [persons[i][1] for i in range(n)]
        for i in range(n):
            for j in range(i):
                if persons[i][1] >= persons[j][1]:
                    dp[i] = max(dp[i], dp[j]+persons[i][1])
        return max(dp)

参考代码:

class Solution:
    def solve(self, nums):
        n = len(nums)
        ans = 1
        def LIS(A):
            d = []
            for a in A:
                i = bisect.bisect_left(d,a)
                if i == len(d): d.append(a)
                else: d[i] = a
            return len(d)
        nums += nums
        for i in range(n):
            ans = max(ans , LIS(nums[i:i+n]))
        return ans

大家把我讲的思路搞懂,这几个题一写,还怕碰到类似的题不会么?只有熟练掌握基础的数据结构与算法,才能对复杂问题迎刃有余。 最长上升子序列就是一个非常经典的基础算法,把它彻底搞懂,再去面对出题人的各种换皮就不怕了。相反,如果你不去思考题目背后的逻辑,就会刷地很痛苦。题目稍微一变化你就不会了,这也是为什么很多人说刷了很多题,但是碰到新的题目还是不会做的原因之一。关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。

更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 38K star 啦。

上一页一文带你看懂二叉树的序列化下一页你的衣服我扒了 * 《最长公共子序列》

最后更新于1年前

这有帮助吗?

比如 (滴滴面试题)。 不就是求出最长序列,之后再循环比对一次就可以得出答案了么?

由于需要找到所有的递增子序列,因此动态规划就不行了,妥妥回溯就行了,套一个模板就出来了。回溯的模板可以看我之前写的。

关于为什么 10 ^ 5 就必须使用 $NlogN$ 甚至更优的算法我在提过。更多复杂度速查可参考我的刷题插件,公众号《力扣加加》回复插件获取即可。

再比如 无非就是加了一个条件,我们可以结合循环移位的技巧来做。

关于循环移位算法西法在之前的文章 也做了详细讲解,不再赘述。

673. 最长递增子序列的个数
491. 递增子序列
回溯专题
面试题 08.13. 堆箱子
354. 俄罗斯套娃信封问题
960. 删列造序 III
5644. 得到子序列的最少操作次数
刷题技巧
1626. 无矛盾的最佳球队
这道题
文科生都能看懂的循环移位算法