[NOI]2024 登山 題解

tevenqwq發表於2024-08-19

好像在洛谷題解區裡還沒人和我做法一樣,,?

考場做法,只用到了倍增和線段樹,感覺挺好寫。考場上從開題到過題只用了 2h。

最底下有省流版(?)。

以下是我考場裡比較詳細的思路,所以比較長。

先考慮如何 \(O(n^2)\) 做,然後再想最佳化。

容易先想到一個狀態數是 \(O(n^2)\) 的 DP,即記錄起點,並將向上跳多少的限制記在狀態裡。這個 DP 的轉移順序看起來比較的混亂。那麼我們再觀察一下這個轉移的過程。

記一個節點 \(x\) 的限制為至少跳到深度為 \(v_x\) 的祖先,\(v_x=d_x-(h_x+1)\)。我們改 \(l_x\)\(r_x\) 的定義為能跳到的祖先深度區間。

假如說我現在以節點 \(x\) 作為起點,首先有個限制 \(lim=v_x\),然後接下來走一步會有兩種選擇:

  1. 向上跳到祖先 \(p\),那麼限制會變為 \(lim=\min(lim,v_p)\),容易發現一定會是 \(lim=v_p\)
  2. 向下走到一個兒子 \(y\),那麼限制會變為 \(lim=\min(lim,v_y)\)

在第一種情況裡,限制只跟 \(v_p\) 有關。因此可以看作以節點 \(p\) 作為起點。那麼此時就變成了一個相同的子問題。

在第二種情況裡,若 \(lim\) 不變,那麼限制還是 \(v_x\);若 \(lim\) 發生了改變,那麼限制就只跟 \(v_y\) 有關了。假如說 \(lim\) 發生了改變,那麼就又可以將 \(y\) 作為一個新的起點。

那麼此時我們就有了一個狀態數 \(O(n)\) 的 DP:\(f(x)\) 表示以 \(x\) 作為起點的方案數。

由上述分析我們容易知道轉移的順序:因為 \(lim\) 是越來越小的,所以我們按 \(v_x\) 從小到大求出每個 \(f(x)\) 即可。

那麼我們也容易得知求 \(f(x)\) 的一個暴力轉移:

\(x\) 開始向子樹內 BFS,會遇到兩種點。

  1. 如果碰到了已經求過 DP 值的節點,那麼這個節點的限制一定比 \(x\) 嚴格,直接用這個節點的 DP 值更新 \(f(x)\)
  2. 若 BFS 到的當前節點還沒有求出 DP 值,那麼計算從這個節點向上跳的方案數(即將其可以向上跳到的節點與 \(x\) 的限制求交得到的所有節點的 DP 值之和),然後繼續向下 BFS。

這個 DP 暴力轉移是 \(O(n^2)\) 的,瓶頸在於上述 BFS 的過程。

對於 1 類點來說,我們希望找到從 \(x\) 向下 BFS 第一次碰到的所有限制嚴於 \(x\) 的所有節點的 DP 值。

對於 2 類點來說,我們希望把 \(x\) 子樹中比 \(x\) 限制更嚴的點的子樹刪掉,剩下的點就是 2. 中的情況。

此時我不知道從何處下手了,接著想到了一個不知道能不能繼續做的做法:求了根號次 DP 值就重構?但是這個對我來說太難往下繼續思考了。

於是我先看了部分分:樹為一條鏈。

這種情況下,對於 \(x\) 來說,向它子樹內 BFS 時碰到的 1 類點只有一個,2 類點形成了一段區間。我們記 \(nxt_x\) 代表 \(x\) 子樹內的 1 類點(即 \([x+1,n]\) 中第一個限制嚴於 \(x\) 的點)。

那麼 \(f(x)\) 就很好求了(注意 \(l_x,r_x\) 的含義與原題中的含義不同,在題解一開始的時候講過):

\[f(x)=f(nxt_x)+\sum_{x<y<nxt_x}\sum_{l_y\leq i\leq \min(r_y,v_x)}f(i) \]

其中 \(\sum f(i)\) 可以用字首和表示,記 \(s(x)\)\(f\) 的字首和,那麼

\[f(x)=f(nxt_x)+\sum_{x<y<nxt_x}s(\min(r_y,v_x))-s(l_y-1) \]

其中 \(-s(l_y-1)\)\(x\) 無關,可以直接預處理,\(s(\min(r_y,v_x))\) 也可以根據 \(r_y\)\(v_x\) 的大小關係來分類討論,從而去掉 \(\min\)

總結一下,我們得出了樹為一條鏈的做法:從前往後掃描,碰到 \(l_y-1\) 的時候將 \(-s(l_y-1)\) 的貢獻放在 \(y\) 上,碰到 \(r_y\) 的時候將 \(s(r_y)\) 的貢獻放在 \(y\) 上。碰到 \(v_x\) 的時候求 \(f(x)\) 的值,我們需要求出 \((x,nxt_x)\) 中的貢獻之和,以及有多少個 \(y\) 假了 \(-s(l_y-1)\) 的貢獻但是沒有加 \(s(r_y)\) 的貢獻:這些 \(y\) 都對 \(x\) 產生了 \(s(v_x)\) 的貢獻。

這個統計 2 類點對 \(x\) 造成貢獻的方法可以很容易地擴充到樹上。那麼我們又回到了一個問題上:我們如何刪掉一個子樹?

此時我想到了一個做法:用 \(u(x)=0/1\) 代表 \(x\) 有沒有被刪除。求出一個點 \(x\) 的 DP 值時,將 \(x\) 子樹裡的點的 \(u\) 值全都改為 \(1\)

這顯然是個錯的做法:我們 DFS 到 \(x\) 子樹裡的時候可是要用到這個子樹裡的點啊,怎麼能刪掉呢?可是我們 DFS 到 \(x\) 子樹時,直接遍歷 \(x\) 子樹將 \(u\) 值全都變為 \(0\) 也是不對的,因為 \(x\) 子樹裡可能已經有要刪掉的子樹了。

那麼我們可以更改 \(u(x)\) 的定義,將有沒有被刪除改成被刪除了幾次。這時候豁然開朗了:我驚奇地發現,\(x\) 子樹裡的 \(u\) 值,它們都 \(\ge u(x)\),並且 \(x\) 子樹裡的 2 類點,它們的 \(u\) 值都等於 \(u(x)\)

(這個東西證明很簡單,但是我考場上沒有思考正確性我就不寫了)

那麼我們利用 DFS 序將子樹變成區間,記錄一個 pair,表示區間中 \(u\) 的最小值,以及 \(u\) 等於最小值的那些點的資訊就能求出所有 2 類點的貢獻了!

1 類點的貢獻也迎刃而解了:在打刪除標記的時候不將子樹的根打標記,然後維護 \(u\) 最小值的 DP 值之和即可。(2 類點的 DP 值都為 \(0\) 所以 2 類點不會造成影響)

省流

\(f(x)\) 為從 \(x\) 開始走的答案。假如走到某個點,限制突然改變了那麼就可以直接用這個點的 \(f\) 值用來更新 \(f(x)\)

向上跳肯定會改變限制,那麼考慮朝子樹裡走的情況:

  1. 朝子樹裡走到一個改變限制的點,需要求出這些點的 \(f\) 之和。
  2. 走到一個不改變限制的點,然後向上跳。

第二種情況是跳到一條祖先後代鏈,假如說找到了這些點就可以用字首和解決。

那麼我們來找這些點。我們求出一個點的 \(f\) 就對其子樹裡的所有點打上一個刪除標記。記 \(u(x)\) 表示 \(x\) 被刪除了幾次。那麼求 \(f(x)\) 時,所有 \(u\) 值等於 \(u(x)\) 的那些點都是第二種情況裡的點。透過 DFS 序將子樹變成區間,用線段樹維護 pair 的方法維護區間內 \(u\) 最小值的資訊即可。

第一種情況就是打刪除標記時不將根打標記即可。複雜度 \(O(n\log n)\)

程式碼只拍了照片,而且寫的比較醜陋,如果某天我想寫了就重寫一份吧(

程式碼照片