Solution -「JOISC 2017」「LOJ #2392」煙花棒
\(\mathscr{Description}\)
Link.
有 \(n\) 個人站在數軸上,第從左往右第 \(i\) 個人的座標是 \(x_i\),每個人手上有一支菸花棒,每支菸花棒能燃燒 \(T\) 秒,燃盡後無法再點燃。初始時只有 \(k\) 手上的煙花棒是點燃的,求這些人的移動速度上限的最小值 \(v\in\mathbb N\),使得能夠用這支菸花棒薪火相傳,點燃所有煙花棒。
\(n\le10^5\)。
\(\mathscr{Solution}\)
跪拜出題人系列。
顯然二分答案,考慮如何檢查當前答案 \(v\) 的合法性。
先得到一些有基礎性意義的貪心轉化:
-
任意時刻,只需要一支燃燒的煙花棒存在。
-
所有人會向燃著的煙花棒移動。
-
當一支剩餘燃燒時間為 \(t\) 的煙花棒點燃另一支菸花棒,等價於另一支菸花棒獲得 \(T+t\) 的燃燒時間,這支菸花棒立即燃盡。(轉化到原問題情景:兩個人一起跑,要燃盡時再傳火。)
想像從 \(k\) 出發點燃煙花棒的情景,對於某個時刻,已經點燃過的煙花棒一定是一個區間 \([l,r]\),且 \(k\in[l,r]\)。根據性質 1,此時 \([l,r]\) 之外的人的相對位置是不變的,我們可以想象作區間 \([l,r]\) 被縮成了一個點。
進一步,在這個條件下,“時刻”這一概念已經不重要了——無論何時,把 \(x_i\)
嘗試貪心?唯一顯然的結論是若 \(P\) 的開頭或者 \(Q\) 的開頭不小於 \(0\),我們可以放心大膽取出來,而都小於 \(0\) 的情況就很難討論清楚了。當然,每次“涉險”取走負數後,我們又會立馬把露出來的非負數全部取走。我們能否規避“都小於 \(0\)”的討論,用已知結論描述完整的貪心過程?
接下來這步,或說是構造,說起來平淡:我們“如果”能把 \(P\) 和 \(Q\) 從開頭起,分為若干極短的段,每段描述為二元組 \((t_0,\Delta t)\),滿足 \(\Delta t\ge0\),表示為了消除這一段,初始時至少有 \(t_0\) 的剩餘燃燒時間,消除後獲得 \(\Delta t\) 的燃燒時間。由於劃分的極短性,每次必然消除完整的一段而不會半途而廢,不然穩虧不賺。
這種“虛妄”有什麼用?如果“如果”不成立,不還是乾瞪眼?
等等,如果“如果”不成立,例如 \(P\) 的“如果”不成立,那麼就存在 \(P\) 的一段極長字尾 \(P'\),滿足 \(P'\) 任意字首和為負數。我們引入《TENET》的宇宙觀,如果有一個逆熵的人從結束狀態開始貪心,他的初始時間 \(t\)——也就是我們的結束時間 \(t\),是確定的;他每次必須從 \(P\) 的末尾取數,當然 \(P\) 中的數也取反,那麼……
極長字尾 \(P'\),取相反數再翻轉得到 \(P''\),滿足 \(P''\) 任意字尾和為正數……那麼 \(P''\) 就不存在任意字首和為負數的字尾……再者,\(P''\) 可以被劃分為上文中的若干極短段,也就是說,這個逆熵的人完全可以在“如果”的美好願景下做貪心,直到把 \(P'\) 消掉!
然後呢然後呢?順熵的人把 \(P\) 消除剩下一個 \(P'\),逆熵的人把 \(P'\) 反著消除,兩個人相遇,我們作為順熵的人,靈機一動,逆著逆熵的人的行動構造方案,不就把 \(P\) 消除了嗎?
神諭啊!
實現上,可以把 \(\Delta t_i\) 轉化到勢差 \(h_{i+1}-h_i\) 上,所用東西乘上 \(2v\),構造 \(x_i\) 的勢 \(h_i=2vTi-x_i\),順帶把初始的 \(t=T\) 一塊兒囊括;在消去區間 \([l,r]\) 時保持 \(h_r\ge h_l\) 即可。演算法複雜度是 \(\mathcal O(n\log V)\) 的,其中 \(V\) 是一堆亂七八糟東西的值域上限。
從神諭中體會到什麼?
——如果末狀態已知,那它何不可做初狀態?
——用時間的流向逆轉“死局”,讓它成為“必勝”條件。(好魔怔。
出題人 nb。
\(\mathscr{Code}\)
/*+Rainybunny+*/
#include <bits/stdc++.h>
#define rep(i, l, r) for (int i = l, rep##i = r; i <= rep##i; ++i)
#define per(i, r, l) for (int i = r, per##i = l; i >= per##i; --i)
typedef long long LL;
const int MAXN = 1e5;
int n, st, T, x[MAXN + 5];
LL hgt[MAXN + 5];
inline bool check(const int v) {
rep (i, 1, n) hgt[i] = 2ll * v * T * i - x[i];
if (hgt[1] > hgt[n]) return false;
int tarL = st, tarR = st;
per (i, st - 1, 1) if (hgt[i] <= hgt[tarL]) tarL = i;
rep (i, st + 1, n) if (hgt[i] >= hgt[tarR]) tarR = i;
int l = st, r = st;
while (tarL < l || r < tarR) {
int pl = l, pr = r;
for (int i = pl - 1; i >= tarL && hgt[i] <= hgt[pr]; --i) {
if (hgt[i] <= hgt[pl]) pl = i;
}
for (int i = pr + 1; i <= tarR && hgt[i] >= hgt[pl]; ++i) {
if (hgt[i] >= hgt[pr]) pr = i;
}
if (pl == l && pr == r) return false;
l = pl, r = pr;
}
l = 1, r = n;
while (l < tarL || tarR < r) {
int pl = l, pr = r;
for (int i = pl + 1; i <= tarL && hgt[i] <= hgt[pr]; ++i) {
if (hgt[i] <= hgt[pl]) pl = i;
}
for (int i = pr - 1; i >= tarR && hgt[i] >= hgt[pl]; --i) {
if (hgt[i] >= hgt[pr]) pr = i;
}
if (pl == l && pr == r) return false;
l = pl, r = pr;
}
return true;
}
int main() {
scanf("%d %d %d", &n, &st, &T);
rep (i, 1, n) scanf("%d", &x[i]);
int l = 0, r = 1e9;
while (l < r) {
int mid = l + r >> 1;
// printf("%d?\n", mid);
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", l);
return 0;
}