[yLOI2023] D 腐草为萤

· · 题解

[yLOI2023] D 腐草为萤

这个题上线前经过了巨大削弱,导致结论变得很一眼,可能对思维量的要求变弱了。

Description

数轴上有 n 个点,每个点的初始坐标为 x_i,权重为 a_i。在任何时刻,每个点会向与它相邻的两个点中权重较大的那个点移动,速度为一个单位长度每秒,如果相邻两个点的权重都比它自身权重小,则不移动。两个点相遇时,权重较小的点消失。

求出每个点消失时的坐标。

## Analysis ### 算法一 $n = 2$ 时只要比较两个点谁权重大即可,一定是权重较小的走到权重较大的然后去世。期望得分 $5$ 分。 ### 算法二 当 $n \leq 100$,$x_i \leq 200$ 时,显然每隔 $200$ 秒至少会有一个点消失。于是逐秒模拟即可。时间复杂度 $O(n x_i)$,期望得分 $30$ 分。 ### 算法三 找到离得最近的一对相邻点 $(i,j)$,使得一方静止,另一方在朝着对方移动。容易发现,在 $(i,j)$ 发生碰撞之前,其他点不会发生碰撞。于是可以直接把时间跳到 $(i,j)$ 碰撞的时刻,容易算出此时其他所有点的坐标:在这个过程中,其他点的运动方向和速度都是不变的,所以假设经过了 $T\mathrm s$ 发生碰撞,则一个 $T \mathrm s$ 前向右运动的点 $i$ 的新坐标就是 $x_i + T$。向左运动同理。暴力修改仍存活的点的坐标。每次修改 $O(n)$ 个点,一共修改 $O(n)$ 次。时间复杂度为 $O(n^2)$。期望得分 $60$ 分。 ### 算法四 $a_i$ 单调递增的情况:除了最后一个点,所有的点都向右移动。因为点的移动速度是一样的,所以移动方向相同的两个点的间距是不会变的,不会发生碰撞。大家最后都会和 $n$ 号点碰撞,然后去世。 时间复杂度 $O(n)$,期望得分 $5$ 分。结合算法三可得 $65$ 分。 ### 算法五 $a_i$ 单峰的情况:类似的,所有点都会朝着权值最大的点移动,且同向的两个点间距不变。最后大家都和权重最大的点碰撞,去世。 时间复杂度 $O(n)$,期望得分 $5$ 分。结合算法三、四可得 $70$ 分。 ### 算法六 结合算法三、四、五,我们尝试分析运动的形式,得出 key conclusion: 1. 相邻两个同向运动的点(在一方方向改变前)的间距不会改变。 2. 发生一次碰撞后,**至多只有两个点的运动方向会改变**。 3. 只有运动方向不同的两个点(把静止也看做一个运动方向)会发生碰撞。 第一条显然,考虑证明第二条:假设 $j,i,k$ 是三个相邻的点,某一时刻 $i$ 碰撞到了 $j$。此时与 $j$ 相邻和与 $k$ 相邻的点发生了变化(从 $k$ 变成 $i$ 或从 $j$ 变成 $i$),因此可能导致 $j$ 和 $k$ 的运动方向改变。对 $j$ 左侧和 $k$ 右侧的点而言,与它们相邻的点没有改变,所以它们的运动方向不会改变。 结合第三条,我们考虑维护所有运动方向不同的相邻点对。那么一次碰撞后,只需要对点对做出 $O(1)$ 个修改就能得到新的状态。根据算法三,在任何时刻,最先发生碰撞的一定是离得最近的点对,我们按时间顺序维护每次碰撞。 每次碰撞是找间距最小的两个点对,于是需要一个支持插入、删除、查找最小值的数据结构。可以使用 `std::set` 或者 `std::priority_queue`。以下约定使用 `std::set`。 我们需要能找出这些点对里当前间距最小的,这个点对会最先发生碰撞。这里产生了一个问题:每个点对的间距是在加入 set 的时刻计算的。当时间增加时,不可能暴力修改 set 里点对的距离。那么如何找出**当前**间距最小的点对呢? 可以发现,无法直接比较点对间距的原因是它们加入 set 时计算间距的时间是不同的。自然可以想到,如果统一维护两个点对在某一固定时刻时的**等效间距**,就可以比较大小了。这里我们选择维护它们在时刻 $0$ 的等效间距。具体来说,在时刻 $T$,间距为 $d$ 的点对的等效间距为 $T + d$。也就是说,等效间距的含义是,假设点对的两个点是以每秒一个单位长度不停靠近时,他们在第 $0$ 秒时的距离。 当一次碰撞发生后,可能会改变一些点的运动方向,导致一些点对消失,一些点对增加。这样的改变量都是 $O(1)$ 个。例如,对四个相邻的点 $i,j,k,l$($k$ 和 $l$ 相距较远),假设它们的权重分别是 $4,1,2,3$。则初始时 $k$ 向 $l$ 移动,此时 $(i,j)$ 是一个点对,$(k,l)$ 是一个点对。当 $(i,j)$ 碰撞后,$k$ 左侧的权重比右侧大,则它会回头向左侧走。$(k,l)$ 这个点对被删除了,$(i,k)$ 这个点对增加了。 最后一个问题是,在插入 set 时,要求出两点当前的间距。这要求我们能快速求出每个点的坐标。考虑懒更新,仅当一次碰撞发生后,更新与消失的点相邻的点的坐标(因为只有这两个点可能凑成新点对)。记 $lastChange_i$ 表示上次更新 $i$ 的坐标的时刻,上次更新后 $i$ 的坐标是 $x_i$,当前时刻为 $T$,则 $i$ 当前的坐标就是 $x_i + (T - lastChange_i) \times aspect_i$。其中,当 $i$ 向左移动时,$aspect_i = -1$,$i$ 向右移动时,$aspect_i = 1$,静止不动时 $aspect_i = 0$。这是因为两次更新之间 $i$ 的运动方向不可能变化(想一想,为什么)。这其实也是能使用等效间距维护最小间距的原因。 此外,为了能求出当前与某个点相邻的点,需要按顺序维护当前还存活的点,支持查询上一个、下一个。这部分可以使用链表完成,std 图省事使用了 `std::set`。 总的来说,一共发生了 $O(n)$ 次碰撞,每次碰撞对 set 有 $O(1)$ 次修改,单次修改复杂度 $O(\log n)$。于是算法的总时间复杂度为 $O(n \log n)$。期望得分 $100$ 分。 验题人给出的代码中,直接维护了每个点在时刻 $0$ 的**等效坐标**。原理与等效间距相似,也就是假设一个点一直按当前的方向移动,则它在时刻 $0$ 应有的坐标。相当于把上文做法里 $lastChange$ 固定为 $0$。 因为篇幅原因,本文省去了一些证明比较简单(但可能并不直观)的可行性、正确性证明,仅给出了做法。这些被省去的可行性可以通过自己尝试举反例(发现举不出)来感性理解,进而通过反证法证明,在此略去。 ## Code 有一个小细节是,注意到同一秒可能会有多只萤火虫去世,会导致一些萤火虫的方向被反复更新。如果更新顺序不正确就会出问题。一个简单的解决方法是在 set 里以不动(显然碰撞都是一只动的撞一只不动的)的那只萤火虫亮度为第二关键字排序。 如果没有想到这个技巧,那么有一个很普适的解决方案是先把同一秒发生的碰撞全都拿出来,集体更新完以后再扫一遍受影响的点,求出这一秒的末状态,最后塞入 set。std 使用的是第一种解决方法。 ```cpp #include <set> #include <tuple> #include <iostream> #include <algorithm> const int maxn = 500005; const int INF = 2000000001; int n, bcnt, T; int x[maxn], a[maxn], dis[maxn], aspect[maxn], ans[maxn], lastChange[maxn]; std::set<std::tuple<int, int, int>> s; std::set<int> alive; inline int getAspect(int pre, int x, int post) { if (a[pre] > a[post]) { if (a[pre] > a[x]) return -1; } else { if (a[post] > a[x]) return 1; } return 0; } inline int getp(int i) { return x[i] + (T - lastChange[i]) * aspect[i];} int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cin >> n; auto read = []() { int x; std::cin >> x; return x; }; std::generate(x + 1, x + 1 + n, read); std::generate(a + 1, a + 1 + n, read); for (int i = 1; i <= n; ++i) aspect[i] = getAspect(i - 1, i, i + 1); x[0] = -INF, x[n + 1] = INF; alive.insert(0); alive.insert(n + 1); for (int i = 1; i <= n; ++i) if (aspect[i] == -1) dis[i] = dis[i - 1] - x[i - 1] + x[i]; for (int i = n; i; --i) if (aspect[i] == 1) dis[i] = dis[i + 1]- x[i] + x[i + 1]; for (int i = 1; i <= n; ++i) if (aspect[i] != 0) { if (aspect[i + aspect[i]] != aspect[i]) s.insert(std::make_tuple(dis[i], a[i + aspect[i]], i)); } for (int Cnt = 1; Cnt < n; ++Cnt) { auto begin = s.begin(); int i = std::get<2>(*begin); T = lastChange[i] + dis[i]; s.erase(s.begin()); int j = (aspect[i] == 1) ? *(++alive.find(i)) : *(--alive.find(i)); ans[i] = x[j]; int k = (aspect[i] == -1) ? *(++alive.find(i)) : *(--alive.find(i)); alive.erase(i); if (aspect[i] == aspect[k]) { x[k] = getp(k); lastChange[k] = T; s.insert(std::make_tuple((dis[k] = abs(x[k] - x[j])) + T, a[j], k)); } else { if (k == 0 || k == n + 1) continue; int as = getAspect(*(--alive.find(k)), k, *(++alive.find(k))); if (as != aspect[k]) { if (aspect[k] == 0) { int h = (as == -1) ? *(++alive.find(k)) : *(--alive.find(k)); if (s.find(std::make_tuple(dis[h] + lastChange[h], a[k], h)) != s.end()) { s.erase(std::make_tuple(dis[h] + lastChange[h], a[k], h)); } } x[k] = getp(k); int prenode = (aspect[k] == 1) ? *(++alive.find(k)) : *(--alive.find(k)); if (s.find(std::make_tuple(dis[k] + lastChange[k], a[prenode], k)) != s.end()) { s.erase(std::make_tuple(dis[k] + lastChange[k], a[prenode], k)); } aspect[k] = as; s.insert(std::make_tuple(T + (dis[k] = abs(x[k] - x[j])), a[j], k)); lastChange[k] = T; } } } for (int i = 1; i <= n; ++i) std::cout << ans[i] << " \n"[i == n]; } ``` ## Generator ```cpp const int maxn = 500005; int a[maxn], x[maxn]; void makedata(int T) { int n = 500000, lim = 500000000; if (T <= 6) { if (T == 1) n = 2; else n = 100; lim = 200; for (int i = 1; i <= n; ++i) a[i] = i; std::random_shuffle(a + 1, a + 1 + n, modx<int>); for (int i = 1; i <= lim; ++i) x[i] = i; std::random_shuffle(x + 1, x + 1 + lim, modx<int>); std::sort(x + 1, x + 1 + n); } else { if (T <= 12) n = 1000; int block = lim / 7, tn = n / 7, nn = 0; for (int cnt = 1, i = 1; cnt <= 3; ++cnt) { int delta = block / (tn + 1); for (int j = 1, p = i; j <= tn; ++j) { x[++nn] = p; p += delta; } i += block; } int beg = tn * 3 + 1; std::set<int> oc; for (int i = nn; i <= n; ++i) { int p; do p = modx(lim - block * 3) + block * 3; while (oc.find(p) != oc.end()); oc.insert(p); x[i] = p; } std::sort(x + nn, x + 1 + n); if (T == 13) { for (int i = 1; i <= n; ++i) a[i] = i; } else if (T == 14) { for (int i = 1; i <= n; ++i) a[i] = i; std::random_shuffle(a + 1, a + 1 + n); std::sort(a + 1, std::find(a + 1, a + 1 + n, n)); std::sort(std::find(a + 1, a + 1 + n, n), a + 1 + n, std::greater<int>()); } else if (T > 14 && T <= 17) { for (int i = 1; i <= n; ++i) a[i] = i; std::random_shuffle(a + 1, a + 1 + n); for (int i = 2, c0 = 1, c1 = 1; i <= n; ++i) if (a[i] > a[i - 1]) { c0++; c1 = 1; if (c0 > 5) std::swap(a[i - 2], a[i - 3]); } else { c1++; c0 = 1; if (c1 > 5) std::swap(a[i - 2], a[i - 3]); } } else { for (int i = 1; i <= tn; ++i) a[i] = 2 * i; for (int i = tn; i > 0; --i) a[i + tn] = 2 * i - 1; for (int i = 2 * tn + 1; i <= n; ++i) a[i] = i; std::random_shuffle(a + tn + tn + 1, a + 1 + n, modx<int>); } } printf("%d\n", n); for (int i = 1; i <= n; ++i) printf("%d%c", x[i] * 2, i == n ? '\n' : ' '); for (int i = 1; i <= n; ++i) printf("%d%c", a[i], i == n ? '\n' : ' '); } ```