二分查找(整数)
查找最后一个<=5的数的下标
//查找最后一个<=5的数的下标
int find(int q)
{
int l=0,r=n+1;//开区间
while (l+1<r)//l+1=r时结束
{
int mid=l+r>>1;
if(a[mid]<=q)l=mid;
else r=mid;
}
return l;
}
查找最后一个>=5的数的下标
//查找最后一个>=5的数的下标
int find(int q)
{
int l=0,r=n+1;//开区间
while (l+1<r)//l+1=r时结束
{
int mid=l+r>>1;
if(a[mid]>=q)r=mid;
else l=mid;
}
return r;
}
1.指针跳跃了logn次
2.可行区的指针最后一定指向答案
3,把指针设置成开区间
二分查找(浮点数)
double find(double y)
{
double l=-100,r=100;
while(r-1>1e-5)
{
double mid=(l+r)/2;
if(mid*mid*mid<=y)l=mid;
else r=mid;
}
return l;
}
二分答案
二分查找与二分答案有何区别?
二分查找:在一个已知的有序数据集上进行二分地查找
二分答案:答案有一个区间,在这个区间中二分,直到找到最优答案
什么是二分答案?
答案属于一个区间,当这个区间很大时,暴力超时。但重要的是——这个区间是对题目中的某个量有单调性的,此时,我们就会二分答案。每一次二分会做一次判断,看是否对应的那个量达到了需要的大小。
判断:根据题意写个check函数,如果满足check,就放弃右半区间(或左半区间),如果不满足,就放弃左半区间(或右半区间)。一直往复,直至到最终的答案。
如何判断一个题是不是用二分答案做的呢?
1、答案在一个区间内(一般情况下,区间会很大,暴力超时)
2、直接搜索不好搜,但是容易判断一个答案可行不可行
3、该区间对题目具有单调性,即:在区间中的值越大或越小,题目中的某个量对应增加或减少。
此外,可能还会有一个典型的特征:求…最大值的最小 、 求…最小值的最大。
1、求…最大值的最小,我们二分答案(即二分最大值)的时候,判断条件满足后,尽量让答案往前来(即:让r=mid),对应模板1;
2、同样,求…最小值的最大时,我们二分答案(即二分最小值)的时候,判断条件满足后,尽量让答案往后走(即:让l=mid),对应模板2;
最大化答案
bool check(int x)
{
}
int find()
{
int l=下界-1,r=上界+1;
while(l+1<r)
{
int mid=l+r>>1;
if(check(mid))l=mid;//最大化
else r=mid;
}
return l;
}
最小化答案
bool check(int x)
{
}
int find()
{
int l=下界-1,r=上界+1;
while(l+1<r)
{
int mid=l+r>>1;
if(check(mid))r=mid;//最小化
else l=mid;
}
return r;
}
整数二分查找习题
P2249 【深基13.例1】查找
查找
第一次出现,最小化,>=q
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=1e8,M=1e3+10;
//常用的数组
int a[N],pre[N];
int n,m;
int find(int q) {
int l = 0, r = n + 1;//开区间
while (l + 1 < r)//l+1=r时结束
{
int mid = l + r >> 1;
if (a[mid] >= q)r = mid;
else l = mid;
}
return a[r]==q?r:-1;
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
while(m--)
{
int b;
cin>>b;
int res=find(b);
cout<<res<<" ";
}
return 0;
}
P1102 A-B 数对
P1102
法一双指针
//双指针
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=2e5+10;
int n,c,a[N];
int main(){
cin>>n>>c;
for(int i=1; i<=n; i++) cin>>a[i];
sort(a+1,a+1+n);
long long ans=0;
for(int k=1,i=1,j=1; k<=n; k++){ //i<=j
while(i<=n && a[i]-a[k]<c) i++;
while(j<=n && a[j]-a[k]<=c) j++;
ans+=j-i;
}
cout<<ans<<endl;
return 0;
}
//k负责枚举每个数,i指针负责指向a[i]-a[k]=c的左端
//j负责指向右端+1
//ans维护这段数的个数,ans+=j-i
法二暴力枚举(有三个TLE)
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=200010,M=1e3+10;
//常用的数组
int n,C;
int q[N];
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>C;
for(int i=1;i<=n;i++)
cin>>q[i];
int cnt=0;
for(int a=1;a<=n;a++)
for(int b=1;b<=n;b++)
if(q[a]-q[b]==C)
cnt++;
cout<<cnt<<endl;
return 0;
}
法三写两个二分
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=2e5+10,M=1e3+10;
//常用的数组
int a[N],pre[N];
int n,C;
int q[N];
int find1(int q[],int len,int x)
{
int l=0,r=n+1;
while (l+1<r)
{
int mid=l+r>>1;
if(q[mid]<x)
{
l=mid;
}
else
{
r=mid;
}
}
if(q[r]==x)return r;
else return -1;
}
int find2(int q[],int len,int x)
{
int l=0,r=n+1;
while (l+1<r)
{
int mid=l+r>>1;
if(q[mid]<=x)
{
l=mid;
}
else
{
r=mid;
}
}
if(q[l]==x)return l;
else return -1;
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>C;
for(int i=1;i<=n;i++)
cin>>q[i];
sort(q+1,q+n+1);
int cnt=0;
for(int B=1;B<=n;B++)
{
int A=q[B]+C;
int res1=find1(q,n,A);//数组名,长度,A
if(res1==-1)
{
continue;
}
else
{
int res2=find2(q,n,A);//如果res!=-1,应该记录在哪里结束
cnt+=res2-res1+1;
}
}
cout<<cnt<<endl;
return 0;
}
浮点数二分查找习题
P1024 [NOIP2001 提高组] 一元三次方程求解
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=2e5+10,M=1e3+10;
//常用的数组
double a,b,c,d;
double fun(double x)
{
return a*x*x*x+b*x*x+c*x+d;
}
double find(double l,double r)
{
while (r-l>0.0001)
{
double mid=(l+r)/2;
if(fun(mid)*fun(r)<0)l=mid;//最大化
else r=mid;
}
return l;
}
int main()
{
scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
for(int i=-100;i<100;i++)
{
double y1=fun(i),y2=fun(i+1);//把区间一段一段的判断,左闭右开
if(!y1)printf("%.2lf ",1.0*i);//特判一下左边界
if(y1*y2<0)printf("%.2lf ",find(i,i+1));//判断一下左边界到右边界之间存不存在根
}
return 0;
}
二分答案习题
P2440 木材加工
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=2e5+10,M=1e3+10;
//常用的数组
int n,k,a[N];
//找一个数给每个a[i]除,看结果加起来是不是大于k
bool check(int x)
{
int y=0;//段数
for(int i=1;i<=n;i++)
y+=a[i]/x;
return y>=k;//x小y大
}
//把段的长度二分,从而找到x的最大值
int find()
{
int l=0,r=1e8+1;
while (l+1<r)
{
int mid=l+r>>1;
if(check(mid))l=mid;//最大化
else r=mid;
}
return l;
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i];
cout<<find()<<endl;
return 0;
}
//x是最大的长度,y是段数
P2678 [NOIP2015 提高组] 跳石头
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
#define int long long //(有超时风险)
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=2e6+10;
//常用的数组
int L,n,M,a[N];
bool check(int x)
{
int last=0,cnt=0;//last当前站的石头的编号,cnt移走的石头的块数
for(int i=1;i<=n+1;i++)
if(a[i]-a[last]<x)cnt++;//贪心,如果距离小于x,就把石头移走
else
last=i;//否则就跳到这块石头上
return cnt<=M;//x小,cnt小
}
int find()
{
int l=0,r=1e9+1;
while (l+1<r)
{
int mid=l+r>>1;
if(check(mid))l=mid;//最大化
else r=mid;
}
return l;
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>L>>n>>M;
for(int i=1;i<=n;i++)
cin>>a[i];
a[n+1]=L;//右边界
cout<<find()<<endl;
return 0;
}
//x是最短跳跃距离的最大值
//y是至多从起点和终点之间移走M块岩石,y<=M
//二分枚举x,移走一定的石头,剩余的石头之间的距离>=x
//站在一块石头上,看下一块,间距<x就移走,>=x就保留
//特判倒数第二块岩石到最后一块岩石的距离,如果小于x,就移走倒数第二块岩石,把终点的岩石的编号改成n+1
P1314 [NOIP2011 提高组] 聪明的质监员
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
//需要改动数组直接改动N,M即可
const int N=200010;
//常用的数组
int n,m,w[N],v[N],l[N],r[N];
LL s,sn[N],sv[N],ans=1e18;
bool check(int W)
{
//前缀和
memset(sn,0, sizeof(sn));
memset(sv,0, sizeof(sv));
for(int i=1;i<=n;i++)
{
if(w[i]>=W)sn[i]=sn[i-1]+1,sv[i]=sv[i-1]+v[i];//sn是个数,sv是价值
else sn[i]=sn[i-1],sv[i]=sv[i-1];//不符合就不用加上
}
//区间和
LL y=0;
for(int i=1;i<=m;i++)
y+=(sn[r[i]]-sn[l[i]-1])*(sv[r[i]]-sv[l[i]-1]);
ans=min(ans,llabs(y-s));//最优解
return y<=s;//W大,y小,看第32行
}
LL find()
{
int l=0,r=1e6+1;
while (l+1<r)
{
int mid=l+r>>1;
if(check(mid))r=mid;//最小化
else l=mid;
}
return ans;
}
int main(){
scanf("%d %d %lld",&n,&m,&s);
for(int i=1;i<=n;i++)
scanf("%d%d",&w[i],&v[i]);
for(int i=1;i<=m;i++)
scanf("%d%d",&l[i],&r[i]);
printf("%lld",find());
return 0;
}
//调整参数w,作为变量x
//y为各个区间的检验值之和
//yi=重量*重量*价值
//多次算区间和,需要先处理下前缀和
//计算出y后,让y和标准值比较,求最优解,没有说y要小于s还是大于s,假设一种情况就可以
//假设y<=s,x在右侧,可行区在右边,用最小化的板子
为什么用memset
这段代码使用 memset 函数来清空数组 sn 和 sv,确保它们在每次调用 check 函数时都是空数组的状态。这是因为这两个数组在 check 函数中用于计算前缀和 sn 和 sv,以及用于保存每个区间的数量和价值之和。在每次调用 check 函数之前,需要确保这两个数组是干净的,即里面的值都是初始化的零,以避免之前的计算结果对当前计算的结果产生影响。
所以,通过 memset 函数将数组初始化为零,可以确保每次计算前都是从一个干净的状态开始,避免了可能的错误结果。
P1083 [NOIP2012 提高组] 借教室
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 1000010;
int n,m;
int r[N],d[N],s[N],t[N];
long long num[N]; //每天教室需求数
bool check(int x){
memset(num,0,sizeof num);
for(int i=1;i<=x;i++){//枚举订单数
num[s[i]]+=d[i];
num[t[i]+1]-=d[i]; //差分
}
for(int i=1;i<=n;i++){//枚举天数
num[i]+=num[i-1]; //前缀和
if(num[i]>r[i])return false;//是否型,当订单超过某个数量,那就失败了,后面的订单也失败了
}
return true; //x小,true
}
int find(){
int l=0,r=m+1;
while(l+1<r){
int mid=l+r>>1;
if(check(mid)) l=mid; //最大化
else r=mid;
}
return l;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&r[i]);
for(int i=1;i<=m;i++)
scanf("%d%d%d",&d[i],&s[i],&t[i]);
if(check(m)){puts("0");return 0;}
printf("-1\n%d",find()+1);//匹配失败的订单
return 0;
}
//差分:开始的位置加上2,段尾之后的位置加上2,不用都加上,这样可以降低时间复杂度
//先差分然后求前缀和,前1,2,3,4,5```n个位置的和
//把时间复杂度从O(mn)降低到O(m+n)
//返回true,就让指针右移,最大化,如果不满足输出l+1,因为l是最后一个满足的,l+1不满足
P1902 刺杀大使
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//开longlong防止爆掉int,int范围2e9,longlong范围9e18
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=2e3+10,M=1e3+10;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int n,m,p[N][N];
bool vis[N][N];
bool dfs(int x,int y,int P)
{
if(x==n)return true;
vis[x][y]= true;//标记
for(int i=0;i<4;i++)
{
int a=x+dx[i],b=y+dy[i];
if(a>=1&&a<=n&&b>=1&&b<=m&&!vis[a][b]&&p[a][b]<=P)
if(dfs(a,b,P))return true;//P大,易true
}
return false;
}
int find()
{
int l=-1,r=1001;
while (l+1<r)
{
int mid=l+r>>1;
memset(vis,0,sizeof vis);
if(dfs(1,1,mid))r=mid;//最小化
else l=mid;
}
return r;
}
signed main(){
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
scanf("%d",&p[i][j]);
printf("%d",find());
return 0;
}
//把伤害值作为x,y是走通1,走不通0,是否型
//能走通返回true,也就是右边.右边怎么样靠近答案?最小化,左移
P1873 [COCI 2011/2012 #5] EKO / 砍树
P1873
法一常规做法,会TLE,能过4-%
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=2e6+10,M=1e3+10;
int n,m;
int q[N];
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
int hightest=0;
for(int i=1;i<=n;i++)
{
cin>>q[i];
hightest=max(q[i],hightest);
}
int res=0;
int sum=0;
for(int i=1;i<=hightest;i++)
{
sum=0;
for(int j=1;j<=n;j++)
{
sum+=max(0,q[j]-i);
if(sum>=m)
{
res=max(res,i);
continue;
}
}
}
cout<<res<<endl;
return 0;
}
法二最小值最大,答案在蓝色区域,绿色是最优解(最大化答案)
//比赛中可以直接用万能头文件
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
#include<queue>
//简写,如果要改动PII,直接该这个就行了,vector常用
#define PII pair<int,int>
//#define x first
//#define y second
using namespace std;
//需要改动数组直接改动N,M即可
const int N=2e6+10,M=1e3+10;
int n,m;
int q[N];
bool check(int x)
{
int sum=0;
for(int i=1;i<=n;i++)
{
sum+=max(0,q[i]-x);
if(sum>=m)return true;
}
return false;
}
signed main()
{
//关掉流同步,cin变快,但是不能用scanf,可以用printf;
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
int hightest=0;
for(int i=1;i<=n;i++)
{
cin>>q[i];
hightest=max(q[i],hightest);
}
int res=0;//记录daan
int sum=0;//记录当前得到的树的总高度
int l=0,r=hightest;
while (l+1<r)
{
int mid=l+r>>1;
if(check(mid))l=mid;
else r=mid;
}
if(check(r))//题目有可能卡你,答案在分界线的右边第一个,需要check一下
cout<<r<<endl;
else
cout<<l<<endl;//大部分情况不会卡你
return 0;
}
总结
画出图像,确定可行区,在左边就是最大化答案l=mid
在右边就是最小化答案r=mid