二分的学习及刷题
1.什么是二分
-
二分法 是一种常见的算法,它也被称为二分查找或折半查找。该算法是用于在有序数组中查找特定值的一种搜索算法。
二分法的基本思想是:将有序数组分成两个部分,然后判断目标值可能存在于哪个部分,再对该部分进行二分查找,直到找到目标值或者确定目标值不存在为止。
二分法的时间复杂度:O(log n)
-
例子:
-
给定一个有序数组
arr[7] = {1,3,6,9,12,32,44},又给定一个要查询的数字number,求number在数组中的位置。假定
number = 32,用二分法来查找的话,首先我们要定义三个变量int l = 0, r = 6, mid = (l+r)/2,接着判断一下number与arr[mid]大小关系,若number > arr[mid]说明number在mid的右边,所以左指针l右移到mid+1;若number < arr[mid]说明number在mid的左边,所以右指针r左移到mid-1。继续使mid = (l+r)/2继续判断number与arr[mid]大小关系,直到找到number代码如下:
#include <iostream> using namespace std; int ans; int main() { int len int number; int arr[len]; cin >> len ; //输入数组的长度 for(int i = 0; i < len-1; i++) cin >> arr[i];//输入数组 cin >> number; //输入要查找的数字 int l = 0, r = len-1; while(l <= r) { mid = (l + r)/2; if(number == arr[mid]) ans = mid; else if(number > arr[mid]) { l = mid + 1; } else if(number < arr[mid]) { r = mid ; } } cout << ans; return 0; }
-
2.二分法的应用场景
- 二分法应用场景:
- 有序数组的查找: 最常见的应用场景就是在有序数组中查找特定元素。由于二分查找每次可以将搜索范围减半,因此其时间复杂度为O(log n),远远快于线性搜索。
- 查找某个值的位置或插入位置: 当需要确定某个值在数组中的位置,或者在有序数组中插入某个值时,二分查找也非常实用。
- 寻找边界: 有时候需要找到满足某种条件的最小或最大的元素位置,二分查找可以高效地解决这类问题。
- 旋转数组的搜索: 在旋转数组(部分有序数组)中查找特定元素时,可以先确定有序的部分,然后再在有序部分上运用二分查找。
- 数值范围的搜索: 在一定数值范围内查找满足特定条件的元素,比如在一组整数中查找第一个大于等于某个值的元素。
3. 应用二分的判断及二分模版
-
判断是否可用二分法:
1、是否具有二段性
2、是否具有单调性
3、快速判断某个数是否在某一堆数里面出现过
-
二分法解题步骤:
1、通过二分的数据搜索范围,确定
rl初始值2、确定二分
rl指针的更新条件3、确定
rl指针的更新操作- 第一种情况:目标值不可能是
arr[l]时
r = mid;或者l = mid + 1; - 第二种情况:目标值不可能是
arr[r]时
l = mid;或者r = mid - 1;
4、
mid的更新操作- 当
r = mid时,更新mid = (r + l) / 2 - 当
l = mid时,更新mid = (r + l + 1) / 2(加一的原因是:求右边界要加一个1的原因在于当rl相邻时,若不加1就除于二,求得的mid就是l,然后判断arr[mid] <= k令l=mid,此时 l 没有改变位置,l将永远不能等于r进入死循环。)
5、确定二分循环的跳出条件
- 一般为
l < r(根据题目要求修改)
- 第一种情况:目标值不可能是
4.例题
-
【Acwing 789.数的范围】 (二分)
分析:
-
根据题意:要求我们输入一个有序数组,输入一个查询数k,接着输出k的左右下标边界。具体情况如下:

对于上述的情况我们知道题目的意思是要我们寻找查询数
k的左右边界。 -
查找一个数或是边界问题,我们可以通过二分法进行查找。对于左右边界我们可以先用二分法找左边界,再用一次二分法找右边界。在使用二分法查询的时候,我们首先定义一个左右指针
r l,计算出中指针mid,接着判断arr[mid]和k的大小关系,来改变r l的值。 -
时间复杂度分析:数组长度
N最大值为100000,要查询的数最多有q10000,对于每个数的查找,查找其左边界需要用时为log N查找出两个边界所用时间就是2log N。所以总的时间是2log N * q = 2 * lg100000 / lg2 * 10000 = 330000,远小于1亿所以不会超时,方法可行。
设计思路:
-
对于左边界,当
arr[mid] >= k时,我们将r = mid, 当arr[mid] < k时,我们将l = mid + 1;直到r==l此时l的值就是k的左边界。下图是一个升序的数组查询左边界的模拟过程:
-
对于右边界也是相同的二分查询法。当
arr[mid] <= k时,我们将l = mid, 当arr[mid] >= k时,我们将r = mid - 1;直到r==l此时r的值就是k的右边界。 -
细节问题:在用
r l计算mid的大小的时候,对于查询左右边界时有所不同。求左边界时有mid = r+l >> 1,求右边界时有mid = r+l+1 >> 1。求右边界要加一个1的原因在于当rl相邻时,若不加1就除于二,求得的mid就是l,然后判断arr[mid] <= kl令l=mid,此时 l 没有改变位置,l将永远不能等于r进入死循环。
代码:
-
-
#include <iostream> using namespace std; const int N = 100010; int n, q; int arr[N]; int main() { scanf("%d%d", &n, &q); for(int i = 0; i < n; i++) { scanf("%d", &arr[i]); } while( q -- ) { int k; scanf("%d", &k); int l = 0, r = n -1; //先查找左边界 while(l < r) { int mid = l + r >> 1; if(arr[mid] >= k) r = mid; else l = mid + 1; } if(arr[r] == k) { cout << l << " "; r = n-1; //接着查找右边界 while(l < r) { int mid = l + r + 1 >> 1;//加一的原因是当rl相邻的时候,若不加1,mid==l,l=mid无法移动l指针,这样rl永远都不可能相等 if(arr[mid] <= k) l = mid; else r = mid - 1; } } else cout << "-1 -1" << endl; } return 0; } -
【Acwing 790.数的三次方】 (二分)
分析:
- 按照题目要求就是输入一个浮点数
n,输出其三次方根,结果保留6位小数。 - 求一个数的三次方根我们的第一反应可能是直接用cmath库中的库函数
pow来实现,但这个函数有个问题就是当n为负数且所求的三次方根接近于零但不等于零的时候会返回NAN,因此不建议使用pow函数。 - 对于
n题目给定了范围为-10000 ~ 10000,而n的三次方根就在这个范围里面,因此我们可以考虑用 二分法 不断地去逼近n的三次方根。
设计思路:
- 首先定义双精度浮点型左右指针和中指针,并赋予初始
rl的值确定数据范围。 - 确定二分跳出循环的条件:当
r - l < 1e-8说明此时rl的值已经很接近n的三次方根,且rl小数点后8位相同。 - 移动
rl指针缩小数据范围:当mid * mid * mid > n说明mid比n的三次方根要大,因此将右指针r更新为mid;当mid * mid * mid < n说明mid比n的三次方根要小,因此将左指针l更新为mid - 最后用printf函数格式化输出
r或者l即可。
代码:
- 按照题目要求就是输入一个浮点数
-
#include <iostream> using namespace std; double n; int main() { cin >> n; double l = -10000, r = 10000; while(r - l > 1e-8) { double mid = (l + r)/2; if(mid * mid * mid > n) r = mid; else l = mid; } printf("%lf", l); return 0; } -
【Acwing 730.机器人跳跃问题】(二分,递推)
分析:
- 根据题目要求,要返回一个最小初始能量值
E使得可以成功完成游戏。 - 这个初始能量值
E的取值范围是1 ~ 1e5,因此我们可以想到一个最简单粗暴的方法,就是枚举取值范围里面的每一个数,并对每一个数判断其是否能完成游戏。 - 直接暴力枚举,我们可以先分析一下该方案的时间复杂度,要从
1~1e5进行枚举,枚举每一个数要对数组进行判断,数组里最多的元素个数为1e5,因此总的时间为1e10超过了一个亿,该方法会超时。 - 对于枚举的问题,我们可以考虑二分,dfs,dp,贪心。分析题目,对于最终的结果
E,我们知道比E大的数值一定可以完成游戏,比E小的数值一定不能成功完成游戏,因此数据具有二段性,故可以用**二分法**进行枚举。
设计思路:
-
确定
rl的初始值。由数据范围可知l = 0, r = 1e5 -
更新
rl值的判断条件。定义一个返回值为bool类型的函数check,输入的参数是E,判断输入的E是否可以完成游戏。若check返回为true说明输入的E可以完成游戏,因此对右指针r进行更新,否则对左指针l进行更新。 -
对于
rl的 更新操作。更新r时,由于此时check函数为true,因此当前的r可能为最终的E,也有可能是在E的右边,因此r = mid;更新l时,由于此时check函数为false,因此当前的l不可能为最终的E,同时l一定在E的左边,因此l = mid + 1; -
对于
mid的更新操作。mid 的更新操作取决于rl的 更新操作,由于r = mid因此mid = (r + l)/2。 -
二分循环的条件。当
l < r时,说明还没找到最终的初始能量E。跳出循环时,则l == r,此时有E == l == r。 -
check函数的编写。通过题目意思我们可以得到以下的递推公式:

上式中的 k 表示第几个建筑,通过上面的递推式子我们可以求出机器人在第 k 座建筑的能量值
E,判断每一个E是否有出现小于零的情况,若有返回false,若无返回true(由于E是每次都是上一次的两倍,极有可能空间溢出,但我们发现当E大于数组中的最大值,遍历到后面的建筑的E一定大于零,因此当E大于数组最大值就直接返回true,这样可以解决E空间溢出的问题)
代码:
#include <iostream> using namespace std; const int N = 1e5 + 10; int n; int h[N]; bool check(int e) { for(int i = 0; i < n; i++) { e = 2*e-h[i];//递推公式 if(e < 0) return false; if(e > 1e5) return true;//当能量E大于数组最大的数时,说明后面的E不可能有小于零的情况,直接返回true。此步是为了防止E的空间溢出出现负数的情况 } } return true; } int main() { cin >> n; for(int i = 0; i < n; i++) scanf("%d", &h[i]); int l = 0, r = 1e5; while(l < r) { int mid = (l + r) /2; if(check(mid)) r = mid; else l = mid + 1; } cout << l; return 0; } - 根据题目要求,要返回一个最小初始能量值
-
分析:
- 根据题目意思,就是输入一个整数
N,输出N的四个整数a b c d,该四个整数满足N = a^2 + b^2 + c^2 + d^2。 - 由于输入的整数
N的范围是确定的:0~5e6,由上式知道a b c d是一定小于N,所以a b c d的范围就是确定的,我们可以暴力枚举 从大到小a b c d这四个整数,判断哪一组数据满足N = a^2 + b^2 + c^2 + d^2输出即可。 - 我们分析一下以上暴力枚举的时间复杂度:O(&n^4&),很显样这样肯定会爆时间,其实遍历前面三个数可以通过
N计算出的d, 判断一下d的范围即可,这样的时间复杂度是O(&n^3&)。但这样的时间复杂度还是很勉强。因此我们只能枚举两个整数。 - 由于最多只能枚举两个整数,我们可以通过空间换时间的方式实现题目的要求。我们可以先枚举
c d,将c^2 + d^2的结果全都存储起来;再枚举a b,将a^2 + b^2的结果全都存储起来,最后从头到尾判断哪一组a^2 + b^2加上哪一组c^2 + d^2等于N。时间复杂度大致是O(3*n^2),该方法可行。 - 对于上述的问题,我们可以简化为:如何在
c^2 + d^2的数组中找到一个数等于a^2 + b^2。像这种如何快速判断一个数在某一堆数中是否出现过,我们一般考虑两个做法:1、二分法(O(logN),2、哈希表(O(1))。因此本道题也可以用二分法和哈希做。
设计思路:(基于二分法)
-
首先定义一个结构体
Sum,用于存储两个数和两数的平方和int c d s。 -
接着枚举
c d,并将它们及它们的平方和存储到结构体数组sum[N]中 -
然后对结构体数组中的每个结构体进行排序,平方和小的排在前面,相同平方和的结构体,字典序小的排在前面
-
再枚举
a b,一边枚举一边开始二分查找对应的那组满足题意的c d1、确定左右指针
lr的初始值:l = 0, r = m-1(m是结构体数组的长度)。2、确定
rl的更新条件:当sum[mid].s >= n-a*a-b*b,说明要查找的那组c d在mid的左边,因此更新r;当sum[mid].s < n-a*a-b*b,说明要查找的那组c d在mid的右边,因此更新l;3、对于
rl的更新操作:r = mid和l = mid + 14、二分查找的循环条件:
l < r。当l == r时,对sum[l].s进行判断,若sum[mid].s == n-a*a-b*b说明当前查找出来的这组c d是合法的;否则,说明不存在这样一组合法的a b c d,继续枚举新的a b,接着进行二分查找对应的c d。
代码:
-
二分法:
#include <iostream> #include <algorithm> using namespace std; const int N = 5e6+10; int n, m; struct Sum { int s, c, d; bool operator< (const Sum &t)const { if (s != t.s) return s < t.s; if (c != t.c) return c < t.c; return d < t.d; } }sum1[N]; int main() { cin >> n; for(int c = 0; c * c <= n; c++) for(int d = c; d * d + c * c <= n; d++) { sum[m++] = {d * d + c * c, c, d}; } sort(sum, sum+m);//排序是为了当两数平方和相等时,字典序最小的排在最前面,方便后面二分查找。 for(int a = 0; a * a <= n; a++) for(int b = a; a * a + b * b <= n; b++) { int l = 0, r = m-1; int t = n - a * a - b * b; while(l < r) { int mid = (r+l)/2; if(sum[mid].s >= t) r = mid;//sum[mid].s相同的值可能有多个,但我们取最左边的那个,因为字典序最小 else l = mid + 1; } if(sum[l].s == t) { printf("%d %d %d %d", a, b, sum[l].c, sum[l].d); return 0; } } } -
暴力枚举法:
#include <iostream> #include <cmath> using namespace std; const int N = 2500010; int n; int main() { cin >> n; for (int a = 0; a * a <= n; a ++ ) for (int b = a; a * a + b * b <= n; b ++ ) for (int c = b; a * a + b * b + c * c <= n; c ++ ) { int t = n - a * a - b * b - c * c; int d = sqrt(t); if (d * d == t) { printf("%d %d %d %d\n", a, b, c, d); return 0; } } } -
哈希法:
#include <iostream> #include <unordered_map> #define x first #define y second using namespace std; typedef pair<int, int> PII; const int N = 2500010; int n, m; unordered_map<int, PII> S; int main() { cin >> n; for (int c = 0; c * c <= n; c ++ ) for (int d = c; c * c + d * d <= n; d ++ ) { int t = c * c + d * d; if (S.count(t) == 0) S[t] = {c, d}; } for (int a = 0; a * a <= n; a ++ ) for (int b = 0; a * a + b * b <= n; b ++ ) { int t = n - a * a - b * b; if (S.count(t))//查询哈希表中是否存在t { printf("%d %d %d %d\n", a, b, S[t].x, S[t].y); return 0; } } return 0; }
- 根据题目意思,就是输入一个整数
-
分析:
- 根据题目意思,要求我们要输出巧克力最大的分割边长,同时要满足
K位小朋友都要分到。 - 已知巧克力的边长取值范围是从
1~1e5,所以巧克力的最大分割长度范围也是1~1e5,既然知道范围了,我们第一个想法就是进行暴力枚举,从 1 作为最大分割长度开始枚举,统计分割后的巧克力块数,若大于人数K,继续枚举;若枚举到分割后巧克力块数小于K的分割长度时,输出该长度减一(由于分割长度越大,块数越少,因此比最大分割长度还要大的长度,块数一定是不满足人数K的)。 - 分析一下暴力枚举的时间复杂度:设最大分割长度为
X,X最大可取到1e5,每枚举一个分割长度X都要对所有的巧克力进行分割并统计分割后的块数,巧克力块数最大为1e5。因此暴力枚举用到的最大时间为1e5*1e5 = 1e10 > 1e9,故暴力枚举的方法会爆时间。 - 枚举问题我们还是可以考虑二分法。首先我们考虑一下数据是否具有二段性,对于最大分割长度
X来说,比它大的,一定不符合题意;比它小的一定满足题意,因此数据具有二段性。故可采用二分法来枚举X。 - 分析二分法的时间复杂度:枚举最大分割长度
X所用的最大时间为log1e5 = 17,遍历每个巧克力统计分割后的块数所用的最大时间是1e5,因此总共用的最大时间为17e5,远小于1亿,故该方案可行。
设计思路:
-
定义两个数组用于存储每个巧克力的宽和长
-
编写一个bool类型的
check函数,传入的参数是所枚举到的分割长度值X,判断传入的X是否满足题意,若是,返回true;否则返回false。 -
用二分法查找出最大分割长度
X:1、确定左右指针
lr的初始值:由数据范围可知l = 1, r = 1e52、确定
rl的更新条件:当check(mid) == true,说明要查找的最大分割长度为当前mid或在mid的右边,因此更新l;当check(mid) == false,说明要查找的最大分割长度在mid的左边,因此更新r;3、对于
rl的更新操作:l = mid和r = mid - 14、对于
mid的更新操作:由于是l = mid因此更新方式为mid = (r + l + 1)/2。5、二分查找的循环条件:
l < r。当l == r时,对sum[l].s进行判断,若sum[mid].s == n-a*a-b*b说明当前查找出来的这组c d是合法的;否则,说明不存在这样一组合法的a b c d,继续枚举新的a b,接着进行二分查找对应的c d。 -
check函数的编写:遍历每一块巧克力,每块巧克力的分割块数为(h[i]/x) * (w[i]/x),因此统计所有的分割块数后,判断与K的关系,若cnt >= k返回true;若cnt < k,返回false。
代码:
#include <iostream> using namespace std; const int N = 1e5+10; int n, k; int h[N], w[N]; bool check(int x) { int cnt = 0; for(int i = 0; i < n; i++) { cnt += (h[i]/x) * ( w[i]/x); if(cnt >= k) return true; } return false; } int main() { scanf("%d %d", &n, &k); for(int i = 0; i < n; i++) { scanf("%d %d", &h[i], &w[i]); } int l = 1, r = 1e5; while(l < r) { int mid = l + r + 1 >> 1; if(check(mid)) l = mid; else r = mid -1; } printf("%d", l); return 0; } - 根据题目意思,要求我们要输出巧克力最大的分割边长,同时要满足
本文围绕二分算法展开,介绍了二分法的定义、基本思想和时间复杂度,阐述了其在有序数组查找、确定值位置等方面的应用场景,说明了应用二分的判断方法和解题步骤,并通过多个例题,如Acwing的数的范围、数的三次方等题目,展示了二分算法的具体运用。
996

被折叠的 条评论
为什么被折叠?



