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

最大战力

Vjudge 原题


题解

形式化题意

给定两个数组 a[n]b[n] ,需要在数组 b 中选择一个区间 b[l,r] ,替换掉区间 a[l,r] ,并且使替换后的 a 数组的中位数最大。其中 1n3105 ,且 n 为奇数。

概述

本题思维难度较大,需要将中位数的浮动转化为 11 的贡献,然后求贡献的最大子段和。

但是直接这样做,可能会得到并不是最大的的中位数,因为如果我们选择对一个静态的区间计算贡献,则可能在有多个最大贡献的子段和的情况下( 因为贡献的值只可能是 11 ),没有办法选择出 贡献与其他几个区间同样最大,但拥有这几个区间中最大的中位数的 区间。

因此,我们可以倒着来,在保证中位数合法的情况下,使中位数最大,而不是用贡献来直接确定最大中位数。这个过程需要二分中位数来实现。

总的时间复杂度为 O(nlogn)

分析

二分

首先,要明确有奇数个元素的序列的中位数的求法。

1.把原数组排序,第 n+12 个元素就是中位数,时间 O(nlogn)
2.采用快速选择算法,即快速排序的简单应用,时间 O(n)
3.二分中位数,把 mid 的数标记为 1mid 表示中位数 ), <mid 的数标记为 1。当所有标记总和 >0 时,说明当前的 mid 真实的中位数;当所有标记总和 0 时,表示当前的 mid> 真实的中位数。( “当所有标记总和 0 时,表示当前的 mid> 真实的中位数”是因为中位数自身会被标记成 1 ,且可能有多个中位数,所以选到中位数时总和一定 >0 。 ),时间 O(nlogn)

可以注意到,对于 二分中位数 的做法,我们可以在二分时做一些手脚,对于当前二分的中位数 mid ,按前文所述来标记每个元素后,看看当前能否选出一个区间,能让这段区间替换原数组中的位置后使 替换后的中位数 变成合法的中位数,也就是说要让替换后的中位数最大 (因为最大中位数能选出来,那么比他小的中位数就一定能选出来)。

而判断一个中位数是否合法,就可以用前文中的标记总和的大小来判断。

这是二分答案题的基本套路。


最大子段和

有了二分,那么接下来就该实现选择区间,使中位数最大的模块了。

下文中,我们记 a[N] 为原数组,b[N] 为要替换的数组,c[N] 为替换的贡献的数组。

首先可以发现,标记的功能可以看做是一个一个贡献,比当前二分的数 mid 小的数贡献 1 ,其他贡献 1 ,也就是说,这些贡献的总和就是 mid 的数的数量 与 <mid 的数的数量 的差

而修改的区间则也需要这样标记,因为它可能被替换进原数组中。

有了这些标记,我们就可以计算出替换一个数的贡献 c[i]=b[i]a[i]

因为改了一个数,如果原本它 mid ,而替换后 <mid ,则 a[i]=1,b[i]=1 ,所以 c[i]=2而修改后少了一个 mid 的数,多了个 <mid 的数,故 mid 的数 变少了一个, <mid 的数 变多了一个,因此 mid 的数的数量 与 <mid 的数的数量 的差 变小了 2 ,所以修改贡献 2 是正确的。

如果原本它 <mid ,而替换后 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 ,而正常情况下 贡献总和应该 1 ,所以错误。

坑点

本题二分答案时 lr 最大为 2109 ,此时它们相加求 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 @   KS_Fszha  阅读(94)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示