[HNOI2008]玩具裝箱TOY
"這是一道經典的斜率優化入門題,就用這題來作個總結好了."
這道題用到的是單調隊列(我只會這玩意兒)的斜率優化.
我們整理一下題意會發現它的狀態轉移方程就是下面這東西:
上面這張圖講得已經很清楚了.
我們如果把含\(j\)的相關變量都看成點的坐標的話,此時我們要做的就是盡量讓截距更小.
怎么讓截距最小呢?難道一個一個比較嗎?
我們再來看下面這張圖:
上面三個點是我們可供選擇的三個點,這條直線就是我們就是要使這條一直斜率的截距最小.
高中數學學線性規划的時候我們都知道,顯然是選途中的\(B\)點.
那么對於這一條直線,我們根據斜率和坐標可以計算截距,從而得到dp值.
那剩下兩個點呢?
對於\(A\)點,我們是不是可以丟掉它了?是的,由於我們的斜率是不斷增大的,\(A\)點是不可能用來轉移后面的狀態了,所以把它剔除.
還有\(C\)點,當斜率到達一定大小,例如下圖:
此時我們就要用到\(C\),而\(B\)又可以剔除.
於是我們只要維護一個凸包,而且這個凸包相鄰兩個點連的斜率要大於當前這條線的斜率.就像剛剛這個例子一樣.一旦最左端的一個點和次左端的點的連線要小於當前的斜率了,就把最左端的點剔除.
這樣每次遇到新的直線,直接拿最左端的點(隊頭)來轉移,加入一個新點就加到最右邊(隊尾),因為橫坐標也是遞增的.再加入這個點之前,我們一定要保證下凸的性質,例如下面這個例子:
\(B\)顯然要被剔除.
為什么一定維護凸包呢?為什么一定是彈掉\(B\).\(C\)為什么更優呢?自己想象一下,一條直線斜率大於\(CG\)的直線從下面平移上來,走啊走,最后一定會在\(C\)這里停下.如果是一條斜率小於\(GC\)大於\(GF\)的,顯然會在\(G\)停下,這樣\(B\)就沒有人和用武之地了.
在這里我們總結一下,單調隊列斜率優化的步驟:
1.彈隊頭,就是最左邊的點.
2.放直線,算答案,得到當前狀態的答案,得到新的待加入的點.
3.彈隊尾,把插入新點之后不合法的點彈掉.最后加入新點就好了.
代碼:
#include<iostream>
#include<cstdio>
#include<cmath>
#define maxn 50005
#define ll long long
using namespace std;
int q[maxn];
double A[maxn],B[maxn],dp[maxn],sum[maxn];
double X(int x){return B[x];}
double Y(int x){return dp[x]+B[x]*B[x];}
double slope(int a,int b){return (Y(a)-Y(b))/(X(a)-X(b));}
int main()
{
int n,l;cin>>n>>l;
for(int i=1;i<=n;i++)scanf("%lf",&sum[i]);
for(int i=1;i<=n;i++)
{
sum[i]+=sum[i-1];
A[i]=sum[i]+i;B[i]=sum[i]+i+l+1;
}
B[0]=l+1;//B[0]=sum[0]+0+l+1=l+1
int tail=1,head=1;
for(int i=1;i<=n;i++)
{
while(head<tail&&slope(q[head],q[head+1])<2*A[i])head++;
int j=q[head];dp[i]=dp[j]+(A[i]-B[j])*(A[i]-B[j]);
//why:dp[j]+B^2=2*A*B-A^2+dp[i];
// dp[i]=dp[j]+A^2+B^2-2*A*B=dp[j]+(A-B)(A-B)
while(head<tail&&slope(i,q[tail-1])<slope(q[tail-1],q[tail]))tail--;
q[++tail]=i;
}
printf("%lld",(ll)dp[n]);
return 0;
}