CF671E Organizing a Race
題目大意
K 國有 \(n\) 座城市和 \(n-1\) 條道路將它們相連。第 \(i\) 條道路連接了編號為 \(i\) 和 \(i + 1\) 的兩座城市,道路長度為 \(w_i\)。
在 K 國駕駛時,每當到達城市 \(i\),你的車會立即得到能使它行駛 \(g_i\) 個單位長度的油。
你現在要舉辦一場賽車比賽。比賽在 \(l,r\) 兩座城市間進行。比賽分為兩個獨立的階段。第一階段,車從 \(l\) 駛向 \(r\),第二階段,車從 \(r\) 回到 \(l\)。每個階段內部,不能走回頭路(也就是在第一階段只能向右走,第二階段只能向左走)。定義一場 \(l,r\) 之間的比賽的美麗度為 \(r - l + 1\)。你可以認為車的油箱是無窮大的,所以參賽者在途中會領取所有可獲得的油。
在每個階段的開始,油箱都是空的(也就是第一階段剩余的油,不會留到第二階段)。開始后的一瞬間,車會立刻得到起點處的油(在第一階段是 \(g_l\),在第二階段是 \(g_r\))。
如果在途中車沒油了,則比賽無法進行。因此不是所有的 \(l,r\) 之間都能舉行比賽。
你有 \(k\) 次機會。每次你可以選擇一個城市 \(i\),並令 \(g_i\) 提升 \(1\)。可以對一個城市重復操作,並多次獲得提升。問,經過操作后,你能舉辦的美麗度最大的比賽,美麗度是多少?
數據范圍:\(2\leq n\leq 10^5\),\(1\leq w_i\leq 10^9\),\(0\leq k,g_i\leq 10^9\)。
本題題解
初步轉化:前綴和
考慮 \(l,r\) 之間能舉辦比賽,需要滿足什么條件。對兩個階段分別看,不難列出條件:
做前綴和。設 \(G_i = \sum_{j = 1}^{i}g_j\),\(W_i=\sum_{j = 1}^{i}w_j\)。則條件也可以寫成:
移項,把關於 \(i\) 的放在一邊,關於 \(l\) 或 \(r\) 的放在另一邊:
設 \(a_i= G_i - W_i\),\(b_i = G_i - W_{i - 1}\)。特別地,\(a_{0} = 0\)。則條件也可以寫成:
考慮一次操作(\(g_i\texttt{++}\))對 \(a\), \(b\) 的影響。發現相當於對所有 \(j\geq i\),令 \(a_j\texttt{++}\),\(b_j\texttt{++}\)。
至此我們把問題轉化為了關於兩個序列 \(a\), \(b\) 的,更簡明的問題。
考慮枚舉 \(l\) 或 \(r\),再按一定順序枚舉另一個,我們很容易寫出一個 \(\mathcal{O}(n^2)\) 的暴力。
進一步轉化:單調棧,貪心
首先 \(l = r\) 一定是可行的。故以下只討論 \(l < r\) 的情況。
先不考慮 \(b\) 的限制。如果只有 \(a_i\geq a_{l-1}\) 這一個要求,我們會如何操作呢?顯然,全部都在 \(l\) 處操作是最優的,所需的操作次數是 \(\max\{0, a_{l - 1} - \min_{i \in[l,r)}\{a_i\}\}\)。
但當有了 \(b\) 的限制,全部在 \(l\) 處操作就不一定可行了。因為可能存在一個 \(b_p > b_r\) (\(p\in[l,r)\)),全在 \(l\) 操作時,\(b_p\) 會和 \(b_r\) 一起變大,則 \(b_p\) 永遠大於 \(b_r\),不滿足 \(b\) 的限制。
為了讓 \(b\) 合法,我們必須使 \(b_r\) 比 \(b_p\) 被多加了 \(b_p - b_r\) 次。也就是說,至少要有 \(b_p - b_r\) 次操作,是在 \(p\) 右邊的位置進行的。
我們從 \(r\) 向左,依次找到:第一個大於 \(b_r\) 的位置 \(p_1\);第一個大於 \(b_{p_1}\) 的位置 \(p_2\);......。也就是序列 \(b_{1\dots r}\) 的后綴最大值所在的位置,從右向左記為 \(p_1,p_2,\dots,p_m\)。另記 \(p_0 = r, p_{m + 1} = 0\)。如果從小到大枚舉 \(r\),則這些位置可以用一個單調棧維護出來。現在,對每個 \(p_i\) (\(1\leq i\leq m, p_i \geq l\)),必須有至少 \(b_{p_i} - b_r\) 次操作是在 \(p_i\) 后面進行的。根據一開始的貪心原則(盡可能使 \(a\) 合法),我們一定會在盡量靠前的位置使用這些操作,也就是對所有 \(i\),在 \(p_i + 1\) 的位置,使用 \(b_{p_i} - b_{p_{i - 1}}\) 次操作。
前面說過,我們從小到大枚舉 \(r\),並用單調棧維護出 \(p\) 序列。現在假設已知了 \(l\),那我們就知道了所有 \(p_i\geq l\) 的 \(p_i\),進而能夠確定,為了使 \(b\) 合法,所必須做的這些操作(對所有 \(p_i\geq l\),在 \(p_i + 1\) 的位置,使用 \(b_{p_i} - b_{p_{i - 1}}\) 次操作)。操作完成后,\(a\) 序列的數值會有所更新,記更新后的序列為 \(a'\),即:\(a'_j = a_j + \sum_{l\leq p_i < j}(b_{p_i} - b_{p_{i - 1}})\)。現在 \(b\) 的限制已經滿足,接下來要通過最少的操作使 \(a\) 合法,也就是最開始的貪心:在 \(l\) 處再進行 \(\max\{0, a_{l - 1} - \min_{j \in[l,r)}\{a'_j\}\}\) 次操作即可。
發現當 \(l\) 存在於某個區間 \((p_{i + 1}, p_i]\) 時,使 \(b\) 合法所需的操作次數(記為 \(x\))是一樣的,得到的 \(a'\) 序列也是一樣的(只考慮 \(a'_{l\dots r - 1}\) 這段區間,其他位置不管)。所以可以枚舉 \(l\) 所在的區間。在保證 \(x\leq k\) 的前提下,剩余操作次數 \(\max\{0, a_{l - 1} - \min_{j \in[l,r)}\{a'_j\}\}\) 這個式子里的 \(\max\) 可以去掉(因為 \(x +\max\{0,y\}\leq k\) 和 \(x + y\leq k\),在 \(x\leq k\) 時是一樣的)。於是我們可以用數據結構維護 \(a'\) 序列,並支持求 \(a_{l - 1} - \min_{j \in[l,r)}\{a'_j\}\leq k - x\) 的最小的 \(l\)(不知道具體實現方法沒關系,后面會講)。
但是當 \(b\) 序列近似單調遞減時,\(p\) 序列的長度是 \(\mathcal{O}(n)\) 的,上述做法的時間復雜度可能退化為 \(\mathcal{O}(n^2\log n)\),還不如暴力。但這可以通過一個巧妙的觀察來避免。
巧妙觀察,省去枚舉
設區間 \((p_{i + 1}, p_i]\) 所需的操作次數為 \(x_i\),即 \(x_i = \sum_{1\leq j\leq i}(b_{p_j} - b_{p_{j - 1}})\)。完成這 \(x_i\) 次操作后,我們會把剩余的 \(k - x_i\) 次操作,全部作用於位置 \(l\) 上。也就是說,每個 \(a'_j\) (\(l\leq j < r\)) 還會再增加 \(k - x_i\)。那把這 \(k - x_i\) 次操作作用於 \(l\),和把這些操作作用於任意位置 \(1\leq t < l\),對 \(a'_{l\dots r-1}\) 來說,效果是一模一樣的。因此,如果我們找出了滿足 \(x_i\leq k\) 的最大的 \(i\),那么可以假裝進行了這 \(x_i\) 次操作。這不會影響 \(l > p_i\) 時的正確性。
在單調棧加入、彈出元素時,可以順便維護出 \(x_i\) 序列。因此可以很方便地二分出 \(x_i\leq k\) 的最大的 \(i\)。同時也就確定了對應的 \(a'\) 序列(具體如何維護后面會講)。問題轉化為求哪些 \(l\) 能在 \(k - x_i\) 次操作內,使 \(a\) 合法。具體來說是要找到 \(a_{l - 1} - \min_{j \in[l,r)}\{a'_j\}\leq k - x_i\) 的最小的 \(l\)。
數據結構維護:一類有技巧的線段樹
用線段樹來維護序列 \(c_i = a_{i - 1} - \min_{j \geq i}\{a'_j\}\)。其中 \(a_{i - 1}\) 是從一開始就確定不變的值。對 \(a'\) 我們會在整個過程中不斷修改。注意,我們要查詢的其實是:\(a_{l - 1} - \min_{j \in[l,r)}\{a'_j\}\),但這可以通過把 \(j \geq r\) 的 \(a'_j\) 全部維護為 \(\infty\) 來轉化為 \(c_l\)。
考慮具體要支持哪些操作:
- 對 \(a'\) 進行區間加。在單調棧加入、彈出元素,以及確定了 \(i\) 后,對 \(a'\) 的所有修改都可以歸結為區間加(減法就是加負數)。
- 查詢:在線段樹上二分出 \(c_l\leq v\) 的最小的 \(l\),其中 \(v = k - x_i\) 是一個定值。這相當於要維護出 \(c\) 序列的區間最小值。
對區間 \([l,r]\) 做區間加時,還會影響到 \(i < l\) 的 \(c_i\) 的值,所以並不是很容易直接維護。於是這里就需要用到一類有技巧的線段樹,讀者可以去粉兔的博客學習一下。
在本題里,線段樹的每個節點上,維護四個信息。設這個節點對應的區間為 \([l,r]\)。
- \(\text{mn1}\):區間內 \(a'\) 的最小值。即 \(\text{mn1}(l,r) = \min_{l\leq i\leq r}\{a'_i\}\)。
- \(\text{tag}\):對 \(a'\) 進行區間加的懶標記。這和普通線段樹是一樣的。
- \(\text{mn}2\):區間內 \(a_{i - 1}\) 的最小值。即 \(\text{mn2}(l,r) = \min_{l\leq i\leq r}\{a_{i - 1}\}\)。
- \(\text{lans}\):這就是這類線段樹特有的東西了。葉子節點是沒有 \(\text{lans}\) 的。對非葉子節點,它的 \(\text{lans}\) 表示只考慮區間 \([l,r]\) 里的數(而不是整個序列 \(1\dots n\))時,它左兒子區間的 \(c_i\) 的最小值。形式化地,設該節點兩個兒子區間分別為:\([l,\text{mid}]\) 和 \([\text{mid} + 1, r]\),則 \(\text{lans}(l,r) = \min_{l\leq i\leq \text{mid}}\{a_{i - 1} - \min_{i\leq j\leq r}\{a'_j\}\}\)。這個“只考慮區間 \([l,r]\) 里的數”,最關鍵就體現在,里面 \(a'_j\) 並不是取 \(j\in[i, n]\) 的 \(\min\),而是 \(j\in[i,r]\)。
考慮如何 \(\text{push up}\),也就是用兒子區間的信息,更新父親區間。\(\text{mn1}\), \(\text{mn2}\) 的更新是簡單的,對兩個兒子取較小者即可。關鍵是 \(\text{lans}\)。現在假設已經知道了當前節點子樹里,除它以外所有節點的 \(\text{lans}\)。
考慮定義一個函數 \(\text{calc}(u, v)\)。其中 \(u\) 是線段樹上一個節點,\(v\) 可以是任意數值。\(\text{calc}(u, v)\) 返回的是:假設后綴最小值為 \(v\) 時,點 \(u\) 所代表的區間里,\(c_i\) 的最小值。則 \(\text{lans}(u)\) 就等於 \(\text{calc}(\text{LeftSon}_u, \text{mn1}(\text{RightSon}_u))\)。
考慮如何實現 \(\text{calc}\) 函數。這里給出偽代碼:
代碼的含義是:
- 當 \(u\) 是葉子節點的情況顯然。
- 否則,若 \(\text{mn1}(\text{RightSon}_u) > v\),則對右區間的所有位置來說,它們后面的最小值都是 \(v\),所以右區間 \(c_i\) 的最小值就是 \(\text{mn2}(\text{RightSon}_u) - v\)。
- 否則,\(\text{mn1}(\text{RightSon}_u) \leq v\),此時 \(v\) 不影響左區間的答案。
因為每次只向兩兒子中的一個遞歸,所以執行一次 \(\text{calc}\) 函數的時間復雜度為 \(\mathcal{O}(\log n)\)。因為每次 \(\text{push up}\) 時都要 \(\text{calc}\),所以一次區間加的時間復雜度為 \(\mathcal{O}(\log^2 n)\)。
線段樹上二分也與普通的線段樹上二分是類似的。只不過判斷向哪邊遞歸時需要調用 \(\text{calc}\) 函數。所以時間復雜度也是 \(\mathcal{O}(\log^2 n)\)。
至此,我們以 \(\mathcal{O}(n\log^2 n)\) 的總時間復雜度解決了本題。
參考代碼
// problem: CF671E
#include <bits/stdc++.h>
using namespace std;
#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 1e5;
const int INF = 1e9;
const ll LL_INF = 1e16;
int n, g[MAXN + 5], w[MAXN + 5];
ll K, a[MAXN + 5], b[MAXN + 5];
int sta[MAXN + 5], top;
ll val[MAXN + 5], presum_val[MAXN + 5];
struct SegmentTree {
ll mn[MAXN * 4 + 5], tag[MAXN * 4 + 5];
ll mn2[MAXN * 4 + 5];
ll lans[MAXN * 4 + 5];
void push_down(int p) {
if (tag[p] != 0) {
mn[p << 1] += tag[p];
tag[p << 1] += tag[p];
lans[p << 1] -= tag[p];
mn[p << 1 | 1] += tag[p];
tag[p << 1 | 1] += tag[p];
lans[p << 1 | 1] -= tag[p];
tag[p] = 0;
}
}
ll calc(int p, int l, int r, ll right_min) {
if (l == r) {
return mn2[p] - min(right_min, mn[p]);
}
push_down(p);
int mid = (l + r) >> 1;
if (right_min >= mn[p << 1 | 1]) {
return min(lans[p], calc(p << 1 | 1, mid + 1, r, right_min));
} else {
return min(calc(p << 1, l, mid, right_min), mn2[p << 1 | 1] - right_min);
}
}
void push_up(int p, int l, int mid) {
mn[p] = min(mn[p << 1], mn[p << 1 | 1]);
lans[p] = calc(p << 1, l, mid, mn[p << 1 | 1]);
}
void build(int p, int l, int r) {
if (l == r) {
mn[p] = a[l];
mn2[p] = a[l - 1];
return;
}
int mid = (l + r) >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
mn2[p] = min(mn2[p << 1], mn2[p << 1 | 1]);
push_up(p, l, mid);
}
void range_add(int p, int l, int r, int ql, int qr, ll v) {
if (ql <= l && qr >= r) {
mn[p] += v;
tag[p] += v;
lans[p] -= v;
return;
}
push_down(p);
int mid = (l + r) >> 1;
if (ql <= mid) {
range_add(p << 1, l, mid, ql, qr, v);
}
if (qr > mid) {
range_add(p << 1 | 1, mid + 1, r, ql, qr, v);
}
push_up(p, l, mid);
}
int _find(int p, int l, int r, ll right_min, ll v) {
if (l == r) {
return l;
}
push_down(p);
int mid = (l + r) >> 1;
if (calc(p << 1, l, mid, min(right_min, mn[p << 1 | 1])) <= v) {
return _find(p << 1, l, mid, min(right_min, mn[p << 1 | 1]), v);
} else {
return _find(p << 1 | 1, mid + 1, r, right_min, v);
}
}
int find(int p, int l, int r, ll right_min, int ql, int qr, ll v) {
if (ql <= l && qr >= r) {
if (calc(p, l, r, right_min) <= v) {
return _find(p, l, r, right_min, v);
} else {
return INF;
}
}
push_down(p);
int mid = (l + r) >> 1;
int res = INF;
if (ql <= mid) {
res = find(p << 1, l, mid, min(right_min, mn[p << 1 | 1]), ql, qr, v);
if (res != INF)
return res;
}
if (qr > mid) {
res = find(p << 1 | 1, mid + 1, r, right_min, ql, qr, v);
}
return res;
}
SegmentTree() {}
};
SegmentTree T;
int calc(int p, int r) {
if (sta[p - 1] + 1 > r - 1) {
return 0;
}
ll sum = presum_val[top - 1] - presum_val[p - 1];
T.range_add(1, 1, n, r, n, LL_INF); // 強制不考慮 >= r 的部分
T.range_add(1, 1, n, sta[p - 1] + 1, r - 1, -presum_val[p - 1]);
int res = T.find(1, 1, n, LL_INF, sta[p - 1] + 1, r - 1, K - sum);
T.range_add(1, 1, n, r, n, -LL_INF); // 恢復原狀
T.range_add(1, 1, n, sta[p - 1] + 1, r - 1, presum_val[p - 1]);
return r - res + 1;
}
int main() {
cin >> n >> K;
for (int i = 1; i < n; ++i) {
cin >> w[i];
}
for (int i = 1; i <= n; ++i) {
cin >> g[i];
}
for (int i = 1; i < n; ++i) {
a[i] = a[i - 1] + g[i] - w[i];
//cerr << a[i] << " \n"[i == n - 1];
}
for (int i = 1; i <= n; ++i) {
b[i] = b[i - 1] + g[i] - w[i - 1];
//cerr << b[i] << " \n"[i == n];
}
T.build(1, 1, n);
int ans = 1;
for (int i = 1; i <= n; ++i) {
while (top && b[sta[top]] <= b[i]) {
if (top > 1) {
T.range_add(1, 1, n, sta[top - 1] + 1, sta[top], -presum_val[top - 1]);
}
--top;
}
sta[++top] = i;
if (top > 1) {
val[top - 1] = b[sta[top - 1]] - b[i];
presum_val[top - 1] = presum_val[top - 2] + val[top - 1];
T.range_add(1, 1, n, sta[top - 1] + 1, i, presum_val[top - 1]);
}
/*
for (int j = 1; j <= i; ++j) f[j] = a[j];
ll sum = 0;
for (int j = top; j >= 1; --j) {
// l in (sta[j - 1], sta[j]]
for (int k = sta[j] + 1; k <= i; ++k) {
f[k] += b[sta[j]] - b[sta[j + 1]];
}
if (j < top) {
sum += b[sta[j]] - b[sta[j + 1]];
}
ll mn = LL_INF;
for (int k = sta[j] + 1; k < i; ++k)
ckmin(mn, f[k]);
for (int l = sta[j] - (j == top); l > sta[j - 1]; --l) {
ckmin(mn, f[l]);
ll cost = sum + max(0LL, a[l - 1] - mn);
if (cost <= K) {
ckmax(ans, i - l + 1);
}
}
}
*/
int l = 1, r = top;
while (l < r) {
int mid = (l + r) >> 1;
if (presum_val[top - 1] - presum_val[mid - 1] <= K) {
r = mid;
} else {
l = mid + 1;
}
}
ckmax(ans, calc(l, i));
}
cout << ans << endl;
return 0;
}
相關題目
本題里用到的“一類有技巧的線段樹”,在一些其他問題中也有出現。讀者可以自行選擇練習:
正睿2018暑假集訓day4 數組:我的題解