題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。
Problem Statement
題目簡述
有 n 個玩具,必須按照原本順序分成若干個連續段,每一段放進一個容器。
若一個容器裝下標 i 到 j 的玩具,容器長度為
(j−i)+k=i∑jCk
其中相鄰兩個玩具之間需要 1 單位填充物。容器長度為 x 時,費用為 (x−L)2。
求所有容器費用總和的最小值。
Constraints
約束條件
- 1≤n≤5×104
- 1≤L≤107
- 1≤Ck≤107,0≤k<n
思路:劃分型 DP → 斜率優化/凸包優化(Convex Hull Trick)
先寫出自然 DP
這題的核心是:把一個序列切成若干個連續段,每段有一個只和段內總長度有關的代價,求總代價最小。這是典型的劃分型 DP。
先看最後一箱。若前面已經處理完前若干個玩具,最後一箱裝的是後面一段連續玩具,那麼答案可以由「前綴的最小代價 + 最後一箱的代價」轉移而來。
令前綴長度和為 Si=∑k=0i−1Ck,並定義 fi 表示裝完前 i 個玩具,也就是下標區間 [0,i) 的最小代價。若最後一箱裝的是下標區間 [j,i),這箱的玩具總長度是 Si−Sj,中間填充物有 i−j−1 個,所以這箱的費用是
((i−j−1)+(Si−Sj)−L)2
因此有轉移:
fi=0≤j<imin{fj+((i−j−1)+(Si−Sj)−L)2}
直接枚舉每個 i 的所有 j 是 O(n2),而 n 到 5×104,需要優化內層的最小值查詢。
看到「前綴最小值 + 某段代價」,先寫出劃分型 DP;如果段代價展開後有二次項,常見方向就是把轉移式整理成一次函數或內積形式,用斜率優化 / 凸包優化處理。
把二次式整理成內積
先把最後一箱的式子壓縮一下。對固定的右端點 i,有
(i−j−1)+(Si−Sj)−L=(i+Si)−(j+Sj+L+1)
不妨定義
Ai=i+Si,Bj=j+Sj+L+1
則轉移變成
fi=0≤j<imin{fj+(Ai−Bj)2}
展開平方:
fi=0≤j<imin{fj+Ai2−2AiBj+Bj2}=Ai2+0≤j<imin{fj+Bj2−2AiBj}
現在,和目前右端點有關的 Ai2 已經被提出去了;剩下要做的是,從所有歷史決策中找一個讓
fj+Bj2−2AiBj
最小的決策。
定義每個歷史決策對應的候選點為
vj=(Bj,fj+Bj2)
定義目前右端點對應的查詢向量為
pi=(−2Ai,1)
那麼內層最小值就是
0≤j<iminpi⋅vj
也就是查詢向量與候選點的內積最小值。
因此,這個轉移已經變成標準的 CHT 查詢形式:答案只可能出現在下凸包上。
幾何意義
對同一個查詢向量來說,最小化內積可以理解成找投影最小的候選點。凸包內部的點一定會被凸包邊界上的某些點支配,因此不可能成為最優決策。於是我們只需要維護下凸包,而不是保留所有歷史點逐一比較。
為什麼可以依序加入候選點?
程式每算到新的位置,就把「前一個位置作為最後一箱起點前綴」的候選點加入凸包,再查詢目前位置的答案。這樣保證查詢時凸包中剛好包含所有合法的 j<i。
候選點的橫座標是
Bj=j+Sj+L+1
因為每個玩具長度都是正數,Sj 隨著 j 嚴格遞增,所以 Bj 也嚴格遞增。這正好滿足 Andrew 式維護下凸包的前提:點按照橫座標遞增加入,不需要額外排序。
維護時看最後兩個凸包點與新點的轉向。如果中間點讓下凸包不再凸,代表它不可能在任何查詢中成為最優點,就可以刪除。程式中的外積判斷做的就是這件事。
查詢方式:二分,或利用單調性改成隊列
本份程式實際呼叫的是 query_bisect()。對固定查詢向量,凸包頂點上的內積值會先下降再上升,所以可以二分找到最小點,單次查詢 O(logn)。
但本題還有更強的單調性,可以把查詢改成註解中的 query_mono()。
目前位置往右移時,
Ai=i+Si
嚴格遞增,因此查詢向量
(−2Ai,1)
的第一維嚴格遞減,第二維不變。也就是說,查詢方向只會往同一側旋轉。
在一個按橫座標遞增的下凸包上,查詢方向單調旋轉時,最優頂點的索引也只會往右移,不會往左退。於是如果當前隊首已經不如下一個點,即
p⋅q0≥p⋅q1
那麼之後查詢方向繼續單調變化,q0 也不可能重新變成最優點,可以永久彈出。這就是單調隊列查詢的正確性來源。
程式保守使用二分查詢,時間複雜度是 O(nlogn)。若把查詢改成已寫好的 query_mono(),由於本題同時滿足「加入點橫座標遞增」與「查詢方向單調」,每個凸包點最多進出隊一次,可降到 O(n)。
複雜度分析
- 時間複雜度:O(nlogn),程式使用凸包上的二分查詢;若改用單調隊列查詢,可做到 O(n)。
- 空間複雜度:O(n)。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| """ 根據題意,cost(l, r) = ((r - l) + s[r + 1] - s[l] - L)^2,其中 s[i] 是 A 的前綴和。 可以得到轉移式:f[i] = min_{j < i} f[j] + ((i - (j + 1)) + s[i + 1] - s[j + 1] - L)^2
令 a[i] = i + s[i + 1], b[j] = j + s[j + 1] + L + 1, 則轉移式可以寫成 f[i] = min_{j < i} f[j] + (a[i] - b[j])^2 展開後得到 f[i] = min_{j < i} (f[j] + a[i]^2 - 2 * a[i] * b[j] + b[j]^2), 把與 j 無關的 a[i]^2 提出來,得到 f[i] = a[i]^2 + min_{j < i} (f[j] + b[j]^2 - 2 * a[i] * b[j]), 令 p = (-2 * a[i], 1), vj = (b[j], f[j] + b[j]^2),則可以使用 CHT 來維護 vj,查詢 min(p·vj)。 """
from itertools import accumulate from math import inf from collections import deque
class Vec: __slots__ = "x", "y"
def __init__(self, x: int, y: int): self.x = x self.y = y
def __add__(self, b: "Vec") -> "Vec": return Vec(self.x + b.x, self.y + b.y)
def __sub__(self, b: "Vec") -> "Vec": return Vec(self.x - b.x, self.y - b.y)
def det(self, b: "Vec") -> int: return self.x * b.y - self.y * b.x
def dot(self, b: "Vec") -> int: return self.x * b.x + self.y * b.y
class ConvexHull: """ 注意: - add() 的點需要依 x 遞增加入。 - query_bisect() 不要求查詢向量單調,使用二分搜尋。 - query_mono() 使用單調隊列優化,因此查詢向量 p 需要滿足最佳點索引單調往前移動。 """
def __init__(self): self.hull = deque()
def add(self, v: Vec) -> None: hull = self.hull
if hull and hull[-1].x == v.x: if hull[-1].y <= v.y: return hull.pop()
while len(hull) >= 2 and (hull[-1] - hull[-2]).det(v - hull[-1]) <= 0: hull.pop()
hull.append(v)
def query_bisect(self, p: Vec) -> int: hull = self.hull
left, right = 0, len(hull) - 2 while left <= right: mid = (left + right) // 2 curr = p.dot(hull[mid]) nxxt = p.dot(hull[mid + 1]) if curr >= nxxt: left = mid + 1 else: right = mid - 1
return p.dot(hull[left])
def query_mono(self, p: Vec) -> int: hull = self.hull
while len(hull) >= 2 and p.dot(hull[0]) >= p.dot(hull[1]): hull.popleft() return p.dot(hull[0])
def solve(): n, L = map(int, input().split()) A = [int(input()) for _ in range(n)]
s = list(accumulate(A, initial=0))
f = [inf] * (n + 1) f[0] = 0
cht = ConvexHull() for i in range(1, n + 1): j = i - 1 b_j = j + s[j] + L + 1 vj = Vec(b_j, f[j] + b_j * b_j) cht.add(vj)
a_i = i + s[i] p = Vec(-2 * a_i, 1)
best = cht.query_bisect(p) f[i] = best + a_i * a_i
print(f[n])
if __name__ == "__main__": solve()
|