題意
給一顆有根樹。多次詢問 \(p\)、\(k\),求 \(p\) 的子樹往下最多走 \(k\) 步可以到達幾個節點。
觀察
若從 \(u\) 往下走 \(k\) 步可以到達 \(v\),等價於 \(d[v] \le d[u]+k\)。題目相當於詢問 \(u\) 子樹內有多少子節點深度 \(\le d[u]+k\)。
作法
區間查詢 \(x\) 數量
給你一個序列 \(a\),多筆查詢 \(\le k\) 的數字有多少。我們可以對值域開一個序列上的資料結構 \(cnt\),當出現值為 \(x\) 的元素就 \(cnt[x]++\),查詢時答案就是 \(cnt[1]+...+cnt[k]\)。
進階一點,給你一個序列 \(a\),多筆區間查詢 \(\le k\) 的數字有多少。只要我們能只把 \(a[l], a[l+1], ..., a[r]\) 放進資料結構,就能用同樣的方式查詢。
如果序列只有一種值,那等於只要 \(r\) 時有多少個減 \(l\) 時有多少個就可以得出答案。推廣到整個序列的很多種值,我們只要每個資料結構的每個位置相減就行:把 \(a[i]\) 放進陣列後,我們紀錄現在的 \(cnt\) 為 \(cnt_i\),當只要 \(a[l], a[l+1], ..., a[r]\) 放進陣列的結果,就把 \(cnt_r\) 跟 \(cnt_l\) 的每個元素相減得到 \(cnt_p\) 。
我們的 \(cnt\) 以持久化線段樹實作,支援單點修改、查詢版本 \(i\) 下的區間和。
持久化線段樹
因為每次修改只會更動 \(logn\) 個值,我們只要紀錄被更改的節點就好。
對於每次修改我們想像開一顆新的線段樹作為最新的版本,但是子樹若長得完全ㄧ樣就指回去上個版本。
實作要注意:
- 修改時必須回傳新的節點
- 若子樹一樣必須指回去上個版本
- 從外面呼叫修改時,要拿一個陣列儲存新的線段樹的根
- 可以不用事先 build,全部指到 \(0\)
1 | int root[N]; |
樹壓平
題目都是查詢子樹,因此只要保證子樹在連續區間內,就能套用上面的方法。
dfs 進去時紀錄 \(tin[u] = ++time\),出來時紀錄 \(tout[u] = ++time\)。因為一定子樹走完才會出來,因此子樹一定被 \(tin[u]\) 和 \(tout[u]\) 夾住。
有個優化的方式,出來時不佔用另一個 \(time\),就讓多個 \(tout\) 指在同一個地方,如此序列大小減半且答案不需要除以 \(2\)。
1 | int dseq[N], tin[N], tout[N], tim; |
AC Code
1 | const int N = 1e6+10; |