本文转载自力扣讨论区,作者:灵茶山艾府(endlesscheng)。 原文链接:https://leetcode.cn/circle/discuss/mOr1u6/
零、常用枚举技巧
§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
- 1031. 两个无重叠子数组的最大和 约 2000
- 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
§1.2 前缀和与哈希表
通常要用到「枚举右,维护左」的技巧(见本题单 §0.1 节)。
- 560. 和为 K 的子数组 我的题解包含变形题
- 930. 和相同的二元子数组 1592 同 560 题
- 1524. 和为奇数的子数组数目 1611
- 974. 和可被 K 整除的子数组 1676
- 523. 连续的子数组和 类似 974 题
- 2588. 统计美丽子数组数目 1697
- 525. 连续数组
和 个数相同的最长子数组 - 面试题 17.05. 字母与数字 同 525 题
- 3755. 最大平衡异或子数组的长度 pair
- 3026. 最大好子数组和 1817
- 1477. 找两个和为目标值且不重叠的子数组 1851
- 1546. 和为目标值且不重叠的非空子数组的最大数目 1855
- 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 子数组去重 / 避免重复统计
- 3969. 求和后首尾数字相同的有效子数组 I 非暴力做法
- 2949. 统计美丽子字符串 II 2445 pair
- 325. 和等于 k 的最长子数组长度(会员题)
- 548. 将数组分割成和相等的子数组(会员题)
- 1983. 范围和相等的最宽索引对(会员题)
- 2489. 固定比率的子字符串数(会员题)
- 2031. 1 比 0 多的子数组个数(会员题)做到
- 2950. 可整除子串的数量(会员题)做到
,其中 。
前缀和与有序集合:
- 3364. 最小正和子数组 非暴力做法
- 363. 矩形区域不超过 K 的最大数值和
- 3739. 统计主要元素子数组数目 II 2090
- 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]class NumMatrix {
private final int[][] sum;
public NumMatrix(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
sum = new int[m + 1][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sum[i + 1][j + 1] = sum[i + 1][j] + sum[i][j + 1] - sum[i][j] + matrix[i][j];
}
}
}
// 返回左上角在 (r1, c1),右下角在 (r2, c2) 的子矩阵元素和
public int sumRegion(int r1, int c1, int r2, int c2) {
return sum[r2 + 1][c2 + 1] - sum[r2 + 1][c1] - sum[r1][c2 + 1] + sum[r1][c1];
}
}class NumMatrix {
vector<vector<int>> sum;
public:
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
sum.resize(m + 1, vector<int>(n + 1));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sum[i + 1][j + 1] = sum[i + 1][j] + sum[i][j + 1] - sum[i][j] + matrix[i][j];
}
}
}
// 返回左上角在 (r1, c1),右下角在 (r2, c2) 的子矩阵元素和
int sumRegion(int r1, int c1, int r2, int c2) {
return sum[r2 + 1][c2 + 1] - sum[r2 + 1][c1] - sum[r1][c2 + 1] + sum[r1][c1];
}
};type NumMatrix [][]int
func Constructor(matrix [][]int) NumMatrix {
m, n := len(matrix), len(matrix[0])
sum := make([][]int, m+1)
sum[0] = make([]int, n+1)
for i, row := range matrix {
sum[i+1] = make([]int, n+1)
for j, x := range row {
sum[i+1][j+1] = sum[i+1][j] + sum[i][j+1] - sum[i][j] + x
}
}
return sum
}
// 返回左上角在 (r1, c1),右下角在 (r2, c2) 的子矩阵元素和
func (s NumMatrix) SumRegion(r1, c1, r2, c2 int) int {
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
- 2216. 美化数组的最少删除数 1510
- 1209. 删除字符串中的所有相邻重复项 II 1542
- 3703. 移除K-平衡子字符串 1802 联系 1209 题
- 1717. 删除子字符串的最大得分 1868
- 2197. 替换数组中的非互质数 2057
- 735. 小行星碰撞
- 2751. 机器人碰撞 2092 联系 735 题
§3.4 合法括号字符串(RBS)
注:部分题目可以不用栈,而是用一个数字记录嵌套深度。
- 20. 有效的括号
- 921. 使括号有效的最少添加 1242
- 1021. 删除最外层的括号 1311
- 1614. 括号的最大嵌套深度 1323
- 1190. 反转每对括号间的子串 1486
- 856. 括号的分数 1563
- 1249. 移除无效的括号 1657
- 1963. 使字符串平衡的最小交换次数 1689
- 678. 有效的括号字符串 约 1700
- 1111. 有效括号的嵌套深度 1749
- 1541. 平衡括号字符串的最少插入次数 1759
- 2116. 判断一个括号字符串是否有效 2038 进阶问题:CF1709C
- 32. 最长有效括号
§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 ansclass Solution {
// 计算 nums 的每个长为 k 的窗口的最大值
// 时间复杂度 O(n),其中 n 是 nums 的长度
private int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n - k + 1]; // 窗口个数
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 1. 右边入
while (!q.isEmpty() && nums[q.getLast()] <= nums[i]) {
q.removeLast(); // 维护 q 的单调性
}
q.addLast(i); // 注意保存的是下标,这样下面可以判断队首是否离开窗口
// 2. 左边出
int left = i - k + 1; // 窗口左端点
if (q.getFirst() < left) { // 队首离开窗口
q.removeFirst();
}
// 3. 在窗口左端点处记录答案
if (left >= 0) {
// 由于队首到队尾单调递减,所以窗口最大值就在队首
ans[left] = nums[q.getFirst()];
}
}
return ans;
}
}// 计算 nums 的每个长为 k 的窗口的最大值
// 时间复杂度 O(n),其中 n 是 nums 的长度
vector<int> maxSlidingWindow(const vector<int>& nums, int k) {
int n = nums.size();
vector<int> ans(n - k + 1); // 窗口个数
deque<int> q; // 双端队列
for (int i = 0; i < n; i++) {
// 1. 右边入
while (!q.empty() && nums[q.back()] <= nums[i]) {
q.pop_back(); // 维护 q 的单调性
}
q.push_back(i); // 注意保存的是下标,这样下面可以判断队首是否离开窗口
// 2. 左边出
int left = i - k + 1; // 窗口左端点
if (q.front() < left) { // 队首离开窗口
q.pop_front();
}
// 3. 在窗口左端点处记录答案
if (left >= 0) {
// 由于队首到队尾单调递减,所以窗口最大值就在队首
ans[left] = nums[q.front()];
}
}
return ans;
}// 计算 nums 的每个长为 k 的窗口的最大值
// 时间复杂度 O(n),其中 n 是 nums 的长度
func maxSlidingWindow(nums []int, k int) []int {
ans := make([]int, len(nums)-k+1) // 窗口个数
q := []int{}
for i, x := range nums {
// 1. 右边入
for len(q) > 0 && nums[q[len(q)-1]] <= x {
q = q[:len(q)-1] // 维护 q 的单调性
}
q = append(q, i) // 注意保存的是下标,这样下面可以判断队首是否离开窗口
// 2. 左边出
left := i - k + 1 // 窗口左端点
if q[0] < left { // 队首离开窗口
q = q[1:] // Go 的切片是 O(1) 的
}
// 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 懒删除堆
支持删除堆中任意元素。
模板:
# 模板来源 ./09-常用数据结构
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// 模板来源 ./09-常用数据结构
// 最小堆 LazyHeap minPQ = new LazyHeap(Integer::compare);
// 最大堆 LazyHeap maxPQ = new LazyHeap((a, b) -> Integer.compare(b, a));
class LazyHeap extends PriorityQueue<Integer> {
private final Map<Integer, Integer> removeCnt = new HashMap<>(); // 每个元素剩余需要删除的次数
private int size = 0; // 堆的实际大小
public LazyHeap(Comparator<Integer> comparator) {
super(comparator);
}
// 删除堆中所有应该删除的元素后,堆的实际大小
public int size() {
return size;
}
// 删除
public void remove(int x) {
removeCnt.merge(x, 1, Integer::sum); // 懒删除
size--;
}
// 正式执行删除操作
private void applyRemove() {
while (removeCnt.getOrDefault(peek(), 0) > 0) {
removeCnt.merge(poll(), -1, Integer::sum);
}
}
// 查看堆顶
public int top() {
applyRemove();
return peek(); // 真正的堆顶
}
// 出堆
public int pop() {
applyRemove();
size--;
return poll();
}
// 入堆
public void push(int x) {
int c = removeCnt.getOrDefault(x, 0);
if (c > 0) {
removeCnt.put(x, c - 1); // 抵消之前的删除
} else {
offer(x);
}
size++;
}
}// 模板来源 ./09-常用数据结构
// 最大堆 LazyHeap<int> max_pq;
// 最小堆 LazyHeap<int, greater<>> min_pq;
template<typename T, typename Compare = less<T>>
class LazyHeap {
priority_queue<T, vector<T>, Compare> pq;
unordered_map<T, int> remove_cnt; // 每个元素剩余需要删除的次数
size_t sz = 0; // 堆的实际大小
// 正式执行删除操作
void apply_remove() {
while (!pq.empty() && remove_cnt[pq.top()] > 0) {
remove_cnt[pq.top()]--;
pq.pop();
}
}
public:
// 删除堆中所有应该删除的元素后,堆的实际大小
size_t size() {
return sz;
}
// 删除
void remove(T x) {
remove_cnt[x]++; // 懒删除
sz--;
}
// 查看堆顶
T top() {
apply_remove();
return pq.top(); // 真正的堆顶
}
// 出堆
T pop() {
apply_remove();
sz--;
T x = pq.top();
pq.pop();
return x;
}
// 入堆
void push(T x) {
if (remove_cnt[x] > 0) {
remove_cnt[x]--; // 抵消之前的删除
} else {
pq.push(x);
}
sz++;
}
};// 模板来源 ./09-常用数据结构
func newLazyHeap() *lazyHeap {
return &lazyHeap{removeCnt: map[int]int{}}
}
// 懒删除堆(默认是最小堆)
type lazyHeap struct {
sort.IntSlice
removeCnt map[int]int // 每个元素剩余需要删除的次数
size int // 堆的实际大小
}
// 必须实现的两个接口
func (h *lazyHeap) Push(v any) { h.IntSlice = append(h.IntSlice, v.(int)) }
func (h *lazyHeap) Pop() any { a := h.IntSlice; v := a[len(a)-1]; h.IntSlice = a[:len(a)-1]; return v }
// 加上这行变成最大堆
// func (h *lazyHeap) Less(i, j int) bool { return h.IntSlice[i] > h.IntSlice[j] }
// 删除
func (h *lazyHeap) remove(v int) {
h.removeCnt[v]++ // 懒删除
h.size--
}
// 正式执行删除操作
func (h *lazyHeap) applyRemove() {
for h.removeCnt[h.IntSlice[0]] > 0 {
h.removeCnt[h.IntSlice[0]]--
heap.Pop(h)
}
}
// 查看堆顶
func (h *lazyHeap) top() int {
h.applyRemove()
return h.IntSlice[0] // 真正的堆顶
}
// 出堆
func (h *lazyHeap) pop() int {
h.applyRemove()
h.size--
return heap.Pop(h).(int)
}
// 入堆
func (h *lazyHeap) push(v int) {
if h.removeCnt[v] > 0 {
h.removeCnt[v]-- // 抵消之前的删除
} else {
heap.Push(h, v)
}
h.size++
}- 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
§6.4 0-1 字典树(异或字典树)
部分题目也可以用试填法解决。
- 421. 数组中两个数的最大异或值 约 2000
- 2935. 找出强数对的最大异或值 II 2349
- 3845. 最大子数组异或值
- 1707. 与数组中元素的最大异或值 2359
- 1803. 统计异或值在范围内的数对有多少 2479
- 1938. 查询最大基因差 2503
- 3632. 异或至少为 K 的子数组数目(会员题)
- 2479. 两个不重叠子树的最大异或值(会员题)
七、并查集
模板:
# 模板来源 ./09-常用数据结构
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)] # 集合大小保存在代表元上// 模板来源 ./09-常用数据结构
class UnionFind {
private final int[] fa; // 代表元
private final int[] size; // 集合大小
public int cc; // 连通块个数
UnionFind(int n) {
// 一开始有 n 个集合 {0}, {1}, ..., {n-1}
// 集合 i 的代表元是自己,大小为 1
fa = new int[n];
for (int i = 0; i < n; i++) {
fa[i] = i;
}
size = new int[n];
Arrays.fill(size, 1);
cc = n;
}
// 返回 x 所在集合的代表元
// 同时做路径压缩,也就是把 x 所在集合中的所有元素的 fa 都改成代表元
public int find(int x) {
// 如果 fa[x] == x,则表示 x 是代表元
if (fa[x] != x) {
fa[x] = find(fa[x]); // fa 改成代表元
}
return fa[x];
}
// 判断 x 和 y 是否在同一个集合
public boolean isSame(int x, int y) {
// 如果 x 的代表元和 y 的代表元相同,那么 x 和 y 就在同一个集合
// 这就是代表元的作用:用来快速判断两个元素是否在同一个集合
return find(x) == find(y);
}
// 把 from 所在集合合并到 to 所在集合中
// 返回是否合并成功
public boolean merge(int from, int to) {
int x = find(from);
int y = find(to);
if (x == y) { // from 和 to 在同一个集合,不做合并
return false;
}
fa[x] = y; // 合并集合。修改后就可以认为 from 和 to 在同一个集合了
size[y] += size[x]; // 更新集合大小(注意集合大小保存在代表元上)
// 无需更新 size[x],因为我们不用 size[x] 而是用 size[find(x)] 获取集合大小,但 find(x) == y,我们不会再访问 size[x]
cc--; // 成功合并,连通块个数减一
return true;
}
// 返回 x 所在集合的大小
public int getSize(int x) {
return size[find(x)]; // 集合大小保存在代表元上
}
}// 模板来源 ./09-常用数据结构
class UnionFind {
vector<int> fa; // 代表元
vector<int> sz; // 集合大小
public:
int cc; // 连通块个数
UnionFind(int n) : fa(n), sz(n, 1), cc(n) {
// 一开始有 n 个集合 {0}, {1}, ..., {n-1}
// 集合 i 的代表元是自己,大小为 1
ranges::iota(fa, 0); // iota(fa.begin(), fa.end(), 0);
}
// 返回 x 所在集合的代表元
// 同时做路径压缩,也就是把 x 所在集合中的所有元素的 fa 都改成代表元
int find(int x) {
// 如果 fa[x] == x,则表示 x 是代表元
if (fa[x] != x) {
fa[x] = find(fa[x]); // fa 改成代表元
}
return fa[x];
}
// 判断 x 和 y 是否在同一个集合
bool is_same(int x, int y) {
// 如果 x 的代表元和 y 的代表元相同,那么 x 和 y 就在同一个集合
// 这就是代表元的作用:用来快速判断两个元素是否在同一个集合
return find(x) == find(y);
}
// 把 from 所在集合合并到 to 所在集合中
// 返回是否合并成功
bool merge(int from, int to) {
int x = find(from), y = find(to);
if (x == y) { // from 和 to 在同一个集合,不做合并
return false;
}
fa[x] = y; // 合并集合。修改后就可以认为 from 和 to 在同一个集合了
sz[y] += sz[x]; // 更新集合大小(注意集合大小保存在代表元上)
// 无需更新 sz[x],因为我们不用 sz[x] 而是用 sz[find(x)] 获取集合大小,但 find(x) == y,我们不会再访问 sz[x]
cc--; // 成功合并,连通块个数减一
return true;
}
// 返回 x 所在集合的大小
int get_size(int x) {
return sz[find(x)]; // 集合大小保存在代表元上
}
};// 模板来源 ./09-常用数据结构
type unionFind struct {
fa []int // 代表元
sz []int // 集合大小
cc int // 连通块个数
}
func newUnionFind(n int) unionFind {
fa := make([]int, n)
sz := make([]int, n)
// 一开始有 n 个集合 {0}, {1}, ..., {n-1}
// 集合 i 的代表元是自己,大小为 1
for i := range fa {
fa[i] = i
sz[i] = 1
}
return unionFind{fa, sz, n}
}
// 返回 x 所在集合的代表元
// 同时做路径压缩,也就是把 x 所在集合中的所有元素的 fa 都改成代表元
func (u unionFind) find(x int) int {
// 如果 fa[x] == x,则表示 x 是代表元
if u.fa[x] != x {
u.fa[x] = u.find(u.fa[x]) // fa 改成代表元
}
return u.fa[x]
}
// 判断 x 和 y 是否在同一个集合
func (u unionFind) same(x, y int) bool {
// 如果 x 的代表元和 y 的代表元相同,那么 x 和 y 就在同一个集合
// 这就是代表元的作用:用来快速判断两个元素是否在同一个集合
return u.find(x) == u.find(y)
}
// 把 from 所在集合合并到 to 所在集合中
// 返回是否合并成功
func (u *unionFind) merge(from, to int) bool {
x, y := u.find(from), u.find(to)
if x == y { // from 和 to 在同一个集合,不做合并
return false
}
u.fa[x] = y // 合并集合。修改后就可以认为 from 和 to 在同一个集合了
u.sz[y] += u.sz[x] // 更新集合大小(注意集合大小保存在代表元上)
// 无需更新 sz[x],因为我们不用 sz[x] 而是用 sz[find(x)] 获取集合大小,但 find(x) == y,我们不会再访问 sz[x]
u.cc-- // 成功合并,连通块个数减一
return true
}
// 返回 x 所在集合的大小
func (u unionFind) size(x int) int {
return u.sz[u.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 区间并查集
§7.6 带权并查集(边权并查集)
模板:
# 模板来源 ./09-常用数据结构
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// 模板来源 ./09-常用数据结构
class UnionFind {
private final int[] fa; // 代表元
private final int[] dis; // dis[x] 表示 x 到(x 所在集合的)代表元的距离
// 注意数据范围,必要时使用 long[] dis
public UnionFind(int n) {
// 一开始有 n 个集合 {0}, {1}, ..., {n-1}
// 集合 i 的代表元是自己,自己到自己的距离是 0
fa = new int[n];
dis = new int[n];
for (int i = 0; i < n; i++) {
fa[i] = i;
}
}
// 返回 x 所在集合的代表元
// 同时做路径压缩
public int find(int x) {
if (fa[x] != x) {
int root = find(fa[x]);
dis[x] += dis[fa[x]]; // 递归更新 x 到其代表元的距离
fa[x] = root;
}
return fa[x];
}
// 判断 x 和 y 是否在同一个集合(同普通并查集)
public boolean same(int x, int y) {
return find(x) == find(y);
}
// 计算从 from 到 to 的相对距离
// 调用时需保证 from 和 to 在同一个集合中,否则返回值无意义
public int getRelativeDistance(int from, int to) {
find(from);
find(to);
// to-from = (x-from) - (x-to) = dis[from] - dis[to]
return dis[from] - dis[to];
}
// 合并 from 和 to,新增信息 to - from = value
// 其中 to 和 from 表示未知量,下文的 x 和 y 也表示未知量
// 如果 from 和 to 不在同一个集合,返回 true,否则返回是否与已知信息矛盾
public boolean merge(int from, int to, int value) {
int x = find(from), y = find(to);
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 的距离
fa[x] = y;
return true;
}
}// 模板来源 ./09-常用数据结构
// 根据题目用 UnionFind<int> uf(n) 或者 UnionFind<long long> uf(n) 初始化
template<typename T>
class UnionFind {
public:
vector<int> fa; // 代表元
vector<T> dis; // dis[x] 表示 x 到(x 所在集合的)代表元的距离
UnionFind(int n) : fa(n), dis(n) {
// 一开始有 n 个集合 {0}, {1}, ..., {n-1}
// 集合 i 的代表元是自己,自己到自己的距离是 0
ranges::iota(fa, 0); // iota(fa.begin(), fa.end(), 0);
}
// 返回 x 所在集合的代表元
// 同时做路径压缩
int find(int x) {
if (fa[x] != x) {
int root = find(fa[x]);
dis[x] += dis[fa[x]]; // 递归更新 x 到其代表元的距离
fa[x] = root;
}
return fa[x];
}
// 判断 x 和 y 是否在同一个集合(同普通并查集)
bool same(int x, int y) {
return find(x) == find(y);
}
// 计算从 from 到 to 的相对距离
// 调用时需保证 from 和 to 在同一个集合中,否则返回值无意义
T get_relative_distance(int from, int to) {
find(from);
find(to);
// to-from = (x-from) - (x-to) = dis[from] - dis[to]
return dis[from] - dis[to];
}
// 合并 from 和 to,新增信息 to - from = value
// 其中 to 和 from 表示未知量,下文的 x 和 y 也表示未知量
// 如果 from 和 to 不在同一个集合,返回 true,否则返回是否与已知信息矛盾
bool merge(int from, int to, T value) {
int x = find(from), y = find(to);
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 的距离
fa[x] = y;
return true;
}
};// 模板来源 ./09-常用数据结构
type unionFind struct {
fa []int // 代表元
dis []int // dis[x] 表示 x 到(x 所在集合的)代表元的距离
}
func newUnionFind(n int) unionFind {
// 一开始有 n 个集合 {0}, {1}, ..., {n-1}
// 集合 i 的代表元是自己,自己到自己的距离是 0
fa := make([]int, n)
dis := make([]int, n)
for i := range fa {
fa[i] = i
}
return unionFind{fa, dis}
}
// 返回 x 所在集合的代表元
// 同时做路径压缩
func (u unionFind) find(x int) int {
if u.fa[x] != x {
root := u.find(u.fa[x])
u.dis[x] += u.dis[u.fa[x]] // 递归更新 x 到其代表元的距离
u.fa[x] = root
}
return u.fa[x]
}
// 判断 x 和 y 是否在同一个集合(同普通并查集)
func (u unionFind) same(x, y int) bool {
return u.find(x) == u.find(y)
}
// 计算从 from 到 to 的相对距离
// 调用时需保证 from 和 to 在同一个集合中,否则返回值无意义
func (u unionFind) getRelativeDistance(from, to int) int {
u.find(from)
u.find(to)
// to-from = (x-from) - (x-to) = dis[from] - dis[to]
return u.dis[from] - u.dis[to]
}
// 合并 from 和 to,新增信息 to - from = value
// 其中 to 和 from 表示未知量,下文的 x 和 y 也表示未知量
// 如果 from 和 to 不在同一个集合,返回 true,否则返回是否与已知信息矛盾
func (u unionFind) merge(from, to int, value int) bool {
x, y := u.find(from), u.find(to)
if x == y { // from 和 to 在同一个集合,不做合并
// to-from = (x-from) - (x-to) = dis[from] - dis[to] = value
return u.dis[from]-u.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]
u.dis[x] = value + u.dis[to] - u.dis[from] // 计算 x 到其代表元 y 的距离
u.fa[x] = y
return true
}- 399. 除法求值 模板题
- 3887. 增量偶权环查询 2128
- 3710. 最大划分因子 2135
- 2307. 检查方程中的矛盾之处(会员题)
附加模板题:CF1850H
八、树状数组和线段树
能用树状数组解决的题目,也能用线段树解决(反过来不一定)。但树状数组实现简单,代码短。
为方便大家练习,我把适合用树状数组解决的题目分到树状数组中,其余分到线段树中。
部分题目需要用到离散化,推荐先完成 1331. 数组序号转换。
§8.1 树状数组
模板一:普通树状数组
# 模板来源 ./09-常用数据结构
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 r < l:
return 0
return self.pre(r) - self.pre(l - 1)// 模板来源 ./09-常用数据结构
class FenwickTree {
private final long[] tree; // 如果计算结果没有超出 int 范围,可以改成 int
public FenwickTree(int n) {
tree = new long[n + 1]; // 使用下标 1 到 n
}
// a[i] 增加 val
// 1 <= i <= n
// 时间复杂度 O(log n)
public void update(int i, long val) {
for (; i < tree.length; i += i & -i) {
tree[i] += val;
}
}
// 求前缀和 a[1] + ... + a[i]
// 1 <= i <= n
// 时间复杂度 O(log n)
public long pre(int i) {
long res = 0;
for (; i > 0; i &= i - 1) {
res += tree[i];
}
return res;
}
// 求区间和 a[l] + ... + a[r]
// 1 <= l <= r <= n
// 时间复杂度 O(log n)
public long query(int l, int r) {
if (r < l) {
return 0;
}
return pre(r) - pre(l - 1);
}
}// 模板来源 ./09-常用数据结构
// 根据题目用 FenwickTree<int> t(n) 或者 FenwickTree<long long> t(n) 初始化
template<typename T>
class FenwickTree {
vector<T> tree;
public:
// 使用下标 1 到 n
FenwickTree(int n) : tree(n + 1) {}
// a[i] 增加 val
// 1 <= i <= n
// 时间复杂度 O(log n)
void update(int i, T val) {
for (; i < tree.size(); i += i & -i) {
tree[i] += val;
}
}
// 求前缀和 a[1] + ... + a[i]
// 1 <= i <= n
// 时间复杂度 O(log n)
T pre(int i) const {
T res{};
for (; i > 0; i &= i - 1) {
res += tree[i];
}
return res;
}
// 求区间和 a[l] + ... + a[r]
// 1 <= l <= r <= n
// 时间复杂度 O(log n)
T query(int l, int r) const {
if (r < l) {
return 0;
}
return pre(r) - pre(l - 1);
}
};// 模板来源 ./09-常用数据结构
type fenwick []int
func newFenwickTree(n int) fenwick {
return make(fenwick, n+1) // 使用下标 1 到 n
}
// a[i] 增加 val
// 1 <= i <= n
// 时间复杂度 O(log n)
func (f fenwick) update(i int, val int) {
for ; i < len(f); i += i & -i {
f[i] += val
}
}
// 求前缀和 a[1] + ... + a[i]
// 1 <= i <= n
// 时间复杂度 O(log n)
func (f fenwick) pre(i int) (res int) {
for ; i > 0; i &= i - 1 {
res += f[i]
}
return
}
// 求区间和 a[l] + ... + a[r]
// 1 <= l <= r <= n
// 时间复杂度 O(log n)
func (f fenwick) query(l, r int) int {
if r < l {
return 0
}
return f.pre(r) - f.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 开始)
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 开始)
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// 应用:https://leetcode.cn/problems/maximum-subarray-sum-after-at-most-k-swaps/
class FenwickTree {
private final int highBit;
private final int[] sorted;
private final int[] cnt;
private final long[] sum;
public FenwickTree(int[] sorted) {
int n = sorted.length;
highBit = Integer.highestOneBit(n);
this.sorted = sorted;
cnt = new int[n + 1];
sum = new long[n + 1];
}
// 添加 num 个 val,其中 val 离散化后的值为 i(i 从 1 开始)
// 如果 num < 0,表示减少 -num 个 val
// 注意 val = sorted[i - 1],无需手动传入
public void update(int i, int num) {
int val = sorted[i - 1];
for (; i < cnt.length; i += i & -i) {
cnt[i] += num;
sum[i] += (long) num * val;
}
}
// 返回第 k 小的数(k 从 1 开始)
public int kth(int k) {
int i = 0;
for (int b = highBit; b > 0; b >>= 1) {
int nxt = i | b;
if (nxt < cnt.length && cnt[nxt] < k) {
k -= cnt[nxt];
i = nxt;
}
}
return sorted[i];
}
// 返回前 k 小的数之和(k 从 1 开始)
public long preSum(int k) {
long s = 0;
int i = 0;
for (int b = highBit; b > 0; b >>= 1) {
int nxt = i | b;
if (nxt < cnt.length && cnt[nxt] < k) {
k -= cnt[nxt];
s += sum[nxt];
i = nxt;
}
}
// 加上等于第 k 小的数
return s + (long) sorted[i] * k;
}
}// 应用:https://leetcode.cn/problems/maximum-subarray-sum-after-at-most-k-swaps/
class FenwickTree {
const vector<int>& sorted;
const int high_bit;
vector<int> cnt;
vector<long long> sum;
public:
FenwickTree(const vector<int>& sorted) :
sorted(sorted),
high_bit(1 << (bit_width(sorted.size()) - 1)),
cnt(sorted.size() + 1),
sum(sorted.size() + 1) {}
// 添加 num 个 val,其中 val 离散化后的值为 i(i 从 1 开始)
// 如果 num < 0,表示减少 -num 个 val
// 注意 val = sorted[i - 1],无需手动传入
void update(int i, int num) {
auto val = sorted[i - 1];
for (; i < cnt.size(); i += i & -i) {
cnt[i] += num;
sum[i] += 1LL * num * val;
}
}
// 返回第 k 小的数(k 从 1 开始)
int kth(int k) const {
int i = 0;
for (int b = high_bit; b > 0; b >>= 1) {
int nxt = i | b;
if (nxt < cnt.size() && cnt[nxt] < k) {
k -= cnt[nxt];
i = nxt;
}
}
return sorted[i];
}
// 返回前 k 小的数之和(k 从 1 开始)
long long pre_sum(int k) const {
long long s = 0;
int i = 0;
for (int b = high_bit; b > 0; b >>= 1) {
int nxt = i | b;
if (nxt < cnt.size() && cnt[nxt] < k) {
k -= cnt[nxt];
s += sum[nxt];
i = nxt;
}
}
// 加上等于第 k 小的数
return s + 1LL * sorted[i] * k;
}
};// 应用:https://leetcode.cn/problems/maximum-subarray-sum-after-at-most-k-swaps/
type pair struct{ cnt, sum int }
type fenwick struct {
t []pair
sorted []int
highBit int
}
func newFenwickTree(sorted []int) fenwick {
n := len(sorted)
return fenwick{
t: make([]pair, n+1),
sorted: sorted,
highBit: 1 << (bits.Len(uint(n)) - 1),
}
}
// 添加 num 个 val,其中 val 离散化后的值为 i(i 从 1 开始)
// 如果 num < 0,表示减少 -num 个 val
// 注意 val = sorted[i-1],无需手动传入
func (f *fenwick) update(i, num int) {
val := f.sorted[i-1]
for ; i < len(f.t); i += i & -i {
f.t[i].cnt += num
f.t[i].sum += num * val
}
}
// 返回第 k 小的数(k 从 1 开始)
func (f *fenwick) kth(k int) int {
i := 0
for b := f.highBit; b > 0; b >>= 1 {
if nxt := i | b; nxt < len(f.t) && f.t[nxt].cnt < k {
k -= f.t[nxt].cnt
i = nxt
}
}
return f.sorted[i]
}
// 返回前 k 小的数之和(k 从 1 开始)
func (f *fenwick) preSum(k int) (s int) {
i := 0
for b := f.highBit; b > 0; b >>= 1 {
if nxt := i | b; nxt < len(f.t) && f.t[nxt].cnt < k {
k -= f.t[nxt].cnt
s += f.t[nxt].sum
i = nxt
}
}
// 加上等于第 k 小的数
s += f.sorted[i] * k
return
}- 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 题
- 1505. 最多 K 次交换相邻数位后得到的最小整数 2337
- 3841. 查询树上回文路径 2384 差分树状数组
- 2926. 平衡子序列的最大和 2448
- 2736. 最大和查询 2533
- 3671. 子序列美丽值求和 2647 时间戳 懒初始化
- 3962. 至多 K 次交换后最大子数组和 2672 前
小之和 - 3382. 用点构造面积最大的矩形 II 2723 静态二维数点
- 3590. 第 K 小的路径异或和 第
小 - 3245. 交替组 III 3112 环形数组
- 3027. 人员站位的方案数 II CDQ 分治
- 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
- 3768. 固定长度子数组中的最小逆序对数目 2158
- 1850. 邻位交换的最小次数 非暴力做法
- 2193. 得到回文串的最少操作次数 非暴力做法
- 1885. 统计数对(会员题)
§8.3 线段树(无区间更新)
线段树本质是二叉树,在学习之前,建议先做做 104. 二叉树的最大深度 和 111. 二叉树的最小深度(自底向上写法),当作热身。
线段树:为什么要这样设计? 理解线段树发明的动机。
把任意区间用
- 询问:把询问区间拆分成
个区间,对应着线段树的 个节点,把这 个节点的信息合并,即为答案。 - 单点更新:有
个区间包含被修改的位置,需要更新 个节点的信息。
基础模板代码如下。为方便入门理解,我没有做复杂封装。更通用的写法可以参考 AtCoder Library 的 segtree.hpp。
# 模板来源 ./09-常用数据结构
# 线段树有两个下标,一个是线段树节点的下标,另一个是线段树维护的区间的下标
# 节点的下标:从 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)// 模板来源 ./09-常用数据结构
// 线段树有两个下标,一个是线段树节点的下标,另一个是线段树维护的区间的下标
// 节点的下标:从 1 开始,如果你想改成从 0 开始,需要把左右儿子下标分别改成 node*2+1 和 node*2+2
// 区间的下标:从 0 开始
class SegmentTree {
private final int n;
private final long[] tree; // 如果计算结果没有超出 int 范围,可以改成 int
// 合并两个 val
private long mergeVal(long a, long b) {
return Math.max(a, b); // **根据题目修改**
}
// 线段树维护一个长为 n 的数组(下标从 0 到 n-1),元素初始值为 initVal
public SegmentTree(int n, long initVal) {
this.n = n;
long[] a = new long[n];
Arrays.fill(a, initVal);
tree = new long[2 << (32 - Integer.numberOfLeadingZeros(n - 1))];
build(a, 1, 0, n - 1);
}
// 线段树维护数组 a
public SegmentTree(long[] a) {
n = a.length;
tree = new long[2 << (32 - Integer.numberOfLeadingZeros(n - 1))];
build(a, 1, 0, n - 1);
}
// 更新 a[i] 为 mergeVal(a[i], val)
// 时间复杂度 O(log n)
public void update(int i, long val) {
update(1, 0, n - 1, i, val);
}
// 返回用 mergeVal 合并所有 a[i] 的计算结果,其中 i 在闭区间 [ql, qr] 中
// 时间复杂度 O(log n)
public long query(int ql, int qr) {
return query(1, 0, n - 1, ql, qr);
}
// 获取 a[i] 的值
// 时间复杂度 O(log n)
public long get(int i) {
return query(1, 0, n - 1, i, i);
}
// 合并左右儿子的 val 到当前节点的 val
private void maintain(int node) {
tree[node] = mergeVal(tree[node * 2], tree[node * 2 + 1]);
}
// 用 a 初始化线段树
// 时间复杂度 O(n)
private void build(long[] a, int node, int l, int r) {
if (l == r) { // 叶子
tree[node] = a[l]; // 初始化叶节点的值
return;
}
int m = (l + r) >>> 1;
build(a, node * 2, l, m); // 初始化左子树
build(a, node * 2 + 1, m + 1, r); // 初始化右子树
maintain(node);
}
private void update(int node, int l, int r, int i, long val) {
if (l == r) { // 叶子(到达目标)
// 如果想直接替换的话,可以写 tree[node] = val
tree[node] = mergeVal(tree[node], val);
return;
}
int m = (l + r) >>> 1;
if (i <= m) { // i 在左子树
update(node * 2, l, m, i, val);
} else { // i 在右子树
update(node * 2 + 1, m + 1, r, i, val);
}
maintain(node);
}
private long query(int node, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
return tree[node];
}
int m = (l + r) >>> 1;
if (qr <= m) { // [ql, qr] 与右子树无交集,仅需递归左子树
return query(node * 2, l, m, ql, qr);
}
if (ql > m) { // [ql, qr] 与左子树无交集,仅需递归右子树
return query(node * 2 + 1, m + 1, r, ql, qr);
}
// [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
long lRes = query(node * 2, l, m, ql, qr);
long rRes = query(node * 2 + 1, m + 1, r, ql, qr);
return mergeVal(lRes, rRes);
}
}// 模板来源 ./09-常用数据结构
// 线段树有两个下标,一个是线段树节点的下标,另一个是线段树维护的区间的下标
// 节点的下标:从 1 开始,如果你想改成从 0 开始,需要把左右儿子下标分别改成 node*2+1 和 node*2+2
// 区间的下标:从 0 开始
template<typename T>
class SegmentTree {
// 注:也可以去掉 template<typename T>,改在这里定义 T
// using T = pair<int, int>;
int n;
vector<T> tree;
// 合并两个 val
T merge_val(T a, T b) const {
return max(a, b); // **根据题目修改**
}
// 合并左右儿子的 val 到当前节点的 val
void maintain(int node) {
tree[node] = merge_val(tree[node * 2], tree[node * 2 + 1]);
}
// 用 a 初始化线段树
// 时间复杂度 O(n)
void build(const vector<T>& a, int node, int l, int r) {
if (l == r) { // 叶子
tree[node] = a[l]; // 初始化叶节点的值
return;
}
int m = (l + r) >> 1;
build(a, node * 2, l, m); // 初始化左子树
build(a, node * 2 + 1, m + 1, r); // 初始化右子树
maintain(node);
}
void update(int node, int l, int r, int i, T val) {
if (l == r) { // 叶子(到达目标)
// 如果想直接替换的话,可以写 tree[node] = val
tree[node] = merge_val(tree[node], val);
return;
}
int m = (l + r) >> 1;
if (i <= m) { // i 在左子树
update(node * 2, l, m, i, val);
} else { // i 在右子树
update(node * 2 + 1, m + 1, r, i, val);
}
maintain(node);
}
T query(int node, int l, int r, int ql, int qr) const {
if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
return tree[node];
}
int m = (l + r) >> 1;
if (qr <= m) { // [ql, qr] 与右子树无交集,仅需递归左子树
return query(node * 2, l, m, ql, qr);
}
if (ql > m) { // [ql, qr] 与左子树无交集,仅需递归右子树
return query(node * 2 + 1, m + 1, r, ql, qr);
}
// [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
T l_res = query(node * 2, l, m, ql, qr);
T r_res = query(node * 2 + 1, m + 1, r, ql, qr);
return merge_val(l_res, r_res);
}
public:
// 线段树维护一个长为 n 的数组(下标从 0 到 n-1),元素初始值为 init_val
SegmentTree(int n, T init_val) : SegmentTree(vector<T>(n, init_val)) {}
// 线段树维护数组 a
SegmentTree(const vector<T>& a) : n(a.size()), tree(2 << bit_width(a.size() - 1)) {
build(a, 1, 0, n - 1);
}
// 更新 a[i] 为 merge_val(a[i], val)
// 时间复杂度 O(log n)
void update(int i, T val) {
update(1, 0, n - 1, i, val);
}
// 返回用 merge_val 合并所有 a[i] 的计算结果,其中 i 在闭区间 [ql, qr] 中
// 时间复杂度 O(log n)
T query(int ql, int qr) const {
return query(1, 0, n - 1, ql, qr);
}
// 获取 a[i] 的值
// 时间复杂度 O(log n)
T get(int i) const {
return query(1, 0, n - 1, i, i);
}
};
int main() {
SegmentTree t(8, 0LL); // 如果这里写 0LL,那么 SegmentTree 存储的就是 long long 数据
t.update(0, 1LL << 60);
cout << t.query(0, 7) << endl;
vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
// 注意:如果要让 SegmentTree 存储 long long 数据,需要传入 vector<long long>
SegmentTree t2(nums); // 这里 SegmentTree 存储的是 int 数据
cout << t2.query(0, 7) << endl;
return 0;
}// 模板来源 ./09-常用数据结构
// 线段树有两个下标,一个是线段树节点的下标,另一个是线段树维护的区间的下标
// 节点的下标:从 1 开始,如果你想改成从 0 开始,需要把左右儿子下标分别改成 node*2+1 和 node*2+2
// 区间的下标:从 0 开始
type seg []struct {
val int // **根据题目修改**
}
// 合并两个 val
func (seg) mergeVal(a, b int) int {
return max(a, b) // **根据题目修改**
}
// 线段树维护一个长为 n 的数组(下标从 0 到 n-1),元素初始值为 initVal
func newSegmentTree(n int, initVal int) seg {
a := make([]int, n)
for i := range a {
a[i] = initVal
}
return newSegmentTreeWithArray(a)
}
// 线段树维护数组 a
func newSegmentTreeWithArray(a []int) seg {
n := len(a)
t := make(seg, 2<<bits.Len(uint(n-1)))
t.build(a, 1, 0, n-1)
return t
}
// 合并左右儿子的 val 到当前节点的 val
func (t seg) maintain(node int) {
t[node].val = t.mergeVal(t[node*2].val, t[node*2+1].val)
}
// 用 a 初始化线段树
// 时间复杂度 O(n)
func (t seg) build(a []int, node, l, r int) {
if l == r { // 叶子
t[node].val = a[l] // 初始化叶节点的值
return
}
m := (l + r) >> 1
t.build(a, node*2, l, m) // 初始化左子树
t.build(a, node*2+1, m+1, r) // 初始化右子树
t.maintain(node)
}
// 更新 a[i] 为 mergeVal(a[i], val)
// 调用 t.update(1, 0, n-1, i, val)
// 0 <= i <= n-1
// 时间复杂度 O(log n)
func (t seg) update(node, l, r, i int, val int) {
if l == r { // 叶子(到达目标)
// 如果想直接替换的话,可以写 t[o].val = val
t[node].val = t.mergeVal(t[node].val, val)
return
}
m := (l + r) >> 1
if i <= m { // i 在左子树
t.update(node*2, l, m, i, val)
} else { // i 在右子树
t.update(node*2+1, m+1, r, i, val)
}
t.maintain(node)
}
// 返回用 mergeVal 合并所有 a[i] 的计算结果,其中 i 在闭区间 [ql, qr] 中
// 调用 t.query(1, 0, n-1, ql, qr)
// 如果只想获取 a[i],可以调用 t.query(1, 0, n-1, i, i)
// 0 <= ql <= qr <= n-1
// 时间复杂度 O(log n)
func (t seg) query(node, l, r, ql, qr int) int {
if ql <= l && r <= qr { // 当前子树完全在 [ql, qr] 内
return t[node].val
}
m := (l + r) >> 1
if qr <= m { // [ql, qr] 与右子树无交集,仅需递归左子树
return t.query(node*2, l, m, ql, qr)
}
if ql > m { // [ql, qr] 与左子树无交集,仅需递归右子树
return t.query(node*2+1, m+1, r, ql, qr)
}
// [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
lRes := t.query(node*2, l, m, ql, qr)
rRes := t.query(node*2+1, m+1, r, ql, qr)
return t.mergeVal(lRes, rRes)
}- 3479. 水果成篮 III 2178 线段树二分
- 2940. 找到 Alice 和 Bob 可以相遇的建筑 2327 线段树二分
- 2286. 以组为单位订音乐会的门票 2470 线段树二分
- 3161. 物块放置查询 2513
- 3901. 好子序列查询 2545
- 2213. 由单个字符重复的最长子字符串 2629
- 3777. 使子字符串变交替的最少删除次数
- 3525. 求出数组的 X 值 II 2645
- 3165. 不包含相邻元素的子序列的最大和 2697
- 3410. 删除所有值为某个元素后的最大子数组和 2844 做法不止一种
- 3501. 操作后最大活跃区段数 II 2941 也可以用 ST 表做
- LCP 81. 与非的谜题
思维扩展:
§8.4 Lazy 线段树(有区间更新)
把任意区间用
- 询问:把询问区间拆分成
个区间,对应着线段树的 个节点,把这 个节点的信息合并,即为答案。 - 区间更新:仍然是拆分成
个区间,对应着线段树的 个节点。但对于其中的非叶节点,不把更新的内容往下传递给子节点,而是记录「发生了更新,内容为 xxx」,把更新的内容记录下来。直到后续的询问或更新操作,需要访问或修改更下面的子节点信息时,才把更新的内容往下传。
基础模板代码如下。为方便入门理解,我没有做复杂封装。更通用的写法可以参考 AtCoder Library 的 lazysegtree.hpp。
# 模板来源 ./09-常用数据结构
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)// 模板来源 ./09-常用数据结构
class LazySegmentTree {
// 懒标记初始值
private static final long TODO_INIT = 0; // **根据题目修改**
private static final class Node {
long val; // **根据题目修改**
long todo;
}
// 合并两个 val
private long mergeVal(long a, long b) {
return a + b; // **根据题目修改**
}
// 合并两个懒标记
private long mergeTodo(long a, long b) {
return a + b; // **根据题目修改**
}
// 把懒标记作用到 node 子树(本例为区间加)
private void apply(int node, int l, int r, long todo) {
Node cur = tree[node];
// 计算 tree[node] 区间的整体变化
cur.val += todo * (r - l + 1); // **根据题目修改**
cur.todo = mergeTodo(todo, cur.todo);
}
private final int n;
private final Node[] tree;
// 线段树维护一个长为 n 的数组(下标从 0 到 n-1),元素初始值为 initVal
public LazySegmentTree(int n, long initVal) {
this.n = n;
long[] a = new long[n];
Arrays.fill(a, initVal);
tree = new Node[2 << (32 - Integer.numberOfLeadingZeros(n - 1))];
build(a, 1, 0, n - 1);
}
// 线段树维护数组 a
public LazySegmentTree(long[] a) {
n = a.length;
tree = new Node[2 << (32 - Integer.numberOfLeadingZeros(n - 1))];
build(a, 1, 0, n - 1);
}
// 用 f 更新 [ql, qr] 中的每个 a[i]
// 0 <= ql <= qr <= n-1
// 时间复杂度 O(log n)
public void update(int ql, int qr, long f) {
update(1, 0, n - 1, ql, qr, f);
}
// 返回用 mergeVal 合并所有 a[i] 的计算结果,其中 i 在闭区间 [ql, qr] 中
// 0 <= ql <= qr <= n-1
// 时间复杂度 O(log n)
public long query(int ql, int qr) {
return query(1, 0, n - 1, ql, qr);
}
// 把当前节点的懒标记下传给左右儿子
private void spread(int node, int l, int r) {
long todo = tree[node].todo;
if (todo == TODO_INIT) { // 没有需要下传的信息
return;
}
int m = (l + r) >>> 1;
apply(node * 2, l, m, todo);
apply(node * 2 + 1, m + 1, r, todo);
tree[node].todo = TODO_INIT; // 下传完毕
}
// 合并左右儿子的 val 到当前节点的 val
private void maintain(int node) {
tree[node].val = mergeVal(tree[node * 2].val, tree[node * 2 + 1].val);
}
// 用 a 初始化线段树
// 时间复杂度 O(n)
private void build(long[] a, int node, int l, int r) {
tree[node] = new Node();
tree[node].todo = TODO_INIT;
if (l == r) { // 叶子
tree[node].val = a[l]; // 初始化叶节点的值
return;
}
int m = (l + r) >>> 1;
build(a, node * 2, l, m); // 初始化左子树
build(a, node * 2 + 1, m + 1, r); // 初始化右子树
maintain(node);
}
private void update(int node, int l, int r, int ql, int qr, long f) {
if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
apply(node, l, r, f);
return;
}
spread(node, l, r);
int m = (l + r) >>> 1;
if (ql <= m) { // 更新左子树
update(node * 2, l, m, ql, qr, f);
}
if (qr > m) { // 更新右子树
update(node * 2 + 1, m + 1, r, ql, qr, f);
}
maintain(node);
}
private long query(int node, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
return tree[node].val;
}
spread(node, l, r);
int m = (l + r) >>> 1;
if (qr <= m) { // [ql, qr] 与右子树无交集,仅需递归左子树
return query(node * 2, l, m, ql, qr);
}
if (ql > m) { // [ql, qr] 与左子树无交集,仅需递归右子树
return query(node * 2 + 1, m + 1, r, ql, qr);
}
// [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
long lRes = query(node * 2, l, m, ql, qr);
long rRes = query(node * 2 + 1, m + 1, r, ql, qr);
return mergeVal(lRes, rRes);
}
}// 模板来源 ./09-常用数据结构
template<typename T, typename F>
class LazySegmentTree {
// 注:也可以去掉 template<typename T, typename F>,改在这里定义 T 和 F
// using T = pair<int, int>;
// using F = pair<int, int>;
// 懒标记初始值
const F TODO_INIT = 0; // **根据题目修改**
struct Node {
T val;
F todo;
};
int n;
vector<Node> tree;
// 合并两个 val
T merge_val(const T& a, const T& b) const {
return a + b; // **根据题目修改**
}
// 合并两个懒标记
F merge_todo(const F& a, const F& b) const {
return a + b; // **根据题目修改**
}
// 把懒标记作用到 node 子树(本例为区间加)
void apply(int node, int l, int r, F todo) {
Node& cur = tree[node];
// 计算 tree[node] 区间的整体变化
cur.val += todo * (r - l + 1); // **根据题目修改**
cur.todo = merge_todo(todo, cur.todo);
}
// 把当前节点的懒标记下传给左右儿子
void spread(int node, int l, int r) {
Node& cur = tree[node];
F todo = cur.todo;
if (todo == TODO_INIT) { // 没有需要下传的信息
return;
}
int m = (l + r) >> 1;
apply(node * 2, l, m, todo);
apply(node * 2 + 1, m + 1, r, todo);
cur.todo = TODO_INIT; // 下传完毕
}
// 合并左右儿子的 val 到当前节点的 val
void maintain(int node) {
tree[node].val = merge_val(tree[node * 2].val, tree[node * 2 + 1].val);
}
// 用 a 初始化线段树
// 时间复杂度 O(n)
void build(const vector<T>& a, int node, int l, int r) {
Node& cur = tree[node];
cur.todo = TODO_INIT;
if (l == r) { // 叶子
cur.val = a[l]; // 初始化叶节点的值
return;
}
int m = (l + r) >> 1;
build(a, node * 2, l, m); // 初始化左子树
build(a, node * 2 + 1, m + 1, r); // 初始化右子树
maintain(node);
}
void update(int node, int l, int r, int ql, int qr, F f) {
if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
apply(node, l, r, f);
return;
}
spread(node, l, r);
int m = (l + r) >> 1;
if (ql <= m) { // 更新左子树
update(node * 2, l, m, ql, qr, f);
}
if (qr > m) { // 更新右子树
update(node * 2 + 1, m + 1, r, ql, qr, f);
}
maintain(node);
}
T query(int node, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
return tree[node].val;
}
spread(node, l, r);
int m = (l + r) >> 1;
if (qr <= m) { // [ql, qr] 与右子树无交集,仅需递归左子树
return query(node * 2, l, m, ql, qr);
}
if (ql > m) { // [ql, qr] 与左子树无交集,仅需递归右子树
return query(node * 2 + 1, m + 1, r, ql, qr);
}
// [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
T l_res = query(node * 2, l, m, ql, qr);
T r_res = query(node * 2 + 1, m + 1, r, ql, qr);
return merge_val(l_res, r_res);
}
public:
// 线段树维护一个长为 n 的数组(下标从 0 到 n-1),元素初始值为 init_val
LazySegmentTree(int n, T init_val = 0) : LazySegmentTree(vector<T>(n, init_val)) {}
// 线段树维护数组 a
LazySegmentTree(const vector<T>& a) : n(a.size()), tree(2 << bit_width(a.size() - 1)) {
build(a, 1, 0, n - 1);
}
// 用 f 更新 [ql, qr] 中的每个 a[i]
// 0 <= ql <= qr <= n-1
// 时间复杂度 O(log n)
void update(int ql, int qr, F f) {
update(1, 0, n - 1, ql, qr, f);
}
// 返回用 merge_val 合并所有 a[i] 的计算结果,其中 i 在闭区间 [ql, qr] 中
// 0 <= ql <= qr <= n-1
// 时间复杂度 O(log n)
T query(int ql, int qr) {
return query(1, 0, n - 1, ql, qr);
}
};
int main() {
LazySegmentTree<long long, long long> t(8); // 默认值为 0
t.update(3, 5, 100);
t.update(4, 6, 10);
cout << t.query(0, 7) << endl;
vector<long long> nums = {3, 1, 4, 1, 5, 9, 2, 6};
LazySegmentTree<long long, long long> t2(nums);
t2.update(3, 5, 1);
t2.update(4, 6, 1);
cout << t2.query(0, 7) << endl;
return 0;
}// 模板来源 ./09-常用数据结构
// 懒标记初始值
const todoInit int = 0 // **根据题目修改**
type lazySeg []struct {
val int // **根据题目修改**
todo int
}
// 合并两个 val
func (lazySeg) mergeVal(a, b int) int {
return a + b // **根据题目修改**
}
// 合并两个懒标记
func (lazySeg) mergeTodo(a, b int) int {
return a + b // **根据题目修改**
}
// 把懒标记作用到 node 子树(本例为区间加)
func (t lazySeg) apply(node, l, r int, todo int) {
cur := &t[node]
// 计算 tree[node] 区间的整体变化
cur.val += todo * (r - l + 1) // **根据题目修改**
cur.todo = t.mergeTodo(todo, cur.todo)
}
// 线段树维护一个长为 n 的数组(下标从 0 到 n-1),元素初始值为 initVal
func newLazySegmentTree(n int, initVal int) lazySeg {
a := make([]int, n)
for i := range a {
a[i] = initVal
}
return newLazySegmentTreeWithArray(a)
}
// 线段树维护数组 a
func newLazySegmentTreeWithArray(a []int) lazySeg {
n := len(a)
t := make(lazySeg, 2<<bits.Len(uint(n-1)))
t.build(a, 1, 0, n-1)
return t
}
// 把当前节点的懒标记下传给左右儿子
func (t lazySeg) spread(node, l, r int) {
// 类似「断点续传」,接着完成之前没完成的下传任务
todo := t[node].todo
if todo == todoInit { // 没有需要下传的信息
return
}
m := (l + r) >> 1
t.apply(node*2, l, m, todo)
t.apply(node*2+1, m+1, r, todo)
t[node].todo = todoInit // 下传完毕
}
// 合并左右儿子的 val 到当前节点的 val
func (t lazySeg) maintain(node int) {
t[node].val = t.mergeVal(t[node*2].val, t[node*2+1].val)
}
// 用 a 初始化线段树
// 时间复杂度 O(n)
func (t lazySeg) build(a []int, node, l, r int) {
t[node].todo = todoInit
if l == r { // 叶子
t[node].val = a[l] // 初始化叶节点的值
return
}
m := (l + r) >> 1
t.build(a, node*2, l, m) // 初始化左子树
t.build(a, node*2+1, m+1, r) // 初始化右子树
t.maintain(node)
}
// 用 f 更新 [ql, qr] 中的每个 a[i]
// 调用 t.update(1, 0, n-1, ql, qr, f)
// 0 <= ql <= qr <= n-1
// 时间复杂度 O(log n)
func (t lazySeg) update(node, l, r, ql, qr int, f int) {
if ql <= l && r <= qr { // 当前子树完全在 [ql, qr] 内
t.apply(node, l, r, f)
return
}
t.spread(node, l, r)
m := (l + r) >> 1
if ql <= m { // 更新左子树
t.update(node*2, l, m, ql, qr, f)
}
if qr > m { // 更新右子树
t.update(node*2+1, m+1, r, ql, qr, f)
}
t.maintain(node)
}
// 返回用 mergeVal 合并所有 a[i] 的计算结果,其中 i 在闭区间 [ql, qr] 中
// 调用 t.query(1, 0, n-1, ql, qr)
// 0 <= ql <= qr <= n-1
// 时间复杂度 O(log n)
func (t lazySeg) query(node, l, r, ql, qr int) int {
if ql <= l && r <= qr { // 当前子树完全在 [ql, qr] 内
return t[node].val
}
t.spread(node, l, r)
m := (l + r) >> 1
if qr <= m { // [ql, qr] 与右子树无交集,仅需递归左子树
return t.query(node*2, l, m, ql, qr)
}
if ql > m { // [ql, qr] 与左子树无交集,仅需递归右子树
return t.query(node*2+1, m+1, r, ql, qr)
}
// [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
lRes := t.query(node*2, l, m, ql, qr)
rRes := t.query(node*2+1, m+1, r, ql, qr)
return t.mergeVal(lRes, rRes)
}- 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 < rclass SparseTable {
private final int[][] stMin;
private final int[][] stMax;
// 时间复杂度 O(n * log n)
public SparseTable(int[] nums) {
int n = nums.length;
int w = 32 - Integer.numberOfLeadingZeros(n);
stMin = new int[w][n];
stMax = new int[w][n];
for (int j = 0; j < n; j++) {
stMin[0][j] = nums[j];
stMax[0][j] = nums[j];
}
for (int i = 1; i < w; i++) {
for (int j = 0; j <= n - (1 << i); j++) {
stMin[i][j] = Math.min(stMin[i - 1][j], stMin[i - 1][j + (1 << (i - 1))]);
stMax[i][j] = Math.max(stMax[i - 1][j], stMax[i - 1][j + (1 << (i - 1))]);
}
}
}
// [l, r) 左闭右开,下标从 0 开始
// 必须保证 l < r
// 时间复杂度 O(1)
public int queryMin(int l, int r) {
int k = 31 - Integer.numberOfLeadingZeros(r - l);
return Math.min(stMin[k][l], stMin[k][r - (1 << k)]);
}
// [l, r) 左闭右开,下标从 0 开始
// 必须保证 l < r
// 时间复杂度 O(1)
public int queryMax(int l, int r) {
int k = 31 - Integer.numberOfLeadingZeros(r - l);
return Math.max(stMax[k][l], stMax[k][r - (1 << k)]);
}
}class SparseTable {
vector<vector<int>> st_min;
vector<vector<int>> st_max;
public:
// 时间复杂度 O(n * log n)
SparseTable(const vector<int>& nums) {
int n = nums.size();
int w = bit_width((uint32_t) n);
st_min.resize(w, vector<int>(n));
st_max.resize(w, vector<int>(n));
for (int j = 0; j < n; j++) {
st_min[0][j] = nums[j];
st_max[0][j] = nums[j];
}
for (int i = 1; i < w; i++) {
for (int j = 0; j <= n - (1 << i); j++) {
st_min[i][j] = min(st_min[i - 1][j], st_min[i - 1][j + (1 << (i - 1))]);
st_max[i][j] = max(st_max[i - 1][j], st_max[i - 1][j + (1 << (i - 1))]);
}
}
}
// [l, r) 左闭右开,下标从 0 开始
// 必须保证 l < r
// 时间复杂度 O(1)
int query_min(int l, int r) const {
int k = bit_width((uint32_t) r - l) - 1;
return min(st_min[k][l], st_min[k][r - (1 << k)]);
}
// [l, r) 左闭右开,下标从 0 开始
// 必须保证 l < r
// 时间复杂度 O(1)
int query_max(int l, int r) const {
int k = bit_width((uint32_t) r - l) - 1;
return max(st_max[k][l], st_max[k][r - (1 << k)]);
}
};type sparseTable[T any] struct {
st [][]T
op func(T, T) T
}
// 时间复杂度 O(n * log n)
func newSparseTable[T any](nums []T, op func(T, T) T) sparseTable[T] {
n := len(nums)
w := bits.Len(uint(n))
st := make([][]T, w)
for i := range st {
st[i] = make([]T, n)
}
copy(st[0], nums)
for i := 1; i < w; i++ {
for j := range n - 1<<i + 1 {
st[i][j] = op(st[i-1][j], st[i-1][j+1<<(i-1)])
}
}
return sparseTable[T]{st, op}
}
// [l, r) 左闭右开,下标从 0 开始
// 返回 op(nums[l:r])
// 时间复杂度 O(1)
func (s sparseTable[T]) query(l, r int) T {
k := bits.Len(uint(r-l)) - 1
return s.op(s.st[k][l], s.st[k][r-1<<k])
}
// 使用方法举例
func main() {
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
st := newSparseTable(nums, func(a, b int) int { return max(a, b) })
fmt.Println(st.query(0, 5)) // 5
fmt.Println(st.query(1, 1)) // 错误:必须保证 l < r
}- 3691. 最大子数组总值 II 2469
- 3501. 操作后最大活跃区段数 II 2941
二维 ST 表:
九、伸展树(Splay 树)
十、根号算法
§10.1 分块
注:上面树状数组和线段树的题目,可以尝试用分块解决。例如 307. 区域和检索 - 数组可修改。
- 3943. 递增后的数对数量 2410
§10.2 根号分解(Sqrt Decomposition)
- 3655. 区间乘法查询后的异或 II 2454
- LCP 16. 游乐园的游览计划
- 1714. 数组中特殊等间距元素的和(会员题)
- 3400. 右移后的最大匹配索引数(会员题)非暴力做法
§10.3 莫队算法
属于离线算法的一种。
- 3636. 查询超过阈值频率最高元素 2451 回滚莫队
- 3590. 第 K 小的路径异或和 2646 做法不止一种
- 2846. 边权重均等查询 树上莫队 和边权范围无关的做法
§10.4 其他
- 3234. 统计 1 显著的字符串的数量 2557
专题:离线算法
通过改变回答询问的顺序,使问题更容易处理。
相应的,在线算法就是按照
的顺序一个一个处理。
- 2343. 裁剪数字后查询第 K 小的数字 1652
- 3607. 电网维护 1700
- 2070. 每一个查询的最大美丽值 1724
- 1847. 最近的房间 2082
- 2503. 矩阵查询可获得的最大分数 2196
- 1851. 包含每个查询的最小区间 2286
- 1697. 检查边长度限制的路径是否存在 2300
- 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站@灵茶山艾府
如果你发现有题目可以补充进来,欢迎评论反馈。