二分策略
作者:互联网
二分策略
二分查找
给定一个从小到大的有序数组,查找元素k是否在数组中。
简单的思路是顺序查找,从前往后,逐个对比,但是效率较低。
可以根据数组从小到大有序这一特点来进行折半查找(二分查找)。
算法思想
待查找的值假定为k。首先在有序序列a中,挑选一个数a[i],如果a[i]小于k,可以直接排除[1,i]之间的数,反之可以排除[i, n]之间的数,原因就在于有序二字。那么数a[i]怎样挑选呢,可以每次选择一半,这样每次都可以缩减一半的区间,当范围缩小到一个数的时候就出现了结果。这就是二分查找的基本思想。
和顺序查找做一个比较,在以下数列中假定要查找数37。(图片来源于网络,侵删)
二分查找实现
可以使用left和right来表示当前要查找的区间左右边界。显然,初始化left = 1, right = n。
- 取出中间元素mid = (left + right) / 2
- 比较a[mid] 和 k的大小关系
- a[mid] > k,答案在左边区间,修改right = mid - 1
- a[mid] < k,答案在右边区间,修改left = mid + 1
- a[mid] == k,找到答案,输出相应信息
- 在left <= right的情况下,执行上述操作,否则查找失败。
参考程序
int binarySearch (int k, int l, int r) {
int mid;
while (l <= r) {
mid = l + r >> 1;
if (a[mid] < k) l = mid + 1;
else if (a[mid] > k) r = mid - 1;
else return mid;
}
return -1; //未找到
}
上述程序只能简单的进行查找,但不能精准的根据要求定位。假定要一个有序序列中有多个相同的要查找的值,而我们需要的是这个值第一次出现的位置,或者最后一次出现的位置,上面的程序则不够。
变式
1、查找序列a中第一个大于等于k的值的位置。
int binarySearch (int k, int l, int r) {
int mid;
while (l + 1 < r) {
mid = l + r >> 1;
if (a[mid] >= k) r = mid;
else l = mid + 1;
}
if (a[l] >= k) return left;
else return right;
}
2、查找序列a中最后一个小于等于k的值的位置
int binarySearch (int k, int l, int r) {
int mid;
while (l + 1 < r) {
mid = l + r >> 1;
if (a[mid] <= k) l = mid;
else r = mid - 1;
}
if (a[r] <= k) return r;
else return l;
}
3、查找序列a中等于k的值的位置,如果有多个值等于k,找到第一个出现的位置
int binarySearch (int k, int l, int r) {
int mid;
while (l < r) {
mid = l + r >> 1;
if (a[mid] >= k) r = mid;
else l = mid + 1;
}
if (a[l] == k) return l;
return -1;
}
4、查找序列a中等于k的值的位置,如果有多个值等于k,找到最后出现的位置
int binarySearch (int k, int l, int r) {
int mid;
while (l + 1 < r) { //左右区间长度为1
mid = l + r >> 1;
if (a[mid] <= k) l = mid;
else r = mid - 1;
}
if (a[r] == k) return r;
else if (a[l] == k) return l;
return -1;
}
例1、A-B问题
给出一串数以及一个数字 C,要求计算出所有 A - B = C的数对的个数(不同位置的数字一样的数对算不同的数对)。
【输入格式】
输入共两行。
第一行,两个整数 N, C。
第二行,N 个整数,作为要求处理的那串数。
【输入范围】
1 ≤ N ≤ 2×10^5。
【输出格式】
一行,表示该串数中包含的满足 A - B = C的数对的个数。
【输入样例】
4 1
1 1 2 3
【输出样例】
3
思路:
可以将A-B=C,转化为A=B+C,C是给定的,而A、B是数组中的数字。这样只需要将数组中的每一个数字加上C之后,然后再和原数组比较,有多少相同的数,就加上多少即可。
需要用到二分的地方在于,在原数组中找到和当前B+C的值相同的数的个数。
参考程序
#include <cstdio>
#include <algorithm>
using namespace std;
#define N 200005
int a[N];
int n, c;
long long cnt;
int main () {
scanf ("%d %d", &n, &c);
for (int i = 1; i <= n; i++)
scanf ("%d", &a[i]);
sort (a+1, a+1+n);
// 找到该数字第一次和最后一次出现的位置做减法,即可找到相同的数的个数
for (int i = 1; i <= n; i++)
cnt += (upper_bound(a+1, a+1+n, a[i] + c) - a) - (lower_bound(a+1, a+1+n, a[i] + c) - a);
printf("%lld\n", cnt);
return 0;
}
小结
二分查找要求数列单调有序
实际上STL库中提供了二分有关的函数。
- binary_search(begin, end, value, cmp) 。在数组中查找元素value,找到返回true,否则返回false
- lower_bound(begin, end, value)。在范围[begin, end-1]中查找第一个大于等于value的数字,找到则返回该数字的地址,不存在则返回end。可以通过减去起始位置的地址拿到下标。
- upper_bound(begin, end, num)。在范围[begin, end-1]中查找第一个大于value的数字,找到则返回该数字的地址,不存在则返回end。可以通过减去起始位置的地址拿到下标。
举例
int a[5] = {1, 3, 3, 3, 5};
int loc1 = lower_bound(a, a+5, 3) - a;
int loc2 = upper_bound(a, a+5, 3) - a;
cout << loc1 << " " << loc2 << endl;
/* 输出结果
1 4
*/
二分答案
二分答案是一种利用二分思想来解决问题的方法。实际上是对答案进行二分,要求答案必须是具有单调性,每次二分,都需要对该答案进行检查,检查是否符合题意。
例1、砍树问题
米尔科需要M米长的木材,米尔科有一台伐木机,工作过程是这样的,米尔科需要设置一个高度参数H,伐木机会把所有的树木高于H的部分砍断。例如,如果一行树的高度分别为20,15,10和17,米尔科设置高度H=15,切割后树木剩下的高度将是15,15,10和15,而米尔科将从第1棵树得到5米,从第4棵树得到2米,共得到7米木材。
帮助米尔科找到伐木机最大的高度H,使得他能得到木材至少为M米。
【输入格式】
第1行:2个整数N和M,N表示树木的数量(1<=N<=1000000),M表示需要的木材总长度(1<=M<=2000000000)
第2行:N个整数表示每棵树的高度,值均不超过1000000000。所有木材长度之和大于M,因此必有解。
【输出格式】
第1行:1个整数,表示砍树的最高高度。
【样例输入】
5 20
4 42 40 26 46
【样例输出】
36
思路
树木的高度答案实际上是一个单调性的序列,高度越低,砍伐的树木越多。因此可以对树木的高度答案进行二分,区间为【0,maxHeightTree】,如果当前H的高度使得获得的木材小于M,那么应该减少H的高度,反之增加H的高度。
参考程序
#include <iostream>
#include <algorithm>
#define N 1000005
#define ll long long
using namespace std;
ll a[N], mx = -1;
int n, m;
int main () {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
if (a[i] > mx) mx = a[i];
}
ll l = 0, r = mx, ans;
while (l <= r) {
ll mid = (l + r) / 2, sum = 0;
for (int i = 1; i <= n; i++)
if (a[i] > mid)
sum += a[i] - mid;
if (sum < m) r = mid - 1;
else ans = mid, l = mid + 1;
}
cout << ans << endl;
return 0;
}
例2、跳石头
【问题描述】
每年奶牛们都要举办各种特殊版本的跳房子比赛,包括在河里从一个岩石跳到另一个岩石。这项激动人心的活动在一条长长的笔直河道中进行,在起点和离起点L远 (1 ≤ L≤ 1,000,000,000) 的终点处均有一个岩石。在起点和终点之间,有N (0 ≤ N ≤ 50,000) 个岩石,每个岩石与起点的距离分别为Di (0 < Di < L)。
在比赛过程中,奶牛轮流从起点出发,尝试到达终点,每一步只能从一个岩石跳到另一个岩石。当然,实力不济的奶牛是没有办法完成目标的。
农夫约翰为他的奶牛们感到自豪并且年年都观看了这项比赛。但随着时间的推移,看着其他农夫的胆小奶牛们在相距很近的岩石之间缓慢前行,他感到非常厌烦。他计划移走一些岩石,使得从起点到终点的过程中,最短的跳跃距离最长。他可以移走除起点和终点外的至多M (0 ≤ M ≤ N) 个岩石。
请帮助约翰确定移走这些岩石后,最长可能的最短跳跃距离是多少?
【输入】
第一行包含三个整数L, N, M,相邻两个整数之间用单个空格隔开。
接下来N行,每行一个整数,表示每个岩石与起点的距离。岩石按与起点距离从近到远给出,且不会有两个岩石出现在同一个位置。
【输出】
一个整数,最长可能的最短跳跃距离。
【输入样例】
25 5 2
2
11
14
17
21
【输出样例】
4
思路分析
二分答案需要满足答案序列是具有单调性且有最大值最小值的。从题目中的描述可以得出,最短跳跃距离只能在[0,L]之间,那么可以在这个范围内二分答案,每一次二分得到一个最短跳跃距离mid,以这个距离来作为判定,验证最短距离为mid的情况下,最少需要移走的石头数量,如果该数量小于等于M,说明满足条件,下一次二分区间为[mid+1, r](因为要找最长的最短跳跃距离)。
关键点在于如何在已知最短跳跃距离mid的情况下,验证是否移走的石块小于等于M,可以使用模拟的方法来尝试移动石块,用一个变量now表示当前的位置,而i表示要跳到的石块的位置,显然如果 a[i]-a[now] < mid。说明i位置的石块需要被移走,然后判断i+1的位置,直到a[i] - a[now] >= mid,说明满足最短跳跃距离,因此now的位置可以移动到这个i的位置上,直到i走到终点。在这个模拟的过程中,统计好需要移走的石块数量,最后和M进行比较,如果小于等于M,说明该最短距离mid是满足条件的。可以尝试通过二分来增加mid的大小,再次判定。
参考程序
#include <iostream>
#include <algorithm>
#define N 50005
#define MX 100000005
using namespace std;
int a[N+1], m, n, L;
bool judge (int x) {
int now = 0, i = 1, cnt = 0;
for (int i = 1; i <= n+1; i++) {
if (a[i] - a[now] < x) cnt ++;
else now = i;
}
if (cnt <= m) return true;
return false;
}
int main () {
cin >> L >> n >> m;
a[n+1] = L;
for (int i = 1; i <= n; i++)
cin >> a[i];
int l = 1, r = L, ans;
while (l <= r) {
int mid = l + r >> 1;
if (judge(mid))
ans = mid, l = mid + 1;
else r = mid - 1;
}
cout << ans;
return 0;
}
例3、路标设置
【题目描述】
B市和T市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的“空旷指数”。现在政府决定在公路上增设一些路标,使得公路的“空旷指数”最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。
【输入格式】
第1行包括三个数L、N、K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。
第2行包括递增排列的N个整数,分别表示原有的N个路标的位置。路标的位置用距起点的距离表示,且一定位于区间[0,L]内。
【输出格式】
输出1行,包含一个整数,表示增设路标后能达到的最小“空旷指数”值。
【输入样例】
101 2 1
0 101
【输出样例】
51
【数据范围】
2 ≤N ≤100000, 0 ≤K ≤100000,0 < L ≤10000000
思路:
题目要求找相邻路标的最大距离的最小值。明显该答案是有一个明确范围的,而空旷指数越大要划分的段越少,题目中明确给出了可最多增设的路标数量,因此可以使用二分答案,来检查在该空旷指数下是否能满足路标上限数量。
检查方法:路标一定是放在相邻的两个原有路标之间的,那么可以通过遍历,检查相邻两个原路标的距离,如果这个距离大于了二分答案要检查的mid,那么这个距离是mid的多少倍,就需要放置多少个路标,但需要注意的是如果这个距离是mid的整数倍,那么路标要少放置一个(具体情况可自行举例)。
参考程序
#include <iostream>
#define N 100005
using namespace std;
int L, n, k, a[N];
bool judge (int x) {
int sum = 0;
for (int i = 2; i <= n; i++) {
if (a[i-1] + x < a[i]) {
sum += (a[i] - a[i-1]) / x;
if ((a[i]-a[i-1]) % x == 0)
sum --;
}
}
if (sum <= k) return true;
return false;
}
int main () {
ios::sync_with_stdio(0);
int ans;
cin >> L >> n >> k;
for (int i = 1; i <= n; i++)
cin >> a[i];
int l = 1, r = L;
while (l <= r) {
int mid = l + r >> 1;
if (judge(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
cout << ans;
return 0;
}
例4、数列分段
【题目描述】
对于给定的一个长度为N的正整数数列 A1∼N,现要将其分成 M(M≤N)段,并要求每段连续,且每段和的最大值最小。关于最大值最小:
例如一数列 4 2 4 5 1 要分成 3 段。
将其如下分段:
[4 2] [4 5] [1] 第一段和为 6,第 2 段和为 9,第 3 段和为 1,和最大值为 9。
将其如下分段:
[4] [2 4] [5 1]第一段和为 4,第 2 段和为 6,第 3 段和为6,和最大值为6。
并且无论如何分段,最大值不会小于 6。所以可以得到要将数列4 2 4 5 1 要分成 3 段,每段和的最大值最小为 6。
【输入格式】
第 11 行包含两个正整数 N,M。
第 22 行包含 N个空格隔开的非负整数 Ai,含义如题目所述。
【输出格式】
一个正整数,即每段和最大值最小为多少。
【输入样例】
5 3
4 2 4 5 1
【输出样例】
6
【数据范围】
对于100% 的数据,1≤N≤10^5,M≤N,A_i<10^8, 答案不超过 10^9。
思路:
同样该题要求划分为M段后,所有情况中M个子段和最大值的最小值。明显需要用到二分答案,直接对这个最小值进行二分,首先要明确初始边界,最小值为数组a中的最大的那个元素的值(每个元素都是一个段),最大值为数组a的累加和,难点就在于二分答案后,怎么去验证这个答案。
验证方法,可以使用贪心的思想,满足在数组a中每段的和都不超过mid,那么可以从头开始遍历,同时进行累加sum,如果在中间某一刻sum的值大于mid,说明这个位置的前一个地方应该断开,重置sum的值。再用一个变量记录断开的数量,最后和m进行比较。(注意假设断开了k次,那么子段的数量是k+1)。
如果验证为真,说明该mid值满足条件,尝试缩小mid的值再次验证,反正增大mid的值。
参考程序
#include <iostream>
#include <algorithm>
#define N 100005
using namespace std;
int n, m;
int a[N];
/**
判断数组内是否每段和都不大于x
以此标准来分段,如果最终分段的结果小于等于m,说明x偏大
否则,说明x偏小,为了保证每个子段的最大不超过x偏小的结果就是会划分出很多段。
**/
bool judge (int x) {
int sum = 0, cnt = 0;
for (int i = 1; i <= n; i++) {
if (a[i] + sum <= x) sum += a[i];
else sum = a[i], cnt ++;
}
if (cnt + 1 <= m) return true; // 注意 cnt+1才是子段的数量
return false;
}
int main () {
int l = -1, r = 0, ans;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
l = max(l, a[i]);
r += a[i];
}
while (l <= r) {
int mid = l + r >> 1;
if (judge(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
cout << ans << endl;
return 0;
}
总结
二分答案适用于明确知道答案边界,且答案具有单调性,该单调性指的是修改题目限定的条件,答案具有单调性的变化。一般用于求解满足某种条件下的最大(小)值,或者最大的最小值、最小的最大值,最靠近的值等。
答案的单调性如下图(图片来源网络,侵删)
参考模板:
//假定是求满足条件下的最小值
bool check (int x) {
for (题目要求下遍历) {
可能使用贪心策略
}
if (该答案x满足题目中的条件)
return true;
else
return false;
}
int l, r, ans;
while (l <= r) { // 找出初始边界
int mid = l + r >> 1;
if (check(mid)) ans = mid, r = mid - 1; // 满足条件,尝试找更小的值,选择mid左半边
else l = mid + 1;
}
cout << ans;
标签:二分,策略,int,mid,路标,查找,答案 来源: https://www.cnblogs.com/s-k-p/p/13661891.html