1. 程式人生 > 實用技巧 >洛谷 P4072 [SDOI2016]征途 斜率優化DP

洛谷 P4072 [SDOI2016]征途 斜率優化DP

洛谷 P4072 [SDOI2016]征途 斜率優化DP

題目描述

\(Pine\) 開始了從 \(S\) 地到 \(T\) 地的征途。

\(S\)地到\(T\)地的路可以劃分成 \(n\) 段,相鄰兩段路的分界點設有休息站。

\(Pine\)計劃用\(m\)天到達\(T\)地。除第\(m\)天外,每一天晚上\(Pine\)都必須在休息站過夜。所以,一段路必須在同一天中走完。

\(Pine\)希望每一天走的路長度儘可能相近,所以他希望每一天走的路的長度的方差儘可能小。

幫助\(Pine\)求出最小方差是多少。

設方差是\(v\),可以證明,\(v\times m^2\)是一個整數。為了避免精度誤差,輸出結果時輸出\(v\times m^2\)

輸入格式

第一行兩個數 \(n\)\(m\)

第二行 \(n\) 個數,表示 \(n\) 段路的長度

輸出格式

一個數,最小方差乘以 \(m^2\) 後的值

輸入輸出樣例

輸入 #1

5 2
1 2 5 8 6

輸出 #1

36

說明/提示

對於 \(30\%\) 的資料,\(1 \le n \le 10\)

對於 \(60\%\) 的資料,\(1 \le n \le 100\)

對於 \(100\%\) 的資料,\(1 \le n \le 3000\)

保證從 \(S\)\(T\) 的總路程不超過 \(30000\)

分析

\[s^2 \times m^2=\frac{(v_1-\overline{v})^2+(v_2-\overline{v})^2+...+(v_m-\overline{v})^2}m \times m^2 \]

\[s^2 \times m^2=((v_1-\overline{v})^2+(v_2-\overline{v})^2+...+(v_m-\overline{v})^2) \times m \]

\[s^2 \times m^2=m\times \sum_{i=1}^mv_i^2+m^2 \times \overline {v}^2-2\times m \times \overline {v} \times sum[n] \]

又因為$$\overline{v}=\frac{sum[n]}{m}$$

所以

\[s^2\times m^2=m\times \sum_{i=1}^mv_i^2-sum[n]\times sum[n] \]

後面的值是固定的,所以我們只需要讓前面的值最小化即可

我們設\(f[i][j]\)為前\(i\)天分成\(j\)段所得到的最小值

那麼就有

\[f[i][k]=\min(f[j][k-1]+(sum[i]-sum[j])^2) \]

展開就有

\[f[i][k]=f[j][k-1]+sum[i]^2+sum[j]^2-2\times sum[i] \times sum[j] \]

移項得

\[f[j][k-1]+sum[j]^2=2\times sum[i] \times sum[j]+f[i][k]-sum[i]^2 \]

可以用斜率優化

我們把\(f[j][k-1]+sum[j]^2\)看成\(y\)

\(2 \times sum[i]\)看成\(k\)

\(sum[j]\)看成\(x\)

\(f[i][k]-sum[i]^2\)看成\(b\)

這樣,對於每一個\(i\)來說,直線的\(k\)是確定的

我們要使\(f[i][k]\)最小,也就是要使\(b\)最小

我們可以把所有的\(j\)想象成空間中的點

知道了斜率,知道了直線上的點,那麼這條直線就確定了

那麼我們考慮什麼樣的點使直線的\(b\)最大

直線\(l\)是我們要移動的直線,平面中的點是可以轉移的\(j\)

我們會發現噹噹前點和後一個點形成的直線的斜率恰好大於直線\(l\)的斜率是,由當前點轉移決策是最優的

這就是程式碼裡面的

while(head<tail && xl(q[head],q[head+1])<2*sum[j]) head++;
f[j]=g[q[head]]+sum[j]*sum[j]+sum[q[head]]*sum[q[head]]-2*sum[j]*sum[q[head]];

我們再去考慮什麼樣的點肯定不會對結果產生貢獻

上面的圖中\(2\)號節點是無論如何也不會更新其它節點的

因為\(1\)號節點或\(3\)號節點總會比它更優

這就是程式碼裡的

 while(head<tail && xl(q[tail-1],q[tail])>=xl(q[tail],i)) tail--;

整個過程就相當於維護了一個下凸包

但是,如果斜率不是單調遞增,我們就不能從前面清空佇列直接轉移,只能二分答案

比如上面這幅圖如果我們一直從前清空佇列的話那麼就會把\(2\)號決策點彈出佇列

但是如果之後遇到一個斜率比較小的直線\(m\)那麼就不能轉移到最優解

程式碼

#include<cstdio>
#include<cstring>
inline int read(){
	int x=0,fh=1;
	char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
int a[maxn],sum[maxn],n,m,f[maxn],g[maxn],q[maxn],head,tail;
double xl(int i,int j){
	return (double)(g[i]+sum[i]*sum[i]-g[j]-sum[j]*sum[j])/(double)(sum[i]-sum[j]);
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		a[i]=read();
		sum[i]=sum[i-1]+a[i];
		g[i]=sum[i]*sum[i];
	}
	for(int i=1;i<m;i++){
		head=tail=1;
		q[1]=i;
		for(int j=i+1;j<=n;j++){
			while(head<tail && xl(q[head],q[head+1])<2*sum[j]) head++;
			f[j]=g[q[head]]+sum[j]*sum[j]+sum[q[head]]*sum[q[head]]-2*sum[j]*sum[q[head]];
			while(head<tail && xl(q[tail],q[tail-1])>xl(q[tail-1],j)) tail--;
			q[++tail]=j;
		}
		for(int j=1;j<=n;j++) g[j]=f[j];
	}
	printf("%d\n",f[n]*m-sum[n]*sum[n]);
	return 0;
}