Hetao P1156 最大戰力 題解 [ 綠 ][ 二分 ][ 最大子段和 ]

KS_Fszha發表於2024-03-09

原題


題解

形式化題意

給定兩個陣列 \(a[n]\)\(b[n]\) ,需要在陣列 \(b\) 中選擇一個區間 \(b[l,r]\) ,替換掉區間 \(a[l,r]\) ,並且使替換後的 \(a'\) 陣列的中位數最大。其中 $ 1 \le n \le 3*10^5 $ ,且 \(n\) 為奇數。

概述

本題思維難度較大,需要將中位數的浮動轉化為 \(-1\)\(1\) 的貢獻,然後求貢獻的最大子段和。

但是直接這樣做,可能會得到並不是最大的的中位數,因為如果我們選擇對一個靜態的區間計算貢獻,則可能在有多個最大貢獻的子段和的情況下( 因為貢獻的值只可能是 \(-1\)\(1\) ),沒有辦法選擇出 貢獻與其他幾個區間同樣最大,但擁有這幾個區間中最大的中位數的 區間。

因此,我們可以倒著來,在保證中位數合法的情況下,使中位數最大,而不是用貢獻來直接確定最大中位數。這個過程需要二分中位數來實現。

總的時間複雜度為 \(O(n log n)\)

分析

二分

首先,要明確有奇數個元素的序列的中位數的求法。

1.把原陣列排序,第 $ \frac{n+1}{2} $ 個元素就是中位數,時間 $ O(nlogn) $。
2.採用快速選擇演算法,即快速排序的簡單應用,時間 $ O(n) $。
3.二分中位數,把 $\ge mid $ 的數標記為 \(1\)\(mid\) 表示中位數 ), $< mid $ 的數標記為 \(-1\)。當所有標記總和 $ > 0$ 時,說明當前的 $mid \le $ 真實的中位數;當所有標記總和 $ \le 0$ 時,表示當前的 $mid > $ 真實的中位數。( “當所有標記總和 $ \le 0$ 時,表示當前的 $mid > $ 真實的中位數”是因為中位數自身會被標記成 \(1\) ,且可能有多箇中位數,所以選到中位數時總和一定 $ > 0$ 。 ),時間 $ O(nlogn)$ 。

可以注意到,對於 二分中位數 的做法,我們可以在二分時做一些手腳,對於當前二分的中位數 \(mid\) ,按前文所述來標記每個元素後,看看當前能否選出一個區間,能讓這段區間替換原陣列中的位置後使 替換後的中位數 變成合法的中位數,也就是說要讓替換後的中位數最大 (因為最大中位數能選出來,那麼比他小的中位數就一定能選出來)。

而判斷一箇中位數是否合法,就可以用前文中的標記總和的大小來判斷。

這是二分答案題的基本套路。


最大子段和

有了二分,那麼接下來就該實現選擇區間,使中位數最大的模組了。

下文中,我們記 \(a[N]\) 為原陣列,\(b[N]\) 為要替換的陣列,\(c[N]\) 為替換的貢獻的陣列。

首先可以發現,標記的功能可以看做是一個一個貢獻,比當前二分的數 \(mid\) 小的數貢獻 \(-1\) ,其他貢獻 \(1\) ,也就是說,這些貢獻的總和就是 \(\ge mid\) 的數的數量 與 \(< mid\) 的數的數量 的差

而修改的區間則也需要這樣標記,因為它可能被替換進原陣列中。

有了這些標記,我們就可以計算出替換一個數的貢獻 \(c[i]=b[i]-a[i]\)

因為改了一個數,如果原本它 $ \ge mid$ ,而替換後 \(< mid\) ,則 \(a[i]=1,b[i]=-1\) ,所以 \(c[i]=-2\)而修改後少了一個 $ \ge mid$ 的數,多了個 \(< mid\) 的數,故 \(\ge mid\) 的數 變少了一個, \(< mid\) 的數 變多了一個,因此 \(\ge mid\) 的數的數量 與 \(< mid\) 的數的數量 的差 變小了 \(2\) ,所以修改貢獻 \(-2\) 是正確的。

如果原本它 $ < mid$ ,而替換後 \(\ge mid\) ,則也是同理,\(c[i]=2\)

但如果修改前和修改後相對中位數 \(mid\) 的大小不變,則 \(c[i]=0\) ,對中位數沒有任何影響。

求完 \(c\) 陣列後,只要用 dp 來求 \(c\) 陣列的最大子段和,就能求出對中位數的最大貢獻了,此時 原本的標記的和 加上現在修改的最大子段和,就是現在標記的總和

最大子段和的做法是對每一個 \(c[i]\) ,求出 \(c[1,i]\) 中的最小字首和,再被 \(c[i]\) 的字首和減去,更新當前最大子段和即可。並且開始時字首和要設為 \(0\)最大子段和也要設為 \(0\)

注意,最大子段和在所有元素為負數時,會選擇空區間,修改貢獻總和為 \(0\) ,恰好對應本題中不替換任何元素的情況。

常見問題

Q1 : 修改一個數後,如果它的中位數變化了,那麼之前的標記是否會失效?

A1 : 不會失效。

對於修改一個區間 \([l,r]\) ,可以把其看做 以任何順序修改這些元素皆可,因此,只要從小到大修改這些元素,那麼中位數就會不斷變大,且中位數最大隻可能是當前修改的這個元素,所以後面的數依舊比中位數要大,之前的標記不會失效。

因此,修改一個區間 \([l,r]\) ,無論什麼順序來修改最終的中位數都是固定的,所以程式碼中不用寫這個功能。


Q2 : 能否在一個元素 \(=\) 中位數時把它標記成 \(0\) ?

A2: 不能。

hack: 1 , 2 , 3 , 3 , 3 , 3 , 3 , 4 , 5
___________________↑ ___________________

這時候有多箇中位數,如果標記成 \(0\) ,那麼相當於中位數全部被抵消,這時候即使選到了中位數,那麼貢獻總和也是 \(-1\) ,而正常情況下 貢獻總和應該 $ \ge 1$ ,所以錯誤。

坑點

本題二分答案時 \(l\)\(r\) 最大為 \(2*10^9\) ,此時它們相加求 \(mid\) 時會爆 int ,所以要開 long long 。

十年OI一場空,不開long long見祖宗

程式碼

#include<bits/stdc++.h>
using namespace std;
int n,a[300005],b[300005],c[300005];
int check(int mid)
{
	int res=0;
	for(int i=1;i<=n;i++)
	{
		if(a[i]>=mid)res++;
		else res--;
                //這裡省略了標記 a[i] b[i] 的步驟,直接賦 c[i] 的值
		if(a[i]>=mid && b[i]>=mid)c[i]=0;
		else if(a[i]>=mid && b[i]<mid)c[i]=-2;
		else if(a[i]<mid && b[i]>=mid)c[i]=2;
		else c[i]=0;
	}
	int minres=0,sumn=0,maxres=0;
	for(int i=1;i<=n;i++)
	{
		sumn+=c[i];
		maxres=max(sumn-minres,maxres);
		minres=min(minres,sumn);
	}
	return res+maxres;
}
int main()
{
    freopen("yone.in","r",stdin);
    freopen("yone.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i]>>b[i];
	}
	long long l=0,r=2e9+10,mid;
	while(l<r)
	{
		mid=((l+r+1)>>1);//l+r加到最大時會爆int
		if(check(mid)>=0)l=mid;
		else r=mid-1;
	}
	cout<<l;
	return 0;
}

相關文章