G2. 大胜! (困难版)
问题要求我们找到一个子数组 ,使得 最大化。这里, 是中位数(排序后长度为 的子数组中位于位置 的元素), 是最小元素。约束条件 对所提供的解法至关重要。
让我们逐步分解这个解法。
1. 重新表述目标和高层方法:
目标是最大化 ,其中 是中位数, 是子数组的最小值。 该解法提出了一种类似双指针的方法。它固定一个候选的最小值 ,然后尝试找到一个可能的最大候选中位数值 ,使得存在一个子数组,其最小值恰好是 ,中位数至少是 。
2. 固定最小值 () 并刻画子数组:
对于一个固定的 ,任何最小值为 的子数组 必须满足两个条件:
- 它必须包含至少一个元素 。
- 子数组 内的所有元素 都必须大于或等于 (即,对于 ,)。
为了高效地处理第二个条件,该解法使用了“ 是严格最小值的最大区间”的概念。对于每个索引 ,我们找到 和 :
- : 左侧第一个使得 的索引(如果没有则为 )。这意味着 中的所有元素都 。
- : 右侧第一个使得 的索引(如果没有则为 )。这意味着 中的所有元素都 。
这些 和 的值可以使用单调栈在 时间内为所有 计算出来。具体来说,对于给定的 , 是其左侧第一个值 小于 的元素的索引, 是其右侧第一个值 小于 的元素的索引。所以,任何以 为最小值且 的子数组 都必须满足 。
3. 为中位数检查定义 sign 数组:
对于一个给定的潜在中位数值 ,我们定义一个 sign 数组:
- 如果 ,则
sign_i = +1 - 如果 ,则
sign_i = -1
一个关键的洞察是:一个子数组 的中位数 当且仅当该子数组上 sign 值的和为正。
设 为子数组的长度。中位数 的条件是至少有 个元素 。
如果 是 中 sign 值的和,设 是 的元素数量, 是 的元素数量。
那么 。
同时,。所以 。
代入得,。
条件 等价于 。
如果 是奇数,,则 。所以 。这意味着 ,即 。
如果 是偶数,,则 。所以 。这意味着 ,即 。
然而,问题陈述中关于“至少一半元素是高的”的说法将此简化为“该区间上 sign 的总和为正”。这是中位数问题中常用的一个技巧。如果和为正,则意味着 ,并且由于 ,这意味着 ,这正是我们需要的使中位数 的条件。
现在,对于一个特定的位置 成为子数组 的最小值 :
子数组 必须在 范围内(即 )。
的 signs 之和必须为正。
为了最大化固定 (即 )的 signs 之和,我们需要找到最好的 和 。
的 signs 之和为 。
为了最大化这个和,我们需要选择 使得 最大化(这是在范围 内以 结尾的 maxSuffix 和),并选择 使得 最大化(这是在范围 内以 开始的 maxPrefix 和)。
因此,对于给定的 ,存在这样一个子数组的条件是:
maxSuffix(l_u, u-1) + sign_u + maxPrefix(u+1, r_u) >= 0(或 > 0,取决于中位数定义的严格性。提供的解法中说 >=0 表示 ‘正’,我们暂时遵循这个,假设它能正确处理边界情况或进行了简化。中位数和的通常解释是 > 0。)
这里,maxSuffix(A, B) 表示 sign 数组在范围 内的最大后缀和,maxPrefix(A, B) 表示 sign 数组在范围 内的最大前缀和。如果 ,这些和为 。
4. 使用线段树:
为了高效地计算任意范围内的 maxSuffix、maxPrefix 和总和,我们使用线段树。线段树中的每个节点存储:
totalSum: 其范围内sign值的和。bestPrefix: 其范围内的最大前缀和。bestSuffix: 其范围内的最大后缀和。ans: 其范围内的最大子数组和(类似Kadane算法)。(虽然解法只提到了ans, bestPrefix, bestSuffix, totalSum,但这里的ans可能指的是该段内任何子数组的最大和,这在我们需要通用最大和时可能有用,但对于我们问题中特定的maxSuffix/maxPrefix,其他三个更相关。)
当合并两个子节点(左孩子 ,右孩子 )时:
totalSum = L.totalSum + R.totalSumbestPrefix = max(L.bestPrefix, L.totalSum + R.bestPrefix)bestSuffix = max(R.bestSuffix, R.totalSum + L.bestSuffix)maxSuffix(l_u, u-1)和maxPrefix(u+1, r_u)的查询可以在这个线段树上以 的时间完成。
5. 主算法循环:
算法从 到 遍历 MN。在这个循环内部,它试图找到可能的最大 MED。
初始化:
- 构建线段树。初始时,假设
MED = 1。由于所有 ,所有 ,所以对所有 ,sign_i = +1。线段树以所有sign_i = +1初始化。 current_MED = 1。max_diff = 0。
外层循环(遍历 MN 从 1 到 ):
对于每个 MN:
将
MN视为最小值: 识别所有 的索引 。对于每个这样的 ,我们需要检查是否能用 当前 的current_MED满足条件maxSuffix(l_u, u-1) + sign_u + maxPrefix(u+1, r_u) >= 0。注意,sign_u的值是 +1 如果 ,是 -1 如果 。由于我们固定 ,这意味着 或 。内层循环(递增
current_MED): 这是核心的贪心部分。只要对于 所有 的索引 (并且 可能是一个中位数至少为current_MED的子数组的最小值),条件maxSuffix(l_u, u-1) + sign_u + maxPrefix(u+1, r_u) >= 0都满足,我们就将current_MED增加 1。 随着current_MED的增加,一些之前 的值 现在可能变得 。对于这些位置,它们的sign_i从 变为 。这需要在段树中进行点更新。具体来说,对于每个使得 的位置 (即, 刚刚好 并且现在 ),我们将sign_i更新为 。 (更准确地说,我们增加current_MED。然后,所有 的位置 在段树中都必须从 更新为 。)当条件失败时: 当对于 任何 的索引 ,不等式
maxSuffix(l_u, u-1) + sign_u + maxPrefix(u+1, r_u) >= 0失败时,这意味着我们无法找到一个最小值为 且中位数 的子数组。所以,对于这个 ,可能的最大中位数是current_MED - 1。记录最大差值: 更新
max_diff = max(max_diff, (current_MED - 1) - MN)。推进
MN: 在移动到下一个MN之前,我们需要确保等于 当前MN的元素不能成为中位数为current_MED的子数组的一部分,如果它们的sign贡献是关键的话。解法通过向上迭代MN来隐式处理这一点。随着MN的增加,元素 现在被“锁定”为潜在的最小值。如果MN < current_MED,它们的sign_u将是 ;如果MN >= current_MED,则是 。
精化的内层循环/更新:
内层循环的描述有点微妙。让我们澄清一下 current_MED 和 MN 的交互。
的值是基于 (索引 处的值),而不是 。它们只计算一次。
sign 数组取决于 current_MED。
一个更精确的 MN 迭代流程:
对于 :
检查
MN之前:while current_MED <= N: 遍历所有 的索引i。对于每个这样的i,在线段树中将sign_i从 更新为 。 (此步骤确保sign数组始终反映当前的current_MED。)现在,检查是否存在 任何 的索引
u(我们正在考虑的当前最小值),使得对于当前的current_MED,条件maxSuffix(l_u, u-1) + sign_u + maxPrefix(u+1, r_u) >= 0不 满足。 对于 特定 元素 的sign_u条件是:- 如果 ,则 。
- 如果 ,则 。
所以,对于每个 的 ,我们需要评估:
max_possible_sum_for_u = maxSuffix(l_u, u-1) + (MN >= current_MED ? 1 : -1) + maxPrefix(u+1, r_u)
如果对于 任何 的 ,
max_possible_sum_for_u < 0,那么我们无法用 作为最小值实现中位数为current_MED。在这种情况下,我们跳出内层循环(不能为这个MN进一步增加current_MED)。对于这个MN,最好的中位数是current_MED - 1。更新max_diff = max(max_diff, (current_MED - 1) - MN)。 否则(如果对于所有 的 ,max_possible_sum_for_u >= 0),那么我们可能可以实现中位数为current_MED。所以,我们增加current_MED并继续内层while循环。
这个逻辑似乎与“贪心提升 med”部分更一致。从 +1 变为 -1 的值是那些变得 小于 当前 med 的值。
数据结构和复杂度:
- 单调栈: 为所有 计算 和 : 时间。
- 线段树:
- 初始化:
- 点更新:
- 范围查询(用于
maxSuffix,maxPrefix):
- 主循环:
- 外层循环从 到 遍历
MN。 - 内层
while current_MED循环意味着current_MED在所有MN迭代中也从 到 整体 递增。每次current_MED增加,我们对所有元素 进行点更新。由于每个 值只出现一次,每个索引 的sign从 变为 最多更新一次。这总共为current_MED的推进提供了 的时间。 - 对于每个
MN,我们遍历所有 的 。假设有 个这样的索引。对于每个索引,我们进行两次范围查询(每次 )。所有MN的 之和为 。所以,所有MN的所有查询总共是 。
- 外层循环从 到 遍历
因此,总时间复杂度为 。 内存复杂度为 ,用于数组、栈和线段树。
正确性为何:
- 贪心
MED: 对于每个固定的MN,内层循环正确地找到了可以实现的最大MED。如果MED可以是 ,它也可以是 。所以我们尝试将其推到尽可能高。 - 迭代
MN: 通过从 到 迭代MN,我们保证考虑了子数组的每个可能的最小值。 - 用于
sign和的线段树: 线段树正确地维护了必要的信息(maxPrefix,maxSuffix)来检查在计算的 范围内的任何潜在子数组的中位数条件。条件maxSuffix(l_u, u-1) + sign_u + maxPrefix(u+1, r_u) >= 0找到了以 为中心(其中 是最小值 )并向左延伸至 、向右延伸至 的任何子数组的最大可能 signs 之和。如果这个最大可能和 ,那么这样的子数组就存在,满足中位数属性。
示例演练(概念性):
设 , .
预计算 : (粗略地说,取决于严格不等式与等式。在解释中,我们假设 值的定义是严格的) : 左侧第一个索引,其值 : 右侧第一个索引,其值
对于 : (没有更小的元素) 对于 : (因为 ), (在 右侧没有 ) … 这很复杂,我们只使用给定的严格定义“左侧第一个值 < a[u]+1”和“右侧第一个值 < a[u]-1”。这似乎不寻常。范围最小值查询的标准通常是“第一个小于 的元素”。我们假设标准的 ,其中 是 中的最小值。
让我们使用典型定义: 是第一个索引 使得 , 是第一个索引 使得 。如果不存在这样的索引,我们可以使用 0 和 。 对于 : (索引 0 是哨兵) (索引 7 是哨兵)
对于 对于 (因为 ) 对于 对于 (因为 ) 对于 对于
初始化线段树:
current_MED = 1。所有 ,所以sign = [1,1,1,1,1,1]。外层循环:
MN = 1的索引 :。内层
while循环 (对于current_MED):current_MED = 1:- 对于 : 。 (1>=1),所以 。
maxSuffix(0,0)(空) 是 0。maxPrefix(2,6):sign数组是 。 的最大前缀是 。 对于 的和:。OK。 - 对于 : 。 (1>=1),所以 。
maxSuffix(0,2):sign对于 是 。 的最大后缀是 2。maxPrefix(4,6):sign对于 是 。 的最大前缀是 3。 对于 的和:。OK。 对于MN=1和current_MED=1都 OK。将current_MED增加到 2。
- 对于 : 。 (1>=1),所以 。
current_MED = 2: 为 更新sign(等于current_MED - 1的值)。索引 现在 ,所以更新sign_1=-1, sign_3=-1。sign数组现在是[-1, 1, -1, 1, 1, 1]。检查 : 。 (1<2),所以 。
maxSuffix(0,0)是 0。maxPrefix(2,6)在[-1, 1, 1, 1, 1]中是 () 或 signs (值 ) 的最大前缀。 的最大前缀是 (对于 )。 对于 的和:。OK。 检查 : 。 (1<2),所以 。maxSuffix(0,2)在[-1,1,-1]中:[-1,1]的最大后缀是 。maxPrefix(4,6)在[1,1,1]中: 的最大前缀是 。 对于 的和:。OK。 对于MN=1和current_MED=2都 OK。将current_MED增加到 3。current_MED = 3: 为 更新sign(无)。sign数组仍然是[-1, 1, -1, 1, 1, 1]。检查 : 。 (1<3),所以 。
maxSuffix(0,0)是 0。maxPrefix(2,6)在[1,-1,1,1,1]中是 1。 对于 的和:。OK。 检查 : 。 (1<3),所以 。maxSuffix(0,2)在[-1,1,-1]中是 1。maxPrefix(4,6)在[1,1,1]中是 3。 对于 的和:。OK。 对于MN=1和current_MED=3都 OK。将current_MED增加到 4。current_MED = 4: 为 更新sign。索引 现在 ,所以更新sign_5=-1, sign_6=-1。sign数组现在是[-1, 1, -1, 1, -1, -1]。检查 : 。 (1<4),所以 。
maxSuffix(0,0)是 0。maxPrefix(2,6)在[1,-1,1,-1,-1]中: 的最大前缀是 。 对于 的和:。OK。 检查 : 。 (1<4),所以 。maxSuffix(0,2)在[-1,1,-1]中是 1。maxPrefix(4,6)在[1,-1,-1]中: 的最大前缀是 。 对于 的和:。OK。 对于MN=1和current_MED=4都 OK。将current_MED增加到 5。current_MED = 5: 为 更新sign。索引 现在 ,所以更新sign_2=-1。sign数组现在是[-1, -1, -1, 1, -1, -1]。检查 : 。 (1<5),所以 。
maxSuffix(0,0)是 0。maxPrefix(2,6)在[-1,-1,1,-1,-1]中: 的最大前缀是 (对于 )。 对于 的和:。OK。 检查 : 。 (1<5),所以 。maxSuffix(0,2)在[-1,-1,-1]中是 。maxPrefix(4,6)在[1,-1,-1]中是 。 对于 的和:。失败! 条件对于 失败。所以,对于MN=1,最大MED是current_MED - 1 = 4。max_diff = max(0, 4 - 1) = 3。
外层循环:
MN = 2(示例中没有元素为2)外层循环:
MN = 3的索引 :。current_MED应从上次离开的地方继续,即 。sign数组是[-1, -1, -1, 1, -1, -1]。检查 : 。 (3<5),所以 。
maxSuffix(3,4)在[1,-1]中是 1。(来自 。Signs: 。对于范围 的 的后缀是 如果 , 如果 。对于 , 当前 时 。对于 , 。所以[-1,1]。最大后缀和是 1。)maxPrefix(6,6)在[-1]中是 -1。 对于 的和:。失败! 条件对于 失败。所以对于MN=3,最大MED是current_MED - 1 = 4。max_diff = max(3, 4 - 3) = 3。
…依此类推。逻辑似乎成立。示例子数组 ,其 得到 。我们的算法为 找到了 ,差值为 。
该解法巧妙地利用了线段树和 sign 数组来高效地检查中位数条件。对 MN 的扫描和对 MED 的贪心递增确保了所有最优解都被考虑到。