零、常用枚举技巧
§0.1 枚举右,维护左
对于 双变量问题,例如两数之和 ,可以枚举右边的 ,转化成 单变量问题,也就是在 左边查找是否有 ,这可以用哈希表维护。
我把这个技巧叫做 枚举右,维护左。
下面这些题目,如果可以,请用一次遍历实现。
§0.1.1 基础
- 1. 两数之和
- 1512. 好数对的数目 1161 相当于两数之差等于
- 2441. 与对应负数同时存在的最大正整数 1168 相当于两数之和等于
- 121. 买卖股票的最佳时机
- 2016. 增量元素之间的最大差值 1246
- 624. 数组列表中的最大距离
- 3880. 两个值之间的最小绝对差值 1257
- 2342. 数位和相等数对的最大和 1309
- 3979. 最大有效数对和 1328
- 1128. 等价多米诺骨牌对的数量 1333
- 1679. K 和数对的最大数目 1346
- 面试题 16.24. 数对和
- 219. 存在重复元素 II
- 2260. 必须拿起的最小连续卡牌数 1365
- 2001. 可互换矩形的组数 1436
- 2815. 数组中的最大数对和 非暴力做法
- 3623. 统计梯形的数目 I 1580
- 2364. 统计坏数对的数目 1622
- 3805. 统计凯撒加密对数目 1624
- 3371. 识别数组中的最大异常值 1644
- 3761. 镜像对之间最小绝对距离 1669
- 1014. 最佳观光组合 1730
- 1814. 统计一个数组中好对子的数目 1738
- 3584. 子序列首尾元素的最大乘积 1763
- 2905. 找出满足差值条件的下标 II 1764 思考题:求下标对的个数
- 3837. 相等元素的延迟计数(会员题)
- 3907. 统计具有相反奇偶性的较小元素(会员题)
§0.1.2 进阶
- 1010. 总持续时间可被 60 整除的歌曲
- 3185. 构成整天的下标对数目 II 同 1010 题
- 2748. 美丽下标对的数目
- 2506. 统计相似字符串对的数目
- 2874. 有序三元组中的最大值 II 1583
- 1497. 检查数组对是否可以被 k 整除 1787
- 1477. 找两个和为目标值且不重叠的子数组 1851
- 1031. 两个无重叠子数组的最大和
- 2555. 两个线段获得的最多奖品 2081
- 1995. 统计特殊四元组 四个数
- 3404. 统计特殊子序列的数目 2445 四个数
- 3267. 统计近似相等数对 II 2545
- 3480. 删除一个冲突对后最大子数组数目 2764
- 1214. 查找两棵二叉搜索树之和(会员题)
- 2964. 可被整除的三元组数量(会员题)
思维扩展:
- 3917. 统计下标的相反奇偶性得分 做到
- 2078. 两栋颜色不同且距离最远的房子 做到
- 454. 四数相加 II
- 220. 存在重复元素 III
- 3027. 人员站位的方案数 II 2020
- 3548. 等和矩阵分割 II 2245 代码复用
- 3713. 最长的平衡子串 I 非暴力写法
§0.2 枚举中间
对于有三个或者四个变量的问题,枚举中间的变量往往更好算。
为什么?比如问题有三个下标,需要满足 ,对比一下:
- 枚举 ,后续计算中还需保证 。枚举 同理。
- 枚举 ,那么 和 自动被 隔开,互相独立,后续计算中无需关心 和 的位置关系。
所以枚举中间的变量更简单。
§0.2.1 基础
- 3583. 统计特殊三元组 1510 也可以一次遍历
- 1930. 长度为 3 的不同回文子序列 1533
- 3128. 直角三角形 1541
§0.2.2 进阶
- 2909. 元素和最小的山形三元组 II 1479
- 2874. 有序三元组中的最大值 II 1583 也可以一次遍历
- 456. 132 模式
- 3067. 在带权树网络中统计可连接服务器对数目 1909
- 1534. 统计好三元组 做到
- 3455. 最短匹配子字符串 2303
- 2242. 节点序列的最大得分 2304
- 2867. 统计树中的合法路径数目 2428
- 2552. 统计上升四元组 2433 做法不止一种
- 3257. 放三个车的价值之和最大 II 2553
- 3073. 最大递增三元组(会员题)
§0.3 遍历对角线
- 3446. 按对角线进行矩阵排序 1373
- 2711. 对角线上不同值的数量差 1429
- 1329. 将矩阵按对角线排序 1548
- 498. 对角线遍历
- 面试题 17.23. 最大黑方阵 做到 ,难度约 2800
- 562. 矩阵中最长的连续1线段(会员题)
思维扩展:
- 1424. 对角线遍历 II 1780
一、前缀和
§1.1 基础
左闭右开公式:子数组 的元素和为 。把下标区间定义成左闭右开,就不需要加一减一了。
- 303. 区域和检索 - 数组不可变 模板题
- 3427. 变长子数组求和 做到
- 2559. 统计范围内的元音字符串数 1435
- 1310. 子数组异或查询 1460
- 3152. 特殊数组 II 1523
- 1749. 任意子数组和的绝对值的最大值 1542
- 53. 最大子数组和
- 3652. 按策略买卖股票的最佳时机 1557
- 3361. 两个字符串的切换距离
- 3511. 构造正数组(会员题)
- 3540. 访问所有房屋的最短时间(会员题)
思维扩展:
- 1523. 在区间范围内统计奇数数目 1209
- 848. 字母移位 1353
- 4044. 统计好循环移位的数量 1418
§1.2 前缀和与哈希表
通常要用到「枚举右,维护左」的技巧(见本题单 §0.1 节)。
- 560. 和为 K 的子数组 我的题解包含变形题
- 930. 和相同的二元子数组 1592 同 560 题
- 1524. 和为奇数的子数组数目 1611
- 974. 和可被 K 整除的子数组 1676
- 523. 连续的子数组和 类似 974 题
- 2588. 统计美丽子数组数目 1697
- 525. 连续数组 和 个数相同的最长子数组
- 面试题 17.05. 字母与数字 同 525 题
- 4063. 至多一次取反能被 K 整除的最长子数组 I 1740
- 3755. 最大平衡异或子数组的长度 pair
- 3026. 最大好子数组和 1817
- 1546. 和为目标值且不重叠的非空子数组的最大数目 1855
- 4058. 一个子数组循环移动后的最大脉冲值 1899
- 1124. 表现良好的最长时间段 1908
- 3728. 边界与内部和相等的稳定子数组 1909 pair 长度有下界
- 3381. 长度可被 K 整除的子数组的最大元素和 1943
- 2488. 统计中位数为 K 的子数组 1999
- 1590. 使数组和能被 P 整除 2039
- 2845. 统计趣味子数组的数目 2073
- 3739. 统计主要元素子数组数目 II 2090
- 3900. 一次交换后的最长平衡子串 2135
- 1074. 元素和为目标值的子矩阵数量 2189 二维版本的 560 题
- 1442. 形成两个异或相等数组的三元组数目 做到
- 3714. 最长的平衡子串 II 2202 pair
- 2025. 分割数组的最多方案数 2218
- 3729. 统计有序数组中可被 K 整除的子数组数量 2248 子数组去重 / 避免重复统计
- 4064. 至多一次取反能被 K 整除的最长子数组 II 2284
- 3969. 求和后首尾数字相同的有效子数组 I 非暴力做法
- 2949. 统计美丽子字符串 II 2445 pair
- 325. 和等于 k 的最长子数组长度(会员题)
- 548. 将数组分割成和相等的子数组(会员题)
- 1983. 范围和相等的最宽索引对(会员题)
- 2489. 固定比率的子字符串数(会员题)
- 2031. 1 比 0 多的子数组个数(会员题)做到
- 2950. 可整除子串的数量(会员题)做到 ,其中 。
前缀和与有序集合:
- 3364. 最小正和子数组 非暴力做法
- 363. 矩形区域不超过 K 的最大数值和
- 3739. 统计主要元素子数组数目 II 2090
- 4051. 统计遥远子数组的数目 2140
- 2031. 1 比 0 多的子数组个数(会员题)有 做法
思维扩展:
§1.3 距离和
- 1685. 有序数组中差绝对值之和 1496
- 2615. 等值距离和 1793
- 2602. 使数组元素全部相等的最少操作次数 1903
- 3937. 使数组变为模交替数组的最少操作次数 I 做到
- 2968. 执行操作使频率分数最大 2444
- 1703. 得到连续 K 个 1 的最少相邻交换次数 2467
- 3086. 拾起 K 个 1 需要的最少行动次数 2673
- 3422. 将子数组元素变为相等所需的最小操作数(会员题)
§1.4 状态压缩前缀和
推荐先阅读:从集合论到位运算,常见位运算技巧分类总结!
- 1177. 构建回文串检测 1848
- 1371. 每个元音包含偶数次的最长子字符串 2041
- 1542. 找出最长的超赞子字符串 2222
- 1915. 最美子字符串的数目 2235
- 2791. 树中可以形成回文的路径数 2677
§1.5 进阶
- 2389. 和有限的最长子序列 非暴力做法
- 3709. 设计考试分数记录器 1648
- 3919. 在下标间移动的最小代价 1777
- 1895. 最大的幻方 1781 斜向前缀和 可以优化到
- 2055. 蜡烛之间的盘子 1819
- 1744. 你能在你最喜欢的那天吃到你最喜欢的糖果吗? 1859
- 1878. 矩阵中最大的三个菱形和 1898 斜向前缀和
- 3756. 连接非零数字并乘以其数字和 II 1968
- 1031. 两个无重叠子数组的最大和 做到
- 2245. 转角路径的乘积中最多能有几个尾随零 2037
- 1712. 将数组分成三个子数组的方案数 2079
- 1862. 向下取整数对和 2170
- 3748. 统计稳定子数组的数目 2209 包含进阶问题
- 2281. 巫师的总力量和 2621
- 3445. 奇偶频次间的最大差值 II 2694
- 2983. 回文串重新排列查询 2780
- 2955. 同端子串的数量(会员题)
- 1788. 最大化花园的美观度(会员题)
- 2819. 购买巧克力后的最小相对损失(会员题)
思维扩展:
§1.6 二维前缀和
模板:
class NumMatrix:
def __init__(self, matrix: List[List[int]]):
m, n = len(matrix), len(matrix[0])
s = [[0] * (n + 1) for _ in range(m + 1)]
for i, row in enumerate(matrix):
for j, x in enumerate(row):
s[i + 1][j + 1] = s[i + 1][j] + s[i][j + 1] - s[i][j] + x
self.s = s
# 返回左上角在 (r1, c1),右下角在 (r2, c2) 的子矩阵元素和
def sumRegion(self, r1: int, c1: int, r2: int, c2: int) -> int:
s = self.s
return s[r2 + 1][c2 + 1] - s[r2 + 1][c1] - s[r1][c2 + 1] + s[r1][c1]- 304. 二维区域和检索 - 矩阵不可变
- 1314. 矩阵区域和 1484
- 3070. 元素和小于等于 k 的子矩阵的数目 1499
- 1738. 找出第 K 大的异或坐标值 1671
- 3212. 统计 X 和 Y 频数相等的子矩阵数量 1673
- 1292. 元素和小于等于阈值的正方形的最大边长 1735 可以优化到
二维前缀最小值:
- 3148. 矩阵中的最大得分 1820
二、差分
§2.1 一维差分
差分与前缀和的关系,类似导数与积分的关系。
数组 的差分的前缀和就是数组 (不变)。
§2.1.1 基础
- 2848. 与车相交的点 1230
- 1893. 检查是否区域内所有整数都被覆盖 1307
- 1854. 人口最多的年份 1370
- 面试题 16.10. 生存人数 同 1854 题
- 2960. 统计已测试设备 差分思想
- 1094. 拼车 1441
- 1109. 航班预订统计 1570
- 3964. 照亮道路的最少灯泡数 1572
- 3355. 零数组变换 I 1591
- 370. 区间加法(会员题)
§2.1.2 进阶
- 3914. 使数组非递减需要的最小累计值 1662 差分思想
- 3453. 分割正方形 I 1735
- 2381. 字母移位 II 1793
- 995. K 连续位的最小翻转次数 1835
- 1589. 所有排列中的最大和 1871
- 1526. 形成目标数组的子数组最少增加次数 1872 差分思想
- 1871. 跳跃游戏 VII 1896 做法不止一种
- 3356. 零数组变换 II 1913 做法不止一种
- 1943. 描述绘画结果 1969
- 3224. 使差值相等的最少数组改动次数 1996 做法不止一种
- 2327. 知道秘密的人数 做到
- 2251. 花期内花的数目 2022
- 2772. 使数组中的所有元素都等于零 2029
- 3229. 使数组等于目标数组所需的最少操作次数 2067 差分思想
- 3529. 统计水平子串和垂直子串重叠格子的数目 2105
- 798. 得分最高的最小轮调 2130
- 3347. 执行操作后元素的最高频率 II 2156
- 2528. 最大化城市的最小电量 2236
- 1674. 使数组互补的最少操作次数 2333
- 3362. 零数组变换 III 2424
- 3655. 区间乘法查询后的异或 II 2454 商分
- 3017. 按距离统计房屋对数目 II 2709
- 2021. 街上最亮的位置(会员题)
- 2015. 每段建筑物的平均高度(会员题)
- 2237. 计算街道上满足所需亮度的位置数量(会员题)
- 3009. 折线图上的最大交点数量(会员题)
- 3279. 活塞占据的最大总面积(会员题)
思维扩展:
- 56. 合并区间 做法见 我的评论
- 57. 插入区间
- 732. 我的日程安排表 III
- 2406. 将区间分为最少组数 1713
- 253. 会议室 II(会员题)
- 759. 员工空闲时间(会员题)
§2.2 二维差分
- 2536. 子矩阵元素加 1 1583
- 850. 矩形面积 II 2236 暴力做法
- 2132. 用邮票贴满网格图 2364
- LCP 74. 最强祝福力场
- 3888. 使所有网格元素相等的最小操作次数(会员题)
三、栈
§3.1 基础
- 1441. 用栈操作构建数组 1180
- 844. 比较含退格的字符串 1228
- 682. 棒球比赛
- 2390. 从字符串中移除星号 1348
- 1472. 设计浏览器历史记录 1454
- 946. 验证栈序列 1462
- 3412. 计算字符串的镜像分数 1578
- 71. 简化路径
§3.2 进阶
- 3170. 删除星号以后字典序最小的字符串 1772
- 155. 最小栈
- 1381. 设计一个支持增量操作的栈
- 636. 函数的独占时间
- 2434. 使用机器人打印字典序最小的字符串 1953
- 895. 最大频率栈 2028
- 1172. 餐盘栈 2110
- 2589. 完成所有任务的最少时间 2381 做法不止一种
- 2524. 子数组的最大频率分数(会员题)
- 716. 最大栈(会员题)
§3.3 邻项消除
- 2696. 删除子串后的字符串最小长度 1282
- 1047. 删除字符串中的所有相邻重复项 1286
- 1544. 整理字符串 1344
- 3561. 移除相邻字符 1397
- 1003. 检查替换后的词是否有效 1427
- 3834. 合并相邻且相等的元素 1429
- 3853. 合并靠近字符 1472
- 2216. 美化数组的最少删除数 1510
- 1209. 删除字符串中的所有相邻重复项 II 1542
- 3703. 移除K-平衡子字符串 1802 联系 1209 题
- 1717. 删除子字符串的最大得分 1868
- 4045. 统计机器人组数 1888
- 2197. 替换数组中的非互质数 2057
- 735. 小行星碰撞
- 2751. 机器人碰撞 2092 联系 735 题
- 853. 车队
- 1776. 车队 II 2531
§3.4 合法括号字符串(RBS)
注:部分题目可以不用栈,而是用一个数字记录嵌套深度。
- 20. 有效的括号
- 921. 使括号有效的最少添加 1242
- 1021. 删除最外层的括号 1311
- 1614. 括号的最大嵌套深度 1323
- 856. 括号的分数 1563
- 1249. 移除无效的括号 1657
- 1963. 使字符串平衡的最小交换次数 1689
- 1111. 有效括号的嵌套深度 1749
- 1541. 平衡括号字符串的最少插入次数 1759
- 1190. 反转每对括号间的子串 做到
- 2116. 判断一个括号字符串是否有效 2038 进阶问题:CF1709C
- 678. 有效的括号字符串 同 2116 题,可以
- 32. 最长有效括号
思维扩展:
- 2267. 检查是否有合法括号字符串路径 2085
§3.5 表达式解析
- 1006. 笨阶乘 1408 引入栈的思想
- 150. 逆波兰表达式求值
- 394. 字符串解码 用栈模拟递归
- 8. 字符串转换整数 (atoi) 为下面的题目热身
- 224. 基本计算器
- 227. 基本计算器 II
- 726. 原子的数量
- 1106. 解析布尔表达式 1880
- 591. 标签验证器
- 736. Lisp 语法解析
- 1096. 花括号展开 II 2349
- 1896. 反转表达式值的最少操作次数 2532
- 65. 有效数字
- 770. 基本计算器 IV 2863
- 439. 三元表达式解析器(会员题)
- 3749. 计算有效表达式(会员题)
- 772. 基本计算器 III(会员题)
- 1087. 花括号展开(会员题)
- 1597. 根据中缀表达式构造二叉表达式树(会员题)
- 1628. 设计带解析函数的表达式树(会员题)
§3.6 对顶栈
- 2296. 设计一个文本编辑器 1912 也可以用 Splay 树
§3.7 单调栈
见 单调栈题单。
四、队列
队列常用在 BFS 中,见 网格图题单 和 图论题单。与此相比,栈常用在 DFS 中,但无需我们手动维护。
§4.1 基础
- 933. 最近的请求次数 1338
- 3829. 设计共享出行系统 1594
- 950. 按递增顺序显示卡牌 1686
- 649. Dota2 参议院
- 346. 数据流中的移动平均值(会员题)
- 362. 敲击计数器(会员题)
- 3851. 不违反限制的最大请求数(会员题)
- 379. 电话目录管理系统(会员题)
- 1429. 第一个唯一数字(会员题)
- 2534. 通过门的时间(会员题)
§4.2 设计
§4.3 双端队列
- 2810. 故障键盘 做到
- 2071. 你可以安排的最多任务数目 2648
§4.4 单调队列
个人觉得叫单调双端队列更准确。
单调队列 = 滑动窗口 + 单调栈。必须先掌握滑动窗口和单调栈这两个知识点,再学单调队列。
问:入队、出队、更新答案,这三步的顺序如何思考?
答:有两种情况。如果更新答案时,用到的数据包含当前元素,那么就需要先入队,再更新答案;如果用到的数据不包含当前元素,那么就需要先更新答案,再入队。至于出队,一般写在前面,每遍历到一个新的元素,就看看队首元素是否失效(不满足要求),失效则弹出队首。
模板:
# 计算 nums 的每个长为 k 的窗口的最大值
# 时间复杂度 O(n),其中 n 是 nums 的长度
def maxSlidingWindow(nums: List[int], k: int) -> List[int]:
ans = [0] * (len(nums) - k + 1) # 窗口个数
q = deque() # 双端队列
for i, x in enumerate(nums):
# 1. 右边入
while q and nums[q[-1]] <= x:
q.pop() # 维护 q 的单调性
q.append(i) # 注意保存的是下标,这样下面可以判断队首是否离开窗口
# 2. 左边出
left = i - k + 1 # 窗口左端点
if q[0] < left: # 队首离开窗口
q.popleft()
# 3. 在窗口左端点处记录答案
if left >= 0:
# 由于队首到队尾单调递减,所以窗口最大值就在队首
ans[left] = nums[q[0]]
return ans- 239. 滑动窗口最大值
- LCR 184. 设计自助结算系统
- 1438. 绝对差不超过限制的最长连续子数组 1672
- 2762. 不间断子数组 同 1438 题
- 3835. 开销小于等于 K 的子数组数目 1759
- 2398. 预算内的最多机器人数目 1917
- 3589. 计数质数间隔平衡子数组 2235
- 862. 和至少为 K 的最短子数组 2307
- 1499. 满足不等式的最大值 2456
关于单调队列优化 DP,见 动态规划题单 的「§11.3 单调队列优化 DP」。
五、堆(优先队列)
§5.1 基础
- 1046. 最后一块石头的重量 1173
- 3264. K 次乘运算后的最终数组 I 1178
- 2558. 从数量最多的堆取走礼物 1277
- 2336. 无限集中的最小数字 1375
- 2530. 执行 K 次操作后的最大分数 1386
- 3066. 超过阈值的最少操作数 II 1400
- 1962. 移除石子使总数最小 1419
- 703. 数据流中的第 K 大元素 经典题
- 3275. 第 K 近障碍物查询 1420
- 1845. 座位预约管理系统 1429 你能做到复杂度与 无关吗?
- 2208. 将数组和减半的最少操作次数 1550
- 2233. K 次增加后的最大乘积 1686
- 3296. 移山所需的最少秒数 1695
- 1942. 最小未被占据椅子的编号 1695
- 1801. 积压订单中的订单总数 1711
- 2406. 将区间分为最少组数 1713 经典题
- 3478. 选出和最大的 K 个元素 1753
- 2462. 雇佣 K 位工人的总代价 1764
- 1834. 单线程 CPU 1798
- 1792. 最大平均通过率 1818
- 1167. 连接木棍的最低费用(会员题)
- 253. 会议室 II(会员题)经典题
§5.2 进阶
- 23. 合并 K 个升序链表
- 2931. 购买物品的最大开销 1822
- 3781. 二进制交换后的最大分数 1823 两种方法
- 502. IPO
- 1705. 吃苹果的最大数目 1930
- 778. 水位上升的泳池中游泳
- 1631. 最小体力消耗路径 1948
- 1882. 使用服务器处理任务 1979
- 1354. 多次求和构造目标数组 2015
- 1353. 最多可以参加的会议数目 2016
- 1235. 规划兼职工作 2023 做法不止一种
- 632. 最小区间 做法不止一种
- 2542. 最大子序列的分数 2056
- 1383. 最大的团队表现值 2091
- 2402. 会议室 III 2093
- 2503. 矩阵查询可获得的最大分数 2196
- 2163. 删除元素后和的最小差值 2225
- 857. 雇佣 K 名工人的最低成本 2260
- 1606. 找到处理最多请求的服务器 2276
- 1851. 包含每个查询的最小区间 2286
- 407. 接雨水 II
- 2940. 找到 Alice 和 Bob 可以相遇的建筑 2327
- 3399. 字符相同的最短子字符串 II 2376
- 3266. K 次乘运算后的最终数组 II 2509
- 1675. 数组的最小偏移量 2533
- 2617. 网格图中最少访问的格子数 2582
- 2532. 过桥的时间 2589
- LCP 33. 蓄水 思考:更快的做法
- 1500. 设计文件分享系统(会员题)
- 1199. 建造街区的最短时间(会员题)
- 3506. 查找消除细菌菌株所需时间(会员题)
有序集合:
- 1348. 推文计数 2037
- 855. 考场就座 2067
- 1912. 设计电影租借系统 2182
§5.3 第 K 小/大
部分题目也可以用二分解决。
- 264. 丑数 II
- 378. 有序矩阵中第 K 小的元素
- 23. 合并 K 个升序链表
- 373. 查找和最小的 K 对数字
- 1439. 有序矩阵中的第 k 个最小数组和 2134
- 786. 第 K 个最小的质数分数 2169
- 3691. 最大子数组总值 II 2469
- 2386. 找出数组的第 K 大和 2648
§5.4 重排元素
- 984. 不含 AAA 或 BBB 的字符串 1474 不需要堆,这题的目的是引入贪心思想
- 767. 重构字符串 1681
- 1054. 距离相等的条形码 1702
- 1405. 最长快乐字符串 1821
- 3081. 替换字符串中的问号使分数最小 1905
- 621. 任务调度器
- 358. K 距离间隔重排字符串(会员题)
§5.5 反悔堆
基于堆的反悔贪心。
- LCP 30. 魔塔游戏
- 1642. 可以到达的最远建筑 1962
- 630. 课程表 III
- 871. 最低加油次数 2074
- 3362. 零数组变换 III 2424 虽然没有反悔的过程,但思路和 871 是类似的
- 2813. 子序列最大优雅度 2582 也可以不用堆
- 1388. 3n 块披萨 做到
- 3892. 产生至少 K 个峰值的最少操作次数 做到
- 3049. 标记所有下标的最早秒数 II 3111
- 3711. 不出现负余额的最大交易额(会员题)
- 2599. 使前缀和数组非负(会员题)
§5.6 懒删除堆
支持删除堆中任意元素。
模板:
# 模板来源 https://leetcode.cn/discuss/post/3583665/
class LazyHeap:
def __init__(self):
self.heap = [] # 最小堆(最大堆可以把数字取反或重载 __lt__)
self.remove_cnt = defaultdict(int) # 每个元素剩余需要删除的次数
self.size = 0 # 堆的实际大小
# 删除
def remove(self, x: Any) -> None:
self.remove_cnt[x] += 1 # 懒删除
self.size -= 1
# 正式执行删除操作
def _apply_remove(self) -> None:
while self.heap and self.remove_cnt[self.heap[0]] > 0:
self.remove_cnt[self.heap[0]] -= 1
heappop(self.heap)
# 查看堆顶
def top(self) -> Any:
self._apply_remove()
return self.heap[0] # 真正的堆顶
# 出堆
def pop(self) -> Any:
self._apply_remove()
self.size -= 1
return heappop(self.heap)
# 入堆
def push(self, x: Any) -> None:
if self.remove_cnt[x] > 0:
self.remove_cnt[x] -= 1 # 抵消之前的删除
else:
heappush(self.heap, x)
self.size += 1- 2349. 设计数字容器系统 1540
- 3885. 设计事件管理器 1548
- 3607. 电网维护 1700
- 2353. 设计食物评分系统 1782
- 3092. 最高频率的 ID 1793
- 3408. 设计任务管理器 1807
- 2034. 股票价格波动 1832
- 3815. 设计拍卖系统 1854
- 1172. 餐盘栈 2110
- 218. 天际线问题 扫描线
- 3510. 移除最小数对使数组有序 II 2608
- 3672. 子数组中加权众数的总和(会员题)
- 3391. 设计一个高效的层跟踪三维二进制矩阵(会员题)
- 716. 最大栈(会员题)
§5.7 对顶堆(动态第 K 小/大)
部分题目需要结合懒删除堆。
做法不止一种,部分题目也可以用有序集合/值域树状数组等数据结构解决,见本题单「§8.1 树状数组」的模板二。
- 2102. 序列顺序查询 2159
- 295. 数据流的中位数
- 480. 滑动窗口中位数
- 2653. 滑动子数组的美丽值 非暴力做法
- 1825. 求出 MK 平均值 2396
- 3505. 使 K 个子数组内元素相等的最少操作数 2539 滑动窗口中位数的距离和
- 3013. 将数组分成最小总代价的子数组 II 2540
- 3321. 计算子数组的 x-sum II 2598
- 3962. 至多 K 次交换后最大子数组和 2672
- LCP 24. 数字游戏
- 3369. 设计数组统计跟踪器(会员题)
- 3422. 将子数组元素变为相等所需的最小操作数(会员题)
另见 图论题单 中的 Dijkstra 算法。
六、字典树(trie)
§6.1 基础
- 208. 实现 Trie (前缀树)
- 3597. 分割字符串 非暴力做法
- 3043. 最长公共前缀的长度 非暴力做法
- 648. 单词替换
- 720. 词典中最长的单词
- 2416. 字符串的前缀分数和 1725
- 677. 键值映射
- 1268. 搜索推荐系统
- 1233. 删除子文件夹
- 820. 单词的压缩编码
- 2261. 含最多 K 个可整除元素的子数组 做到
- 1804. 实现 Trie (前缀树) II(会员题)
- 2168. 每个数字的频率都相同的不同子字符串的数量(会员题)同 2261 题
§6.2 进阶
- 211. 添加与搜索单词 - 数据结构设计
- 676. 实现一个魔法字典
- 212. 单词搜索 II
- 3093. 最长公共后缀查询 2118
- 745. 前缀和后缀搜索
- 3045. 统计前后缀下标对 II 2328
- 336. 回文对
- 1948. 删除系统中的重复文件夹 2534
- 425. 单词方块(会员题)
- 527. 单词缩写(会员题)
- 588. 设计内存文件系统(会员题)
- 616. 给字符串添加加粗标签(会员题)
- 758. 字符串中的加粗单词(会员题)
- 642. 设计搜索自动补全系统(会员题)
- 1065. 字符串的索引对(会员题)
- 1166. 设计文件系统(会员题)
- 1858. 包含所有前缀的最长单词(会员题)
思维扩展:
§6.3 字典树优化 DP
- 139. 单词拆分
- 140. 单词拆分 II
- 面试题 17.13. 恢复空格
- 472. 连接词 约 2300
- 2977. 转换字符串的最小成本 II 2696
§6.4 0-1 字典树(异或字典树)
部分题目也可以用试填法解决。
- 421. 数组中两个数的最大异或值 约 2000
- 2935. 找出强数对的最大异或值 II 2349
- 3845. 最大子数组异或值
- 1707. 与数组中元素的最大异或值 2359
- 1803. 统计异或值在范围内的数对有多少 2479
- 1938. 查询最大基因差 2503
- 3632. 异或至少为 K 的子数组数目(会员题)
- 2479. 两个不重叠子树的最大异或值(会员题)
七、并查集
模板:
# 模板来源 https://leetcode.cn/discuss/post/3583665/
class UnionFind:
def __init__(self, n: int):
# 一开始有 n 个集合 {0}, {1}, ..., {n-1}
# 集合 i 的代表元是自己,大小为 1
self._fa = list(range(n)) # 代表元
self._size = [1] * n # 集合大小
self.cc = n # 连通块个数
# 返回 x 所在集合的代表元
# 同时做路径压缩,也就是把 x 所在集合中的所有元素的 fa 都改成代表元
def find(self, x: int) -> int:
fa = self._fa
# 如果 fa[x] == x,则表示 x 是代表元
if fa[x] != x:
fa[x] = self.find(fa[x]) # fa 改成代表元
return fa[x]
# 判断 x 和 y 是否在同一个集合
def is_same(self, x: int, y: int) -> bool:
# 如果 x 的代表元和 y 的代表元相同,那么 x 和 y 就在同一个集合
# 这就是代表元的作用:用来快速判断两个元素是否在同一个集合
return self.find(x) == self.find(y)
# 把 from 所在集合合并到 to 所在集合中
# 返回是否合并成功
def merge(self, from_: int, to: int) -> bool:
x, y = self.find(from_), self.find(to)
if x == y: # from 和 to 在同一个集合,不做合并
return False
self._fa[x] = y # 合并集合。修改后就可以认为 from 和 to 在同一个集合了
self._size[y] += self._size[x] # 更新集合大小(注意集合大小保存在代表元上)
# 无需更新 _size[x],因为我们不用 _size[x] 而是用 _size[find(x)] 获取集合大小,但 find(x) == y,我们不会再访问 _size[x]
self.cc -= 1 # 成功合并,连通块个数减一
return True
# 返回 x 所在集合的大小
def get_size(self, x: int) -> int:
return self._size[self.find(x)] # 集合大小保存在代表元上§7.1 基础
- 684. 冗余连接
- 3493. 属性图 1565
- 990. 等式方程的可满足性 1638
- 721. 账户合并
- 3532. 针对图的路径存在性查询 I 有更简单的做法
- 737. 句子相似性 II(会员题)
- 1101. 彼此熟识的最早时间(会员题)
- 1258. 近义词句子(会员题)
更多基础题,见 网格图题单 中的 DFS 和 图论题单 中的 DFS,其中大部分题目也可以用并查集实现。
§7.2 进阶
- 3551. 数位和排序需要的最小交换次数 1507 置换环
- 2471. 逐层排序二叉树所需的最少操作数目 1635 置换环
- 1202. 交换字符串中的元素 1855
- 1061. 按字典序排列最小的等效字符串
- 1722. 执行交换操作后的最小汉明距离 1892
- 3608. 包含 K 个连通分量需要的最小时间 1893
- 3613. 最小化连通分量的最大成本 类似 3608 题
- 778. 水位上升的泳池中游泳
- 3695. 交换元素后的最大交替和 1984 证明
- 765. 情侣牵手 1999
- 2092. 找出知晓秘密的所有专家 2004
- 839. 相似字符串组 2054
- 685. 冗余连接 II 非暴力做法
- 1970. 你能穿过矩阵的最后一天 2124
- 2076. 处理含限制条件的好友请求 2131
- 1579. 保证图可完全遍历 2132
- 959. 由斜杠划分区域 2136
- 2812. 找出最安全路径 2154
- 2503. 矩阵查询可获得的最大分数 2196
- 3600. 升级后最大生成树稳定性 2301
- 2867. 统计树中的合法路径数目 2428
- 2421. 好路径的数目 2445
- 2157. 字符串分组 2499
- 803. 打砖块 2765
- 3235. 判断矩形的两个角落是否可达
- LCP 71. 集水器
- 2459. 通过移动项目到空白区域来排序数组(会员题)置换
另见 图论题单 中的最小生成树。
§7.3 中介并查集
把 个元素两两相连,要连 条边,太慢了。而把元素连到中介上,通常只会连 条边。
- 947. 移除最多的同行或同列石头 2035
- 3873. 添加一个点后可激活的最大点数 类似 947 题
- 2709. 最大公约数遍历 2172
- 1627. 带阈值的图连通性 2221
- 952. 按公因数计算最大组件大小 2272
- 1998. 数组的最大公因数排序 2429
- 1632. 矩阵转换后的排名 2530
- 3378. 统计最小公倍数图中的连通块数目 2532 思路类似 1627 题
- 2371. 最小化网格中的最大值(会员题)同 1632 题
§7.4 数组上的并查集
- 1562. 查找大小为 M 的最新分组 1928
- 1488. 避免洪水泛滥 1974
- 1353. 最多可以参加的会议数目 2016
- 2382. 删除操作后的最大子段和 2136
- 2334. 元素值大于变化阈值的子数组 2381
- 3666. 使二进制字符串全为 1 的最少操作次数 2477
- 2612. 最少翻转操作数 2824
§7.5 区间并查集
- 3244. 新增道路查询后的最短距离 II 2270
- 1851. 包含每个查询的最小区间 2286
- LCP 52. 二叉搜索树染色
- 2158. 每天绘制新区域的数量(会员题)
§7.6 带权并查集(边权并查集)
模板:
# 模板来源 https://leetcode.cn/discuss/post/3583665/
class UnionFind:
def __init__(self, n: int):
# 一开始有 n 个集合 {0}, {1}, ..., {n-1}
# 集合 i 的代表元是自己,自己到自己的距离是 0
self.fa = list(range(n)) # 代表元
self.dis = [0] * n # dis[x] 表示 x 到(x 所在集合的)代表元的距离
# 返回 x 所在集合的代表元
# 同时做路径压缩
def find(self, x: int) -> int:
fa = self.fa
if fa[x] != x:
root = self.find(fa[x])
self.dis[x] += self.dis[fa[x]] # 递归更新 x 到其代表元的距离
fa[x] = root
return fa[x]
# 判断 x 和 y 是否在同一个集合(同普通并查集)
def same(self, x: int, y: int) -> bool:
return self.find(x) == self.find(y)
# 计算从 from 到 to 的相对距离
# 调用时需保证 from 和 to 在同一个集合中,否则返回值无意义
def get_relative_distance(self, from_: int, to: int) -> int:
self.find(from_)
self.find(to)
# to-from = (x-from) - (x-to) = dis[from] - dis[to]
return self.dis[from_] - self.dis[to]
# 合并 from 和 to,新增信息 to - from = value
# 其中 to 和 from 表示未知量,下文的 x 和 y 也表示未知量
# 如果 from 和 to 不在同一个集合,返回 True,否则返回是否与已知信息矛盾
def merge(self, from_: int, to: int, value: int) -> bool:
x, y = self.find(from_), self.find(to)
dis = self.dis
if x == y: # from 和 to 在同一个集合,不做合并
# to-from = (x-from) - (x-to) = dis[from] - dis[to] = value
return dis[from_] - dis[to] == value
# x --------- y
# / /
# from ------- to
# 已知 x-from = dis[from] 和 y-to = dis[to],现在合并 from 和 to,新增信息 to-from = value
# 由于 y-from = (y-x) + (x-from) = (y-to) + (to-from)
# 所以 y-x = (to-from) + (y-to) - (x-from) = value + dis[to] - dis[from]
dis[x] = value + dis[to] - dis[from_] # 计算 x 到其代表元 y 的距离
self.fa[x] = y
return True- 399. 除法求值 模板题
- 3887. 增量偶权环查询 2128
- 3710. 最大划分因子 2135
- 2307. 检查方程中的矛盾之处(会员题)
附加模板题:CF1850H
八、树状数组和线段树
能用树状数组解决的题目,也能用线段树解决(反过来不一定)。但树状数组实现简单,代码短。
为方便大家练习,我把适合用树状数组解决的题目分到树状数组中,其余分到线段树中。
部分题目需要离散化,推荐先完成 1331. 数组序号转换。
§8.1 树状数组
模板一:普通树状数组
# 模板来源 https://leetcode.cn/discuss/post/3583665/
class FenwickTree:
def __init__(self, n: int):
self.tree = [0] * (n + 1) # 使用下标 1 到 n
# a[i] 增加 val
# 1 <= i <= n
# 时间复杂度 O(log n)
def update(self, i: int, val: int) -> None:
t = self.tree
while i < len(t):
t[i] += val
i += i & -i
# 计算前缀和 a[1] + ... + a[i]
# 1 <= i <= n
# 时间复杂度 O(log n)
def pre(self, i: int) -> int:
t = self.tree
res = 0
while i > 0:
res += t[i]
i &= i - 1
return res
# 计算区间和 a[l] + ... + a[r]
# 1 <= l <= r <= n
# 时间复杂度 O(log n)
def query(self, l: int, r: int) -> int:
if l > r:
return 0
return self.pre(r) - self.pre(l - 1)模板二:值域树状数组
# 应用:https://leetcode.cn/problems/maximum-subarray-sum-after-at-most-k-swaps/
class FenwickTree:
def __init__(self, sorted_nums: list[int]):
self.n = n = len(sorted_nums)
self.high_bit = 1 << (n.bit_length() - 1)
self.sorted_nums = sorted_nums
self.cnt = [0] * (n + 1)
self.sum = [0] * (n + 1)
# 添加 num 个 val,其中 val 离散化后的值为 i(i 从 1 开始)
# 如果 num < 0,表示减少 -num 个 val
# 注意 val = sorted_nums[i - 1],无需手动传入
def update(self, i: int, num: int) -> None:
val = self.sorted_nums[i - 1]
while i <= self.n:
self.cnt[i] += num
self.sum[i] += num * val
i += i & -i
# 返回第 k 小的数(k 从 1 开始)
# 如果不存在第 k 小的数,while 循环后的 i = self.n,下标越界
def kth(self, k: int) -> int:
i = 0
b = self.high_bit
while b > 0:
nxt = i | b
if nxt <= self.n and self.cnt[nxt] < k:
k -= self.cnt[nxt]
i = nxt
b >>= 1
return self.sorted_nums[i]
# 返回前 k 小的数之和(k 从 1 开始)
# 如果不存在第 k 小的数,while 循环后的 i = self.n,下标越界
def pre_sum(self, k: int) -> int:
s = i = 0
b = self.high_bit
while b > 0:
nxt = i | b
if nxt <= self.n and self.cnt[nxt] < k:
k -= self.cnt[nxt]
s += self.sum[nxt]
i = nxt
b >>= 1
# 加上等于第 k 小的数
return s + self.sorted_nums[i] * k- 307. 区域和检索 - 数组可修改
- 3072. 将元素分配到两个数组中 II 2053 离散化
- 3624. 位计数深度为 K 的整数数目 II 2086
- 3187. 数组中的峰值 2154
- 3777. 使子字符串变交替的最少删除次数 2202 做法不止一种
- 1649. 通过指令创建有序数组 2208
- 1626. 无矛盾的最佳球队
- 1409. 查询带键的排列
- 2250. 统计包含每个点的矩形数目
- 2179. 统计数组中好三元组数目 2272
- 1395. 统计作战单位数
- 2659. 将数组清空 2282
- 3915. 距离至少为 K 的交替子序列的最大和 2288
- 2653. 滑动子数组的美丽值 树状数组二分
- 3515. 带权树中的最短路径 2312 差分树状数组
- LCP 05. 发 LeetCoin 同 3515 题
- 4033. 有效 K 个不同元素子数组 I 2314
- 1505. 最多 K 次交换相邻数位后得到的最小整数 2337
- 3841. 查询树上回文路径 2384 差分树状数组
- 2926. 平衡子序列的最大和 2448
- 2736. 最大和查询 2533
- 3671. 子序列美丽值求和 2647 时间戳 懒初始化
- 3962. 至多 K 次交换后最大子数组和 2672 前 小之和
- 3382. 用点构造面积最大的矩形 II 2723 静态二维数点
- 3590. 第 K 小的路径异或和 第 小
- 3245. 交替组 III 3112 环形数组
- 1756. 设计最近使用(MRU)队列(会员题)
- 60. 排列序列 康托展开,可以做到
- 3109. 查找排列的下标(会员题)60 题的数据加强版
- 2519. 统计 K-Big 索引的数量(会员题)
- 2613. 美数对(会员题)曼哈顿最近点对
- 2921. 价格递增的最大利润三元组 II(会员题)
- 308. 二维区域和检索 - 矩阵可修改(会员题)二维树状数组
另见本题单的「§5.7 对顶堆」,部分题目也可以用权值树状数组第 小解决。
§8.2 逆序对
除了可以用树状数组解决,部分题目也可以在归并排序的同时计算。
- 3994. 划分数组的最少相邻交换次数 1705 入门题(不需要树状数组)
- LCR 170. 交易逆序对的总数
- 315. 计算右侧小于当前元素的个数
- 493. 翻转对
- 327. 区间和的个数
- 2426. 满足不等式的数对数目 2030
- 4051. 统计遥远子数组的数目 2140
- 4013. 按奇偶比统计子数组 II 2156
- 3768. 固定长度子数组中的最小逆序对数目 2158
- 1850. 邻位交换的最小次数 非暴力做法
- 2193. 得到回文串的最少操作次数 非暴力做法
- 1885. 统计数对(会员题)
§8.3 线段树(无区间更新)
线段树本质是二叉树,在学习之前,建议先做做 104. 二叉树的最大深度 和 111. 二叉树的最小深度(自底向上写法),当作热身。
线段树:为什么要这样设计? 理解线段树发明的动机。
把任意区间用 个区间表示,线段树的每个节点记录对应区间的信息。
- 询问:把询问区间拆分成 个区间,对应着线段树的 个节点,把这 个节点的信息合并,即为答案。
- 单点更新:有 个区间包含被修改的位置,需要更新 个节点的信息。
基础模板代码如下。为方便入门理解,我没有做复杂封装。更通用的写法可以参考 AtCoder Library 的 segtree.hpp。
# 模板来源 https://leetcode.cn/discuss/post/3583665/
# 线段树有两个下标,一个是线段树节点的下标,另一个是线段树维护的区间的下标
# 节点的下标:从 1 开始,如果你想改成从 0 开始,需要把左右儿子下标分别改成 node*2+1 和 node*2+2
# 区间的下标:从 0 开始
class SegmentTree:
def __init__(self, arr, default=0):
# 线段树维护一个长为 n 的数组(下标从 0 到 n-1)
# arr 可以是 list 或者 int
# 如果 arr 是 int,视作数组大小,默认值为 default
if isinstance(arr, int):
arr = [default] * arr
n = len(arr)
self._n = n
self._tree = [0] * (2 << (n - 1).bit_length())
self._build(arr, 1, 0, n - 1)
# 合并两个 val
def _merge_val(self, a: int, b: int) -> int:
return max(a, b) # **根据题目修改**
# 合并左右儿子的 val 到当前节点的 val
def _maintain(self, node: int) -> None:
self._tree[node] = self._merge_val(self._tree[node * 2], self._tree[node * 2 + 1])
# 用 a 初始化线段树
# 时间复杂度 O(n)
def _build(self, a: List[int], node: int, l: int, r: int) -> None:
if l == r: # 叶子
self._tree[node] = a[l] # 初始化叶节点的值
return
m = (l + r) // 2
self._build(a, node * 2, l, m) # 初始化左子树
self._build(a, node * 2 + 1, m + 1, r) # 初始化右子树
self._maintain(node)
def _update(self, node: int, l: int, r: int, i: int, val: int) -> None:
if l == r: # 叶子(到达目标)
# 如果想直接替换的话,可以写 self._tree[node] = val
self._tree[node] = self._merge_val(self._tree[node], val)
return
m = (l + r) // 2
if i <= m: # i 在左子树
self._update(node * 2, l, m, i, val)
else: # i 在右子树
self._update(node * 2 + 1, m + 1, r, i, val)
self._maintain(node)
def _query(self, node: int, l: int, r: int, ql: int, qr: int) -> int:
if ql <= l and r <= qr: # 当前子树完全在 [ql, qr] 内
return self._tree[node]
m = (l + r) // 2
if qr <= m: # [ql, qr] 与右子树无交集,仅需递归左子树
return self._query(node * 2, l, m, ql, qr)
if ql > m: # [ql, qr] 与左子树无交集,仅需递归右子树
return self._query(node * 2 + 1, m + 1, r, ql, qr)
# [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
l_res = self._query(node * 2, l, m, ql, qr)
r_res = self._query(node * 2 + 1, m + 1, r, ql, qr)
return self._merge_val(l_res, r_res)
# 更新 a[i] 为 _merge_val(a[i], val)
# 时间复杂度 O(log n)
def update(self, i: int, val: int) -> None:
self._update(1, 0, self._n - 1, i, val)
# 返回用 _merge_val 合并所有 a[i] 的计算结果,其中 i 在闭区间 [ql, qr] 中
# 时间复杂度 O(log n)
def query(self, ql: int, qr: int) -> int:
return self._query(1, 0, self._n - 1, ql, qr)
# 获取 a[i] 的值
# 时间复杂度 O(log n)
def get(self, i: int) -> int:
return self._query(1, 0, self._n - 1, i, i)- 3479. 水果成篮 III 2178 线段树二分
- 2940. 找到 Alice 和 Bob 可以相遇的建筑 2327 线段树二分
- 2286. 以组为单位订音乐会的门票 2470 线段树二分
- 3161. 物块放置查询 2513
- 3901. 好子序列查询 2545
- 3777. 使子字符串变交替的最少删除次数 分治思想
- 2213. 由单个字符重复的最长子字符串 2629 分治思想
- 4017. 数组中的峰值 II 分治思想
- 3525. 求出数组的 X 值 II 2645 分治思想
- 3165. 不包含相邻元素的子序列的最大和 2697 分治思想
- 3410. 删除所有值为某个元素后的最大子数组和 2844 分治思想
- 3501. 操作后最大活跃区段数 II 2941 也可以用 ST 表做
- LCP 81. 与非的谜题
思维扩展:
- 1157. 子数组中占绝大多数的元素 2205
- 2407. 最长递增子序列 II 2280
- 2770. 达到末尾下标所需的最大跳跃次数 做到
§8.4 Lazy 线段树(有区间更新)
把任意区间用 个区间表示,线段树的每个节点记录对应区间的信息。
- 询问:把询问区间拆分成 个区间,对应着线段树的 个节点,把这 个节点的信息合并,即为答案。
- 区间更新:仍然是拆分成 个区间,对应着线段树的 个节点。但对于其中的非叶节点,不把更新的内容往下传递给子节点,而是记录「发生了更新,内容为 xxx」,把更新的内容记录下来。直到后续的询问或更新操作,需要访问或修改更下面的子节点信息时,才把更新的内容往下传。
基础模板代码如下。为方便入门理解,我没有做复杂封装。更通用的写法可以参考 AtCoder Library 的 lazysegtree.hpp。
# 模板来源 https://leetcode.cn/discuss/post/3583665/
class Node:
__slots__ = 'val', 'todo'
class LazySegmentTree:
# 懒标记初始值
_TODO_INIT = 0 # **根据题目修改**
def __init__(self, arr, default=0):
# 线段树维护一个长为 n 的数组(下标从 0 到 n-1)
# arr 可以是 list 或者 int
# 如果 arr 是 int,视作数组大小,默认值为 default
if isinstance(arr, int):
arr = [default] * arr
n = len(arr)
self._n = n
self._tree = [Node() for _ in range(2 << (n - 1).bit_length())]
self._build(arr, 1, 0, n - 1)
# 合并两个 val
def _merge_val(self, a: int, b: int) -> int:
return a + b # **根据题目修改**
# 合并两个懒标记
def _merge_todo(self, a: int, b: int) -> int:
return a + b # **根据题目修改**
# 把懒标记作用到 node 子树(本例为区间加)
def _apply(self, node: int, l: int, r: int, todo: int) -> None:
cur = self._tree[node]
# 计算 tree[node] 区间的整体变化
cur.val += todo * (r - l + 1) # **根据题目修改**
cur.todo = self._merge_todo(todo, cur.todo)
# 把当前节点的懒标记下传给左右儿子
def _spread(self, node: int, l: int, r: int) -> None:
todo = self._tree[node].todo
if todo == self._TODO_INIT: # 没有需要下传的信息
return
m = (l + r) // 2
self._apply(node * 2, l, m, todo)
self._apply(node * 2 + 1, m + 1, r, todo)
self._tree[node].todo = self._TODO_INIT # 下传完毕
# 合并左右儿子的 val 到当前节点的 val
def _maintain(self, node: int) -> None:
self._tree[node].val = self._merge_val(self._tree[node * 2].val, self._tree[node * 2 + 1].val)
# 用 a 初始化线段树
# 时间复杂度 O(n)
def _build(self, a: List[int], node: int, l: int, r: int) -> None:
self._tree[node].todo = self._TODO_INIT
if l == r: # 叶子
self._tree[node].val = a[l] # 初始化叶节点的值
return
m = (l + r) // 2
self._build(a, node * 2, l, m) # 初始化左子树
self._build(a, node * 2 + 1, m + 1, r) # 初始化右子树
self._maintain(node)
def _update(self, node: int, l: int, r: int, ql: int, qr: int, f: int) -> None:
if ql <= l and r <= qr: # 当前子树完全在 [ql, qr] 内
self._apply(node, l, r, f)
return
self._spread(node, l, r)
m = (l + r) // 2
if ql <= m: # 更新左子树
self._update(node * 2, l, m, ql, qr, f)
if qr > m: # 更新右子树
self._update(node * 2 + 1, m + 1, r, ql, qr, f)
self._maintain(node)
def _query(self, node: int, l: int, r: int, ql: int, qr: int) -> int:
if ql <= l and r <= qr: # 当前子树完全在 [ql, qr] 内
return self._tree[node].val
self._spread(node, l, r)
m = (l + r) // 2
if qr <= m: # [ql, qr] 与右子树无交集,仅需递归左子树
return self._query(node * 2, l, m, ql, qr)
if ql > m: # [ql, qr] 与左子树无交集,仅需递归右子树
return self._query(node * 2 + 1, m + 1, r, ql, qr)
# [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
l_res = self._query(node * 2, l, m, ql, qr)
r_res = self._query(node * 2 + 1, m + 1, r, ql, qr)
return self._merge_val(l_res, r_res)
# 用 f 更新 [ql, qr] 中的每个 a[i]
# 0 <= ql <= qr <= n-1
# 时间复杂度 O(log n)
def update(self, ql: int, qr: int, f: int) -> None:
self._update(1, 0, self._n - 1, ql, qr, f)
# 返回用 _merge_val 合并所有 a[i] 的计算结果,其中 i 在闭区间 [ql, qr] 中
# 0 <= ql <= qr <= n-1
# 时间复杂度 O(log n)
def query(self, ql: int, qr: int) -> int:
return self._query(1, 0, self._n - 1, ql, qr)- 2569. 更新数组后处理求和查询 2398
- 1622. 奇妙序列 2476 做法不止一种
- 2502. 设计内存分配器
- 2589. 完成所有任务的最少时间 非暴力做法
- 2547. 拆分数组的最小代价 非暴力做法
- 850. 矩形面积 II 矩形面积并 扫描线 离散化
- 3454. 分割正方形 II 2671 同 850 题
- 3569. 分割数组后不同质数的最大数目 2697
- 3721. 最长平衡子数组 II 2724 HH 项链 trick
- 2916. 子数组不同元素数目的平方和 II 2816
- LCP 52. 二叉搜索树染色 也可以用珂朵莉树或者并查集
§8.5 动态开点线段树
部分题目也可以用珂朵莉树解决。
- 699. 掉落的方块
- 715. Range 模块
- 729. 我的日程安排表 I
- 731. 我的日程安排表 II
- 732. 我的日程安排表 III
- 2276. 统计区间中的整数数目 2222
- 3590. 第 K 小的路径异或和 2646 线段树合并
§8.6 可持久化线段树
- 3762. 使数组元素相等的最小操作次数 2497
§8.7 ST 表(Sparse Table)
ST 表 支持区间最值查询(Range Minimum/Maximum Query,RMQ),但不支持修改。
优点是代码短,且查询的时间复杂度是 。所以作为补充内容,附在此处。
class SparseTable:
# 时间复杂度 O(n * log n)
def __init__(self, nums: List[int], op: Callable[[int, int], int]):
n = len(nums)
w = n.bit_length()
st = [[0] * n for _ in range(w)]
st[0] = nums[:]
for i in range(1, w):
for j in range(n - (1 << i) + 1):
st[i][j] = op(st[i - 1][j], st[i - 1][j + (1 << (i - 1))])
self.st = st
self.op = op
# [l, r) 左闭右开,下标从 0 开始
# 返回 op(nums[l:r])
# 时间复杂度 O(1)
def query(self, l: int, r: int) -> int:
k = (r - l).bit_length() - 1
return self.op(self.st[k][l], self.st[k][r - (1 << k)])
# 使用方法举例
nums = [3, 1, 4, 1, 5, 9, 2, 6]
st = SparseTable(nums, max)
print(st.query(0, 5)) # max(nums[0:5]) = 5
print(st.query(1, 1)) # 错误:必须保证 l < r- 3691. 最大子数组总值 II 2469
- 3501. 操作后最大活跃区段数 II 2941
二维 ST 表:
九、伸展树(Splay 树)
十、CDQ 分治 / 值域分治
- 3027. 人员站位的方案数 II 非暴力做法
- 4055. 统计影子数对 II 2631
十一、根号算法
§11.1 分块
注:上面树状数组和线段树的题目,可以尝试用分块解决。例如 307. 区域和检索 - 数组可修改。
- 3943. 递增后的数对数量 2410
§11.2 莫队算法
属于离线算法的一种。
- 4033. 有效 K 个不同元素子数组 I 2314
- 3636. 查询超过阈值频率最高元素 2451 回滚莫队
- 3590. 第 K 小的路径异或和 2646 做法不止一种
- 2846. 边权重均等查询 树上莫队 和边权范围无关的做法
§11.3 根号分解(Sqrt Decomposition)
针对不同大小的询问使用不同的算法。
- 3655. 区间乘法查询后的异或 II 2454
- LCP 16. 游乐园的游览计划
- 1714. 数组中特殊等间距元素的和(会员题)
- 3400. 右移后的最大匹配索引数(会员题)非暴力做法
§11.4 其他
- 3234. 统计 1 显著的字符串的数量 2557
专题:离线算法
通过改变回答询问的顺序,使问题更容易处理。
相应的,在线算法就是按照 的顺序一个一个处理。
- 2343. 裁剪数字后查询第 K 小的数字 1652
- 3607. 电网维护 1700
- 2070. 每一个查询的最大美丽值 1724
- 1847. 最近的房间 2082
- 2503. 矩阵查询可获得的最大分数 2196
- 1851. 包含每个查询的最小区间 2286
- 1697. 检查边长度限制的路径是否存在 2300
- 4033. 有效 K 个不同元素子数组 I 2314
- 2940. 找到 Alice 和 Bob 可以相遇的建筑 2327
- 2747. 统计没有收到请求的服务器数目 2405
- 1938. 查询最大基因差 2503
- 2736. 最大和查询 2533
- 3590. 第 K 小的路径异或和 2646
- 3382. 用点构造面积最大的矩形 II 2723 静态二维数点
编程能力强化训练
知道大概怎么做,但不好实现的题目。部分题目是模拟题。
Part A
另见本题单的「§3.5 表达式解析」。
Part B
Part C
- 3197. 包含所有 1 的最小矩形面积 II 2541
- 2532. 过桥的时间 2589
- 2056. 棋盘上有效移动组合的数目 2611
- LCP 48. 无限棋局
- LCP 21. 追逐游戏
- LCP 58. 积木拼接
- LCP 13. 寻宝
- LCP 69. Hello LeetCode!
- LCP 76. 魔法棋盘
- LCP 82. 万灵之树
关联题单
算法题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府
如果你发现有题目可以补充进来,欢迎评论反馈。


