首先需要申明的是,真的是淺談,因為我對這個算法的認識還是非常低的。
既然是從《樓房重建》出發,那么當然是先看看這道題:
[清華集訓2013]樓房重建
題意簡述:
有 \(n\) 棟樓,第 \(i\) 棟的高度為 \(H_i\),也就是說第 \(i\) 棟樓可以抽象成一條兩端點為 \((i, 0)\) 和 \((i, H_i)\) 的線段。
初始時 \(H_i\) 均為 \(0\),要支持動態修改單點的 \(H_i\)。
每次詢問從 \(O(0, 0)\) 點可以看到多少棟樓房。
能看到一棟樓 \(i\) 當且僅當 \(H_i > 0\) 且 \((0, 0)\) 與 \((i, H_i)\) 的連線上不經過其它樓房。
題解:
令 \(s_i = H_i / i\),即 \((0, 0)\) 到 \((i, H_i)\) 的斜率,再定義 \(s_0 = 0\)。
則一棟樓房 \(i\) 能被看見,當且僅當 \(\displaystyle \max_{j = 0}^{i - 1} \{ s_j \} < s_i\),也就是說它是 \(s_i\) 的前綴嚴格最大值。
直接進入正題,我們使用線段樹維護這個東西。
考慮線段樹上的某一個節點表示的區間 \([l, r]\),則保存的信息有:
- 這個區間中的 \(s_i\) 的最大值。
- 僅考慮這個區間時的上述答案,也就是不考慮 \([1, l - 1]\) 對本區間的影響,而是看作整體的前綴最大值個數。
可以發現只有單點修改,那么我們只需考慮遞歸到底層節點后,一層層往上維護信息即可。
當前考慮一個節點 \(i\),假設 \(i\) 的子樹內的所有節點(除了 \(i\) 本身)的信息都維護好了,需要維護節點 \(i\) 的信息。
信息 1 是容易維護的,只要兩個子樹取 \(\max\) 即可。
但是信息 2 如果直接用兩個子樹信息相加,是錯誤的,因為沒有考慮左子樹向右子樹的貢獻。
進一步分析:可以發現直接繼承左子樹的信息是沒問題的,但是右子樹信息不能直接繼承。
考慮引入一個新函數:\(\mathrm{calc}(i, pre)\),它的作用是返回 \(i\) 子樹內,考慮了前綴最大值 \(pre\) 的影響后的答案。
為了方便表述,把信息 1 記做 \(\boldsymbol{\max[i]}\),把信息 2 記做 \(\boldsymbol{\mathrm{cnt}[i]}\),則它的偽代碼如下:
\(\displaystyle \begin{array}{l} \textbf{def: } \mathrm{calc}(i, pre) \\ \qquad \textbf{if } (i \text{ is a leaf node}) \\ \qquad \qquad \textbf{return } {\color{green}{[\max[i] > pre]}} \\ \qquad \textbf{else} \\ \qquad \qquad \textbf{if } (\max[\mathrm{leftchild}[i]] > pre) \\ \qquad \qquad \qquad \textbf{return } {\color{blue}{\mathrm{calc}(\mathrm{leftchild}[i], pre)}} + {\color{red}{(\mathrm{cnt}[i] - \mathrm{cnt}[\mathrm{leftchild}[i]])}} \\ \qquad \qquad \textbf{else} \\ \qquad \qquad \qquad \textbf{return } {\color{blue}{0}} + {\color{red}{\mathrm{calc}(\mathrm{rightchild}[i], pre)}} \\ \qquad \qquad \textbf{endif.} \\ \qquad \textbf{endif.} \\ \textbf{enddef.} \end{array}\)
其中藍色的是左子樹貢獻,紅色的是右子樹貢獻。
當當前節點 \(i\) 是葉節點的時候,貢獻很容易計算。
否則考慮左右子樹的貢獻分別計算,分成兩種情況考慮:
- \(pre\) 小於左子樹的最大值:
此時對右子樹來說,\(pre\) 是無意義的,所以遞歸進左子樹,右子樹的貢獻直接用“全部”減“左子樹”計算即可。 - \(pre\) 大於等於左子樹的最大值:
此時對左子樹來說,就不可能貢獻任何前綴最大值了,所以貢獻為 \(0\),然后遞歸進右子樹即可。
可以看出,調用一次 \(\mathrm{calc}\) 函數遞歸的時間復雜度為 \(\mathcal O (\log n)\),因為每次只遞歸進一個孩子。
每次維護當前節點的答案時,只要令 \(\mathrm{cnt}[i] = \mathrm{cnt}[\mathrm{leftchild}[i]] + \mathrm{calc}(\mathrm{rightchild}[i], \max[\mathrm{leftchild}[i]])\) 即可。
可以發現有 \(\mathcal O (\log n)\) 個節點要調用 \(\mathrm{calc}\) 函數,所以一次單點修改的時間復雜度為 \(\mathcal O (\log^2 n)\)。
至此可以寫出本題的代碼:
#include <cstdio>
typedef long long LL;
const int MN = 100005, MS = 1 << 18 | 7;
int N, Q, H[MN];
inline bool gt(int p1, int p2) { // s[p1] is greater than s[p2]
if (!p2) return H[p1];
return (LL)H[p1] * p2 > (LL)H[p2] * p1;
}
#define li (i << 1)
#define ri (li | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
int id[MS], cnt[MS];
void Build(int i, int l, int r) {
id[i] = l, cnt[i] = 1;
if (l == r) return ;
Build(ls), Build(rs);
}
int Calc(int i, int l, int r, int p) {
if (l == r) return gt(l, p);
if (gt(id[li], p)) return Calc(ls, p) + (cnt[i] - cnt[li]);
else return 0 + Calc(rs, p);
}
void Mdf(int i, int l, int r, int p) {
if (l == r) return ;
if (p <= mid) Mdf(ls, p);
else Mdf(rs, p);
id[i] = gt(id[ri], id[li]) ? id[ri] : id[li];
cnt[i] = cnt[li] + Calc(rs, id[li]);
}
int main() {
scanf("%d%d", &N, &Q);
Build(1, 1, N);
while (Q--) {
int p, x;
scanf("%d%d", &p, &x);
H[p] = x, Mdf(1, 1, N, p);
printf("%d\n", Calc(1, 1, N, 0));
}
return 0;
}
但是,我們注意到一個很關鍵的性質:
當 \(pre\) 小於左子樹的最大值時,右子樹對當前節點的貢獻,是通過減法計算的。
也就是說這個信息要滿足一定程度上的可減性。
但是有很多信息是不滿足可減性的,比如 \(\max, \min\)、按位與、按位或等。
為了能讓這種線段樹適應更一般的情況,我們修改維護的信息的意義:
- 仍然維護這個區間中的 \(s_i\) 的最大值。
- 此時並不是維護區間的答案,而是僅考慮該區間的影響后,卻又只統計右子樹的答案。
也就是說令當前節點對應的區間為 \([l, r]\),區間中點為 \(mid\),則:
維護的答案是,只考慮 \(g_l \sim g_r\) 時,在區間 \([mid + 1, r]\) 中的答案。
仍然把信息 1 記做 \(\max[i]\),把信息 2 記做 \(\mathrm{cnt}[i]\)。
對於葉節點,信息 2 則看作是未定義的。
然后考慮維護當前節點的信息(也就是 Pushup),仍然引入一個 \(\mathrm{calc}(i, pre)\) 函數。
此時它的作用仍然是計算在 \(pre\) 的影響下的整個區間內的答案(而不是右子樹),也就是說它的意義沒有改變。
它的偽代碼如下:
\(\displaystyle \begin{array}{l} \textbf{def: } \mathrm{calc}(i, pre) \\ \qquad \textbf{if } (i \text{ is a leaf node}) \\ \qquad \qquad \textbf{return } {\color{green}{[\max[i] > pre]}} \\ \qquad \textbf{else} \\ \qquad \qquad \textbf{if } (\max[\mathrm{leftchild}[i]] > pre) \\ \qquad \qquad \qquad \textbf{return } {\color{blue}{\mathrm{calc}(\mathrm{leftchild}[i], pre)}} + {\color{red}{\mathrm{cnt}[i]}} \\ \qquad \qquad \textbf{else} \\ \qquad \qquad \qquad \textbf{return } {\color{blue}{0}} + {\color{red}{\mathrm{calc}(\mathrm{rightchild}[i], pre)}} \\ \qquad \qquad \textbf{endif.} \\ \qquad \textbf{endif.} \\ \textbf{enddef.} \end{array}\)
其實變化並不大,因為此時 \(\mathrm{cnt}[i]\) 記錄的直接就是右子樹信息,所以不需要做減法。
每次維護當前節點的答案時,只要令 \(\mathrm{cnt}[i] = \mathrm{calc}(\mathrm{rightchild}[i], \max[\mathrm{leftchild}[i]])\) 即可。
其實更好寫了,代碼如下:
#include <cstdio>
typedef long long LL;
const int MN = 100005, MS = 1 << 18 | 7;
int N, Q, H[MN];
inline bool gt(int p1, int p2) { // s[p1] is greater than s[p2]
if (!p2) return H[p1];
return (LL)H[p1] * p2 > (LL)H[p2] * p1;
}
#define li (i << 1)
#define ri (li | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
int id[MS], cnt[MS];
void Build(int i, int l, int r) {
id[i] = l, cnt[i] = 1;
// if i is a leaf node, then cnt[i] can be any value.
// but here, for convenience, we just let it be 1.
if (l == r) return ;
Build(ls), Build(rs);
}
int Calc(int i, int l, int r, int p) {
if (l == r) return gt(l, p);
if (gt(id[li], p)) return Calc(ls, p) + cnt[i];
else return 0 + Calc(rs, p);
}
void Mdf(int i, int l, int r, int p) {
if (l == r) return ;
if (p <= mid) Mdf(ls, p);
else Mdf(rs, p);
id[i] = gt(id[ri], id[li]) ? id[ri] : id[li];
cnt[i] = Calc(rs, id[li]);
}
int main() {
scanf("%d%d", &N, &Q);
Build(1, 1, N);
while (Q--) {
int p, x;
scanf("%d%d", &p, &x);
H[p] = x, Mdf(1, 1, N, p);
printf("%d\n", Calc(1, 1, N, 0));
}
return 0;
}
[CodeForces 671E]Organizing a Race
題意簡述:
題意的抽象過程太復雜了,這里僅考慮抽象后的模型:
給出兩個長度為 \(n\) 的整數序列 \(a_i, b_i\),令 \(\displaystyle c_i = a_i + \max_{j = 1}^{i} \{ b_j \}\)。
你需要動態維護整個數組中滿足 \(\boldsymbol{c_i \le k}\) 的最大下標 \(\boldsymbol{i}\),需要支持 \(b_i\) 的區間加減的修改操作。
而 \(a_i\) 是不會變的(不過,如果加一個 \(a_i\) 的區間加減操作,也可以做)。
題解:
可以發現,因為這里要維護的東西變成 \(c_i\) 的區間 \(\min\) 了,沒有可減性,所以不能用第一種方法。
考慮在線段樹的每個節點維護三個信息:
- 這個區間中 \(a_i\) 的最小值,記做 \({a\mathrm{min}}\)。
- 這個區間中 \(b_i\) 的最大值,記做 \({b\mathrm{max}}\)。
- 僅考慮該區間時,在右子樹內的答案,記做 \(\mathrm{ans}\)。
因為是區間修改 \(b_i\),所以這里需要用到線段樹懶標記的方法,具體不展開講。
此時需要面對兩個問題,下傳標記(Pushdown)和維護信息(Pushup)。
對於打標記,當一個節點被打上區間 \(b\) 加上 \(x\) 的標記的時候,只要把 \({b\mathrm{max}}\) 和 \(\mathrm{ans}\) 都加上 \(x\) 即可。
那么最重要的問題仍然是維護信息(Pushup),仍然是寫出類似函數 \(\mathrm{calc}(i, pre)\) 的偽代碼:
\(\displaystyle \begin{array}{l} \textbf{def: } \mathrm{calc}(i, pre) \\ \qquad \textbf{if } (i \text{ is a leaf node}) \\ \qquad \qquad \textbf{return } {\color{green}{{a\mathrm{min}}[i] + \max \{ pre, {b\mathrm{max}}[i] \} }} \\ \qquad \textbf{else} \\ \qquad \qquad \textbf{if } ({b\mathrm{max}}[\mathrm{leftchild}[i]] > pre) \\ \qquad \qquad \qquad \textbf{return } \min \{ {\color{blue}{\mathrm{calc}(\mathrm{leftchild}[i], pre)}}, {\color{red}{\mathrm{ans}[i]}} \} \\ \qquad \qquad \textbf{else} \\ \qquad \qquad \qquad \textbf{return } \min \{ {\color{blue}{{a\mathrm{min}}[\mathrm{leftchild}[i]] + pre}}, {\color{red}{\mathrm{calc}(\mathrm{rightchild}[i], pre)}} \} \\ \qquad \qquad \textbf{endif.} \\ \qquad \textbf{endif.} \\ \textbf{enddef.} \end{array}\)
對於當前節點 \(i\) 是葉節點的情況顯然。
假如 \(pre < {b\mathrm{max}}[\mathrm{leftchild}[i]]\),那么對右子樹來說直接繼承答案即可,然后遞歸進左子樹。
否則左子樹中所有的 \(b_i\) 都 \(\le pre\),那么 \(b\) 的前綴 \(\max\) 也自然是都等於 \(pre\),只要考慮 \(a_i\) 的最小值即可。
最后需要求整個數組中滿足 \(c_i \le k\) 的最大下標 \(i\),一般情況下可以直接線段樹上二分,但是這里比較特殊。
考慮一個新函數 \(\mathrm{solve}(i, pre)\),表示當前綴最大值為 \(pre\) 時,線段樹中節點 \(i\) 對應的區間 \(c_i \le k\) 的最大下標 \(i\)。
- 如果 \({b\mathrm{max}}[\mathrm{leftchild}[i]] > pre\),也就是說 \(pre\) 影響不到右子樹:
那么,如果 \(\mathrm{ans}[i] \le k\),就遞歸進右子樹,否則遞歸進左子樹。
復雜度顯然是 \(\mathcal O (\log n)\)。 - 如果 \({b\mathrm{max}}[\mathrm{leftchild}[i]] \le pre\),也就是說左子樹完全被 \(pre\) 控制了:
先遞歸進右子樹查詢,如果沒查詢到,則考慮左子樹因為被 \(pre\) 控制了,限制變為 \(a_i + pre \le k\)。
則移項得到 \(a_i \le k - pre\),在左子樹內是一個正常的線段樹上二分的子問題(需要新寫一個函數查詢)。
因為只會進行 \(\mathcal O (\log n)\) 次線段樹上二分,所以時間復雜度為 \(\mathcal O (\log^2 n)\)。
至此我們在 \(\mathcal O (n \log^2 n)\) 的時間復雜度內解決了這個問題。