P1295 [TJOI2011]書架 線段樹優化dp,單調棧
阿新 • • 發佈:2020-09-17
P1295 [TJOI2011]書架
本題思路比較好想(對我來說不是),但程式碼細節很多,奈何洛谷的題解只有思路,然後就是
沒有絲毫解釋的程式碼,讓人看起來很頭疼(~~ 尤其是像我這樣的蒟蒻~~),所以便打算寫一篇帶
註釋的題解;
題目大意
給出一個長度為 n 的序列 h,請將 h 分成若干段,滿足每段數字之和都不超過 m,最小化每段的
最大值之和。
解題思路
我們不難想到30分的n2做法,但期望得分30(據說實際有50)
我們定義f [ i ]為1 - i分段後的最大值和
可得dp方程
f[i]=f[j]+max(a[j+1]...a[i])(s[i]-s[j]<=m)
我們考慮對其進行優化
首先我們計算到i時,有 j< k < i(s[i]-s[j]<=m)
假設a[j] ~a[i] 的最大值與 a[k]~a[i]的最大值相等
那麼 j ,k 實際上對f[i]的值產生的貢獻是由f[j] f[k]決定的,那麼,又f的單調性
即 f[j]<=f[k] (j<k),我們知道選擇f[j]一定比選擇f[k]的值更優秀
所以我們只需在和值小於等於m的範圍內根據到i 的最大值大小分段,使每一段到i的最大值都
相等,我們只需計算出每一段中最小下標對應的a加上該段的max,然後在所有段中取得最小
便可以更新出答案將序列分段我們利用單調佇列來完成,維護沒一個值向左的最遠影響(實際便是
到該點以該點權值為最大值的最小下標) ,利用線段樹將每一段的值載入到線段樹
上,維護合法的最小值,即最終答案即可,時間複雜度為線段樹的時間複雜度,即O(nlogn);
這樣我們就可以在規定時間內算出最優解了
程式碼部分
#include<iostream> #include<cstdio> #include<cstring> #include<string> using namespace std; const int maxn=100005; //快讀 inline int read(){ int ret=0; int f=1; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') f=-f; ch=getchar(); } while(ch<='9'&&ch>='0'){ ret=ret*10+(ch^'0'); ch=getchar(); } return ret*f; } // 我們用line來維護單調佇列陣列中下標在a陣列中的數值 // w為該單調佇列下標在a陣列中對應的下標 // v為對應的長度,f 為該值可以影響的最大範圍的值的a陣列下標 struct node{ int w,v,f; }lin[maxn]; int tre[5*maxn];//線段樹,實際開4倍就夠了 int n,m; int head; int tail; int a[maxn]; //將每一段值載入到線段樹上 //為了因為最終答案為最小值,所以我們線段是維護的也是最小值 void plu(int ro,int l,int r,int v,int nu){ if(l==r){ tre[ro]=v; return ; } int mid=(l+r)>>1; if(mid>=nu){ plu(ro*2,l,mid,v,nu); } else{ plu(ro*2+1,mid+1,r,v,nu); } tre[ro]=min(tre[ro*2],tre[ro*2+1]); return ; } //查詢最小值 int sea(int ro,int l,int r,int lf,int rf){ if(l==r){ return tre[ro]; } if(lf<=l&&rf>=r){ return tre[ro]; } int mid=(l+r)>>1; int ans=0x3f3f3f3f;//理應給最大值,之前給小了,查了半天 if(lf<=mid){ ans=sea(ro*2,l,mid,lf,rf); } if(rf>=mid+1){ ans=min(ans,sea(ro*2+1,mid+1,r,lf,rf)); } return ans; } int t; int h=1; int f[maxn]; int s; int last[maxn];//用於存放以該點為結尾<=m的序列的頭元素下標 int maxx=-1; int main(){ freopen("a.in","r",stdin); n=read(); m=read(); for(int i=1;i<=n;i++){ a[i]=read(); // cout<<a[i]<<endl; } // cout<<n<<" "<<m; head=1,tail=0; //先進行預處理 while(t<n&&a[t+1]+s<=m){ t++; s+=a[t]; last[t]=1; maxx=max(maxx,a[t]); //cout<<t; f[t]=maxx; while(tail>0&&a[lin[tail].w]<=a[t]){ tail--; } tail++; lin[tail].w=t; lin[tail].v=a[t]; lin[tail].f=lin[tail-1].w; // cout<<lin[tail].f<<endl; plu(1,1,n,f[lin[tail-1].w]+a[t],tail);//將該點以本身為最值的f更新到線段樹上 //tail++; } //維護每個到i的和<=m的最遠下標 for(int i=t+1;i<=n;i++){ s+=a[i]; while(s>m){ s-=a[h]; h++; } last[i]=h; } for(int i=t+1;i<=n;i++){ while(lin[head].w<last[i]&&head<=tail){ lin[head].w=0; head++; }//保證head在合法範圍內即不能大於m if(lin[head].f<last[i]-1){ lin[head].f=last[i]-1; plu(1,1,n,f[last[i]-1]+lin[head].v,head);//將更新後的head載入到線段樹上至於為什麼要更新,因為head下的值已經發生變化 } while(head<=tail&&a[i]>=lin[tail].v){ tail--; } tail++; lin[tail].w=i; lin[tail].v=a[i]; lin[tail].f=max(lin[tail-1].w,last[i]-1); plu(1,1,n,f[lin[tail].f]+lin[tail].v,tail);//載入然後搜尋即可 f[i]=sea(1,1,n,head,tail);// //tail++; } cout<<f[n]; return 0; }
完結撒花
以小紬結尾