欢迎大家订阅我的专栏:算法题解:C++与Python实现!*
本专栏旨在帮助大家从基础到进阶 ,逐步提升编程能力,助力信息学竞赛备战!
专栏特色
1.经典算法练习:根据信息学竞赛大纲,精心挑选经典算法题目,提供清晰的代码实现与详细指导,帮助您夯实算法基础。
2.系统化学习路径:按照算法类别和难度分级,从基础到进阶,循序渐进,帮助您全面提升编程能力与算法思维。
适合人群:
- 准备参加蓝桥杯、GESP、CSP-J、CSP-S等信息学竞赛的学生
- 希望系统学习C++/Python编程的初学者
- 想要提升算法与编程能力的编程爱好者
这篇文档其实是我自己学习路上的一份“复习笔记”。之前看了不少教程,零零散散记了一堆代码片段和小技巧,时间久了就容易忘,翻起来也麻烦。所以干脆花时间整理了一下,把常用的模板、容易踩的坑、还有一些个人觉得挺实用的小技巧都归到了一起。
如果你也在学类似的东西,或许能帮你省下一些翻找的时间;如果有哪里写得不清楚或不对,也欢迎随时指正交流。
希望这份整理也能对你有一点点帮助~ ✨
【队列】
算法模板
手搓队列
int q[N], hh, tt=-1;
// 插入
q[++tt] = x;
// 弹出
hh++;
//判断队列是否为空
if (hh<=tt) not empty
else empty
//取出队头元素
q[hh]
STL队列
queue<int> q;
// 插入
q.push();
// 弹出
q.pop();
// 判断队列是否为空
q.empty();
// 取出队头元素
q.front();
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | 829 模拟队列 | 简单 | ⭐ | 队列(数组模拟) | 用数组 q[N] 模拟队列,维护队首指针 hh(初始为
0
0
0)和队尾指针 tt(初始为
−
1
-1
−1),push 时 q[++tt]=x,pop 时 hh++,query 输出 q[hh],empty 判断 hh<=tt 是否成立。 |
| AcWing | 6034 周末舞会 | ||||
| AcWing | 6035 Blah数集 | ||||
| AcWing | 6036 围圈报数 | ||||
| 洛谷 | AT_abc402_b Restaurant Queue | 入门 | |||
| 洛谷 | AT_abc335_c Loong Tracking | 普及- | |||
| 洛谷 | AT_abc413_c Large Queue | 普及- | |||
| 洛谷 | B3616 队列 | 普及- | |||
| 洛谷 | [[B4295 报数游戏]] | 普及- | |||
| 洛谷 | [[P2952 Cow Line]] | 普及- | |||
| 洛谷 | P3887 世界杯 | 普及- | |||
| 洛谷 | P5661 公交换乘 | 普及- | |||
| 洛谷 | [[P11375 树上游走]] | 普及- | |||
| 洛谷 | AT_abc379_d Home Garden | 普及 | |||
| 洛谷 | AT_abc391_d Gravity | 普及 | |||
| 洛谷 | P2058 海港 | 普及 | |||
| 洛谷 | P14924 宝石项链 | 普及+ |
【链表】
算法模板
list基本操作
(1) 获取起始迭代器: it = ls.begin(); // 指向第一个元素的首地址
(2) 获取结束迭代器: it = ls.end(); // 指向最后一个元素的尾地址
(3) 解引用访问数据: int v = *it; // 获取当前迭代器指向的元素值
(4) 向前/后遍历: it++或++it / it--或--it; // 移动到下/上一个节点
it = ls.end(), it--; // it指向名为ls的list的最后一个元素
(5) 移动操作:advance(it, n); // 将迭代器it向前或向后移动n个位置
next(it); // 返回it的下一个位置的迭代器(不修改原迭代器)
prev(it); // 返回it的上一个位置的迭代器(不修改原迭代器)
(6) 判断两个迭代器的关系:== / !=; // 判断两个迭代器是否指向同一元素
注意:链表迭代器不能比较大小
遍历方法一:用迭代器循环遍历链表;
for (list<int>:: iterator it = ls.begin(); it != ls.end(); it++)
cout << *it << endl;
迭代器类型:list<int>:: iterator是双向链表迭代器,支持++和--。
终止条件:it != ls.end() 确保遍历到最后一个有效元素后停止。
前缀递增:++it 或 it++ 即可,表示将迭代器指向下一个元素。
遍历方法二:先定义迭代器,再循环遍历;
list<int>:: iterator it;
for (it = ls.begin(); it != ls.end(); it++)
cout << *it << endl;
遍历方法三:使用auto自动推导迭代器类型来遍历链表;
for (auto it = ls.begin(); it != ls.end(); it++)
cout << *it << endl;
遍历方法四:范围循环——自动匹配元素类型,并自动遍历;
for (auto x : ls) cout << x << endl;
(7) ls.push_back(x) / ls.push_front(x): 在链表ls的尾部/头部插入一个元素x;
(8) ls.insert(): 在链表ls中插入元素,返回新插入元素的迭代器;
① ls.insert(it, y); // 在链表ls中it指向的位置之前插入元素y;
② ls.insert(it, x, y); // 在链表ls中it指向的位置之前插入x个元素y;
③ ls.insert(it, it1, it2); // 在链表ls中it指向的位置之前插入[it1, it2]之间的所有元素;
(9) ls.erase(it): 在链表ls中删除it所指向的元素;
若删除的元素在list的尾部,返回ls.end();
若删除的元素不再list的尾部,返回下一个元素的迭代器;
(10) ls.empty(): 判断链表ls是否为空;
(11) ls.clear(): 清空链表sl
(12) ls.size(): 返回链表ls中的元素个数;
(13) ls.remove(x): 在链表ls中删除所有等于x的元素;
(14) ls.reverse(): 反转链表ls中元素的顺序;
(15) ls.splice(): 将一个链表或子链表移动到当前链表的执行位置;
① 移动整个链表: s.splice(it, t); 将整个链表t都移动到链表s的指定位置it处;
② 移动单个元素: s.splice(it1, t, it2); 将链表t中it2指向的元素移动到链表s的指定位置it1处;
③ 移动子链表: s.splice(it1, t, it2, it3); 将链表t中[it2, it3)之间的元素移动到链表s的指定位置it1处;
单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void add_to_head(int a)
{
e[idx] = a, ne[idx] = head, head = idx++;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx++;
}
// 将头结点删除,需要保证头结点存在
void remove_head()
{
head = ne[head];
}
// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
//在下标是k的点的右边,插入x
void add(int k, int x)
{
e[idx] = x;
r[idx] = r[k], l[idx] = k;
l[r[k]] = idx, r[k] = idx++;
}
//删除第k个点
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | 826 单链表 | 简单 | ⭐ | 链表(数组模拟) | 用数组 e[N] 存储节点值、ne[N] 存储下一个节点下标,维护头指针 head(初始
−
1
-1
−1)和可用节点索引 idx(初始
0
0
0),H x 时头插(e[idx]=x, ne[idx]=head, head=idx++),I k x 时在"第
k
k
k 个插入的节点"后插入(e[idx]=x, ne[idx]=ne[k-1], ne[k-1]=idx++),D k 时删除其后节点(k=0 删头结点,否则 ne[k-1]=ne[ne[k-1]]),最后从头指针遍历输出。 |
| AcWing | 827 双链表 | 简单 | ⭐ | 双链表(数组模拟) | 用 0 和 1 作为左右哨兵节点,e[idx] 存值、l[idx] 和 r[idx] 分别存左右指针,插入时在节点
k
k
k 右侧插入新节点并更新四个指针(r[idx]=r[k], l[idx]=k, l[r[k]]=idx, r[k]=idx),删除时让左右节点互相指向(r[l[k]]=r[k], l[r[k]]=l[k]),L x 在左哨兵右侧插入,R x 在右哨兵左侧插入,IL k x 在
k
k
k 左侧插入(即
l
[
k
]
l[k]
l[k] 右侧),D k 直接删除第
k
k
k 个节点,最后从 r[0] 遍历到哨兵
1
1
1 输出。 |
| AcWing | 4089 小熊的果篮 | ||||
| 洛谷 | [[P10264 接竹竿]] | 普及+ | |||
| 洛谷 | P5688 散步 | 省选 |
【单调队列】
算法模板
struct Node
{
int v, idx;
};
deque<Node> dq;
for (int i = 1; i <= n; i++) // 遍历数组
{
while (!dq.empty() && dq.front().idx <= i - k)
dq.pop_front();
// 维护minq队列:保持队列单调递增
// 如果修改为dq.back().v <= a[i],则维护maxq队列:保持队列单调递减
while (!dq.empty() && dq.back().v >= a[i])
dq.pop_back();
dq.push_back({a[i], i}); // dq.front().v为最小值
// 按照题目要求输出最小值、最大值等
}
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | 135 最大子序和 | ||||
| AcWing | 154 滑动窗口 | 简单 | ⭐⭐ | 单调队列 | 用数组模拟队列存储下标,求最小值时维护单调递增队列:先判断队首是否滑出窗口(q[hh] < i - k + 1 则 hh++),再从队尾弹出所有大于等于当前元素的值(a[q[tt]] >= a[i] 则 tt--),最后将当前下标入队,窗口形成后队首即为最小值;求最大值时同理维护单调递减队列(队尾弹出小于等于当前元素的值),两次遍历分别输出最小值和最大值,总时间复杂度
O
(
n
)
O(n)
O(n)。 |
| AcWing | 1087 修剪草坪 | ||||
| AcWing | 1088 旅行问题 | ||||
| AcWing | 1089 烽火传递 | ||||
| AcWing | 1090 绿色通道 | ||||
| AcWing | 1091 理想的正方形 | ||||
| 洛谷 | P1714 切蛋糕 | 普及 | |||
| 洛谷 | P1886 单调队列 | 普及 | |||
| 洛谷 | P11963 环线 | 普及 | |||
| AtCoder | AT_awc0001_e Temperature Fluctuation Range | ||||
| AtCoder | AT_awc0012_e Stone Crossing Game | ||||
| AtCoder | AT_awc0033_e Minimum Cost of Stepping Stones | ⭐⭐⭐ | 动态规划 + 单调队列优化 | 设 dp[i] 为到达石头
i
i
i 的最小累计成本,转移方程为 dp[i] = min{dp[j]} + A[i](其中
i
−
K
≤
j
<
i
i-K \leq j < i
i−K≤j<i),用单调递增队列维护滑动窗口
[
i
−
K
,
i
−
1
]
[i-K, i-1]
[i−K,i−1] 内的最小 dp 值:先弹出队首过期的元素(下标 < i-K),然后 dp[i] = 队首值 + A[i],再从队尾弹出所有值 >= dp[i] 的元素后入队,保证队首始终为窗口最小值,将转移复杂度从
O
(
N
×
K
)
O(N \times K)
O(N×K) 优化到
O
(
N
)
O(N)
O(N),最终输出 dp[N]。 | |
| 学而思编程 | [[训练计划]] | 提高进阶 | ⭐⭐⭐⭐⭐ | 单调队列优化 DP + 对数比较(避免高精度) | 将问题转化为:将
N
N
N 个数分成至多
K
K
K 段,每段内所有数相乘,求各段乘积之和的最大值;用 Node 结构体记录每个数字
1
∼
9
1\sim9
1∼9 的出现次数,通过对数比较乘积大小(v1 += s[i] * ln[i])避免高精度;sum[i] 为前缀计数数组,d[i] 表示前
i
−
1
i-1
i−1 个数的最优划分;用单调队列维护窗口
[
i
−
K
,
i
−
1
]
[i-K, i-1]
[i−K,i−1] 内的最小值,保证任意连续
K
K
K 个数中至多选
K
K
K 题(即每段长度不超过
K
K
K);d[i] = sum[i-1] - sum[q[st]] + d[q[st]],队列中保持 d[q[j]] + sum[i] - sum[q[j]] 单调递增;最后根据 d[n+1] 中各数字出现次数计算答案乘积并对
10
9
+
7
10^9+7
109+7 取模。 |
【栈】
算法模板
手搓栈
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[++tt] = x;
// 从栈顶弹出一个数
tt--;
// 栈顶的值
stk[tt];
// 判断栈是否为空,如果 tt > 0,则表示不为空
if (tt > 0) not empty
else empty
STL栈
stack<int> stk;
// 向栈顶插入一个数
stk.push(x);
// 栈顶的值
stk.top();
// 判断栈是否为空
stk.empty();
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | 828 模拟栈 | 简单 | ⭐ | 栈(数组模拟) | 用数组 stk[N] 模拟栈,top 指针指向栈顶元素位置(初始为
0
0
0 表示空栈),push x 时 stk[++top] = x(先自增再存值),pop 时 top--(直接下移指针),empty 时判断 top == 0 输出 YES/NO,query 时输出 stk[top],四种操作均为
O
(
1
)
O(1)
O(1),总时间复杂度
O
(
M
)
O(M)
O(M)。 |
| AcWing | [[2769 表达式]] | ||||
| AcWing | 3302 表达式求值 | 中等 | ⭐⭐ | 栈(双栈法) | 用操作数栈 num 和运算符栈 op 分别存储数字和运算符,遍历表达式时遇到数字压入 num 栈,遇到左括号直接入 op 栈,遇到右括号则循环执行 eval()(弹出两个操作数和运算符计算后压回 num)直到遇到左括号,遇到运算符时若栈顶优先级 >= 当前则先 eval() 再入栈,最后清空 op 栈中剩余运算,num 栈顶即为结果;eval() 时注意先弹出的是右操作数 b,后弹出的是左操作数 a。 |
| AcWing | 6027 后缀表达式的值 | ||||
| AcWing | 6028 表达式括号匹配 | ||||
| AcWing | 6029 括弧匹配检验 | ||||
| AcWing | 6030 字符串匹配问题 | ||||
| AcWing | 6031 计算 | ||||
| AcWing | 6032 车厢调度 | ||||
| AcWing | 6033 中缀表达式值 | ||||
| 洛谷 | AT_abc351_c Merge the balls | 普及- | |||
| 洛谷 | AT_abc438_c 1D puyopuyo | 普及- | |||
| 洛谷 | B2165 括号匹配 | 普及- | |||
| 洛谷 | B3614 栈 | 普及- | |||
| 洛谷 | B3758 括号序列 | 普及- | |||
| 洛谷 | P1165 日志分析 | 普及- | |||
| 洛谷 | P1981 表达式求值 | 普及- | |||
| 洛谷 | [[P3056 Clumsy Cows]] | 普及- | |||
| 洛谷 | P10472 括号画家 | 普及- | |||
| 洛谷 | P1944 最长括号匹配 | 普及 | |||
| 洛谷 | [[P1503 鬼子进村]] | 普及+ | |||
| 洛谷 | P3952 时间复杂度 | 普及+ |
【集合】
算法模板
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| 洛谷 | AT_abc366_c Balls and Bag Query | 普及- | |||
| 洛谷 | AT_abc403_c 403 Forbidden | 普及- | |||
| 洛谷 | AT_abc451_c Understory | 普及- | |||
| 洛谷 | P3879 阅读理解 | 普及- | |||
| 洛谷 | AT_abc406_d Garbage Removal | 普及 | |||
| 洛谷 | P5250 木材仓库 | 普及 | |||
| 洛谷 | P7912 小熊的果篮 | 普及+ | |||
| AtCoder | AT_awc0013_b Investigation of Friend Relationships | ||||
| AtCoder | AT_awc0027_d Part-Time Job Shift Assignment |
【并查集】
算法模板
朴素并查集+路径压缩
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
void merge(int a, int b)
{
p[find(a)] = find(b);
}
维护size的并查集
int p[N], siz[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
void merge(int a, int b)
{
if (find(a)==find(b)) return;
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
}
带边权的并查集
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = d[y]-d[x]; // X和Y是同类
d[find(a)] = d[y]+1-d[x]; // X吃Y
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | [[237 程序自动分析]] | 中等 | ⭐⭐⭐ | 并查集 + 离散化 | 先用 unordered_map 对变量编号(可能达
10
9
10^9
109)进行离散化映射为连续编号,然后两遍扫描:第一遍将所有 e=1(相等)约束用并查集合并,第二遍检查所有 e=0(不等)约束是否出现 find(x)==find(y) 的矛盾(即相等与不等冲突),若存在冲突输出 NO,否则输出 YES;注意必须先处理全部相等关系再检查不等关系,否则传递性会导致误判。 |
| AcWing | [[238 银河英雄传说]] | 简单 | ⭐⭐⭐⭐ | 带权并查集(距离向量) | 用 d[x] 记录战舰
x
x
x 到所在列队首的战舰间隔数,siz[x] 记录以
x
x
x 为根的列的战舰总数;find(x) 递归找根的同时用 d[x] += d[p[x]] 累加父节点到根的距离并路径压缩;M a b 合并时将
a
a
a 列接在
b
b
b 列尾部,设 d[pa] = siz[pb](
a
a
a 列根到
b
b
b 列根的距离为
b
b
b 列大小),更新 siz[pb] += siz[pa] 后连边 p[pa] = pb;C a b 时若同根则输出 abs(d[a]-d[b])-1(两舰之间的战舰数),不同根输出 `-1$。 |
| AcWing | [[239 奇偶游戏]] | 中等 | ⭐⭐⭐⭐ | 带权并查集(异或权值) + 离散化 + 前缀和转化 | 将区间
[
l
,
r
]
[l,r]
[l,r] 中 1 的个数的奇偶性转化为前缀和异或关系 sum[r] ^ sum[l-1] = t(t=0 为偶,t=1 为奇),用 unordered_map 对
l
−
1
l-1
l−1 和
r
r
r 做离散化;find(x) 递归找根的同时用 d[x] ^= d[p[x]] 更新到根节点的异或权值并路径压缩,若
a
a
a 和
b
b
b 同根则检查 d[a]^d[b] == t 是否成立,不同根则合并并设 d[pa] = d[a]^d[b]^t 保证新集合奇偶关系一致,遇到矛盾时输出前
i
−
1
i-1
i−1 个回答为最大自洽数。 |
| AcWing | 240 食物链 | 中等 | ⭐⭐⭐⭐ | 带权并查集(扩展域 / 距离向量) | 用带权并查集维护每个节点到根节点的距离 d[x](模 3 意义下,0 表示同类,1 表示 x 被根吃,2 表示 x 吃根),find(x) 递归找根的同时累加路径距离并压缩;1 X Y(同类)时若同根则判 (d[x]-d[y])%3==0,不同根则合并并设 d[px]=d[y]-d[x] 使距离相等;2 X Y(X 吃 Y)时若同根则判 (d[x]-d[y]-1)%3==0(即 d[x]-d[y] ≡ 1 (mod 3)),不同根则合并并设 d[px]=d[y]+1-d[x];同时判 X>N、Y>N、X==Y 且操作为 2 的情况为假话,累计假话数输出。 |
| AcWing | 784 强盗团伙 | ||||
| AcWing | 836 合并集合 | 简单 | ⭐ | 并查集(路径压缩) | 用 p[N] 数组维护每个节点的父节点,初始化 p[i] = i 使每个节点自成一集合,M a b 合并时将 a 的根节点挂到 b 的根节点下(p[find(a)] = find(b)),Q a b 查询时判断 find(a) == find(b) 是否成立;find(x) 函数采用路径压缩优化,递归查找根节点的同时将路径上所有节点的父节点直接指向根节点,使后续查询接近
O
(
1
)
O(1)
O(1),均摊时间复杂度
O
(
α
(
N
)
)
O(\alpha(N))
O(α(N))。 |
| AcWing | 837 连通块中点的数量 | 简单 | ⭐⭐ | 并查集(路径压缩 + 按大小合并) | 在基础并查集上增加 siz[N] 数组记录每个根节点所在集合的大小,初始化 siz[i] = 1;C a b 合并时先判根是否相同,不同则按大小合并(将较小集合挂到较大集合下),更新 siz[find(b)] += siz[find(a)] 后再连边 p[find(a)] = find(b),Q1 直接比较根节点,Q2 输出 siz[find(a)];find(x) 采用路径压缩,保证均摊时间复杂度
O
(
α
(
N
)
)
O(\alpha(N))
O(α(N))。 |
| AcWing | 1249 亲戚 | ||||
| AcWing | 1250 格子游戏 | ||||
| AcWing | 1251 打击犯罪 | ||||
| AcWing | 1252 搭配购买 | ||||
| AcWing | 1253 家谱 | ||||
| AcWing | 6058 亲戚 | ||||
| 洛谷 | [[P2978 Tea Time]] | 普及- | |||
| 洛谷 | P14077 连通图 | 普及- | |||
| 洛谷 | AT_abc399_c Make it Forest | 普及 | |||
| 洛谷 | AT_abc420_e Reachability Query | 普及 | |||
| 洛谷 | [[P1546 Agri-Net]] | 普及 | |||
| 洛谷 | P1551 亲戚 | 普及 | |||
| 洛谷 | P1621 集合 | 普及 | |||
| 洛谷 | [[P1661 扩散]] | 普及 | |||
| 洛谷 | [[P3367 并查集]] | 普及 | |||
| 洛谷 | AT_abc451_f Make Bipartite 3 | 普及+ | |||
| 洛谷 | P1196 银河英雄传说 | 普及+ | |||
| 洛谷 | [[P1197 星球大战]] | 普及+ | |||
| 洛谷 | P1525 关押罪犯 | 普及+ | |||
| 洛谷 | P1892 团伙 | 普及+ | |||
| 洛谷 | P1955 程序自动分析 | 普及+ | |||
| 洛谷 | P2024 食物链 | 普及+ | |||
| 洛谷 | [[P2294 狡猾的商人]] | 普及+ | |||
| 洛谷 | [[P3535 TOU-Tour de Byteotia]] | 普及+ | |||
| 洛谷 | P5689 多叉堆 | 普及+ | |||
| 洛谷 | P9869 三值逻辑 | 普及+ | |||
| AtCoder | AT_awc0080_e Paint Drop | ⭐⭐⭐⭐ | 并查集(按行维护 + 路径压缩) | 对每行独立维护并查集 nxt[i][j] 表示第
i
i
i 行中位置
j
j
j 的下一个未涂色位置,初始化 nxt[i][j] = j;每次查询时遍历菱形区域内的每一行,计算该行的列区间
[
j
L
,
j
R
]
[j_L, j_R]
[jL,jR],用 find(i, j_L) 找到第一个未涂色位置,累加分数后将该位置指向 find(i, j+1)(标记已涂色),再继续 find 查找下一个未涂色位置直到超出
j
R
j_R
jR;find 函数带路径压缩,保证每个格子最多被访问一次,总时间复杂度近似
O
(
H
×
W
×
α
(
W
)
+
Q
×
D
m
a
x
×
α
(
W
)
)
O(H \times W \times \alpha(W) + Q \times D_{max} \times \alpha(W))
O(H×W×α(W)+Q×Dmax×α(W))。 | |
| 其他 | [[道路]] | ||||
| 其他 | [[连通]] | ||||
| 其他 | [[朋友]] |
【线段树】
算法模板
区修+区查(求最值)
// 求最小值,如果要求最大值,把mn改为mx
int n, m;
int w[N];
struct Node
{
int l, r;
int dt, mn; // 求最小值,dt是懒标记
}tr[N*4];
void pushup(int u) // 由子节点的信息,来计算父节点的信息
{
tr[u].mn = min(tr[u<<1].mn, tr[u<<1|1].mn); // 求最小值
}
void pushdown(int u)
{
auto &root = tr[u], &l = tr[u<<1], &r = tr[u<<1|1];
l.dt += root.dt, l.mn += root.dt;
r.dt += root.dt, r.mn += root.dt;
root.dt = 0;
}
void build(int u, int l, int r)
{
if (l==r) tr[u] = {l, r, 0, w[l]};
else
{
tr[u] = {l, r};
int mid = l+r >> 1;
build (u<<1, l, mid), build(u<<1|1, mid+1, r);
pushup(u);
}
}
void update(int u, int l, int r, int d)
{
if (tr[u].l>=l && tr[u].r<=r)
{
tr[u].dt += d, tr[u].mn += d; // 修改值
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l<=mid) update(u<<1, l, r, d);
if (r>mid) update(u<<1|1, l, r, d);
pushup(u);
}
}
int query(int u, int l, int r)
{
if (tr[u].l>=l && tr[u].r<=r) // 树中节点,已经被完全包含在[l,r]中了
{
return tr[u].mn;
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
int res = INF; // 设最大值
if (l<=mid) res = query(u<<1, l, r);
if (r>mid) res = min(res, query(u<<1|1, l, r)); // 求最小值
return res;
}
}
区修+区查(求和)
int n, m;
int w[N];
struct Node
{
int l, r;
int dt, sum; // 求和,dt是懒标记
}tr[N*4];
void pushup(int u) // 由子节点的信息,来计算父节点的信息
{
tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum
}
void pushdown(int u)
{
auto &root = tr[u], &l = tr[u<<1], &r = tr[u<<1|1];
l.dt += root.dt;
r.dt += root.dt;
// 如果要求区间和
l.sum += root.dt * (l.r - l.l + 1);
r.sum += root.dt * (r.r - r.l + 1);
root.dt = 0;
}
void build(int u, int l, int r)
{
if (l==r) tr[u] = {l, r, 0, w[l]};
else
{
tr[u] = {l, r};
int mid = l+r >> 1;
build (u<<1, l, mid), build(u<<1|1, mid+1, r);
pushup(u);
}
}
void update(int u, int l, int r, int d)
{
if (tr[u].l>=l && tr[u].r<=r)
{
tr[u].dt += d;
// 如果要求区间和
tr[u].sum += d * (tr[u].r - tr[u].l + 1)
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l<=mid) update(u<<1, l, r, d);
if (r>mid) update(u<<1|1, l, r, d);
pushup(u);
}
}
int query(int u, int l, int r)
{
if (tr[u].l>=l && tr[u].r<=r) // 树中节点,已经被完全包含在[l,r]中了
{
// 如果要求区间和
return tr[u].sum;
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
int res = INF;
// 如果要求区间和
if (l<=mid) res += query(u<<1, l, r);
if (r>mid) res += query(u<<1|1, l, r);
return res;
}
}
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | [[245 你能回答这些问题吗]] | 中等 | ⭐⭐⭐⭐ | 线段树(最大子段和) | 线段树每个节点维护四个值:sum(区间总和)、pre(最大前缀和)、suf(最大后缀和)、best(最大连续子段和);pushup 时 best = max(左.best,右.best,左.suf + 右.pre) 处理三种情况(完全在左、完全在右、跨过中点);1 x y 查询时若区间横跨左右子树则合并左右 Node 结果并返回完整结构体,2 x y 修改时定位到叶子替换值后自底向上 pushup;注意查询时 x>y 需交换,单次操作
O
(
log
N
)
O(\log N)
O(logN)。 |
| AcWing | [[246 区间最大公约数]] | 困难 | ⭐⭐⭐⭐ | 线段树 + 树状数组 + 差分数组 | 利用数学性质
gcd
(
A
[
l
.
.
r
]
)
=
gcd
(
A
[
l
]
,
gcd
(
b
[
l
+
1..
r
]
)
)
\gcd(A[l..r]) = \gcd(A[l], \gcd(b[l+1..r]))
gcd(A[l..r])=gcd(A[l],gcd(b[l+1..r]))(其中
b
[
i
]
=
A
[
i
]
−
A
[
i
−
1
]
b[i]=A[i]-A[i-1]
b[i]=A[i]−A[i−1] 为差分数组),将区间加转化为差分数组的单点修改;用树状数组维护差分数组前缀和以
O
(
log
N
)
O(\log N)
O(logN) 查询
A
[
l
]
A[l]
A[l],用线段树维护差分数组的区间 GCD 以
O
(
log
N
)
O(\log N)
O(logN) 查询
gcd
(
b
[
l
+
1..
r
]
)
\gcd(b[l+1..r])
gcd(b[l+1..r]);C l r d 时树状数组 add(l,d) 和 add(r+1,-d),线段树 update(l,d) 和 update(r+1,-d),Q l r 时输出 abs(gcd(sum(l), query(l+1,r)))(
l
=
r
l=r
l=r 时直接输出 abs(A[l])),单次操作
O
(
log
N
)
O(\log N)
O(logN)。 |
| AcWing | [[247 亚特兰蒂斯]] | 困难 | ⭐⭐⭐⭐⭐ | 扫描线 + 线段树 + 离散化 | 将每个矩形拆分为入边(
x
=
x
1
,
k
=
+
1
x=x_1, k=+1
x=x1,k=+1)和出边(
x
=
x
2
,
k
=
−
1
x=x_2, k=-1
x=x2,k=−1)两条扫描线,对所有
y
y
y 坐标离散化后建立线段树维护
y
y
y 方向覆盖长度 len 和覆盖次数 cnt;pushup 时若 cnt>0 则 len 为区间实际长度,否则继承子节点之和;按
x
x
x 排序扫描线后从左到右遍历,每次用 tr[1].len * (seg[i].x - seg[i-1].x) 累加条带面积,再用 update 将当前边的
y
y
y 范围加入或移除覆盖(右端点用 find(y2)-1 因为节点
i
i
i 代表区间
[
a
l
l
s
[
i
]
,
a
l
l
s
[
i
+
1
]
]
[alls[i], alls[i+1]]
[alls[i],alls[i+1]]),最终得到所有矩形面积并。 |
| AcWing | 1275 最大数 | 中等 | ⭐⭐⭐ | 线段树(单点修改 + 区间查询) | 用线段树维护动态序列的区间最大值,建树时覆盖
[
1
,
m
]
[1, m]
[1,m](最大可能长度),A t 添加操作时在位置 n+1 做单点修改 update(1, n+1, n+1, (last+t)%p) 并 n++,Q L 询问操作时查询区间 [n-L+1, n] 的最大值并更新 last 为查询结果(供下次添加操作使用);线段树每个节点存区间最大值,pushup 时取左右子树最大值,query 时合并跨越中点的左右区间结果,单次操作
O
(
log
m
)
O(\log m)
O(logm),总时间复杂度
O
(
m
log
m
)
O(m \log m)
O(mlogm)。 |
| 洛谷 | AT_abc340_e Mancala 2 | 普及+ | |||
| 洛谷 | [[P1198 最大数]] | 普及+ | |||
| 洛谷 | P3372 线段树1 | 普及+ | |||
| 洛谷 | AT_abc360_f InterSections | 提高+ | |||
| 洛谷 | [[P3114 Stampede]] | 提高+ | |||
| 洛谷 | P1600 天天爱跑步 | 省选 | |||
| AtCoder | AT_awc0005_e Mountain Height Survey | ||||
| AtCoder | AT_awc0009_e Temperature Fluctuation Survey | ||||
| AtCoder | AT_awc0014_e Flowerbed Watering Management | ||||
| AtCoder | AT_awc0020_e Shelving Books on a Bookshelf | ⭐⭐⭐ | 线段树(区间最大值 + 在线二分查找) | 用线段树维护每个书架位置的最大承重能力,find 函数优先在左子树二分查找第一个容量 >= W[i] 的位置,若当前区间最大值 < W[i] 则剪枝返回 -1;找到位置后 update 将其设为 -INF 标记已占用,成功放置则计数器加一;按书顺序依次处理,总时间复杂度
O
(
(
N
+
M
)
log
M
)
O((N+M) \log M)
O((N+M)logM)。 | |
| AtCoder | AT_awc0062_e Radio Tower and Signal Strength | ⭐⭐⭐⭐ | 差分 + 前缀和 + 线段树(区间最值) | 每个信号源
(
X
i
,
B
i
)
(X_i, B_i)
(Xi,Bi) 的贡献是三角形函数(中心
X
i
X_i
Xi 处为
B
i
B_i
Bi,向两侧线性递减到 0),用差分数组记录斜率变化:在
[
X
i
−
B
i
+
1
,
X
i
]
[X_i-B_i+1, X_i]
[Xi−Bi+1,Xi] 区间斜率 +1,在
[
X
i
+
1
,
X
i
+
B
i
]
[X_i+1, X_i+B_i]
[Xi+1,Xi+Bi] 区间斜率 -1,即 diff[x-b+1] += 1、diff[x+1] -= 2、diff[x+b+1] += 1;对差分数组做两次前缀和(斜率→信号强度)得到每个整数坐标
p
p
p 的
f
(
p
)
f(p)
f(p) 值;最后用线段树维护区间最大值,每次查询
[
L
j
,
R
j
]
[L_j, R_j]
[Lj,Rj] 的区间最大值并输出,坐标用 OFFSET=200000 偏移处理负数,总时间复杂度
O
(
N
+
M
+
Q
log
M
)
O(N + M + Q \log M)
O(N+M+QlogM)(
M
=
400000
M=400000
M=400000 为坐标范围)。 | |
| AtCoder | AT_awc0063_e Number of Blocks in an Interval | ⭐⭐⭐⭐ | 线段树(区间赋值 + 懒标记) | 线段树每个节点维护区间左端点颜色 lc、右端点颜色 rc、块数 cnt 和懒标记 dt(-1 表示无标记);pushup 时合并左右子节点块数,若 左子.rc == 右子.lc 则 cnt--(边界同色合并);pushdown 时将懒标记下传,子节点区间变为同色且 cnt=1;update 区间赋值时直接设置懒标记并令 lc=rc=d、cnt=1;query 时若横跨左右子树则合并结果并处理边界同色情况,单次操作
O
(
log
N
)
O(\log N)
O(logN)。 | |
| AtCoder | AT_awc0081_e Balanced Groups in Tournament Partition | ⭐⭐⭐⭐⭐ | 线段树 + 滑动窗口 + 完全二叉树性质 | 递归二分分组天然形成完全二叉树,线段树节点值维护区间内 1 的数量(白帽人数),val 从 1 开始每层翻倍表示该层组应含叶子数,若 tr[i] == val/2 则该组为平衡组,用 acc 统计当前平衡组数量;滑动窗口枚举操作区间
[
l
,
l
+
K
−
1
]
[l, l+K-1]
[l,l+K−1],将区间内 0 移到左、1 移到右等价于把窗口内 cnt 个 1 集中到右侧,通过 update 单点修改并自底向上更新 tr 和 acc(跨越平衡条件时增减计数),每次窗口移动后 res = max(res, acc),最终输出最大平衡组数;时间复杂度
O
(
2
N
×
N
)
O(2^N \times N)
O(2N×N)。 | |
| AtCoder | AT_awc0082_e Company Organization and Salaries | ⭐⭐⭐⭐⭐ | DFS 序 + 线段树套树状数组(树套树) + 离散化 | 先通过 DFS 求欧拉序 将子树转化为连续区间
[
I
n
[
v
]
,
O
u
t
[
v
]
]
[In[v], Out[v]]
[In[v],Out[v]];离线收集所有初始工资和修改操作中的新工资,对每个线段树节点单独离散化值域后建立树状数组;1 v x 修改时在 In[v] 位置删除旧工资、加入新工资,2 v 查询时在线段树区间
[
I
n
[
v
]
,
O
u
t
[
v
]
]
[In[v], Out[v]]
[In[v],Out[v]] 中统计值域
[
A
[
v
]
+
1
,
I
N
F
]
[A[v]+1, INF]
[A[v]+1,INF] 的个数并输出;外层线段树维护 DFS 序区间,内层树状数组维护值域频次,单次操作
O
(
log
2
N
)
O(\log^2 N)
O(log2N)。 | |
| AtCoder | AT_awc0087_e Change of Assigned Interval | ⭐⭐⭐⭐ | 线段树(最大子段和) + 前缀和 | 将 A 类任务设为
+
P
i
+P_i
+Pi、T 类任务设为
−
P
i
-P_i
−Pi 得到数组
v
v
v,用前缀和 sa 统计不翻转时青木的基础成本;翻转区间
[
l
,
r
]
[l,r]
[l,r] 的净效果等价于基础值减去该区间内的最大子段和(因为翻转后原 A 成本移除、原 T 成本加入),线段树每个节点维护 sum(区间和)、pre(最大前缀和)、suf(最大后缀和)、best(最大子段和),pushup 时 best = max(左.best,右.best,左.suf + 右.pre);查询时 base = sa[R]-sa[L-1],maxReduce = max(0, query(L,R).best),输出 base - maxReduce,单次查询
O
(
log
N
)
O(\log N)
O(logN)。 |
【树状数组】
算法模板
int lowbit(int x) //提出x的低位2次幂数
{
return x & -x;
}
void add(int x, int c) //向后修
{
for (int i=x; i<=n; i+=lowbit(i)) tr[i] += c;
}
int query(int x) //向前查
{
int res = 0;
for (int i=x; i; i-=lowbit(i)) res += tr[i];
return res;
}
// 初始化树状数组
for (int i = 1; i <= n; i++)
cin >> a[i];
// 初始化树状数组
// 将原始数组转换为差分数组存储到树状数组中
for (int i = 1; i <= n; i++)
add(i, a[i] - a[i - 1]); // 存储差分值
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | 241 楼兰图腾 | 简单 | ⭐⭐⭐ | 树状数组(前缀和统计) | 从左到右扫描,用树状数组维护已出现元素的集合,query(n)-query(y) 得到左边比
a
[
i
]
a[i]
a[i] 大的元素个数记为 Greater[i],query(y-1) 得到左边比
a
[
i
]
a[i]
a[i] 小的元素个数记为 Lower[i];清空树状数组后从右到左扫描,用 Greater[i] * (query(n)-query(y)) 累加 V 图腾数(中间点
j
j
j 左边有 Greater[i] 个比它大的,右边有 query(n)-query(y) 个比它大的),用 Lower[i] * query(y-1) 累加 ∧ 图腾数(中间点
j
j
j 左边有 Lower[i] 个比它小的,右边有 query(y-1) 个比它小的),每次扫描后 add(y,1) 将当前元素加入树状数组,总时间复杂度
O
(
N
log
N
)
O(N \log N)
O(NlogN)。 |
| AcWing | 242 一个简单的整数问题 | ||||
| AcWing | 243 一个简单的整数问题2 | ||||
| AcWing | 244 谜一样的牛 | ||||
| 洛谷 | P1996 约瑟夫问题 | 普及- | |||
| 洛谷 | P3184 Counting Haybales | 普及- | |||
| 洛谷 | P10497 Lost Cows | 普及- | |||
| 洛谷 | AT_abc353_c Sigma Problem | 普及 | |||
| 洛谷 | AT_abc441_e A B substring | 普及 | |||
| 洛谷 | [[B3874 小杨的握手问题]] | 普及 | |||
| 洛谷 | P4378 Out of Sorts | 普及 | |||
| 洛谷 | P3368 树状数组2 | 普及+ | |||
| 洛谷 | P3374 树状数组1 | 普及+ | |||
| 洛谷 | AT_abc360_g Suitable Edit for LIS | 提高+ | |||
| 洛谷 | P3605 Promotion Counting | 提高+ | |||
| 洛谷 | P5677 配对统计 | 提高+ | |||
| AtCoder | AT_awc0006_e Store Sales Management | ||||
| AtCoder | AT_awc0008_e Organizing the Bookshelf | ||||
| AtCoder | AT_awc0015_e Count the Types of Flowers | ⭐⭐⭐ | 树状数组 + 离线查询(按右端点排序) | 将查询按右端点升序排序,从左到右扫描数组,用 last[x] 记录物种
x
x
x 的最后出现位置,树状数组维护"当前扫描范围内每个物种最右出现位置"的标记(值为 1);当物种
x
x
x 在位置
i
i
i 出现时,若 last[x] != 0 则先在旧位置 add(last[x], -1) 取消标记,再在当前位置 add(i, 1) 添加新标记,更新 last[x] = i;回答查询时 ans[id] = query(r) - query(l-1) 统计区间
[
l
,
r
]
[l,r]
[l,r] 内有效标记数(即不同物种数),最后按原始编号输出答案,总时间复杂度
O
(
(
N
+
Q
)
log
N
)
O((N+Q) \log N)
O((N+Q)logN)。 | |
| AtCoder | AT_awc0025_e Organizing the Bookshelf | ⭐⭐⭐ | 树状数组 + 二分查找(第 k 小 / 顺序统计) | 树状数组维护每个位置是否存在书(
1
1
1 表示存在,
0
0
0 表示已移除),query(x) 返回前
x
x
x 个位置中书的数量;find(k) 通过二分查找定位第
k
k
k 本书的实际位置(找最小的 pos 使得 query(pos) >= k);每次操作若
T
j
>
t
n
T_j > tn
Tj>tn(当前剩余书数)则直接输出 tn,否则找到实际位置 pos 后将 d[pos]--,若耐久度降为
0
0
0 则 add(pos,-1) 标记移除并 tn--,最后输出 tn;每次操作
O
(
log
2
N
)
O(\log^2 N)
O(log2N)。 | |
| AtCoder | AT_awc0027_e Selection of Contiguous Intervals | ⭐⭐⭐⭐ | 树状数组 + 离散化 | 将条件转化为前缀和形式:定义 s a [ i ] = ∑ j = 1 i ( A j + M ) sa[i] = \sum_{j=1}^{i}(A_j + M) sa[i]=∑j=1i(Aj+M),则 f ( l , r ) ≤ K f(l,r) \leq K f(l,r)≤K 等价于 s a [ l − 1 ] ≥ s a [ r ] − K sa[l-1] \geq sa[r] - K sa[l−1]≥sa[r]−K;收集所有 s a [ i ] sa[i] sa[i] 和 s a [ i ] − K sa[i]-K sa[i]−K 离散化后,遍历右端点 r r r,用树状数组查询已插入的 s a [ l − 1 ] sa[l-1] sa[l−1] 中大于等于 s a [ r ] − K sa[r]-K sa[r]−K 的数量(总数减前缀和),再将 s a [ r ] sa[r] sa[r] 插入树状数组,时间复杂度 O ( N log N ) O(N \log N) O(NlogN)。 | |
| AtCoder | AT_awc0032_e Multiple Bonus | ⭐⭐⭐⭐ | 树状数组 + 分块优化(阈值分治) | 以
B
=
N
B = \sqrt{N}
B=N 为阈值,小
k
k
k(
k
≤
B
k \leq B
k≤B)用 tag[k] 标记累计增量,查询时通过 tag[k] * (x/k) 快速计算前
x
x
x 个位置中
k
k
k 的倍数个数得到总贡献;大
k
k
k(
k
>
B
k > B
k>B)直;接遍历
i
=
k
,
2
k
,
3
k
,
…
≤
N
i=k,2k,3k,\ldots \leq N
i=k,2k,3k,…≤N 用树状数组 add(i,v) 单点修改,查询时 query(x) 累加;最终答案为 sa[x] + query(x) + Σ(tag[k]*(x/k)),修改操作小
k
k
k 为
O
(
1
)
O(1)
O(1)、大
k
k
k 为
O
(
N
/
k
)
≤
O
(
N
)
O(N/k) \leq O(\sqrt{N})
O(N/k)≤O(N),查询为
O
(
N
+
log
N
)
O(\sqrt{N}+\log N)
O(N+logN)。 | |
| AtCoder | AT_awc0047_c Piggy Bank Management | ⭐⭐ | 树状数组(差分思想) | 用树状数组维护差分数组 d[i] = A[i] - A[i-1],初始化时 add(i, A[i]-A[i-1]);1 L R X 区间修改时执行 add(L, X) 和 add(R+1, -X),将区间加转化为两次单点修改;2 P 单点查询时输出 query(P)(差分数组前缀和即为原数组值);利用差分思想使区间修改和单点查询均为
O
(
log
N
)
O(\log N)
O(logN)。 |
【单调栈】
算法模板
// 常见模型:找出每个数左边离它最近的比它大/小的数
// 输入数组元素
for (int i = 1; i <= n; i++)
cin >> a[i];
// 使用单调栈处理每个元素
for (int i = 1; i <= n; i++)
{
// 维护单调递增栈:弹出所有大于等于当前元素的栈顶元素
while (tt && a[stk[tt]] >= a[i])
tt--;
// 如果栈不为空,当前元素左边第一个比它小的元素就是栈顶元素
if (tt)
ans[i] = a[stk[tt]];
else
ans[i] = -1; // 栈为空表示左边没有更小的元素
// 将当前元素的下标压入栈
stk[++tt] = i;
}
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | 830 单调栈 | 简单 | ⭐⭐ | 单调栈(维护递增栈) | 从左到右扫描数组,用单调递增栈维护可能成为"左边第一个更小元素"的候选值,栈中存储下标;对于每个新元素
x
x
x,先 while (tt && a[stk[tt]] >= x) tt-- 弹出所有大于等于
x
x
x 的栈顶元素(这些元素不可能成为后续元素的左边第一个更小值),若栈非空则 ans[i] = a[stk[tt]](栈顶即为左边第一个比它小的数),否则输出 -1,最后将当前下标 i 压入栈;每个元素最多入栈出栈一次,时间复杂度
O
(
N
)
O(N)
O(N)。 |
| 洛谷 | AT_abc372_d Buildings | 普及 | |||
| 洛谷 | [[P1323 删数问题]] | 普及 | |||
| 洛谷 | P1901 发射站 | 普及 | |||
| 洛谷 | [[P5788 单调栈]] | 普及 | |||
| 洛谷 | [[P8082 KEKS]] | 普及 | |||
| AtCoder | AT_awc0045_e Interval Evaluation Value | ⭐⭐⭐⭐⭐ | 单调栈 + 线段树(区间最值) | 先用单调递增栈从左到右求每个元素左边第一个小于等于它的位置
L
[
i
]
L[i]
L[i],再用单调递减栈从右到左求右边第一个大于它的位置
R
[
i
]
R[i]
R[i],确定以
A
[
i
]
A[i]
A[i] 为唯一最小值的合法区间范围;线段树叶子节点存储从位置
l
l
l 开始、长度为
W
W
W 的区间和 sa[l+W-1]-sa[l-1];枚举每个位置
i
i
i 作为最小值,计算合法起始位置范围 left = max(L[i]+1, i-W+1)、right = min(i, R[i]-W),若 left <= right 则 query(left,right) 得到最大区间和,更新答案 ans = max(ans, maxsum + K*A[i]),总时间复杂度
O
(
N
log
N
)
O(N \log N)
O(NlogN)。 |
【哈希表】
算法模板
拉链法
int h[N], e[N], ne[N], idx;
// 向哈希表中插入一个数
void insert(int x)
{
int k = (x % N + N) % N;
e[idx] = x, ne[idx] = h[k], h[k] = idx ++ ;
}
// 在哈希表中查询某个数是否存在
bool find(int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;
return false;
}
开放寻址法
int h[N], null=0x3f3f3f3f;
// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int find(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| AcWing | 840 模拟散列表 | 简单 | ⭐⭐ | 哈希表(拉链法 / 开放寻址法) | 拉链法:用数组 h[k] 存哈希值为
k
k
k 的链表头指针,e[idx] 存值,ne[idx] 存下一个节点下标,insert 时头插法将新节点挂到链表头部,find 时遍历对应链表查找;开放寻址法:数组 h[N] 直接存值,null=0x3f3f3f3f 表示空位,find(x) 通过 (x%N+N)%N 计算哈希值后线性探测(冲突时 k++ 循环到数组开头),返回
x
x
x 所在位置或第一个空位,I x 时直接赋值,Q x 时判断 h[k] != null;两种方法哈希函数均为 (x%N+N)%N 处理负数,时间复杂度均摊
O
(
1
)
O(1)
O(1)。 |
| 洛谷 | P1918 保龄球 | 普及- | |||
| 洛谷 | P3405 Cities and States | 普及- | |||
| 洛谷 | P4305 不重复数字 | 普及- | |||
| 洛谷 | P15799 找数 | 普及- | |||
| 洛谷 | P2814 家谱 | 普及 | |||
| 洛谷 | P5266 学籍管理 | 普及 | |||
| 洛谷 | P2852 Milk Patterns | 普及+ | |||
| 洛谷 | P8819 星战 | 省选 | |||
| AtCoder | AT_awc0036_b Managing the Guest List | ||||
| AtCoder | AT_awc0088_d Control Panel Operation Sequence | ⭐⭐⭐⭐ | 全排列枚举 + 哈希表计数(状态模拟) | 利用
N
≤
9
N \leq 9
N≤9 的小数据范围,用 next_permutation 枚举所有
N
!
N!
N! 种操作顺序,对每个排列模拟灯状态变化:按顺序验证操作
i
i
i 的执行条件(
A
i
A_i
Ai 要求为
1
1
1 的位置灯必须亮、
B
i
B_i
Bi 要求为
1
1
1 的位置灯必须灭),满足后按
X
i
X_i
Xi 翻转对应灯并统计亮灯数存入向量
v
v
v;用 map<vector<int>, int> 记录每种亮灯数量序列的出现次数;查询时直接更新
C
T
q
=
Y
q
C_{T_q}=Y_q
CTq=Yq 后输出 mp[query];预处理检查是否存在
A
i
[
j
]
=
B
i
[
j
]
=
′
1
′
A_i[j]=B_i[j]='1'
Ai[j]=Bi[j]=′1′ 的矛盾操作,存在则所有答案为
0
0
0。 |
【平衡树】
算法模板
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| 洛谷 | [[P3369 普通平衡树]] | 提高+ | |||
| 洛谷 | [[P3391 文艺平衡树]] | 提高+ | |||
| 洛谷 | [[P3835 可持久化平衡树]] | 省选 |
【动态树】
算法模板
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| 洛谷 | [[P3690 动态树(LCT)]] | 省选 |
【主席树】
算法模板
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| 洛谷 | P3834 可持久化线段树2 | 提高+ | |||
| 洛谷 | [[P3919 可持久化线段树1]] | 提高+ |
【树套树】
算法模板
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| 洛谷 | P3380 树套树 | 省选 |
【笛卡尔树】
算法模板
struct DescarTree
{
int ls, rs;
}t[N]; //笛卡尔树
int stk[N], top; //stk[]单调递增栈,存放元素下标,增栈指权值,栈顶元素权值大
for (int i=1; i<=n; i++)
{
cin >> p[i];
int pos = top; //top栈原栈顶,pos栈当前操作位置
while (p[stk[pos]]>p[i]) pos--; //出栈,stk[pos]存放下标,元素权值单调递增栈
if (pos<top) t[i].ls = stk[pos+1]; //最后出栈stk[pos+1]为当前树节点t[i]左儿子
if (pos) t[stk[pos]].rs = i; //将入栈,原栈顶为父,当前(元素下标)为右儿子
stk[++pos] = i; //入栈,当前元素下标i入栈
top = pos; // 新栈顶
}
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| 洛谷 | P5854 笛卡尔树 | 普及+ | ⭐⭐⭐ | 笛卡尔树(单调栈构建) | 笛卡尔树同时满足二叉搜索树性质(节点编号中序遍历为
1
∼
n
1\sim n
1∼n)和小根堆性质(权值
p
[
i
]
p[i]
p[i] 父节点小于子节点);用单调递增栈(栈顶元素权值最大)维护当前右链,对于新元素
i
i
i:从栈顶弹出所有权值
>
p
[
i
]
> p[i]
>p[i] 的元素,若存在出栈元素则最后出栈的元素成为
i
i
i 的左儿子(t[i].ls = stk[pos+1]),若栈非空则
i
i
i 成为栈顶元素的右儿子(t[stk[pos]].rs = i),然后将
i
i
i 入栈;每个节点最多入栈出栈一次,时间复杂度
O
(
N
)
O(N)
O(N)。 |
【点分治】
算法模板
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| 洛谷 | [[P3806 点分治1]] | 提高+ |
【莫队】
算法模板
// 询问结构体
struct Q
{
int l, r; // 区间左右端点
int id; // 询问编号
} q[N];
// 莫队排序比较函数(奇偶性优化)
bool cmp(Q a, Q b)
{
if (a.l / B != b.l / B)
return a.l < b.l; // 不同块按左端点升序
return a.r < b.r; // 同块按右端点升序
}
// 添加一个数到当前区间
void add(int x)
{
sum -= cnt[x] * cnt[x]; // 减去旧贡献
cnt[x]++; // 增加计数
sum += cnt[x] * cnt[x]; // 加上新贡献
}
// 从当前区间删除一个数
void del(int x)
{
sum -= cnt[x] * cnt[x]; // 减去旧贡献
cnt[x]--; // 减少计数
sum += cnt[x] * cnt[x]; // 加上新贡献
}
// 计算分块大小
B = sqrt(n);
// 按照莫队顺序排序询问
sort(q + 1, q + m + 1, cmp);
// 初始化当前区间为空,l=1, r=0表示空区间
for (int i = 1, l = 1, r = 0; i <= m; i++)
{
// 移动指针到目标区间
while (l > q[i].l) // 左边界向左扩展
add(a[--l]);
while (r < q[i].r) // 右边界向右扩展
add(a[++r]);
while (l < q[i].l) // 左边界向右收缩
del(a[l++]);
while (r > q[i].r) // 右边界向左收缩
del(a[r--]);
// 存储当前区间的答案
ans[q[i].id] = sum;
}
for (int i=1; i<=m; i++)
cout << ans[i] << endl;
算法应用
| 题目来源 | 标题 | 难度 | 星级 | 考察算法 | 一句话思路总结 |
|---|---|---|---|---|---|
| 洛谷 | P2709 小 B 的询问 | 提高+ | ⭐⭐⭐ | 莫队算法(分块 + 双指针 + 奇偶性优化) | 将询问按左端点所在块升序排序,同一块内按右端点升序排序(奇偶性优化可进一步减少指针移动);用双指针
l
,
r
l,r
l,r 维护当前区间,通过 add(x)(sum -= cnt[x]^2; cnt[x]++; sum += cnt[x]^2)和 del(x)(sum -= cnt[x]^2; cnt[x]--; sum += cnt[x]^2)在
O
(
1
)
O(1)
O(1) 时间内增量更新答案;每次移动指针后将 sum 存入对应询问编号的位置,最后按原始顺序输出;分块大小
B
=
n
B = \sqrt{n}
B=n,总时间复杂度
O
(
(
n
+
m
)
n
)
O((n+m)\sqrt{n})
O((n+m)n)。 |
| AtCoder | AT_awc0088_e Intervals That Can Be Arranged Alternately | ⭐⭐⭐⭐ | 莫队算法 + 前缀差分转化 | 定义前缀差分数组 cumul [ i ] \text{cumul}[i] cumul[i] 为前 i i i 个石头中白减黑的数量,区间 [ l , r ] [l,r] [l,r] 能交替排列(好区间)当且仅当 $\ |

7493

被折叠的 条评论
为什么被折叠?



