其他分享
首页 > 其他分享> > 二分策略

二分策略

作者:互联网

二分策略

二分查找

给定一个从小到大的有序数组,查找元素k是否在数组中。

简单的思路是顺序查找,从前往后,逐个对比,但是效率较低。

可以根据数组从小到大有序这一特点来进行折半查找(二分查找)。

算法思想

待查找的值假定为k。首先在有序序列a中,挑选一个数a[i],如果a[i]小于k,可以直接排除[1,i]之间的数,反之可以排除[i, n]之间的数,原因就在于有序二字。那么数a[i]怎样挑选呢,可以每次选择一半,这样每次都可以缩减一半的区间,当范围缩小到一个数的时候就出现了结果。这就是二分查找的基本思想。

和顺序查找做一个比较,在以下数列中假定要查找数37。(图片来源于网络,侵删)

二分查找实现

可以使用left和right来表示当前要查找的区间左右边界。显然,初始化left = 1, right = n。

参考程序

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库中提供了二分有关的函数。

举例

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(MN)段,并要求每段连续,且每段和的最大值最小。关于最大值最小:

例如一数列 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,MN,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