题目来源:AcWing789. 数的范围
一、题目描述
给定一个按照升序排列的长度为 n n n 的整数数组,以及 q q q 个查询。
对于每个查询,返回一个元素 k k k 的起始位置和终止位置(位置从 0 0 0 开始计数)。
如果数组中不存在该元素,则返回 -1 -1
。
输入格式
第一行包含整数
n
n
n 和
q
q
q,表示数组长度和询问个数。
第二行包含 n n n 个整数(均在 1 ∼ 10000 1∼10000 1∼10000 范围内),表示完整数组。
接下来 q q q 行,每行包含一个整数 k k k,表示一个询问元素。
输出格式
共
q
q
q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1
。
数据范围
1
≤
n
≤
100000
1≤n≤100000
1≤n≤100000
1
≤
q
≤
10000
1≤q≤10000
1≤q≤10000
1
≤
k
≤
10000
1≤k≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
二、二分原理
二分的本质并不是单调性,二分的本质是通过某一个性质可以将序列一分为二。有单调性一定可以二分,也就是说可以二分的题目不一定非要有单调性。
依据是否某一个性质,序列被一分为二。二分算法可以帮助我们找到满足/不满足这个性质的序列的边界,如下图,红色的区间代表不满足某个性质,绿色的区间代表满足某一个性质,通过二分算法可以找到这两个区间的边界。
因此,根据求不满足性质的序列的边界和满足性质的序列的边界这两个任务,二分算法也就有了两个不同的模板。
求不满足性质序列的边界位置
(1)计算
m
i
d
mid
mid,
m
i
d
=
l
+
r
>
>
1
;
mid = l + r >> 1;
mid=l+r>>1;
(2)如果
m
i
d
mid
mid 在红色区间内,则不满足性质的边界肯定在
[
m
i
d
,
r
]
[mid, r]
[mid,r] 区间中,更新方式
l
=
m
i
d
;
l = mid;
l=mid;
(3)如果
m
i
d
mid
mid 在绿色区间内,则说明不满足性质的边界肯定在
[
l
,
m
i
d
−
1
]
[l, mid - 1]
[l,mid−1] 中,更新方式
r
=
m
i
d
−
1
;
r = mid - 1;
r=mid−1;。
注意:如果说更新方式是
l
=
m
i
d
l = mid
l=mid,则在
m
i
d
mid
mid 计算时模板加上
1
1
1,即 mid = l + r >> 1 变成
m
i
d
=
l
+
r
+
1
>
>
1
mid = l + r + 1 >> 1
mid=l+r+1>>1,需要强记。
下面解释一下mid为什么要加1:
假设当
l
=
r
−
1
l = r - 1
l=r−1 时(即
l
,
r
l, r
l,r 相邻时),如果不加
1
1
1,则
m
i
d
=
[
l
+
(
l
+
1
)
]
2
=
l
mid = \frac{[l + (l + 1)]} { 2} = l
mid=2[l+(l+1)]=l, 如果说
m
i
d
mid
mid 仍然在红色区间中,则更新时
l
=
m
i
d
=
l
l = mid = l
l=mid=l,会陷入死循环;如果加
1
1
1 处理,
m
i
d
=
[
(
r
−
1
)
+
r
+
1
]
2
=
r
mid = \frac{[(r - 1) + r + 1]} {2} = r
mid=2[(r−1)+r+1]=r,如果
m
i
d
mid
mid 仍然在红色区间中, 更新时
l
=
m
i
d
=
r
l = mid = r
l=mid=r,新区间变成
[
r
,
r
]
[r, r]
[r,r],则不会出现问题。
模板
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (not_satisfied(mid)) l = mid; // 写到这里,发现l = mid,则在mid计算中补1
else r = mid - 1;
}
求满足性质序列的边界位置
(1)计算
m
i
d
mid
mid,
m
i
d
=
l
+
r
>
>
1
;
mid = l + r >> 1;
mid=l+r>>1;
(2)如果
m
i
d
mid
mid 在绿色区间中,说明满足性质的边界肯定在
[
l
,
m
i
d
]
[l, mid]
[l,mid] 区间中,更新方式
r
=
m
i
d
;
r = mid;
r=mid;
(3)如果
m
i
d
mid
mid 在红色区间中,说明满足性质的边界肯定在
[
m
i
d
+
1
,
r
]
[mid + 1, r]
[mid+1,r] 区间中,更新方式
l
=
m
i
d
+
1
;
l = mid + 1;
l=mid+1;
模板
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (satisfied(mid)) r = mid;
else l = mid + 1;
}
实际做题时如何考虑
(1)不需要想的很复杂,直接根据题目要求的性质,任意设计一个
c
h
e
c
k
(
)
check()
check() 函数,并写出区间更新的方式;
(2)如果发现其中存在
l
=
m
i
d
l = mid
l=mid 这个更新方式,则修改 mid = l + r >> 1 为
m
i
d
=
l
+
r
+
1
>
>
1
;
mid = l + r + 1 >> 1;
mid=l+r+1>>1;
(3)如果发现是
r
=
m
i
d
r = mid
r=mid 这个更新方式,则不需要修改
m
i
d
mid
mid 的计算方式。
三、代码
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], n, m;
// 寻找左端点
int findL(int x)
{
int l = 0, r = n - 1;
// 这里的任务定义为:寻找不小于x的值的左端点
while (l < r)
{
int mid = l + r >> 1;
if (a[mid] >= x) r = mid;
else l = mid + 1;
}
if (a[l] == x) return l; // 判断是否有解
return -1;
}
// 寻找右端点
int findR(int x)
{
int l = 0, r = n - 1;
// 这里的任务定义为:寻找不大于x的右端点
while (l < r)
{
int mid = l + r + 1 >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
return l; // 因为既然能执行到findR,说明x肯定存在,就不用担心找不到。
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
while (m--)
{
int x;
scanf("%d", &x);
int l = findL(x), r;
if (l == -1) puts("-1 -1");
else
{
r = findR(x);
printf("%d %d\n", l, r);
}
}
return 0;
}