洛谷題目傳送門php
看完洛谷larryzhong巨佬的題解,蒟蒻一臉懵逼html
若是哪年NOI(放心我這樣的蒟蒻是去不了的)又來個決策單調性優化DP,那蒟蒻是否是會看都看不出來直接爆\(0\)?!函數
仍是要想點辦法,不失通常性也能快捷地斷定決策單調。優化
再補一句決策單調性的概念:狀態轉移方程形如\(f_i=\min/\max_{j=1}^{i-1} g_j+w_{i,j}\),且記\(f_i\)的最優決策點爲\(p_i\)(也就是\(f_i\)從\(g_{p_i}+w_{i,p_i}\)處轉移最優)若知足\(p_i\le p_{i+1}\),則該方程知足決策單調性。(摘自蒟蒻的DP優化總結)spa
顯然每一個決策\(j\)能夠用一個關於\(i\)的函數\(f_j(i)\)表示。code
函數的一個重要思想:數形結合!htm
光靠腦子想不到規律,只好先舉一些用語言難以描述的反例。咱們的函數不能這樣blog
看到這裏Dalao們有沒有一點想法呢?蒟蒻反正想到了一點——兩個函數必須只有一個交點!在這一點以前一個函數更優,而以後就被永遠取代了。隊列
感受知足條件的函數其實不多,分類討論一下(若有誤歡迎Dalao指教)get
顯然上面的基本要求都知足。不過要是函數是直線的話均可以用斜率優化搞了(\(k_1x+b_1\ge k_2x+b_2,x\ge\frac{b_2-b_1}{k_1-k_2}\))。
爲了不圖1的尷尬狀況,可能須要全部決策函數之間能夠經過平移相互變換(形如\(f_j(x)=f_k(x-a)+b\))。
爲了不圖2的尷尬狀況,可能須要函數的導函數在各自的定義域內單調遞增/遞減(注意是導函數不是原函數)。
接着,根據蒟蒻肝過的幾個題,好像還有一條規律——
若是導函數遞增、求最大值(檸檬),或者導函數遞減、求最小值,要用單調棧。
若是導函數遞增、求最小值(本題),或者導函數遞減、求最大值(Lightning Conductor),要用單調隊列。
蒟蒻見過這一道(Yet another minimization problem)
感受能夠當作對於每一種數都有一個函數\(\frac{(c_i-c_j)(c_i-c_j+1)}{2}\),單看這一個是知足決策單調性的(\(c_i\ge c_j\),定義域內的導函數是遞增的)。
那麼總函數就能夠寫成\(f_j(i)=g_j+\sum\frac{(c_i-c_j)(c_i-c_j+1)}{2}\),怎麼看也不像是不知足決策單調性的。
那麼就能夠迴歸本題了。
設\(len_i\)爲第\(i\)句的長度,\(s_i=i+\sum\limits_{j=1}^i len_j\)(加上\(i\)是默認一句話後面有空格)
設\(f_i\)爲選前\(i\)句的最小代價,咱們枚舉當前這一行填入最後面的多少個句子,注意行末沒有空格,長度要\(-1\),那麼有方程
\[f_i=\min\limits_{j=0}^{i-1}\{f_j+|s_i-s_j-1-L|^P\}\]
容易發現後面這一坨決策函數是關於直線\(x=s_j+1+L\)對稱的。把它去絕對值,變成兩段,顯然左邊一段和右邊一段的導函數都是遞增的,左邊恆\(<0\),右邊恆\(>0\)。又由於這函數是連續的,因此固然整個函數的導函數也單調遞增咯!
用隊列維護決策二分棧的過程再也不贅述,總結裏也有。時間複雜度\(O(Tn\log n)\)
看到Dalao們都記錄了一個三元組,可蒟蒻仍是以爲沒啥必要啊。。。只要保存隊列中相鄰兩個元素的臨界值\(k\)就行了吧。
一個寫法技巧:
二分決策\(x,y(x<y)\)的臨界值的時候,左端點設成\(x\)就行了,不必設成\(1\)(難怪蒟蒻以前寫Lightning Conductor跑得有點慢)
三個坑點:
無論是轉移仍是輸出,都要去掉行末的空格(怪蒟蒻看題不清)
當答案大於\(10^{18}\)的時候開longlong也炸了,因此要用實數以犧牲精度的代價換來更大的值域。然而double真的WA了。因而要開long double。
cmath的pow太慢了容易TLE,要手寫快速冪。
#include<cstdio> #include<cmath> #include<cstring> #define RG register #define R RG int #define G c=getchar() #define Calc(i,j) f[j]+qpow(abs(s[i]-s[j]-L))//計算函數值 using namespace std; typedef long double LD;//開long double const int N=1e5+9; int n,L,P,s[N],q[N],k[N],pr[N]; LD f[N]; char str[N][33]; inline int in(){ RG char G; while(c<'-')G; R x=c&15;G; while(c>'-')x*=10,x+=c&15,G; return x; } inline LD qpow(RG LD b){//本身寫快速冪 RG LD a=1; for(R k=P;k;k>>=1,b*=b) if(k&1)a*=b; return a; } inline int bound(R x,R y){//二分臨界值 R l=x,r=n+1,m;//左端點設爲x減少常數 while(l<r){ m=(l+r)>>1; Calc(m,x)>=Calc(m,y)?r=m:l=m+1; } return l; } int main(){ R T=in(),i,h,t; while(T--){ n=in();L=in()+1;P=in();//把L處理了一下 for(i=1;i<=n;++i){ if(scanf("%s",str[i])); s[i]=s[i-1]+strlen(str[i])+1;//記前綴和 } for(q[i=h=t=1]=0;i<=n;++i){ while(h<t&&k[h]<=i)++h; f[i]=Calc(i,q[h]);pr[i]=q[h];//記錄轉移位置方便輸出方案 while(h<t&&k[t-1]>=bound(q[t],i))--t; k[t]=bound(q[t],i);q[++t]=i; } if(f[n]>1e18)puts("Too hard to arrange"); else{ printf("%.0Lf\n",f[n]); for(q[t=0]=i=n;i;q[++t]=i=pr[i]); for(;t;--t){ for(i=q[t]+1;i<q[t-1];++i) printf("%s ",str[i]); puts(str[i]);//行末不要搞空格 } } puts("--------------------"); } return 0; }