【队列】
算法模板
手搓队列
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();
算法应用
【链表】
算法模板
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\))和可用节点索引 idx(初始 \(0\)),H x 时头插(e[idx]=x, ne[idx]=head, head=idx++),I k x 时在"第 \(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\) 右侧插入新节点并更新四个指针(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\) 左侧插入(即 \(l[k]\) 右侧),D k 直接删除第 \(k\) 个节点,最后从 r[0] 遍历到哨兵 \(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)\)。 |
| 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\) 的最小累计成本,转移方程为 dp[i] = min{dp[j]} + A[i](其中 \(i-K \leq j < i\)),用单调递增队列维护滑动窗口 \([i-K, i-1]\) 内的最小 dp 值:先弹出队首过期的元素(下标 < i-K),然后 dp[i] = 队首值 + A[i],再从队尾弹出所有值 >= dp[i] 的元素后入队,保证队首始终为窗口最小值,将转移复杂度从 \(O(N \times K)\) 优化到 \(O(N)\),最终输出 dp[N]。 |
| AtCoder |
AT_awc0101_e Choosing Flowerbed Intervals |
|
⭐⭐⭐⭐ |
双指针 + 单调队列 + 哈希表 |
用双指针维护满足两个条件的最大窗口:右指针 \(j\) 从左到右枚举,用单调递增/递减队列分别维护区间 \([i,j]\) 的最小值和最大值(保证条件2),用 map 维护区间内不同品种的数量 \(D\)(保证条件1),当 \(D \times (j-i+1) > K\) 或 \(\max - \min > M\) 时收缩左指针 \(i\) 直到满足条件,以 \(j\) 为右端点的合法区间数为 \((j-i+1)\),累加得到答案,时间复杂度 \(O(N \log N)\)(map 操作),空间复杂度 \(O(N)\)。 |
| 学而思编程 |
训练计划 |
提高进阶 |
⭐⭐⭐⭐⭐ |
单调队列优化 DP + 对数比较(避免高精度) |
将问题转化为:将 \(N\) 个数分成至多 \(K\) 段,每段内所有数相乘,求各段乘积之和的最大值;用 Node 结构体记录每个数字 \(1\sim9\) 的出现次数,通过对数比较乘积大小(v1 += s[i] * ln[i])避免高精度;sum[i] 为前缀计数数组,d[i] 表示前 \(i-1\) 个数的最优划分;用单调队列维护窗口 \([i-K, i-1]\) 内的最小值,保证任意连续 \(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\) 取模。 |
【栈】
算法模板
手搓栈
// 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();
算法应用
【集合】
算法模板
算法应用
【并查集】
算法模板
朴素并查集+路径压缩
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\))进行离散化映射为连续编号,然后两遍扫描:第一遍将所有 e=1(相等)约束用并查集合并,第二遍检查所有 e=0(不等)约束是否出现 find(x)==find(y) 的矛盾(即相等与不等冲突),若存在冲突输出 NO,否则输出 YES;注意必须先处理全部相等关系再检查不等关系,否则传递性会导致误判。 |
| AcWing |
238 银河英雄传说 |
简单 |
⭐⭐⭐⭐ |
带权并查集(距离向量) |
用 d[x] 记录战舰 \(x\) 到所在列队首的战舰间隔数,siz[x] 记录以 \(x\) 为根的列的战舰总数;find(x) 递归找根的同时用 d[x] += d[p[x]] 累加父节点到根的距离并路径压缩;M a b 合并时将 \(a\) 列接在 \(b\) 列尾部,设 d[pa] = siz[pb](\(a\) 列根到 \(b\) 列根的距离为 \(b\) 列大小),更新 siz[pb] += siz[pa] 后连边 p[pa] = pb;C a b 时若同根则输出 abs(d[a]-d[b])-1(两舰之间的战舰数),不同根输出 `-1$。 |
| AcWing |
239 奇偶游戏 |
中等 |
⭐⭐⭐⭐ |
带权并查集(异或权值) + 离散化 + 前缀和转化 |
将区间 \([l,r]\) 中 1 的个数的奇偶性转化为前缀和异或关系 sum[r] ^ sum[l-1] = t(t=0 为偶,t=1 为奇),用 unordered_map 对 \(l-1\) 和 \(r\) 做离散化;find(x) 递归找根的同时用 d[x] ^= d[p[x]] 更新到根节点的异或权值并路径压缩,若 \(a\) 和 \(b\) 同根则检查 d[a]^d[b] == t 是否成立,不同根则合并并设 d[pa] = d[a]^d[b]^t 保证新集合奇偶关系一致,遇到矛盾时输出前 \(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(\alpha(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(\alpha(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\) 行中位置 \(j\) 的下一个未涂色位置,初始化 nxt[i][j] = j;每次查询时遍历菱形区域内的每一行,计算该行的列区间 \([j_L, j_R]\),用 find(i, j_L) 找到第一个未涂色位置,累加分数后将该位置指向 find(i, j+1)(标记已涂色),再继续 find 查找下一个未涂色位置直到超出 \(j_R\);find 函数带路径压缩,保证每个格子最多被访问一次,总时间复杂度近似 \(O(H \times W \times \alpha(W) + Q \times D_{max} \times \alpha(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)\)。 |
| AcWing |
[[246 区间最大公约数]] |
困难 |
⭐⭐⭐⭐ |
线段树 + 树状数组 + 差分数组 |
利用数学性质 \(\gcd(A[l..r]) = \gcd(A[l], \gcd(b[l+1..r]))\)(其中 \(b[i]=A[i]-A[i-1]\) 为差分数组),将区间加转化为差分数组的单点修改;用树状数组维护差分数组前缀和以 \(O(\log N)\) 查询 \(A[l]\),用线段树维护差分数组的区间 GCD 以 \(O(\log N)\) 查询 \(\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\) 时直接输出 abs(A[l])),单次操作 \(O(\log N)\)。 |
| AcWing |
247 亚特兰蒂斯 |
困难 |
⭐⭐⭐⭐⭐ |
扫描线 + 线段树 + 离散化 |
将每个矩形拆分为入边(\(x=x_1, k=+1\))和出边(\(x=x_2, k=-1\))两条扫描线,对所有 \(y\) 坐标离散化后建立线段树维护 \(y\) 方向覆盖长度 len 和覆盖次数 cnt;pushup 时若 cnt>0 则 len 为区间实际长度,否则继承子节点之和;按 \(x\) 排序扫描线后从左到右遍历,每次用 tr[1].len * (seg[i].x - seg[i-1].x) 累加条带面积,再用 update 将当前边的 \(y\) 范围加入或移除覆盖(右端点用 find(y2)-1 因为节点 \(i\) 代表区间 \([alls[i], alls[i+1]]\)),最终得到所有矩形面积并。 |
| AcWing |
1275 最大数 |
中等 |
⭐⭐⭐ |
线段树(单点修改 + 区间查询) |
用线段树维护动态序列的区间最大值,建树时覆盖 \([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(m \log m)\)。 |
| AcWing |
1277 维护序列 |
困难 |
⭐⭐⭐⭐ |
线段树 + 双懒标记(乘法+加法) |
用线段树维护区间和,每个节点同时存乘法懒标记 mul 和加法懒标记 add,下传时按"先乘后加"顺序 eval(sum = sum*mul + len*add,add = add*mul + add_new),支持区间乘、区间加、区间求和,时间复杂度 \(O(m \log n)\),空间复杂度 \(O(n)\) |
| 洛谷 |
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)\)。 |
| AtCoder |
AT_awc0062_e Radio Tower and Signal Strength |
|
⭐⭐⭐⭐ |
差分 + 前缀和 + 线段树(区间最值) |
每个信号源 \((X_i, B_i)\) 的贡献是三角形函数(中心 \(X_i\) 处为 \(B_i\),向两侧线性递减到 0),用差分数组记录斜率变化:在 \([X_i-B_i+1, X_i]\) 区间斜率 +1,在 \([X_i+1, X_i+B_i]\) 区间斜率 -1,即 diff[x-b+1] += 1、diff[x+1] -= 2、diff[x+b+1] += 1;对差分数组做两次前缀和(斜率→信号强度)得到每个整数坐标 \(p\) 的 \(f(p)\) 值;最后用线段树维护区间最大值,每次查询 \([L_j, R_j]\) 的区间最大值并输出,坐标用 OFFSET=200000 偏移处理负数,总时间复杂度 \(O(N + M + Q \log M)\)(\(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)\)。 |
| AtCoder |
AT_awc0081_e Balanced Groups in Tournament Partition |
|
⭐⭐⭐⭐⭐ |
线段树 + 滑动窗口 + 完全二叉树性质 |
递归二分分组天然形成完全二叉树,线段树节点值维护区间内 1 的数量(白帽人数),val 从 1 开始每层翻倍表示该层组应含叶子数,若 tr[i] == val/2 则该组为平衡组,用 acc 统计当前平衡组数量;滑动窗口枚举操作区间 \([l, l+K-1]\),将区间内 0 移到左、1 移到右等价于把窗口内 cnt 个 1 集中到右侧,通过 update 单点修改并自底向上更新 tr 和 acc(跨越平衡条件时增减计数),每次窗口移动后 res = max(res, acc),最终输出最大平衡组数;时间复杂度 \(O(2^N \times N)\)。 |
| AtCoder |
AT_awc0082_e Company Organization and Salaries |
|
⭐⭐⭐⭐⭐ |
DFS 序 + 线段树套树状数组(树套树) + 离散化 |
先通过 DFS 求欧拉序 将子树转化为连续区间 \([In[v], Out[v]]\);离线收集所有初始工资和修改操作中的新工资,对每个线段树节点单独离散化值域后建立树状数组;1 v x 修改时在 In[v] 位置删除旧工资、加入新工资,2 v 查询时在线段树区间 \([In[v], Out[v]]\) 中统计值域 \([A[v]+1, INF]\) 的个数并输出;外层线段树维护 DFS 序区间,内层树状数组维护值域频次,单次操作 \(O(\log^2 N)\)。 |
| AtCoder |
AT_awc0087_e Change of Assigned Interval |
|
⭐⭐⭐⭐ |
线段树(最大子段和) + 前缀和 |
将 A 类任务设为 \(+P_i\)、T 类任务设为 \(-P_i\) 得到数组 \(v\),用前缀和 sa 统计不翻转时青木的基础成本;翻转区间 \([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)\)。 |
【树状数组】
算法模板
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]\) 大的元素个数记为 Greater[i],query(y-1) 得到左边比 \(a[i]\) 小的元素个数记为 Lower[i];清空树状数组后从右到左扫描,用 Greater[i] * (query(n)-query(y)) 累加 V 图腾数(中间点 \(j\) 左边有 Greater[i] 个比它大的,右边有 query(n)-query(y) 个比它大的),用 Lower[i] * query(y-1) 累加 ∧ 图腾数(中间点 \(j\) 左边有 Lower[i] 个比它小的,右边有 query(y-1) 个比它小的),每次扫描后 add(y,1) 将当前元素加入树状数组,总时间复杂度 \(O(N \log N)\)。 |
| 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\) 的最后出现位置,树状数组维护"当前扫描范围内每个物种最右出现位置"的标记(值为 1);当物种 \(x\) 在位置 \(i\) 出现时,若 last[x] != 0 则先在旧位置 add(last[x], -1) 取消标记,再在当前位置 add(i, 1) 添加新标记,更新 last[x] = i;回答查询时 ans[id] = query(r) - query(l-1) 统计区间 \([l,r]\) 内有效标记数(即不同物种数),最后按原始编号输出答案,总时间复杂度 \(O((N+Q) \log N)\)。 |
| AtCoder |
AT_awc0025_e Organizing the Bookshelf |
|
⭐⭐⭐ |
树状数组 + 二分查找(第 k 小 / 顺序统计) |
树状数组维护每个位置是否存在书(\(1\) 表示存在,\(0\) 表示已移除),query(x) 返回前 \(x\) 个位置中书的数量;find(k) 通过二分查找定位第 \(k\) 本书的实际位置(找最小的 pos 使得 query(pos) >= k);每次操作若 \(T_j > tn\)(当前剩余书数)则直接输出 tn,否则找到实际位置 pos 后将 d[pos]--,若耐久度降为 \(0\) 则 add(pos,-1) 标记移除并 tn--,最后输出 tn;每次操作 \(O(\log^2 N)\)。 |
| AtCoder |
AT_awc0027_e Selection of Contiguous Intervals |
|
⭐⭐⭐⭐ |
树状数组 + 离散化 |
将条件转化为前缀和形式:定义 \(sa[i] = \sum_{j=1}^{i}(A_j + M)\),则 \(f(l,r) \leq K\) 等价于 \(sa[l-1] \geq sa[r] - K\);收集所有 \(sa[i]\) 和 \(sa[i]-K\) 离散化后,遍历右端点 \(r\),用树状数组查询已插入的 \(sa[l-1]\) 中大于等于 \(sa[r]-K\) 的数量(总数减前缀和),再将 \(sa[r]\) 插入树状数组,时间复杂度 \(O(N \log N)\)。 |
| AtCoder |
AT_awc0032_e Multiple Bonus |
|
⭐⭐⭐⭐ |
树状数组 + 分块优化(阈值分治) |
以 \(B = \sqrt{N}\) 为阈值,小 \(k\)(\(k \leq B\))用 tag[k] 标记累计增量,查询时通过 tag[k] * (x/k) 快速计算前 \(x\) 个位置中 \(k\) 的倍数个数得到总贡献;大 \(k\)(\(k > B\))直;接遍历 \(i=k,2k,3k,\ldots \leq N\) 用树状数组 add(i,v) 单点修改,查询时 query(x) 累加;最终答案为 sa[x] + query(x) + Σ(tag[k]*(x/k)),修改操作小 \(k\) 为 \(O(1)\)、大 \(k\) 为 \(O(N/k) \leq O(\sqrt{N})\),查询为 \(O(\sqrt{N}+\log N)\)。 |
| 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)\)。 |
【单调栈】
算法模板
// 常见模型:找出每个数左边离它最近的比它大/小的数
// 输入数组元素
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\),先 while (tt && a[stk[tt]] >= x) tt-- 弹出所有大于等于 \(x\) 的栈顶元素(这些元素不可能成为后续元素的左边第一个更小值),若栈非空则 ans[i] = a[stk[tt]](栈顶即为左边第一个比它小的数),否则输出 -1,最后将当前下标 i 压入栈;每个元素最多入栈出栈一次,时间复杂度 \(O(N)\)。 |
| 洛谷 |
AT_abc372_d Buildings |
普及 |
|
|
|
| 洛谷 |
[[P1323 删数问题]] |
普及 |
|
|
|
| 洛谷 |
P1901 发射站 |
普及 |
|
|
|
| 洛谷 |
[[P5788 单调栈]] |
普及 |
|
|
|
| 洛谷 |
[[P8082 KEKS]] |
普及 |
|
|
|
| AtCoder |
AT_awc0045_e Interval Evaluation Value |
|
⭐⭐⭐⭐⭐ |
单调栈 + 线段树(区间最值) |
先用单调递增栈从左到右求每个元素左边第一个小于等于它的位置 \(L[i]\),再用单调递减栈从右到左求右边第一个大于它的位置 \(R[i]\),确定以 \(A[i]\) 为唯一最小值的合法区间范围;线段树叶子节点存储从位置 \(l\) 开始、长度为 \(W\) 的区间和 sa[l+W-1]-sa[l-1];枚举每个位置 \(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)\)。 |
【哈希表】
算法模板
拉链法
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\) 的链表头指针,e[idx] 存值,ne[idx] 存下一个节点下标,insert 时头插法将新节点挂到链表头部,find 时遍历对应链表查找;开放寻址法:数组 h[N] 直接存值,null=0x3f3f3f3f 表示空位,find(x) 通过 (x%N+N)%N 计算哈希值后线性探测(冲突时 k++ 循环到数组开头),返回 \(x\) 所在位置或第一个空位,I x 时直接赋值,Q x 时判断 h[k] != null;两种方法哈希函数均为 (x%N+N)%N 处理负数,时间复杂度均摊 \(O(1)\)。 |
| 洛谷 |
P1918 保龄球 |
普及- |
|
|
|
| 洛谷 |
P3405 Cities and States |
普及- |
|
|
|
| 洛谷 |
P4305 不重复数字 |
普及- |
|
|
|
| 洛谷 |
P15799 找数 |
普及- |
⭐⭐ |
哈希表 / 集合交集 |
用 map 统计两个数组中每个数的出现次数,出现次数为 \(2\) 的即为交集元素,时间复杂度 \(O((n+m) \log(n+m))\),空间复杂度 \(O(n+m)\) |
| 洛谷 |
P2814 家谱 |
普及 |
|
|
|
| 洛谷 |
P5266 学籍管理 |
普及 |
|
|
|
| 洛谷 |
P2852 Milk Patterns |
普及+ |
|
|
|
| 洛谷 |
P8819 星战 |
省选 |
|
|
|
| AtCoder |
AT_awc0036_b Managing the Guest List |
|
|
|
|
| AtCoder |
AT_awc0088_d Control Panel Operation Sequence |
|
⭐⭐⭐⭐ |
全排列枚举 + 哈希表计数(状态模拟) |
利用 \(N \leq 9\) 的小数据范围,用 next_permutation 枚举所有 \(N!\) 种操作顺序,对每个排列模拟灯状态变化:按顺序验证操作 \(i\) 的执行条件(\(A_i\) 要求为 \(1\) 的位置灯必须亮、\(B_i\) 要求为 \(1\) 的位置灯必须灭),满足后按 \(X_i\) 翻转对应灯并统计亮灯数存入向量 \(v\);用 map<vector<int>, int> 记录每种亮灯数量序列的出现次数;查询时直接更新 \(C_{T_q}=Y_q\) 后输出 mp[query];预处理检查是否存在 \(A_i[j]=B_i[j]='1'\) 的矛盾操作,存在则所有答案为 \(0\)。 |
【平衡树】
算法模板
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| 洛谷 |
[[P3369 普通平衡树]] |
提高+ |
|
|
|
| 洛谷 |
[[P3391 文艺平衡树]] |
提高+ |
|
|
|
| 洛谷 |
[[P3835 可持久化平衡树]] |
省选 |
|
|
|
【动态树】
算法模板
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| 洛谷 |
[[P3690 动态树(LCT)]] |
省选 |
|
|
|
【主席树】
算法模板
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
256 最大异或和 |
中等 |
⭐⭐⭐⭐⭐ |
可持久化字典树(主席树) |
前缀异或和转化后,问题变为在区间 \([l-1, r-1]\) 内找与 \(C=s[N] \oplus x\) 异或最大的 \(s[p-1]\),用可持久化字典树维护历史版本,每个节点存子树最大下标 max_id,查询时从高位到低位贪心走异或分支(需满足 max_id >= l-1),时间复杂度 \(O((N+M) \log V)\)(\(V=2^{24}\)),空间复杂度 \(O((N+M) \log V)\) |
| 洛谷 |
P3834 可持久化线段树2 |
提高+ |
|
|
|
| 洛谷 |
[[P3919 可持久化线段树1]] |
提高+ |
|
|
|
【树套树】
算法模板
算法应用
【笛卡尔树】
算法模板
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\sim n\))和小根堆性质(权值 \(p[i]\) 父节点小于子节点);用单调递增栈(栈顶元素权值最大)维护当前右链,对于新元素 \(i\):从栈顶弹出所有权值 \(> p[i]\) 的元素,若存在出栈元素则最后出栈的元素成为 \(i\) 的左儿子(t[i].ls = stk[pos+1]),若栈非空则 \(i\) 成为栈顶元素的右儿子(t[stk[pos]].rs = i),然后将 \(i\) 入栈;每个节点最多入栈出栈一次,时间复杂度 \(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\) 维护当前区间,通过 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)\) 时间内增量更新答案;每次移动指针后将 sum 存入对应询问编号的位置,最后按原始顺序输出;分块大小 \(B = \sqrt{n}\),总时间复杂度 \(O((n+m)\sqrt{n})\)。 |
| AtCoder |
AT_awc0088_e Intervals That Can Be Arranged Alternately |
|
⭐⭐⭐⭐ |
莫队算法 + 前缀差分转化 |
定义前缀差分数组 \(\text{cumul}[i]\) 为前 \(i\) 个石头中白减黑的数量,区间 \([l,r]\) 能交替排列(好区间)当且仅当 $\ |