程序实践--二分算法

一、整数二分

1、二分法的本质

 

 2、情况一:二分出绿色的边界

计算mid时不需要加1
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) 
              r = mid;
        else 
             l = mid + 1;
    }
    return l;
}

3、情况二:二分出红色的边界

 此时为了防止死循环,计算mid时需要加1。
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) 
            l = mid;
        else 
            r = mid - 1;
    }
    return l;
}

4、为什么二分红色边界的时候需要+1

 5、例题分析

·查找指定数的范围

所给的升序数组有重复的元素,查找target的下标范围

分析:

·也就是要查找重复数字的左右两个边界

·如果check函数是nums[mid]>=target,那么查找查出来的是左边界,因为满足大于等于target的数便是左边界

·如果check函数是nums[mid]<=target,那么查找查出来的是右边界,因为满足小等于target的数便是右边界  (以上两点可以借助画数轴理解清楚)

·以上两个边界刚好对应两种模板

·该点理解清楚!!!:以找出的左边界为例,他不一定就是target,准确的说,我们找出的是满足>=target的最小值,所以要加上一个if判断是不是target

//也就是说,二分法一定能找出一个数,但是这个数不一定原问题所要的解

//换句话说:二分一定有解,但是原题目不一定有解!!

·对本题而言,先找左边界,如果左边界有,再找有边界,如果左边界没有(也就是数组里没有这个值),那更不会有有边界,就不用再找了(这样子就可以避免在没有该目标值的情况下浪费时间既找左边界又找右边界,相当于优化了代码)

        int target=0;
		scanf("%d",&target);
		int left=0;
		int right=n-1;
		while(left<right)
		{
			int mid=left+right>>1;
			if(arr[mid]>=target)
				right=mid;
			else
				left=mid+1;
		}
		if(arr[right]==target)
		{
			cout<<right<<" ";
			left=0;
			right=n-1;
			while(left<right)
			{
				int mid=left+right+1>>1;
				if(arr[mid]<=target)
					left=mid;
				else
					right=mid-1;
			}
			cout<<right<<endl;
		}
		else
			cout<<"-1 -1"<<endl;
	}

·防线

讲解:https://www.acwing.com/video/97/

防线是一维的,n组守护者分布在防线的某一段上,并且守护者都是等距离排列的

用三个整数s,e,d来描述一组守护者的位置:起始位置是s,方差是d,最后的位置是e

(!!!注意:e不一定能取到 ----s+kd<=e,s+(k+1)d>e    k为整数)

所给的数据能确保整条防线上最多只有一个位置是有奇数个守护者,其他位置都是偶数个守护者

分析:

·对题目的转换成较数学的语言:在数轴上给出了n个整数等差数列

·数轴上最多只有一个点是奇数个守护者

·

#include <algorithm>
#include <iostream>
using namespace std;

typedef long long LL;

const int N = 200007;

struct Seq { //用这个结构体来表示每个数列
	int s;//起点
	int e;//终点
	int d;
} so[N];

int n = 0;
int main() 
{
	LL qianzhuihe(int x);
	int t = 0;
	
	scanf("%d", &t);
	while(t--)
	{
		int left = 0;
		int right = 0;
		scanf("%d", &n); //n个数列
		for (int i = 0; i < n; i++)
		{
			int s = 0;
			int e = 0;
			int d = 0;
			scanf("%d%d%d", &s, &e, &d);
			so[i] = {s, e, d};
			right = max(right, e);//数轴的最右边 这个求法的技巧要会
		}
		while (left < right) 
		{
			int mid = left + right >> 1;
			if (qianzhuihe(mid) % 2)//如果前置和是奇数
				right = mid;
			else
				left = mid + 1;
		}
		int jieguogeshu = 0;//在right上的个数
		jieguogeshu = qianzhuihe(right) - qianzhuihe(right - 1);
		if (jieguogeshu % 2)//如果在right上的数是奇数,说明原题有解
			cout << right << " " << jieguogeshu << endl;
		else//说明整个数轴上没有奇数个数的位置
			cout << "There's no weakness." << endl;
	}

	return 0;
}
//该函数计算<=x的所以点之和
LL qianzhuihe(int x)  //总和可能超过int范围 所以返回long long
{
	LL num = 0;
	for (int i = 0; i < n; i++)//循环一下所有的数列
	{
		if (so[i].s <= x)
			num += (min(so[i].e, x) - so[i].s) / so[i].d + 1;  //计算前缀和的公式
	}
	return num;
}

 ·我在哪

讲解:https://www.acwing.com/video/3710/

找到一个最小的K 值,使得所有长度为K的字符串一定是两两不同的。

 分析:

  ·有二段性:假设所有长度为ans的字符串两两不同,有反证法克制,如果k>=ans,都满足所有长度为k的字符串两两不同;如果k<ans,都不满足该性质。

  ·本题的k一定是有解的,因为字符串的长度n一定满足这个性质

  ·这个left和right是答案k的区间的最小值和最大值,不要以为是字符下标范围!!

#include <algorithm>
#include <iostream>
#include <cstring>

#include <unordered_set>
using namespace std;


int n=0;
string a;

int main()
{
	bool zuixiaok(int x);
	cin>>n;
	cin>>a;
	int left=0;
	int right=0;
	left=1;
	right=n; 
	while(left<right)
	{
		int mid=left+right>>1;
		if(zuixiaok(mid))
			right=mid;
		else
			left=mid+1;
	}
	cout<<left<<endl;
	return 0;
}



//check函数方法一
bool zuixiaok(int x)
{
	//如果长度为x的字符串不重复,返回true
	for(int i=0;i+x-1<n;i++)
	{
		for(int j=i+1;j+x-1<n;j++)
		{
			int num=0;
			for(int h=0;h<x;h++)
			{
				if(a[i+h]==a[j+h])
				{
					num++;                    //一开始写成一旦有相同的字符,就return false 
				}                             //正确的应该是整个长度为x的都一样才return false
			}
			if(num==x)
				return false;
		}
	}
	return true;
}


//check函数方法二
bool zuixiaok(int mid)
{
	unordered_set<string> hash;
	for(int i=0;i+mid-1<a.size();i++)
	{
		auto s=a.substr(i,mid);
		if(hash.count(s))
			return false;
		hash.insert(s);
	}
	return true;
}

二、浮点数二分

1、简介

·说明

由于浮点数没有整除不整除的说法,所有每次mid一定能把区间严格分成左右两个部分

所以区间更新就是严格的l=mid或者r=mid了(红和绿两个方向都是)

当区间长度很小时,我们就可以认为我们找到了答案,此时我们就可以用l或者r当成我们的答案

除了上述的区别外,其思想和整数二分的思想完全相同

·模板一(推荐此种)

结果保留k位小数:eps= 1e-(k+2)     //这是有经验得到的结论
例如保留4位小数则精度为1e-6      

·模板二

迭代100次,结果基本稳定

·易错点

记得数据类型不要习惯写成int了,记得定义为double

2、例题分析

·求方程的根

求方程的根,要求精确到小数点后9位

分析:

·此方程是单调递增的方程

·本题的边界我们自己找:方程的根一定在f(0)到f(10)之间

所以用浮点数二分法在区间[0,10]之间查找

·check函数写>0或者<0都可以

	double left=0;
	double right=100;
	double eps=1e-11;
	while(right-left>eps)
	{
		double mid=(left+right)/2;
		if(mid*mid*mid-5*mid*mid+10*mid-80>0)
			right=mid;
		else
			left=mid;
	}
	printf("%.9lf\n",right);
		if(mid*mid*mid-5*mid*mid+10*mid-80<0)
			left=mid;
		else
			right=mid;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值