翻转并乘二后的最小极差(二分答案 + 分类讨论)
翻转并乘二后的最小极差(二分答案 + 分类讨论)
在写到这道题的时候,二分思维其实已经有进步了,但是问题还是出现在二分检查的过程中,编程细节中的处理,要多写多见多悟
一、题目描述
给定一个长度为 n 的正整数数组 a。
你可以进行至多一次操作:选择一个区间 [l, r],将该区间翻转,并且区间内的所有数字乘 2。
操作后,数组变成:
[a1, ..., a_{l-1}, 2*a_r, 2*a_{r-1}, ..., 2*a_l, a_{r+1}, ..., a_n]
数组的“间隙值(Gap)”定义为相邻两数差的绝对值的最大值:max(|a_i - a_{i+1}|)。
求进行一次操作后,可能的最小间隙值。
二、思路分析
1. 二分答案
要求“最大值的最小可能值”,典型的二分答案特征。
我们二分间隙值的上限 x,然后写一个 check(x) 函数,判断是否存在一种操作,使得操作后的所有相邻差值都不超过 x。
若能实现,说明答案可以更小,缩小右边界;否则增大左边界。
2. 分析操作前后的相邻元素关系
假设操作前,我们找到第一个不满足 |a_i - a_{i+1}| <= x 的位置(记为 l),和最后一个不满足的位置(记为 r)。
为了把这段“违规”的区间修复,我们的操作区间必须覆盖从 l 到 r+1 的所有元素。即我们要翻转并乘二的区间必须满足 L <= l 且 R >= r + 1。
3. 分类讨论(check 函数的核心)
根据违规区间的情况,分为三类:
情况一:没有违规的地方(l == -1)
直接返回 true。
情况二:只有一个违规位置(l == r)
这种情况下,操作可以把 l 和 l+1 包含进去,并且可以向左或向右延伸,利用乘 2 后差值也可能满足条件的特性。
- 1. 只翻转这两个数(或者以其中一个为边界):
- 向左翻转:
[l, l+1],检查|a[l]*2 - a[l+1]|和左边界的连接。 - 向右翻转:
[l, l+1],检查|a[l] - a[l+1]*2|和右边界的连接。
- 向左翻转:
- 2. 向左延伸翻转:
向左枚举i,如果|a[i] - a[i+1]| * 2 <= x,说明这段区间乘 2 后可以合法。那么尝试把[i, l+1]翻转。检查翻转后的边界2*a[i]和a[l+1],以及2*a[l]和左侧a[i-1]。 - 3. 向右延伸翻转:
向右枚举i,如果|a[i] - a[i-1]| * 2 <= x,说明这段区间乘 2 后可以合法。尝试翻转[l, i]。检查边界a[l]和2*a[i],以及2*a[l+1]和右侧a[i+1]。
情况三:有多个违规位置(l < r)
如果违规的位置不止一处,那么我们的操作区间必须同时覆盖这所有的违规位置。
因此,翻转区间的左端点 L 最大只能到 l,右端点 R 最小必须到 r+1。为了让操作后的边界最容易满足条件,我们贪心地选择:
- 左端点定在
l - 右端点定在
r + 1
翻转后,边缘的配对是:
a[l]与2*a[r]相连2*a[l+1]与a[r+1]相连- 中间的部分
a[i]与a[i+1]翻转后变成2*a[i+1]与2*a[i],差值变为原来的 2 倍。
所以需要满足:
|a[l] - a[r]*2| <= x|a[l+1]*2 - a[r+1]| <= x- 对于中间所有
i \in [l+1, r-1],有|a[i] - a[i+1]| * 2 <= x。
这三个条件全部满足,则返回 true,否则 false。
三、AC 代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5;
int a[N];
int n;
bool check(int x)
{
int l = -1, r = -1;
// 找到第一处和最后一处不满足差值 <= x 的地方
for (int i = 1; i < n; i ++)
{
int ans = a[i] - a[i + 1];
if (abs(ans) > x)
{
if (l == -1) l = i;
r = i;
}
}
// 1. 没有违规的地方
if (l == -1) return true;
// 2. 只有一处违规
if (l == r)
{
// 试探 1:只翻转 l 和 l+1
if (abs(a[l] * 2 - a[l + 1]) <= x && (l - 1 < 1 || abs(a[l - 1] - 2 * a[l]) <= x)) return true;
if (abs(a[l] - 2 * a[l + 1]) <= x && (l + 2 > n || abs(a[l + 1] * 2 - a[l + 2]) <= x)) return true;
// 试探 2:向左延伸
bool flag = true;
for (int i = l - 1; i >= 1 && flag; i --)
{
if (abs(a[i] - a[i + 1]) * 2 <= x)
{
if (abs(a[i] * 2 - a[l + 1]) <= x && (i - 1 < 1 || abs(a[i - 1] - 2 * a[l]) <= x)) return true;
}
else flag = false;
}
// 试探 3:向右延伸
flag = true;
for (int i = l + 2; i <= n && flag; i ++)
{
if (abs(a[i] - a[i - 1]) * 2 <= x)
{
if (abs(a[l] - a[i] * 2) <= x && (i + 1 > n || abs(a[l + 1] * 2 - a[i + 1]) <= x)) return true;
}
else flag = false;
}
return false;
}
// 3. 有多处违规
if (abs(a[l] - a[r] * 2) > x || abs(a[l + 1] * 2 - a[r + 1]) > x) return false;
for (int i = l + 1; i < r; i ++)
{
if (abs(a[i] - a[i + 1]) * 2 > x) return false;
}
return true;
}
int main()
{
int t;
cin >> t;
while (t --)
{
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
// 二分的上界是原数组的最大差值
int low = 0, high = 0;
for (int i = 1; i < n; i ++) high = max(high, abs(a[i] - a[i + 1]));
int ans = high;
while (low <= high)
{
int mid = (low + high) >> 1;
if (check(mid)) ans = mid, high = mid - 1;
else low = mid + 1;
}
cout << ans << endl;
}
return 0;
}
四、复杂度分析
- 时间复杂度:每次
check函数复杂度为O(n)。外层二分查找范围是[0, 2*10^6],最多执行log(10^6) ≈ 20次。总复杂度O(20 * n)。对于n总和3*10^5来说非常快。 - 空间复杂度:
O(n),只需存储原数组。
五、易错点
- if (abs(a[i] * 2 - a[l + 1]) <= x && (i - 1 < 1 || abs(a[i - 1] - 2 * a[l]) <= x)) return true;
等语句括号搞混了 写成))<= x) 找半天没找出来 - 在边界的时候,下标也容易搞错
六、总结
这道题是一道非常好的二分 + 思维的题目。
通过二分将求解最小化问题转化为判定性问题。在判定时,通过找到首尾两个违规点,巧妙地将问题转化为“选择哪一段区间翻转”的分类讨论。虽然代码中的分类逻辑较多,但只要理清了左边界、右边界和延伸时的合并方式,整体思路十分清晰。

浙公网安备 33010602011771号