1. 程式人生 > >小P的單調區間——解題報告

小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;
}