ABC371F Takahashi in Narrow Road 题解 模拟+线段树
题目链接:https://atcoder.jp/contests/abc371/tasks/abc371_f
题目大意:
有一条向东和向西延伸的道路, 个人在这条道路上。道路从一个称为原点的点向东和向西无限延伸。
第 个人 最初位于原点东侧距离原点 米处。
这些人可以沿着道路向东或向西移动。具体来说,他们可以执行以下移动任意次数。
选择一个人。如果目的地没有其他人,则将选择的人向东或向西移动 米。
注:这里说的目的地指的是,如果这个人要向东移动 米,则目的地就是东边 米的地方;如果这个人要向西移动 米,则目的地就是西边 米的地方。
他们总共有 个任务,第 个任务 描述如下:
第 个人到达坐标 。
找出依次完成所有 个任务所需的最小总移动次数。
解题思路:
首先本题中我们定义的 “连通块” 指的是一段挨着的人(即这一段错了最左边那个人以外,每个人的坐标都比它左边那个人大 )。
以下图为例:
- 第一阶段:每个人都属于同一个连通块;
- 第二阶段:第 个人单独一个连通块,第 个人单独一个连通块,第 个人属于同一个连通块;
- 第三阶段:第 个人属于同一个连通块,第 个人单独一个连通块;
- 第四阶段:仍然是第 个人属于同一个连通块,第 个人单独一个连通块;
- 第五阶段:第 个人单独一个连通块,第 个人属于同一个连通块。
在整体的 次移动中,每次移动:
- 最多有 个连通块合并成一个连通块;
- 最多有 个连通块被拆分为两个连通块。
考虑 暴力去合并与拆分连通块? 其实是可行的。
因为每次操作最多(通过拆分)增加一个连通块,初始时最多 个连通块,而最终合并能够得到的最终的连通块的个数最少是一个。所以,我们实际上合并连通块的次数不超过 次。虽然一次合并操作就可能合并 次连通块(看上去很吓人),但是实际整个过程中,合并和拆分连通块的次数是 ,均摊下来次数还是比较少的。
而且在代码实现时,我们可能是同时将一段区间的多个连通块合并成一个连通块,这样实际的合并次数就更少了。
在实现时,我们用线段树维护了一个信息,那就是每个人所在的连通块中最左边那个人的编号。在线段树中支持的操作有两个:
bl(p)
:(单点查询)查询第 个人所在的连通块中最左边那个人的编号;merge_people(l, r)
:(区间更新)将区间 合并成一个连通块。
同时我们实时更新每个区间最左边那个人(第 个人)的位置 。 这很关键,因为以 为最左边的人的连通块中其他人的位置都是由 决定的。
如果 和 属于同一个连通块, 是第 个人此时实际所在的位置,则 所在的位置应为 (因为同一个连通块里的人是挨着的)。具体实现时,我开了一个 get_pos(p)
函数获取第 个人的位置。
题目里面是第 个人移动到坐标 。我在分析移动的时候将其表示为第 个人移动到坐标 。
所限我们一个通过 get_pos(p)
函数获得第 个人当前(即移动前)所在的坐标 。
- 如果 (起点等于中点),不需要移动;
- 如果 ,需要向右移动;
- 如果 ,需要向左移动。
所以接下来我们分 向右移动 和 向左移动两种情况进行分析。
向右移动
如果 向右移动, 和他左边的那个人就差分开来了,第 所属的连通块的信息不变(所以不需要额外处理第 个人左边的那些人的归属信息);同时,以 为左端点肯定向右有连续的一段数字在向右移动后都归属于 ,对于第 个人右边的第 个人,因为 在移动后到达的位置是 ,而 如果和 在同一个连通块,则 最终的位置肯定是在位置 处,所以我们可以通过判断 当前所在的坐标是否 来判断 是否在 移动后和 属于同一个连通块。 然后我们就可以通过二分查找找到最右边的那个和 属于同一个连通块的人 —— 设他是第 个人。
此时,将 合并成一个连通块即可。
这里要注意,如果 ,则合并前后 和 肯定不属于同一个连通块(不然二分找到的就是 或者更后面的数而不是 了)。
向左移动
向左移动时,我们也可以二分得到最左边的那个和 属于同一个连通块的人(设它是第 个人),则 合并成一个连通块。
同时要注意,第 个人和第 个人在移动前可能是属于同一个连通块的,当 向左移动后,第 个人开始的一些人,他们所在的连通块的最左边的人会变为第 个人。
所以我们要在二分查找一下移动前 右边的那些人中最右边的那个和 属于同一个连通块的人,设为第 个人,如果 ,则将 合并成一个新的连通块。
这样我们就实时更新了位置。
但是我们并没有计算移动次数。
所以接下来最关键的就是 cal(l, r)
函数。
它用于计算 这些人的坐标之和。
我的 cal(l, r)
函数保证 范围内是若干个完整的连通块(即 是它所在连通块最左边那个人, 是他所在连通块最右边那个人)。
对于一个连通块 ,如果我们知道 的坐标 ,那么 范围内所有人的坐标总和是一个等差数列之和,即 。
我们只计算每次移动前后变化过的完整区间的坐标和,所以虽然每次移动都会调用 cal
函数,但是 cal
函数实际计算的完整区间的总数也是 数量级的。
- 如果是往右移动(坐标整体变大),移动后
cal(l, r)
的值减去移动前cal(l, r)
的值其实就是 范围内坐标的增量,即最少移动次数; - 如果是往右移动(坐标整体变小),移动前
cal(l, r)
的值减去移动后cal(l, r)
的值其实就是一勺移动次数。
按照这个逻辑就可以得到最少移动次数总和了。
示例程序:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int n, q, x[maxn];
long long ans;
int tr[maxn<<2];
#define lson l, mid, rt<<1
#define rson mid+1, r, rt<<1|1
void push_up(int rt) {
// ...
}
void push_down(int rt) {
if (tr[rt]) {
tr[rt<<1] = tr[rt<<1|1] = tr[rt];
tr[rt] = 0;
}
}
void build(int l, int r, int rt) {
if (l == r) {
tr[rt] = l;
return;
}
int mid = (l + r) / 2;
push_down(rt);
build(lson), build(rson);
push_up(rt);
}
int query(int p, int l, int r, int rt) { // 查询第 p 个人归属于哪个人
if (l == r)
return tr[rt];
int mid = (l + r) / 2;
push_down(rt);
return (p <= mid) ? query(p, lson) : query(p, rson);
}
int bl(int p) { // 查询第 p 个人归属于哪个人
return query(p, 1, n, 1);
}
void update(int L, int R, int p, int l, int r, int rt) { // [L, R] 都归 p
if (L <= l && r <= R) {
tr[rt] = p;
return;
}
int mid = (l + r) / 2;
push_down(rt);
if (L <= mid) update(L, R, p, lson);
if (R > mid) update(L, R, p, rson);
push_up(rt);
}
void merge_people(int l, int r) { // 将 [l, r] 合并成一个连通块
update(l, r, l, 1, n, 1);
}
int get_pos(int p) { // 获得第 p 个人当前实际所在的位置
return x[ bl(p) ] + p - bl(p);
}
long long cal(int l, int r) {
long long res = 0;
int p = r;
while (p >= l) {
int pp = bl(p);
assert(bl(pp) == pp);
res += (long long) (get_pos(pp) + get_pos(p)) * (p - pp + 1) / 2;
p = pp - 1;
}
assert(p == l - 1);
return res;
}
void go_right(int p, int G) {
// q 对应最右边一个和 p 挨着的人
int l = p, r = n, q;
while (l <= r) {
int mid = (l + r) / 2;
if (G + mid - p >= get_pos(mid))
q = mid, l = mid + 1;
else
r = mid - 1;
}
int pp = bl(p);
ans -= cal(pp, q);
x[p] = G;
merge_people(p, q);
ans += cal(pp, q);
}
void go_left(int p, int G) {
// pp对应最左边一个和 p 挨着的人
int l = 1, r = p, pp;
while (l <= r) {
int mid = (l + r) / 2;
if (G + mid - p <= get_pos(mid))
pp = mid, r = mid - 1;
else
l = mid + 1;
}
// q对应(向左移动前)最右边一个和 p 挨着的人(没有则 q = -1)
int q = p;
l = p+1, r = n;
while (l <= r) {
int mid = (l + r) / 2;
if (bl(mid) <= p)
q = mid, l = mid + 1;
else
r = mid - 1;
}
ans += cal(pp, q);
if (p < q) {
x[p+1] = get_pos(p+1);
merge_people(p+1, q);
}
x[pp] = G + pp - p;
merge_people(pp, p);
ans -= cal(pp, q);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", x+i);
build(1, n, 1);
scanf("%d", &q);
while (q--) {
int p, G; // 第 p 个人走到下标 G
scanf("%d%d", &p, &G);
int P = get_pos(p);
if (P < G)
go_right(p, G);
else if (P > G)
go_left(p, G);
}
printf("%lld\n", ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】