算法入门之二分(《算法笔记》)

二分查找——基于有序序列的查找算法

#高效之处:每一步都可以去除当前区间中的一半元素。
#时间复杂度:O(logn)

EG.1 查找序列中是否存在满足某条件的元素

👉如,在严格递增序列中找出给定的数x

👇搬个代码

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
//A[]为严格递增序列,x为欲查询的数,二分区间为左闭右闭的[left, right],初始区间为[0,n-1]
int binarySearch(vector<int> &A, int left, int right, int x){  //传引用 
	int mid;  //mid为left和right的中点
	while( left<=right ){  //如果left>right就无法形成闭区间了 
		mid = (left+right) / 2;  //取中点 
		if ( A[mid]==x ) return mid;  //找到x,返回下标 
		else if ( A[mid]>x ){  //中间的数大于x 
			right = mid - 1;  //往左子区间[left,mid-1]查找 
		}
		else{  //中间的数小于x
			left = mid +1;  //往右子区间[mid+1,right]查找 
		}
	} 
	return -1;  //查找失败,返回-1 
}
int main()
{
	int maxn;
	cin >> maxn;  //输入待查数组大小 
	vector<int> v(maxn);
	for ( int i=0; i<maxn; i++ ){
		cin >> v[i];
	}
	sort(v.begin(),v.end());  //默认从小到大排序
	
	int n;
	cin >> n;  //输入待查数个数
	for ( int i=0; i<n; i++ ){
		int x;
		cin >> x;  //输入待查数
		printf("%d\n", binarySearch(v,0,maxn-1,x));
	} 

	return 0;
}

注意:当二分上界超过int型数据范围的一半*

此时,当欲查询元素在序列较靠后的位置时,语句mid = (left + right) / 2 中的 left + right 就有可能超过int范围而导致溢出,可改为👇

mid = left + (right - left) / 2;

EG.2 寻找有序序列中第一个满足某条件的元素的位置

👉如,求递增序列(可能有重复)中的第一个大于等于(或大于)x的元素的位置

分析(大于等于为例): ①若A[mid]≥x,则第一个大于等于x的元素的位置一定在mid处或mid的左侧,令right = mid(注意这里不是mid-1)。
②若A[mid]<x,则第一个大于等于x的元素的位置一定在mid的右侧,令left = mid +1。

👇搬个函数(大于等于为例)

//A[]为递增序列,x为欲查询的数,函数返回第一个大于等于x的元素的位置
int lower_bound(vector<int> &A, int left, int right, int x){
	int mid;   //left与right的中点
	while( left<right ){  //对[left, right]来说,left==right意味着找到唯一位置 
		mid=(left + right) / 2;  //取中点
		if ( A[mid]>=x ){   //中间的数大于等于x 
			right = mid;  //往左子区间[left, mid]查找 
		} 
		else{   //中间的数小于x 
			left = mid + 1;   //往右子区间[mid+1, right]查找 
		} 
	} 
	return left;  //返回夹出来的位置 
} 

【注意】①二分的初始区间应为[0,n],即初值应当能覆盖到所有可能返回的结果,其中包含欲查询元素比序列中所有元素都要大的情况。
循环条件为left<right而非前面的left≤right。EG.1中判定元素不存在的原则是left>right即[left, right]不再是区间时;EG.2中,不需要判断x本身是否存在,就算不存在,也是返回“假设x存在则x的位置”,则若left==right,[left, right]刚好能夹出唯一的位置,就是需要的结果,故最后返回left或right都一样。

而求第一个大于x的元素的位置,只是把代码中A[mid]>=x换成A[mid]<x。

二分法拓展

给定一个定义在[L, R]上的单调函数f(x),求方程f(x)=0的根

👉如,计算 √2 的近似值,即对f(x)=x2,在x∈[1,2]内f(x)随x的增大而增大(“有序”逻辑→二分法)

const double eps = 1e-5;  //精度为10^-5
double f(double x){  //计算f(x) = x*x 
	return x*x;
} 
double calSqrt(){  //计算根号2 
	double left = 1, right = 2, mid;  //[left, right] = [1, 2]
	while( right-left>eps ){   //精度要求 
		mid = (left+right) / 2;  //取中点
		if ( f(mid)>2 ){  //mid>sqrt(2) 
			right = mid;   //往左子区间[left, mid]继续逼近 
		} 
		else{   //mid<sqrt(2) 
			left = mid;  //往右子区间[mid, right]继续逼近 
		}
	}
	return mid;   //返回mid即为sqrt(2)的近似值 
}

【注】这里的循环条件看精度要求。

对于f(x)=0的根,只要将f(mid)>2改为f(mid)>0即可;若f(x)递减,则只要改为f(mid)<0即可。

装水问题

问题描述: 如下图,装入高为h的水。现给定R和r,求高度h。
watering
分析: 水面高度h与面积比例r的关系:面积比例r随水面h升高而增大 → 在[0, R]内对水面高度h进行二分,计算在高度下面积比例r的值;则其他思路与上题相同。

木棒切割问题

问题描述: 给出N根木棒,长度已知,要求通过切割它们得到至少K段长度相等的木棒(长度必须是整数),那么,这些长度相等的木棒最长能有多长。
分析: 如果长度相同的木棒的长度L越长,则可以得到的木棒段数k越少 → 二分法(最大长度为L):根据对当前长度L来说能得到的木棒段数k与K的大小关系进行二分;题目可以改写为求解最后一个满足条件“k≥K”的长度L,因此不妨转换成求解第一个满足条件“k<K”的长度L,然后减1即可。

凸多边形外接圆问题*

快速幂(二分幂)

👉下述以求 ab % m 为例

#1 若用循环求幂,则时间复杂度是O(b)。

#2 快速幂思想——二分法
关键事实: ①若b是奇数,则有ab = a * ab-1;若b是偶数,则有ab = ab/2 * ab/2
②经过log(b)级别次的转换后,就可以把b变成0,而任何正整数的0次方都是1。

递归写法

分析: ①递归边界:任何正整数的0次方都是1。
②递归式:同上述关键事实①。

👇搬个代码

typedef long long LL;
LL binaryPow(LL a, LL b, LL m){
	if ( b==0 ) return 1;  //递归边界:a的0次方为1
	if ( b%2==1 ){  //b为奇数,转换为b-1 
		return a * binaryPow(a, b-1, m) % m;
	} 
	else{
		LL mul = binaryPow(a, b/2, m);
		return mul * mul % m;
	}
} 

【注】
#1 b为偶数,先单独算出 binaryPow(a, b/2, m),再做返回;否则,直接返回两个相同递归函数之积徒增复杂度。

#2条件 b%2==1 可以用 b&1 代替new

解释: b&1进行位与操作,判断b(二进制)的末位是否为1,当b为奇数时b&1=1。
优势: 写执行速度会快一点。

#3 针对不同题目,需注意:
①若初始时a有可能大于等于m,则需在进入函数钱就让a对m取模。
②若m为1,可以直接在函数外部特判为0。

迭代写法

思路: 对ab来说,把b写成二进制,则b可以表示成若干二次幂之和,那么ab就可以拆成…、a8、a4、a2、a1中若干项的乘积,b可表示为2i
步骤分析: ①设置ans初始为1,用来存放累积的结果。
②判断“当前b”的二进制末位是否为1(即判断b&1是否为1,或通过判断b是否为奇数),若是的话,ans乘上a的值(相当于二进制转换成十进制的过程
③令a平方(根据b=2i),并将b右移一位(相当于将b除以2)。
循环条件: 只要b大于0,就返回②。

👇搬个代码

typedef long long LL;
//求a^b%m的迭代写法 
LL binaryPow(LL a, LL b, LL m){
	LL ans = 1;
	while( b>0 ){
		if ( b&1 ){  //b的二进制末位为1 
			ans = ans * a % m;  //令ans累计上a 
		}
		a = a * a % m;  //令a平方
		b >>= 1;  //将b的二进制右移1位,即b = b >> 1 或 b = b / 2 
	}
	return ans;
}

位与&1 和 移位>>

&1 位与&1 用于 ①判断二进制末尾是否为1 或 ②判断是否是奇数。

if ( b&1==1 )
	cout << "b是奇数。" << endl;

&2 移位>> ①符号 >> 左边是操作数,右边是被移动的位数;②向右移动一位相当于除以2。

b = b >> 1;
b >>= 1;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值