<style> .reveal .slides { text-align: left; font-size:30px } </style> # DP 優化 & 一些幾何 ---- - Convex Hull Optimization - Divide and Conquer Optimization - Li Chao Segment Tree - Dynamic Convex Hull --- ## Convex Hull Optimization ### DP 斜率優化 ---- 先來複習單調隊列優化 轉移式大概長這樣 `dp[i] = a[i] + min/max{dp[j]} (i-k `$\le$` j < i)` 並且運用其單調性將複雜度從 `O(nk)` 降到 `O(n)` ---- ## 轉移式 而斜率優化的轉移式通常會長這樣 <div style="font-size:32px"> `dp[i] = a[i] + min/max`$_{j<i}$`{dp[j]+` <span style="color:red"> `x[i]*m[j]`</span>`+b[j]`} </div> 會發現紅色部分,跟 `i、j` 都有關 這時候就不能用單調隊列優化 ---- ## 幾何意義上的性質 以下面這條轉移式為例 `dp[i] = a[i] + max`$_{j<i}$`{dp[j]+x[i]*m[j]+b[j]}` 對於每個 `j` 可以視為一條斜率為 `m[j]` 、截距為 `b[j]` 的直線 計算 `dp[i]` 可以視為把 `x[i]` 帶進去所有的直線 `y = m[j] * x[i] + b[j]` 中取最大的那條 + `a[i]` ---- ## 幾何意義上的性質 `y = m[j] * x[i] + b[j]` 中取最大的那條加上 `a[i]` ![](https://i.imgur.com/I5OPjGZ.png =300x) ---- 每次對於 `x[i]` 要找出所有直線中帶進去最大的 y 等價於對於這些直線,維護下凸包 ![](https://i.imgur.com/vBWiTAK.png =300x) ---- ## 性質 對於這些下凸包上的直線 假設有兩條分別為 $L_1:m_1x+b_1$ 和 $L_2:m_2x+b_2$ 且 $m_1 < m_2$,必定有個分界點(交點) $x*$ 使得 $x*$ 以左帶進去 $L_1$ 比較大 $x*$ 以右帶進去 $L_2$ 比較大 ![](https://i.imgur.com/vBWiTAK.png =300x) 因此下凸包從左到右的直線斜率會遞增 ---- ## 如何維護下凸包 ---- ### case 1. 斜率 m[i] 遞增、詢問 x[i] 遞增 * 斜率 m[i] 遞增 插入的直線遞增代表每次插入一定會放到下凸包的最右邊 因此每次插入新直線時,先判斷當前在下凸包上最後一條直線會不會被覆蓋,會要先刪除 * 詢問 x[i] 遞增 詢問的位置遞增則可以跟單調對列優化的做法相同,只需維護到當前的詢問位置 x[i] 為止, 的下凸包最左邊會詢問到哪,更左的直線都可以刪除 作法如同單調對列優化:可以用 deque 維護 `push_back`, `pop_front`, `pop_back` 等操作 ---- ### case 2. 斜率 m[i] 遞增、詢問 x[i] 不單調 * 斜率 m[i] 遞增 如同前一種 case,好好維護下凸包最右端的直線,把不會出現在下凸包上的直線刪除 * 詢問 x[i] 不單調 如果不單調,代表用到的直線不一定在最左端,則我們可以透過二分搜找詢問位置 x[i] 的是哪一條直線維護的 只需要 `push_back`, `pop_back` 可以用 vector 維護 ---- ### case 3. 斜率 m[i] 不單調、詢問 x[i] 遞增 * 斜率 m[i] 不單調 下凸包可以改用 set 維護,判斷 m[i] 要插入的位置會不會讓下凸包更好,會就插入並且往左右判斷會不會使得相鄰的直線不在下凸包上,會就從 set 上刪掉 * 詢問 x[i] 單調 如同單調對列,當 set 最前面的線段過期了就丟掉 ---- ### case 4. 斜率 m[i] 不單調、詢問 x[i] 不單調 由於兩者都沒單調性,若用 set 維護 詢問會變不好做 除非自己刻一棵 Treap 維護 或者可以用 [下凸包模板](https://github.com/jakao0907/CompetitiveProgrammingCodebook/blob/master/geometry/lowerConcaveHull3.cpp) 或者 [李超線段樹](https://github.com/jakao0907/CompetitiveProgrammingCodebook/blob/master/geometry/LiChaoST.cpp) 維護 ---- ## 操作 - insert line(ax+b) 插入一條一次方程式 - query f(x) 詢問 $x=Si$ 時最大的 $y$ 是多少 兩個操作都是 $O(\log N)$ ---- ## 引入例題 [HNOI 2008 玩具裝箱](https://vjudge.net/problem/LibreOJ-10188) 有 $n$ 個玩具,第 $i$ 個玩具價值為$C_i$。要求將這 $n$ 個玩具排成一排,分成若干段。對於一段$[l, r]$,它的代價為$(r-l+ \sum_{i=l}^{r}C_i -L)^2$。求分段的最小代價。 <span>先來列出轉移式吧!<!-- .element: class="fragment" data-fragment-index="1" --></span> ---- 再來看看範圍 $1\le n\le 5\times 10^4$ 太大不能 $O(N^2)$ QQ ---- 接著就是斜率優化出場的時候 ---- ## 作法 1. 列出轉移式 2. 簡化轉移式變成一次函數 `ax+b` 3. 維護下凸包 4. AC ---- ## 轉移式 $[l, r]$ 公式:$(r-l+\sum_{i=l}^{r}C_i -L)^2$ `dp[i] = min`$_{j<i}$`{ dp[j] + (pre`$_{i}$`-pre`$_{j}$`+i-(j+1)-L)`$^2$` }` 其中 `pre`$_{j}$ 為 $\sum_{i=1}^{j} C_i$ ---- `dp[i] = min`$_{j<i}$`{ dp[j] + (pre`$_{i}$`-pre`$_{j}$`+i-(j+1)-L)`$^2$` }` 整理一下公式 令 `S`$_{i}$ = `pre`$_{i}$` + i` `L' = L+1` $\to$ `dp[i] = min`$_{j<i}$`{ dp[j] + (S`$_{i}$`-S`$_{j}$`-L')`$^2$` }` ---- 再把跟 `j` 無關的變數移到左邊 `dp[i] = min`$_{j<i}$`{ dp[j] + (S`$_{i}$`-S`$_{j}$`-L')`$^2$` }` 先包起來 `dp[i] = min`$_{j<i}$`{ dp[j] + ((S`$_{i}$`-L')-S`$_{j}$`)`$^2$` }` $\to$ `dp[i]-(S`$_{i}$`-L')`$^2$ `= min`$_{j<i}$`{ dp[j] + S`$_{j}$$^2$` - 2 S`$_j$`(S`$_i$`-L')}` ---- 整理等式右邊 `dp[i]-(S`$_{i}$`-L')`$^2$`= min`$_{j<i}$`{dp[j]+S`$_{j}$$^2$`-2S`$_j$`(S`$_i$`-L')}` $\to$ `dp[i]-(S`$_{i}$`-L')`$^2$`= min`$_{j<i}$`{dp[j]+S`$_{j}$$^2$`+2S`$_j$`L'-2S`$_i$`S`$_j$`)}` ---- `dp[i]-(S`$_{i}$`-L')`$^2$`= min`$_{j<i}$`{dp[j]+S`$_{j}$$^2$`+2S`$_j$`L'-2S`$_i$`S`$_j$`}` 把右側只跟`j`有關的變數設成`b` `b = dp[j] + S`$_{j}$$^2$`+2S`$_{j}$`L'` 跟`i、j`都有關的設成 `ax` `ax = -2S`$_i$`S`$_j$ 變成 `dp[i]-(S`$_{i}$`-L')`$^2$`= min`$_{j<i}$`{ax+b}` ---- ## 一次函數 ax + b `ax = -2S`$_i$`S`$_j$ `b = dp[j] + S`$_{j}$$^2$`+2S`$_{j}$`L'` 其中 `a`為`-2S`$_{j}$ `x` 為當前的 `S`$_{i}$ ---- ## 求 dp[i] 下凸包求當前位置 y 的最大值,但題目是求最小值,因此要加負號 `dp[i]-(S`$_{i}$`-L')`$^2$`= min`$_{j<i}$`{dp[j]+S`$_{j}$$^2$`-2S`$_j$`L'+2S`$_i$`S`$_j$`}` ```cpp= dp[i] = -eval(S[i]) + (S[i]-L')*(S[i]-L'); ``` ---- ## 插入一次方程式 下凸包求最大值,但題目是求最小值,因此要加負號 `a = -2S`$_j$ `b = dp[j] + S`$_{j}$$^2$`+2S`$_{j}$`L'` ```cpp= insert_line(2*S[j], -(dp[j]+S[j]*S[j] + 2*S[j]*L')); ``` ---- ## 複雜度 `dp[i] = min`$_{j<i}$`{ dp[j] + (pre`$_{i}$`-pre`$_{j}$`+i-j-1-L)`$^2$` }` 原本的 $O(N^2)$ 降低到 $O(N\log N)$ ---- ## 單調隊列優化 一樣用 deque 維護 deque 維護在下凸包上的直線 ![](https://i.imgur.com/mcH5Vjw.png =400x) ---- 可以發現每次丟進去 ax+b 的 x (詢問) 都是遞增的 x 為 S$_i$ = `pre`$_{i}$` + i` ![](https://i.imgur.com/xnqPT2J.png =400x) 因此放在左邊的線會過期 ---- ## 檢查過期 ![](https://i.imgur.com/xnqPT2J.png =400x) 直接用當前的詢問 x 帶進去,如果 deque 裡最左的兩條直線相比 $a_{1}x+b_1\le a_{2}x+b_2$ 則代表最前面的線過期了 就可以直接 pop 掉 ---- ## 新增當前的線 而這題新增的線段也是斜率遞增的 因此也有單調性 ---- ## 新增當前的線 假設紅色為當前要新增的線,藍色為原本的最後一條線 ![](https://i.imgur.com/4utrfPq.png =400x) 會發現如果新增紅色線的話,藍色線就沒用了(不在下凸包上) 因此每次新增線的時候要檢查是否在會讓前一條在移除凸包 ---- ## 檢查是否移除 下圖可以看到下凸包裡最後一條線與倒數第二條線的交界在 x=h 的位置,而我們可以檢查最後一條線$a_1$x+$b_1$ 跟 欲新增的線 $a_2$x+$b_2$的大小關係,如果 $a_1$x+$b_1$ < $a_2$x+$b_2$ 則可以刪除最後一條線 刪除後再把新的線加進去 ![](https://i.imgur.com/xNxH0bm.png =500x) ---- 下凸包求最大值,但題目是求最小值,因此要加負號 ```cpp= deque<pair<ll,ll>> deq; //ax+b int eval(ll x){ return deq.front().first*x+deq.front().second; } void solve(){ for(int i=0;i<n;i++){ while(前面過期) deq.pop_front(); dp[i] = -eval(s[i]) + (S[i]-L')*(S[i]-L'); while(最後一條被覆蓋) deq.pop_back(); deq.push_back({2*S[i], -(dp[i]+S[i]*S[i] + 2*S[i]*L')}); } } ``` ---- ## 漂亮的寫法 線段也可以用結構包起來,多載 () ```cpp struct line{ int a, b; int operator() (int x){ return a * x + b; } }; ``` 詢問時就能直接 l(x) 求出當前直線在 x 的值 ```cpp line l; cout << l(x) << endl; ``` reference: [DP斜率優化I](https://hackmd.io/@yuspring/SylgZEOpu) ---- 如此以來就能在 $O(n)$ 做完了! --- ## Divide and Conquer DP ### 分治優化 ---- - 常見 2D/1D 轉移式:$dp(i, j)=\min\limits_{1\le k\le j}\{dp(i-1, j) + f(i,j)\}$ - 且第二維度 $j$ 滿足凸性 - 滿足凸性也就是令 $p[i][j]$ 代表 $dp[i][j]$ 的轉移點為 $k$,那麼滿足凸性代表會有 $p[i][1]\le\dots\le p[i][m],\forall i$ - $dp(i-1, j) + f(i,j)$ 代表 $dp(i, j)$ 的計算只跟 $dp[i-1][k]$ 和其他資料有關,不需要用到任何 $dp[i]$ ---- 上述會有兩個性質 - 一個是 $dp[i][1\sim j]$ 的計算順序不重要可以任意選擇計算順序(因為只依賴 $dp[i-1]$ 層的東西,以及該方向上有轉移點單調遞增的性質 - 既然有轉移點單調遞增,那麼假設算好了 $dp[i][j]$ 和 $dp[i][k]$ 的值且 $j<k$,那麼對於所有 $j<r<k$ 都可以知道轉移點會滿足 $p[i][j]\le p[i][r]\le p[i][k]$,那麼在計算 $dp[i][r]$ 的最大值時只需要考慮枚舉 $p[i][j]\sim p[i][k]$ 這個範圍內即可 ---- - 因此就可以考慮套用分治: - 假設現在要計算 $dp[i][l\sim r]$ 且轉移點範圍已知是 $vl\sim vr$ - 那麼先暴力(直接枚舉掃過所有 $vl\sim vr$ 找出最大值)計算出 $dp[i][m=\frac{l+r}{2}]$ 位置的值及其轉移點 $vm=p[i][m]$,那麼之後就能知道 $l\sim m-1$ 的區間只需要考慮 $vl\sim vm$,而 $m+1\sim r$ 的範圍則是只要考慮 $vm\sim vr$ - 直接遞迴下去分別做這兩部份即可 - 初始化則是 $l=1,r=m,vl=1,vr=m$ 之類的 ---- ### 複雜度 - 可以看出這個作法非常簡單非常暴力,只有一個短短且實做很直觀簡單的遞迴分治而已,對於計算函數 $f$ 除了要通靈/證明出他具有凸性且不依賴 $dp[i]$ 之外,不需要額外的性質 - 複雜度則是能從暴力計算的 $O(nm^2)$ 降至 $O(nm\log m)$ 以 n, m = 3000 為例 $O(nm\log m)\to 10^8\to AC$ ---- ### Houses and Schools 有 $n$ 個房子在一維數線上,兩個房子在位置 a, b,則距離為 |a-b| 而要選 $k$ 個點建立學校,使得每個房子到最近的學校距離總和最小。 - subtask 1 : n, k = 300 [link](https://zerojudge.tw/ShowProblem?problemid=e165) - subtask 2 : n, k = 3000 ---- ## 轉移式 `dp[k][n]` 建了 $k$ 個學校覆蓋前 $n$ 個房子的最小距離總和 轉移為每次從 dp[k-1][$j$] 轉移,代表前 k-1 個學校覆蓋前 $j$ 個房子 而第 $i$ 個學校覆蓋 $(j+1)\sim n$ 個房子 (設置點為第 $(j+1)\sim n$ 個房子的中點) $dp[k][n] = _{j<n}min(dp[k-1][j] + sum(j+1, n))$ 直接做複雜度 $O(kn^2)$ ---- $dp[k][n] = _{j<n}min(dp[k-1][j] + sum(j+1, n))$ 觀察以上 dp 式可以發現 dp[k][n] 只跟 dp[k-1] 有關, 且轉移點有凸性(每次選的房子只會往右) 因此可以使用 分治優化 ---- ## 核心程式碼 ```cpp= void solve(int k, int l=1, int r=n, int vl=1, int vr=n){ if(l > r) return; int mid = (l + r) >> 1, p = 0; dp[k][mid] = 1e18; for(int i = vl; i <= min(vr, mid - 1); i ++){ if(dp[k][mid] >= dp[k - 1][i] + f(i,j)){ //窮舉最佳轉移點 dp[k][mid] = dp[k - 1][i] + f(i,j); p = i; } } solve(k, l, mid - 1, vl, p); // dp[i][1~mid-1] 的轉移點會在區間 vl~p 之間 solve(k, mid + 1, r, p, vr); // dp[i][mid+1~r] 的轉移點會在區間 p~vr 之間 } solve(k, k, n, 1, n); ``` --- ## 李超線段樹 能處理的問題: - 在二維平面中插入一個一次函數 $f(x) = ax+b$ - 給定 $x$,查詢最大/最小 $f(x)$ ---- 每個節點儲存一個區間的線段 ``` struct{ node *l, *r; line f; } ``` 並且線段樹一樣,有左右子節點,直到區間 l = r 為止 以及儲存一個線段 f,為這段區間完整覆蓋的線段 接下來的例子以查詢最大值為例 ---- ## 插入線段 以動態開點線段樹實做,如果當前區間沒有線段,則把線段 $v$ 設成當前區間的線段。 ```cpp void insert(line v, int l, int r, node *nd){ if(nd == nullptr){ nd = new node(v); return; } } ``` 而如果該線段本來就有區間呢? ---- 可以分為以下兩種 case ### case1: 新的函數完全覆蓋/被覆蓋 ![image](https://hackmd.io/_uploads/H1Ar67EX1g.png =500x) 如果完全覆蓋則把當前區間的函數換成新的。 ---- ### case2: 兩條線在區間內有交點 以區間中間切一半,必定有其中一個子區間(左半或右半), 被其中一個函數完全覆蓋。 以下圖為例,藍色函數覆蓋了整個右半區間,而左半的答案不確定,因此使用藍色函數當成當前區間的函數,而左半區間繼續遞迴,直到區間完全被同一個函數覆蓋或者區間 Left = Right 為止。 ![image](https://hackmd.io/_uploads/Sy-DamN7ke.png =500x) 而其實不需要考慮 Left = Right 的 case,因為此 case 能被恰好一個函數所覆蓋。 ---- ## 給定 $x$,查詢最大 $f(x)$ 直接來看 code,查詢時如果到位置 x,則回傳當前區間的函數 $f(x)$ 值,否則回傳值為 max {當前區間的函數值,根據 x 往左/右區間的最大函數值} ```cpp= LL query(int x, int l, int r, node *nd){ if(!nd) return -INF; if(l == r) return nd->f.eval(x); if(mid >= x) return max(nd->f.eval(x), query(x, l, mid, nd->l)); return max(nd->f.eval(x), query(x, mid + 1, r, nd->r)); } ``` ---- 回顧插入函數時,在區間內有交點的情況 ![image](https://hackmd.io/_uploads/Sy-DamN7ke.png =500x) 如果 x 在右半區間,則答案在當前區間就能求出來, 因此所有走過的區間的函數都要 query 若 x 在左半區間,則插入線段會繼續遞迴處理 根據以上就能好好維護所有區間的最大函數值了 ---- ## 插入的函數改為區間 在二維平面中插入一個一次函數 $f(x) = ax+b$ 在區間 $[l, r]$ 把函數拆分到線段樹上的 $\log n$ 個區間, 對每個區間進行 insert ```cpp= void update(line v, int ql, int qr, int l, int r, node *nd){ if(nd == nullptr) nd = new node(); if(ql <= l && r <= qr){ insert(v, l, r, nd); return; } int mid = (l+r)/2; if(ql <= mid) update(v, ql, qr, l, mid, nd->left); if(qr > mid) update(v, ql, qr, mid+1, r, nd->right); } ``` ---- ## 複雜度 插入一個函數複雜度為 $O(\log n)$, 而如果插入的函數為區間插入則要拆成 $O(\log n)$ 個分別插入 複雜度為 $O(\log^2 n)$ 詢問為 $O(\log n)$,每次遞迴到最深 --- ## Dynamic Convex Hull ### 動態凸包 有 $q$ 筆操作,每次操作為以下其中一種: - 平面上加入一個點 $(x, y)$ - 每次詢問一個點 $(x, y)$ 是否在凸包內 ---- 只有詢問可以直接砸模板, 而新增點呢? ---- 回到找凸包的算法 Andrew’s Monotone Chain 分成上下凸包,分別維護 ---- 加入一個新的點時,如果可能構成更大的凸包, 如何快速的找到在原本上/下凸包上要改變的位置? <span><!-- .element: class="fragment" data-fragment-index="1" -->使用 set !</span> ---- 由於上/下凸包上的點分別都有照 x, y 排序,因此有單調性,可以在上面二分搜。 對於新增的點位置,在 set 上二分搜 ---- 對於新增的點,如果本來就在凸包內/上,則不用任何操作 否則要判斷當前新增的點,凸包上位置的左右兩側的點是否需要刪除(判斷外積) 記得好好處理上下凸包 $\le 3$ 個點的情況 ![image](https://hackmd.io/_uploads/rkascJSQkx.png =600x) 以上圖為例,新增綠色點時,右側的點需要被移除。 ---- ## 複雜度 每次判斷/新增點,複雜度 $O(\log n)$ 每個點最多被移除一次,均攤下來複雜度是好的 $q$ 筆操作,總複雜度 $O(q\log n)$
{"title":"DP & Geometry","description":"先來複習單調隊列優化","contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":11187,\"del\":560},{\"id\":\"08326fa4-ca9b-4ca9-873e-239ebe76662c\",\"add\":4,\"del\":5}]"}
    242 views
   Owned this note