2020.05.24日常總結——又一道特別好的 dp 題

洛谷P1121   環狀最大兩段子段和\color{green}{\texttt{洛谷P1121\ \ \ 環狀最大兩段子段和}}

【題意】:\color{blue}{\texttt{【題意】:}}

  • 給你一個首尾相接的數組 aa(即環),選出其中連續 不重疊非空兩段 使得這兩段和最大。
  • 2n2×105,1×104ai1×104(1in)2 \leq n \leq 2 \times 10^5,-1\times 10^4 \leq a_i \leq 1 \times 10^4(1 \leq i \leq n)
  • 注意兩段子段可以相連(相當於一段)。

【思路】:\color{blue}{\texttt{【思路】:}}

對於環上的 dp,我們一般考慮破環爲鏈法,即把環轉化爲鏈。

破環爲鏈法中,又有幾種比較常見的處理方法,這裏列舉兩種:

  1. 把數組複製兩遍,即把數組 a1..na_{1..n} 變成數組 a1..2na_{1..2n},其中 ai=ain(n+1i2n)a_i=a_{i-n}(n+1 \leqslant i\leqslant 2n)。這是最常用的一種破環爲鏈法。
  2. 不復制,直接用其它方法把環轉化爲鏈。

在本題中,我們發現雖然第一種很常用,但是我們仍然無法用它來解決問題。所以我們考慮第二種方法。

我們發現用第二種方法時,我們無法把所有的情況統一在一起討論,這個時候,我們就需要用到另一種數學(OI\texttt{OI} 也適用)的方法了,那就是:分類討論。

  • 第一種情況:兩段子段都不跨越首尾,即類似於這樣的:

    00011111000022200000001111100002220000

    00 表示該位不選,11 表示選且屬於第一子段,22 表示選且屬於第二子段。

    我們記 fif_i 表示 a1..ia_{1..i} 的最大子段和,gig_i 表示 ai..na_{i..n} 的最大子段和,則答案爲:

    maxi=1nfi+gi+1\max\limits_{i=1}^{n} f_i+g_{i+1}

  • 第二種情況:其中一個子段跨越了首尾,即類似於此:

    22200011111100000222220001111110000022

    命名方法同上。

    直接求很難,我們就考慮反着做,畢竟正難則反。

    我們發現 00 所佔據的正好就是兩個子段,於是,我們即 fif_i 表示 a1..ia_{1..i} 的最小子段和,gig_i 表示 ai+1..na_{i+1..n} 的最小子段和。則答案爲:

    i=1naimini=1nfi+gi+1\sum\limits_{i=1}^{n} a_i - \min\limits_{i=1}^{n} f_i + g_{i+1}

    注意當 fi+gi+1f_i+g_{i+1} 正好就是整個區間的情況,需要特判,因爲這樣相當於選得數爲空。

【代碼】:\color{blue}{\texttt{【代碼】:}}

const int N=2e5+100;
#define ll long long
int n;ll ans,res,sum;
ll f[N],g[N],h[N],a[N];
const ll inf=0x3f3f3f3f3f;
inline ll calc_min_sum(){
	h[0]=h[n+1]=f[0]=g[n+1]=inf;
	for(register int i=1;i<=n;i++){
		h[i]=min(h[i-1]+a[i],a[i]);
		f[i]=min(f[i-1],h[i]);
	}
	for(register int i=n;i>=1;i--){
		h[i]=min(h[i+1]+a[i],a[i]);
		g[i]=min(g[i+1],h[i]);
	}
	register ll ret=inf;
	for(int i=1;i<=n;i++)
		ret=min(ret,f[i]+g[i+1]);
	return ret;
}
inline ll calc_max_sum(){
	h[0]=h[n+1]=f[0]=g[n+1]=-inf;
	for(register int i=1;i<=n;i++){
		h[i]=max(h[i-1]+a[i],a[i]);
		f[i]=max(f[i-1],h[i]);
	}
	for(register int i=n;i>=1;i--){
		h[i]=max(h[i+1]+a[i],a[i]);
		g[i]=max(g[i+1],h[i]);
	}
	register ll ret=-inf;
	for(int i=1;i<=n;i++)
		ret=max(ret,f[i]+g[i+1]);
	return ret;
}
int main(){
	freopen("t1.in","r",stdin);
	n=read();//我們的程序開始了 
	for(int i=1;i<=n;i++)
		sum+=(a[i]=read());
	ans=calc_max_sum();//求最大 
	res=calc_min_sum();//求最小 
	if (res==sum){//全部數子爲負 
		res=ans=-inf;//最大;次大 
		for(int i=1;i<=n;i++){
			if (a[i]>res){
				ans=res;
				res=a[i];
			}
			else ans=max(ans,a[i]);
		}
		printf("%lld",ans+res);
	}
	else{//數字有正有負(普通情況) 
		printf("%lld",max(ans,sum-res));
	}
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章