整数二分
整数二分的模板有两个,牢记模板做题即可
模板1:
int mid=l+r+1>>1;
if(xxx) l=mid;
else r=mid-1;
模板2:
int mid=l+r>>1;
if(xxx) r=mid;
else l=mid+1;
例题:
Acwing_789. 数的范围
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回 -1 -1。
输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1。
数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
Ac代码:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int q[N];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%d",&q[i]);
while(m--)
{
int x;
scanf("%d",&x);
int l=0,r=n-1;
while(l<r)
{
int mid=l+r>>1;
if(q[mid]>=x) r=mid;
else l=mid+1;
}
if(q[l]==x) cout<<l<<" ";
else cout<<"-1 ";
l=0,r=n-1;
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<=x) l=mid;
else r=mid-1;
}
if(q[l]==x) cout<<l<<endl;
else cout<<"-1 "<<endl;
}
return 0;
}
例题:
Acwing_1221.完全平方和
四平方和定理,又称为拉格朗日定理:
每个正整数都可以表示为至多 4 个正整数的平方和。
如果把 0 包括进去,就正好可以表示为 4 个数的平方和。
比如:
5=02+02+12+22
7=12+12+12+22
对于一个给定的正整数,可能存在多种平方和的表示法。
要求你对 4 个数排序:
0≤a≤b≤c≤d
并对所有的可能表示法按 a,b,c,d 为联合主键升序排列,最后输出第一个表示法。
输入格式
输入一个正整数 N。
输出格式
输出4个非负整数,按从小到大排序,中间用空格分开。
数据范围
0<N<5∗106
输入样例:
5
输出样例:
0 0 1 2
题目分析:首先要根据时间复杂度进行分析,题目给定的最大的数字是N=5*106;由于是枚举平方和,所以如果直接使用暴力做法的话每一层也只是需要枚举到sqrt(N)即可,下边先给出暴力写法:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int n;
int main()
{
scanf("%d",&n);
for(int a=0;a*a<=n;a++)
for(int b=a;b*b+a*a<=n;b++)
for(int c=b;c*c+b*b+a*b<=n;c++)
{
int t=sqrt(n-a*a+b*b+c*c);
if(t*t=n-a*a+b*b+c*c)
{
cout<<a<<b<<c<<d<<endl;
return 0;
}
}
return 0;
}
理论暴力写法的时间复杂度大约是5e6*2e3=1e10是会TLE的;
所以我们最多只能枚举两个数字,由此引出一个重要的思想:就是可以拿空间换时间的做法:
我们可以先枚举其中两个数字,并将这两个数字的平方和保存下来,这个步骤的时间复杂度是N^2;
然后再枚举另外两个数字,同样得到平方和,然后用一个变量保存n-这个平方和的结果,在前边保存的另外两个数字的平方和中查找是否存在这样的平方和与这个变量相等,如果存在的话就找到了。这个过程可以用二分或哈希来做,时间复杂分别是O(N2logN)和O(N2);均不会超时。
"并对所有的可能表示法按 a,b,c,d 为联合主键升序排列,最后输出第一个表示法"意思就是取所有结果的字典序最小的那个。
在二分写法中需要重载小于运算符(排序的时候会按照升序排列,且为字典序)
而在哈希表写法只记录第一个就可以了,因为c,d都是从小到大枚举的,所以第一个一定是字典序最小的那个。
二分优化写法:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
int n;
const int N=5*10e6+10;
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;
}
}s[N];
int main(){
int m=0;
scanf("%d",&n);
for(int c=0;c*c<=n;c++)
for(int d=c;c*c+d*d<=n;d++)
s[m++]={c*c+d*d,c,d};
sort(s,s+m);
//二分法枚举:
for(int a=0;a*a<=n;a++)
for(int b=a;a*a+b*b<=n;b++)
{
int l=0,r=m;//m-1????
while(l<r)
{
int mid=l+r>>1;
int t=a*a+b*b;
if(t+s[mid].s>=n) r=mid;
else l=mid+1;
}
if(s[l].s+a*a+b*b==n)
{
cout<<a<<" "<<b<<" "<<s[l].c<<" "<<s[l].d<<endl;
return 0;
}
}
return 0;
}
哈希表优化写法:
//哈希表写法;
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_map>
using namespace std;
typedef pair<int,int> PII;
unordered_map<int,PII> h;
int n,m;
int main()
{
scanf("%d",&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(!h.count(t)) h[t]={c,d};
}
for(int a=0;a*a<=n;a++)
for(int b=a;a*a+b*b<=n;b++)
{
int t=n-a*a-b*b;
if(h.count(t))
{
cout<<a<<" "<<b<<" "<<h[t].first<<" "<<h[t].second<<endl;
return 0;
}
}
return 0;
}
浮点数二分
浮点数二分相对于整数二分较为简单。
需要注意的是浮点数二分在结束条件判定的时候,r-l要比题目要求的至少多一位,比如题目最后结果保留6位小数,那么r-l至少要<10e-7;
模板题:
Acwing_790. 数的三次方根
给定一个浮点数 n,求它的三次方根。
输入格式
共一行,包含一个浮点数 n。
输出格式
共一行,包含一个浮点数,表示问题的解。
注意,结果保留 6 位小数。
数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000
Ac代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int main()
{
double n;
scanf("%lf",&n);
double l=-100,r=100;
while(r-l>1e-7)//6此方不行,所以至少要比给定的位数多一位。
{
double mid=(l+r)/2;
double t=pow(mid,3);
if(t>=n) r=mid;
else l=mid;
}
printf("%.6f",l);
return 0;
}
Acwing_102最佳牛围栏
农夫约翰的农场由 N 块田地组成,每块地里都有一定数量的牛,其数量不会少于 1 头,也不会超过 2000 头。
约翰希望用围栏将一部分连续的田地围起来,并使得围起来的区域内每块地包含的牛的数量的平均值达到最大。
围起区域内至少需要包含 F 块地,其中 F 会在输入中给出。
在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少。
输入格式
第一行输入整数 N 和 F,数据间用空格隔开。
接下来 N 行,每行输入一个整数,第 i+1 行输入的整数代表第 i 片区域内包含的牛的数目。
输出格式
输出一个整数,表示平均值的最大值乘以 1000 再 向下取整 之后得到的结果。
数据范围
1≤N≤100000
1≤F≤N
输入样例:
10 6
6
4
2
10
3
8
5
9
4
1
输出样例:
6500
思路分析:
本题的核心题意需要注意:连续的牛围栏的数量是要>=F的。所以这里可以使用双指针算法;
然后由于是要求这些连续的牛围栏的牛的数量的平均值最大值,所以可以分解到每个牛栏的牛的数量(每个牛栏的牛的数量实际上可以认为是所有情况下的连续围栏内牛的总数量除以牛栏数的平均值),那么可以用二分法来从1-2000(每个牛栏的牛的数量的范围)寻找答案;如果说当前二分到的单个牛栏的牛的数量(平均值,实际上二分就是在枚举平均值)是比我实际上算得的平均值要小的,让l=mid(枚举右半区间),否则找左半区间r=mid;直到l-r<=1e-5为止,因为最后要给答案*1000,所以这个浮点数的范围至少要在1e-3以内。
AC代码:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int q[N],n,f;
double s[N];
bool check(double avg)
{
//求前缀和
for(int i=1;i<=n;i++) s[i]=s[i-1]+q[i]-avg;
//mins保存的实际上是avg*一个区间长度的和,但是因为是上边的处理,
//所以区间长度不管是多少,这个和都是0。
double mins=0;
for(int i=0,j=f;j<=n;i++,j++)
{
//这里s[j]要减去尽可能小的前边的和来使得这个数字最大。
mins=min(mins,s[i]);
if(s[j]-mins>=0) return true;//如果找到了比我给定的均值大的数字就返回true;fe
}
return false;
}
int main()
{
scanf("%d%d",&n,&f);
int l=0,r=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&q[i]);
r=max(r,q[i]);
}
//二分寻找答案:
while(r-l>1e-5)
{
double mid=l+r>>1;
//如果算得了比mid更大的平均值,
if(check(mid)) l=mid;
else r=mid;
}
cout<<(int)r*1000<<endl;
return 0;
}