[斜率優化DP] [APIO2014] 序列分割
題目描述
你正在玩一個關於長度為 \(n\) 的非負整數序列的遊戲。這個遊戲中你需要把序列分成 \(k + 1\) 個非空的塊。為了得到 \(k + 1\) 塊,你需要重複下面的操作 \(k\) 次:
選擇一個有超過一個元素的塊(初始時你只有一塊,即整個序列)
選擇兩個相鄰元素把這個塊從中間分開,得到兩個非空的塊。
每次操作後你將獲得那兩個新產生的塊的元素和的乘積的分數。你想要最大化最後的總得分。
輸入格式
第一行包含兩個整數 \(n\) 和 \(k\)。保證 \(k + 1 \leq n\)。
第二行包含 \(n\) 個非負整數 \(a_1, a_2, \cdots, a_n\) (\(0 \leq a_i \leq 10^4\)
輸出格式
第一行輸出你能獲得的最大總得分。
第二行輸出 \(k\) 個介於 \(1\) 到 \(n - 1\) 之間的整數,表示為了使得總得分最大,你每次操作中分開兩個塊的位置。第 \(i\) 個整數 \(s_i\) 表示第 \(i\) 次操作將在 \(s_i\) 和 \(s_{i + 1}\) 之間把塊分開。
如果有多種方案使得總得分最大,輸出任意一種方案即可。
限制與約定
第一個子任務共 \(11\) 分,滿足 \(1 \leq k < n \leq 10\)。
第二個子任務共 \(11\) 分,滿足 \(1 \leq k < n \leq 50\)。
第三個子任務共 \(11\)
第四個子任務共 \(17\) 分,滿足 \(2 \leq n \leq 1000, 1 \leq k \leq \min\{n - 1, 200\}\)。
第五個子任務共 \(21\) 分,滿足 \(2 \leq n \leq 10000, 1 \leq k \leq \min\{n - 1, 200\}\)。
第六個子任務共 \(29\) 分,滿足 \(2 \leq n \leq 100000, 1 \leq k \leq \min\{n - 1, 200\}\)。
首先可以通過數學歸納法證明: 只要切割位置相同, 切割順序不會影響答案.
首先有一個 \(拿衣服\)
用 \(i\) 表示當前的位置, \(k\) 表示切割的次數, 列舉之前的每一個狀態 \(j\).
\[F\left(i, k\right) = \max\left\{F\left(j, k-1\right) + \left[S\left(i\right)-S\left(j\right)\right]\cdot\left[S\left(n\right)-S\left(i\right)\right]\right\}, j \in \left[1, i-1\right] \]
但是由於它太 \(拿衣服\) 了, 所以不好處理(實際可做).
反向考慮整個序列, 轉移方程變形為:
\[F\left(i, k\right) = \max\left\{F\left(j, k-1\right) + S\left(j\right)\cdot\left[S\left(i\right)-S\left(j\right)\right]\right\}, j \in \left[1, i-1\right] \]
看資料 \(1e6\), \(O\left(n^2k\right)\) 顯然不可做, 憑感覺考慮斜率優化
設 \(F\left(i, k\right) = F(i)\), $F\left(i, k-1\right) = G(i) $ , 進行移項, 有:
\[G\left(j\right) - S^2\left(j\right) = -S\left(i\right)S\left(j\right) + F\left(i\right) \]
斜率 \(-S(i)\) 單調遞減.
考慮當前決策 \(j\) 和前一個決策 \(j-1\), 若 \(j\) 優於 \(j-1\), 有:
\[k = \frac{[G(j)-S^2(j)] - [G(j-1)-S^2(j-1)]}{S(j) - S(j-1)} \gt -S(i) \]
- 注意 \(k\) 是負值.
即:
\[\frac{[G(j)-S^2(j)] - [G(j-1)-S^2(j-1)]}{S(j-1) - S(j)} \le S(i) \]
然後就可以大力斜率優化並WA掉
注意一個細節: \(a_i\) 可能為 \(0\), 上式的分母可能為 \(0\), 需要在程式中特判一下.
程式碼:
# include <iostream>
# include <cstdio>
# include <deque>
# define LL long long
# define MAXN 1000005
using namespace std;
LL a[MAXN], sum[MAXN];
LL f[MAXN][2];
int sol[MAXN][205]; // 記錄轉移順序
int q[MAXN];
double slope(int i,int j, int g) {
if(sum[i]==sum[j]) return -1e18;
return 1.0*((f[i][g]-sum[i]*sum[i])-(f[j][g]-sum[j]*sum[j]))/(sum[j]-sum[i]);
}
int main(){
int n, S;
scanf("%d%d", &n, &S);
for(int i = 1; i <= n; i++){
scanf("%lld", &a[i]);
sum[i] = a[i] + sum[i-1];
}
for(int k = 0, lim = 1; lim <= S; k^=1, lim++){
int l = 1, r = 0;
q[++r] = 0;
for(int i = 1; i <= n; i++){
int g = k^1;
while(l < r && slope(q[l], q[l+1], g)<=sum[i]){
l++;
}
f[i][k] = f[q[l]][g]+sum[q[l]]*(sum[i]-sum[q[l]]);
sol[i][lim] = q[l];
while(l < r && slope(q[r-1],q[r], g)>=slope(q[r],i, g)){
r--;
}
q[++r] = i;
}
}
printf("%lld\n", max(f[n][1], f[n][0]));
for(int x=n,i=S;i>=1;--i){
x=sol[x][i];
printf("%d%c",x," \n"[i==1]);
}
return 0;
}
時間複雜度: \(O(nk)\)
玄學 \(1\): 最開始寫的 deque
的時候莫名其妙赤橙黃綠青藍紫, 改手寫單調佇列就過了 估計是我哪寫錯了
玄學 \(2\): 本來打算移項逃避精度問題的, 結果鍋掉 估計就是式子移錯了