1. 程式人生 > 其它 >【筆記】斜率優化 DP

【筆記】斜率優化 DP

玩具裝箱題解 - 洛谷
玩具裝箱題解 - cnblogs
斜率優化 - OIWiki

玩具裝箱(HAOI2008)

P 教授要去看奧運,但是他舍不下他的玩具,於是他決定把所有的玩具運到北京。他使用自己的壓縮器進行壓縮,其可以將任意物品變成一堆,再放到一種特殊的一維容器中。

P 教授有編號為 \(1 \cdots n\)\(n\) 件玩具,第 \(i\) 件玩具經過壓縮後的一維長度為 \(C_i\)

為了方便整理,P教授要求:

  • 在一個一維容器中的玩具編號是連續的。
  • 同時如果一個一維容器中有多個玩具,那麼兩件玩具之間要加入一個單位長度的填充物。形式地說,如果將第 \(i\) 件玩具到第 \(j\)
    個玩具放到一個容器中,那麼容器的長度將為 \(x=j-i+\sum\limits_{k=i}^{j}C_k\)

製作容器的費用與容器的長度有關,根據教授研究,如果容器長度為 \(x\),其製作費用為 \((x-L)^2\)。其中 \(L\) 是一個常量。P 教授不關心容器的數目,他可以製作出任意長度的容器,甚至超過 \(L\)。但他希望所有容器的總費用最小。

\(f_i\) 表示前 \(i\) 個玩具裝箱的最小費用,\(s_i\) 表示 \(c_i\) 的字首和,則有

\[f_i=\min_{j<i}\{f_j+(s_i-s_j+i-j-1-L)^2\} \]

如一個一個遍歷,複雜度為 \(O(n^2)\)

。展開後會出現 \(-2s_is_j\) 這一項,不滿足單調佇列優化判定式 \(\displaystyle f_i=\min_{j<i}\{a_i+b_j\}\)。即需要最小化的多項式和 \(i,j\) 均有關。

\(()^2\) 括號內與 \(i\) 有關的設為 \(A=s_i+i\),與 \(j\) 有關,或與 \(i,j\) 均無關的設為 \(B=s_j+j+1+L\),先不考慮 \(\min\),則有

\[f_i=f_j+(A-B)^2 \]

展開後移項得到

\[f_j+B^2=2AB+f_i-A^2 \]

上式只有 \(f_i-A^2\) 未知。通過考慮其的幾何意義,在均攤 \(O(1)\)

的時間內轉移求得最小值,便是「斜率優化」。

\((B,f_j+B^2)\) 視作平面中的點,記為 \(P_j\)。對於 \(j\),當等式成立時,相當於有一條斜率為 \(2A\) 的直線經過了點 \(P_j\)。而這條直線在 \(y\) 軸上的截距便是 \(f_i-A^2\)

要求出最小的截距,可以把斜率為 \(2A\) 的直線從下往上平移,直到碰到第一個點,此時的 \(y\) 軸截距即為最小。

考慮凸包的幾何意義(點集的「邊界」),可以發現使得截距最小的點一定在點集的下凸包上。

繼續觀察,還可以發現一個事實。若設構成下凸包的點集為 \(S\),並從左往右標號為 \(S_1,S_2,\dots, S_n\),設兩點 \(P_u,P_v\) 連成的直線的斜率為 \(K(P_u,P_v)\)。則使得截距最小的點 \(S_k\) 必滿足 \(K(S_{k-1},S_k)<2A\)\(K(S_k,S_{k+1})> 2A\)

凸包本身可以用佇列維護。維護方法是在加入一個點 \(P_i\),判斷隊尾的點 \(P_r\) 是否滿足 \(K(P_r,P_{r-1})>K(P_r,P_i)\)。如果滿足,則彈出 \(P_r\)。重複此過程直到 \(K(P_r,P_{r-1})<K(P_r,P_i)\) 或佇列中元素不多於一個。

而此題的斜率 \(2A\) 單調遞增,則 \(S_k\) 左邊點的個數也單調遞增,所以可以不斷 pop 使得 \(K(P_l,P_{l+1})<2A\) 成立的隊頭。pop 完之後,隊頭即為 \(S_k\)

此時來歸納一下本題的做法,對於每個點 \(P_i\)

  1. \(K(P_l,P_{l+1})<2A\),則將隊頭彈出,直到佇列中元素數量不多於一個或條件不成立。
  2. 取出隊頭,計算 \(f_i\)
  3. \(K(P_r,P_{r-1})>K(P_r,P_i)\),則彈出隊尾,直到佇列中元素不多於一個或條件不成立。
  4. \(P_i\) 加入隊尾。

Code:

  1. 佇列中元素多於一個的「程式碼意義」為 head < tail
  2. 將簡化後的式子中的變數用函式寫出來。

換成註釋裡的寫法就莫名其妙是錯的,可能是因為精度問題,我暫且諤諤。

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int N = 5e4 + 5;
int n, L, q[N], hd, tl;
ll s[N], f[N];
inline double A(int i) { return s[i] + i; }
inline double B(int i) { return s[i] + i + L + 1; }
inline double Y(int i) { return B(i) * B(i) + f[i]; }
inline double X(int i) { return B(i); }
inline double K(int i, int j) { return (Y(j) - Y(i))/(X(j) - X(i)); }

int main() {
	ios::sync_with_stdio(false); cin.tie(nullptr);
	cin >> n >> L;
	for(int i = 1; i <= n; i++) 
		cin >> s[i], s[i] += s[i - 1];
	hd = tl = 1; // 相當於 q[1] = f[0] = s[0] = 0 
	for(int i = 1; i <= n; i++) {
		while(hd < tl && K(q[hd], q[hd + 1]) < 2 * A(i)) ++hd;
		f[i] = f[q[hd]] + (ll)(pow((A(i) - B(q[hd])), 2) + 0.1); // 避免精度誤差
//		f[i] = f[q[hd]] + B(q[hd]) * B(q[hd]) - 2 * A(i) * B(q[hd]) + A(i) * A(i);
		while(hd < tl && K(q[tl - 1], q[tl]) > K(q[tl], i)) --tl;
		q[++tl] = i;
	}
	cout << f[n] << '\n';

	return 0;
}