Hetao P1156 最大战力 题解 [ 绿 ][ 二分 ][ 最大子段和 ]

最大战力

Vjudge 原题


题解

形式化题意

给定两个数组 \(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;
}
posted @ 2024-03-09 00:16  KS_Fszha  阅读(81)  评论(0编辑  收藏  举报