小P的單調區間——解題報告
題目連結:http://172.25.37.251/problem/115 題目大意:給定一個序列,選出若干個數,將其分成若干單調的子序列(可不連續),相鄰序列單調性不同,第一個序列一定為單調遞增。求出所有方案中序列和的平均值的最大值。如3,7,9,2,4,5,把它劃分為[3,7,9],[2,4],[5],答案為:(3+7+9+2+4+5)/3=10。把它劃分為[3,9],[5],答案為:(3+9+5)/2=8.5。 題目分析: 1.我們很明顯可以通過列舉選取第i個點時,共分成了j個序列來進行轉移,那麼我們可以在列舉上一個選擇的數來進行轉移。我們就可以愉快的暴力DP了。如下:
//dp[i][j]表示選擇第i個數後,分為j個單調序列時,數的總和的最大值 for(ll i=1;i<=n;i++)//列舉當前選擇的數 { for(ll j=1;j<=n;j++)//列舉分成了多少個序列 { for(ll k=0;k<i;k++)//列舉上一個選擇的點 { if(j%2==0)//當前序列要求單調遞減 { if(a[k]>a[i])//滿足遞減,可繼承dp[k][j]的值,加入第j個序列中 dp[i][j]=max(dp[i][j],max(dp[k][j],dp[k][j-1])+a[i]); else dp[i][j]=max(dp[i][j],dp[k][j-1]+a[i]); } else//當前序列要求單調遞增 { if(a[k]<a[i])//滿足遞增,可繼承dp[k][j]的值,加入第j個序列中 dp[i][j]=max(dp[i][j],max(dp[k][j],dp[k][j-1])+a[i]); else dp[i][j]=max(dp[i][j],dp[k][j-1]+a[i]); } } ans=max(ans,dp[i][j]/(double)j); } }
2.很顯然的一點是這樣的做法複雜度為O(n^3),是不能通過此題的,我們需要更優的方法。我們可以發現,無論能不能繼承前一個dp值時,我們都要算上將這個數單獨作為一個序列時的值。因為k<i,每一個k都要參與計算,那麼很明顯我們可以利用一個字首和來優化這個過程。如下:
for(ll j=1;j<=n;j++)//列舉序列數 { for(ll i=1;i<=n;i++) { f[i][j]=f[i-1][j]; dp[i][j]=max(dp[i][j],f[i-1][j-1]+a[i]); for(ll k=0;k<i;k++) { if(j%2==0 && a[k]>a[i]) dp[i][j]=max(dp[i][j],dp[k][j]+a[i]); else if(j%2!=0 && a[k]<a[i]) dp[i][j]=max(dp[i][j],dp[k][j]+a[i]); } f[i][j]=max(f[i][j],dp[i][j]); ans=max(ans,dp[i][j]/(double)j); } }
3.雖然我們用字首和來優化,但是我們並沒有解決時間複雜度的問題。我們通過分析可以發現,列舉k的這一維,由於不具備單調性,所以不存在O(1)的轉移方式。但是我們很快可以發現,這個地方是可以利用線段樹來維護的,每次查詢根據j的奇偶性選擇查詢[0,a[i])還是(a[i],Max],每次將得到的dp[i][j]插入線段樹。(由於資料範圍的問題,這裡需要離散化)。現在的時間複雜度為O(n^2logn)。 4.繼續分析我們發現我們無法優化列舉i的過程,但要通過n<=100000的資料,我們需要O(nlogn)的方法。所以我們只能優化列舉j的過程。這裡要求是O(1)的複雜度,那麼說明j的數目一定是一個小的常數。下面去找到這個常數: ❶如果分成了奇數個序列,總和為S,數量為m,那麼值為S/m,我們將最後一個序列分離(一定遞增),和為T。我們將整個序列分為了(S-T)/(m-1)和T的序列。而T-S/m=(Tm-S)/m,S/m-(S-T)/(m-1)=(mT-S)/m(m-1),一定是一正一負的,所以一定存在一個序列數更少的情況,答案比原來要大。 ❷如果分成了偶數個序列,總和為S,數量為m,那麼值為S/m,我們將最後兩個序列分離 (第一個序列一定遞增),和為T。我們將整個序列分為了(S-T)/(m-2)和T/2的序列。而T/2-S/m=(Tm-2S)/2m,S/m-(S-T)/(m-2)=(mT-2S)/m(m-2),一定是一正一負的,所以一定存在一個序列數更少的情況,答案比原來要大。 ❸如此遞迴下去,我們會發現一個驚人的結論,那就是序列的個數<=2。 如此一來時間複雜度就變成了O(nlogn),解題完畢。 正解程式
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <map>
#define inf 1e15
using namespace std;
typedef long long ll;
const ll maxn=100010;
ll n,a[maxn],temp[maxn];
ll dp[maxn][4];
ll f[maxn][4];
ll tree[4*maxn];
map<ll,ll> mp;
void change(ll pos,ll l,ll r,ll pla,ll val)
{
if(l==r)
{
tree[pos]=val;
return;
}
ll mid=(l+r)>>1;
if(pla<=mid)
change(pos<<1,l,mid,pla,val);
else
change(pos<<1|1,mid+1,r,pla,val);
tree[pos]=max(tree[pos<<1],tree[pos<<1|1]);
}
ll getans(ll pos,ll l,ll r,ll s,ll e)
{
if(s<=l && r<=e)
return tree[pos];
ll mid=(l+r)>>1;
ll t1=0,t2=0;
if(s<=mid)
t1=getans(pos<<1,l,mid,s,e);
if(e>mid)
t2=getans(pos<<1|1,mid+1,r,s,e);
return max(t1,t2);
}
int main()
{
memset(dp,0x80,sizeof(dp));
memset(f,0x80,sizeof(f));
scanf("%lld",&n);
for(ll i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
temp[i]=a[i];
}
temp[0]=0;
sort(temp,temp+1+n);
ll cnt=unique(temp,temp+1+n)-temp-1;
for(ll i=0;i<=cnt;i++)
mp[temp[i]]=i+1;
dp[0][1]=0;
f[0][1]=0;
double ans=-inf;
cnt++;
for(ll j=1;j<=2;j++)
{
memset(tree,0x80,sizeof(tree));
change(1,1,cnt,1,0);
for(ll i=1;i<=n;i++)
{
f[i][j]=f[i-1][j];
dp[i][j]=max(dp[i][j],f[i-1][j-1]+a[i]);
if(j%2==1)
{
ll value=getans(1,1,cnt,1,mp[a[i]]-1);
dp[i][j]=max(dp[i][j],value+a[i]);
}
else
{
ll value=getans(1,1,cnt,mp[a[i]]+1,cnt);
dp[i][j]=max(dp[i][j],value+a[i]);
}
change(1,1,cnt,mp[a[i]],dp[i][j]);
f[i][j]=max(f[i][j],dp[i][j]);
ans=max(ans,dp[i][j]/(double)j);
}
}
printf("%.3lf",ans);
return 0;
}