【专题集训】二分搜索与二分答案
(2001·提高·T1) 一元三次方程求解
题目
求解 \(ax^3+bx^2+cx+d=0\),保证有三个实数根且在 \(\pm 100\) 之间。
思路
提示:记方程 \(f(x) = 0\),若存在 \(2\) 个数 \(x_1\) 和 \(x_2\),且 \(x_1 < x_2\),\(f(x_1) \times f(x_2) < 0\),则在 \((x_1, x_2)\) 之间一定有一个根。
根据题面的提示,我们可以在 \([-100,100]\) 之间枚举一个整数 \(i\),然后判断 \((i,i+1)\) 之间是否有一个根,如果有则进入二分,搜索根的位置。注意这个根的范围是开区间,所以要特判一下,判断 \(i\) 是否本身就是方程的根。把所有的根丢进一个 multiset 里面然后输出就好了。
代码
(注:freopen 是为了方便直接自动测样例 懒得每次都粘贴 neuvillette 只是一个随机的字符组合,并不是一个角色的名字)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=-1;
constexpr double eps=0.0005;
double a,b,c,d;
multiset<double> res;
inline double f(double x)
{
return a*x*x*x+b*x*x+c*x+d;
}
int main()
{
// freopen("neuvillette.in","r",stdin);
// freopen("neuvillette.out","w",stdout);
scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
for(int i=-100;i<=100;i++)
{
if(f(i)==0) res.insert(i);
double x1=i,x2=i+1;
if(f(x1)*f(x2)<0)
{
while(x2-x1>0.001)
{
double mid=(x1+x2)/2.0;
if(f(x1)*f(mid)<=0)
x2=mid-eps;
else x1=mid+eps;
}
res.insert(x1);
}
}
for(double i:res) printf("%.2f ",i);
return 0;
}
查找最接近的元素
题目
在一个长度为 \(n(n \le 10^5)\) 非降序列 \(a(a_i \le 10^9)\) 中,\(m(m \le 10^5)\) 组询问,每一次询问查找与给定值 \(x(x \le 10^9)\) 最接近的元素。若有多个值满足条件,输出最小的一个。
思路
首先将原数列的最左侧和最右侧(即 \(a_0\) 和 \(a_{n+1}\))设置一个哨兵值,最左边设置成 \(-M\),最右边设置成 \(+M\)。
接着每次查询的时候只需要用 lower_bound 二分查找一下 \(x\),设查找的结果的下标为 \(p\)。记录指针的数 \(m\),左边和右边的数 \(l,r\)。
- 若 \(p\) 正好在数列中间,则 \(l=a_{p-1},m=a_p,r=a_{p+1}\)(对应数列中的 \(a_1,a_2,a_3\))
- 若 \(p\) 在数列的左哨兵处,此时 \(x<a_0\),\(l\) 不存在(因为左哨兵左侧没有数)\(m=a_p,r=a_{p+1}\)(对应数列中的 \(-M\) 和 \(a_1\))
- 若 \(p\) 在数列的右哨兵处,此时 \(x>a_n\),\(r\) 不存在(因为右哨兵右侧没有数)\(l=a_{p-1},m=a_p\)(对应数列中的 \(a_3\) 和 \(M\))
最后 \(a_l,a_m,a_r\) 到 \(x\) 的距离(\(x,y\) 之间的距离为 \(\left|x-y\right|\)),取最小的输出即可。
注意 \(M\) 的取值,之前取到 \(M=10^9+1218\) 虽然交到 OJ 上可以过,但是可以 hack 掉:当 \(a=\left\{-10^9\right\},x=10^9\) 时查询出最近的数是 \(10^9+1218\)。所以 \(M\) 应满足 \(M > 10^9-(-10^9) = 2 \times 10^9\). 这里用的是 \(M=2 \times 10^9 + 1218\).
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=100000+7;
constexpr int INF=2e9+1218;
int n,Q,a[N];
inline int dis(int x,int y)
{
return abs(x-y);
}
inline int query(int x)
{
auto p=lower_bound(a+1,a+n+2,x);
if(*p==x) return *p; //刚好就是这个数
int l,m,r; //left mid right
if(*p==-INF) l=-INF-1,m=*p,r=*(p+1); //找到了左边界外
else if(*p==INF) l=*(p-1),m=*p,r=INF+1; //找到了右边界外
else l=*(p-1),m=*p,r=*(p+1); //找到了数列中的数
if(dis(l,x)<=dis(m,x)&&dis(l,x)<=dis(r,x)) return l;
if(dis(m,x)<dis(l,x)&&dis(m,x)<dis(r,x)) return m;
else return r;
}
int main()
{
freopen("neuvillette.in","r",stdin);
freopen("neuvillette.out","w",stdout);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
a[0]=-INF,a[n+1]=INF;
cin>>Q;
while(Q--)
{
int x;
cin>>x;
cout<<query(x)<<endl;
}
return 0;
}
分身数对
题目
给出 \(n\) 个不同的正整数 \(a_1 \sim a_n\),它们的值在 \([1,10^6]\) 之间。再给定一个整数 \(x\),编程计算这样的数对个数 \((a[i],a[j]),1 \le i<j \le n\) 并且 \(a_i+a_j=x\)。
思路 1
若我们要合成 \(x\),那么对于每一个 \(a_i\) 可以算出还需要补上的数 \(E=x-a_i\),然后二分查找 \(E\) 看看存不存在。
时间复杂度 \(O(n \log n)\)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=100000+7;
int n,x,a[N];
int main()
{
freopen("neuvillette.in","r",stdin);
freopen("neuvillette.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
sort(a+1,a+n+1);
scanf("%d",&x);
int cnt=0;
for(int i=1;i<=n;i++)
{
int expect=x-a[i];
if(expect==0) continue;
if(*lower_bound(a+i+1,a+n+1,expect)==expect)
{
cnt++;
// cout<<a[i]<<' '<<expect<<endl;
}
}
printf("%d",cnt);
return 0;
}
思路 2
(思路来自机房数竞大佬)
记 \(f(y)\) 为:和 \(y\) 凑成 \(x\) 的数的个数(即满足 \(y+t=x\) 的不同的 \(t\) 的个数),将整个数列扫一遍,如果有数字可以和 \(a_i\) 凑成 \(x\) 那就直接凑,如果没有数字可以凑那就打个标记,等下一个可以凑的数字来就凑上去。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e5+7;
int n,x,a[N],f[100005]; //a表示数列,f[k]:有f[k]个数字可以和k凑成x
int ans;
int main()
{
// freopen("neuvillette.in","r",stdin);
// freopen("neuvillette.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
scanf("%d",&x);
for(int i=1;i<=n;i++)
{
if(x-a[i]<0) continue;
if(f[a[i]]>0) //如果有数字可以和a[i]凑
{
f[a[i]]--; //两个数凑成一对
ans++;
}
else f[x-a[i]]++;
//如果没有数字可以凑,那就打个标记,等下一个可以凑的数字来就凑上去
}
printf("%d",ans);
}
分派
题目
你有 \(N\) 个不同口味、不同大小的派(每个派是高为 \(1\)、半径不等的圆柱体),以及 \(F\) 个朋友来参加你的生日派对。每个人(包括你自己)都要拿到一块大小相同的派,且每块派必须来自同一个完整的派,不能拼接。目标是让每个人拿到的派尽可能大,同时避免浪费。求每个人能拿到的最大派的大小。
思路
实数域二分。二分每个人可分到的派的体积(记为 \(V\)),然后对于 check 函数只需检查能分到的派的个数是否够分给每一个人一份,即
最后输出答案即可。\(\cos \pi =-1\) 所以 \(\pi\) 直接写成 acos(-1) 即可。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e4+7;
constexpr double pi=acos(-1);
double a[N];
int f;
int n;
inline bool check(double mid)
{
int cnt=0;
for(int i=1;i<=n;i++)
cnt+=a[i]/mid;
return cnt>=f;
}
int main()
{
freopen("neuvillette.in","r",stdin);
freopen("neuvillette.out","w",stdout);
scanf("%d%d",&n,&f); f++;
int tmp=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&tmp);
a[i]=tmp*tmp*pi;
}
double l=1,r=1e17;
while(r-l>=1e-5)
{
double mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
printf("%.3f",l);
return 0;
}
(2015·提高·D2T1) 跳石头
题目
一年一度的“跳石头”比赛又要开始了!
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 \(N\) 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 \(M\) 块岩石(不能移走起点和终点的岩石)。\(0 \le M \le N \le 50000,1 \le L \le 10^9\)。
思路
二分跳跃距离,check 函数检查:当最小位移为 \(s\) 时,移走的岩石个数是否 \(\le m\)?
模拟一下跳的过程,设选手所在位置为 \(x\),如果跳到下一个岩石需要的位移 \(\Delta x \ge s\) 就直接跳过去,否则需要将这个岩石移走。(因为位移太小了,难度太低)最后判断一下移走的岩石个数是否满足要求即可。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=50007;
int s,n,m,d[N];
inline bool check(int mid) //当最小位移为mid时,移走的岩石个数是否<=m?
{
int x=0,cnt=0; //x为当前位置,cnt为移走个数
for(int i=1;i<=n;i++)
{
int deltax=d[i]-x; //需要的位移(Δx)
if(deltax>=mid) x=d[i]; //可以直接跳
else cnt++; //必须得移走岩石(位移太小,要增大难度)
}
return cnt<=m;
}
int main()
{
// freopen("stone.in","r",stdin);
// freopen("stone.out","w",stdout);
scanf("%d%d%d",&s,&n,&m);
if(n==0&&m==0) return 0&printf("%d",s);
for(int i=1;i<=n;i++) scanf("%d",&d[i]);
int l=1,r=1e9+10;
while(l+1<r)
{
int mid=(l+r)>>1;
if(check(mid)) l=mid;
else r=mid;
}
printf("%d",l);
return 0;
}
砍伐树木
题目
你需要砍倒 \(M(M \le 2 \times 10^9)\) 米长的木材,你的伐木机工作过程如下:设置一个高度参数 \(H(H \le 10^9)\)(米),伐木机升起一个巨大的锯片到高度 \(H\),并锯掉所有的树比 \(H\) 高的部分,你就得到树木被锯下的部分。请找到伐木机锯片的最大的整数高度 \(H\),使得你能得到的木材至少为 \(M\) 米。
思路
简单二分题,二分高度 \(H\) 即可。对于 check 函数,只需模拟一下砍树的过程,看看得到的总树木够不够即可。时间复杂度 \(O(\log M)\).
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1000000+7;
int n;
ll h[N],m;
inline bool check(int ans)
{
ll s=0;
for(int i=1;i<=n;i++) if(h[i]-ans>0) s+=h[i]-ans;
if(s>=m) return 1;
else return 0;
}
int main()
{
// freopen("neuvillette.in","r",stdin);
// freopen("neuvillette.out","w",stdout);
scanf("%d%lld",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&h[i]);
int l=0,r=2000000007;
while(l+1<r)
{
int mid=l+r>>1;
if(check(mid)) l=mid;
else r=mid;
}
cout<<l;
return 0;
}
(2021·入门·T4) 小熊的果篮
依稀记得当年考 CSP 2023 那一周的周四,下午没上第八九节课,晚上没上晚自习,然后讲了一大堆 J 组压轴题,刚写完这一题就去吃了个饭晚上继续开始写
题目
小熊的水果店里摆放着一排 \(n\) 个水果。每个水果只可能是苹果或桔子(用 \(1\) 表示苹果,\(0\) 表示橘子,下文用“0/1 号水果”代替),从左到右依次用正整数 \(1, 2, \ldots, n\) 编号。连续排在一起的同一种水果称为一个“块”。小熊要把这一排水果挑到若干个果篮里,具体方法是:每次都把每一个“块”中最左边的水果同时挑出,组成一个果篮。重复这一操作,直至水果用完。注意,每次挑完一个果篮后,“块”可能会发生变化。比如两个苹果“块”之间的唯一桔子被挑走后,两个苹果“块”就变成了一个“块”。请帮小熊计算每个果篮里包含的水果。
思路
可以考虑开两个 set,一个丢 0 号水果,一个丢 1 号水果,在读入时处理。接着在两种水果都还有的情况下循环以下操作:找出第一个水果拿出来并记录编号和品种,接着再交替着在两个 set 中找下一个水果并拿出。最后剩下的水果就全是一个品种了,所以只会连成一个块,就只能一次取一个地把剩下的水果取完。
“找下一个水果”的操作可以使用 STL 的 upper_bound(x) 函数,其作用为找出第一个大于 \(x\) 的数,即找出最小的 \(i\) 使得 \(a_i > x\)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=2e5+7;
int n;
set<int> s0,s1; //0和1分别表示两种水果
int main()
{
// freopen("fruit.in","r",stdin);
// freopen("fruit.out","w",stdout);
scanf("%d",&n);
int u;
for(int i=1;i<=n;i++)
{
scanf("%d",&u);
if(u==1) s1.insert(i);
else s0.insert(i);
}
while(!s0.empty()&&!s1.empty()) //篮子里都有水果那就一直处理
{
int lst; //上一个拿的水果的编号
bool flag; //上一个拿的水果的品种
set<int>::iterator s00=s0.upper_bound(0);
set<int>::iterator s11=s1.upper_bound(0); //找出第一个水果
if(*s11<*s00) //把第一个水果拿出来
{
lst=*s11; //记录第一个的编号
s1.erase(s11); //拿走
flag=1; //标记
}
else //同上
{
lst=*s00;
s0.erase(s00);
flag=0;
}
printf("%d ",lst);
while(1)
{
if(flag==1) //上一个拿的水果是1,那就从0号水果里面找
{
set<int>::iterator it=s0.upper_bound(lst); //找下一个水果
if(it==s0.end()) break; //找到末尾了
printf("%d ",*it);
flag=0,lst=*it; //更新上一个拿的水果的标记
s0.erase(it); //拿出来
}
else //同上
{
set<int>::iterator it=s1.upper_bound(lst);
if(it==s1.end()) break;
printf("%d ",*it);
flag=1,lst=*it;
s1.erase(it);
}
}
puts(""); //处理完了一个果篮
}
vector<int> left; //剩下的水果一个就放一个果篮
while(!s0.empty()) //把剩下的水果全部倒到left里
{
left.push_back(*s0.begin());
s0.erase(s0.begin());
}
while(!s1.empty()) //把剩下的水果全部倒到left里
{
left.push_back(*s1.begin());
s1.erase(s1.begin());
}
sort(left.begin(),left.end());
for(int i:left) printf("%d\n",i);
return 0;
}
求逆序对
题目
给定一个序列 \(a_1,a_2, \cdots ,a_n\),如果存在 \(i<j\) 并且 \(a_i>a_j\),那么我们称之为逆序对,求逆序对的数目。\(a_i,n \le 10^5\).
思路 1(树状数组)
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=5e5+7;
int n;
int a[N],b[N];
ll c[N];
inline void modify(int x)
{
for(int i=x;i<=n;i+=(i&-i)) c[i]++;
}
inline ll query(int x)
{
ll res=0;
for(int i=x;i;i-=(i&-i)) res+=c[i];
return res;
}
int main()
{
freopen("neuvillette.in","r",stdin);
freopen("neuvillette.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+n+1); //离散化
int m=unique(b+1,b+n+1)-b-1; //离散后的数组大小
//即数据不同的数的个数
ll s=0;
for(int i=1;i<=n;i++)
{
int rnk=lower_bound(b+1,b+m+1,a[i])-b; //当前数字的排名
modify(rnk);
s+=query(n)-query(rnk);
}
printf("%lld",s);
return 0;
}
(2013·提高·D1T1) 转圈游戏
题目
\(n\) 个小伙伴(编号从 \(0\) 到 \(n-1\))围坐一圈玩游戏。按照顺时针方向给 \(n\) 个位置编号,从 \(0\) 到 \(n-1\)。最初,第 \(0\) 号小伙伴在第 \(0\) 号位置,第 \(1\) 号小伙伴在第 \(1\) 号位置,……,依此类推。游戏规则如下:每一轮第 \(0\) 号位置上的小伙伴顺时针走到第 \(m\) 号位置,第 \(1\) 号位置小伙伴走到第 \(m+1\) 号位置,……,依此类推,第 \(n - m\) 号位置上的小伙伴走到第 \(0\) 号位置,第 \(n - m+1\) 号位置上的小伙伴走到第 \(1\) 号位置,……,第 \(n-1\) 号位置上的小伙伴顺时针走到第 \(m-1\) 号位置。
现在,一共进行了 \({10}^k\) 轮,请问 \(x\) 号小伙伴最后走到了第几号位置。
思路
感觉这个题没什么二分的含量,首先可以发现一轮的位是 \(m\),进行了 \(10^k\) 轮,所以总的位移量就是 \(m \times 10^k\),原来位置在 \(x\),一个圈的长度为 \(n\),所以最后的位置在 \((x+m \times 10^k) \bmod n\) 处。用快速幂处理一下 \(10^k \bmod n\),时间复杂度 \(O(\log_2 n)\)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=-1;
inline ll ksm(ll a,ll b,ll p)
{
ll s=1;
while(b)
{
if(b&1) s=s*a%p;
a=a*a%p;
b>>=1;
}
return s;
}
int main()
{
// freopen("circle.in","r",stdin);
// freopen("circle.out","w",stdout);
ll n,m,k,x;
scanf("%lld%lld%lld%lld",&n,&m,&k,&x);
printf("%lld",(x+m*ksm(10,k,n))%n);
return 0;
}
快速幂
题目
求 \(a^b \bmod p\) 的值。
思路
板子。根据幼儿园学的幂的运算法则,利用 \(a^{2b}=a^b+a^b\) 来分治即可。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=-1;
inline ll ksm(ll a,ll b,ll p)
{
ll s=1;
while(b)
{
if(b&1) s=s*a%p;
a=a*a%p;
b>>=1;
}
return s;
}
int main()
{
// freopen("neuvillette.in","r",stdin);
// freopen("neuvillette.out","w",stdout);
ll a,b,p;
cin>>a>>b>>p;
printf("%lld^%lld mod %lld=%lld",a,b,p,ksm(a,b,p));
return 0;
}
(2011·普及·T3) 瑞士轮
题目
\(2 \times N\) 名编号为 \(1\sim 2N\) 的选手共进行R 轮比赛。每轮比赛开始前,以及所有比赛结束后,都会按照总分从高到低对选手进行一次排名。选手的总分为第一轮开始前的初始分数加上已参加过的所有比赛的得分和。总分相同的,约定编号较小的选手排名靠前。
每轮比赛的对阵安排与该轮比赛开始前的排名有关:第 \(1\) 名和第 \(2\) 名、第 \(3\) 名和第 \(4\) 名、……、第 \(2K - 1\) 名和第 \(2K\) 名、…… 、第 \(2N-1\) 名和第 \(2N\) 名,各进行一场比赛。每场比赛胜者得 $1 $ 分,负者得 \(0\) 分。也就是说除了首轮以外,其它轮比赛的安排均不能事先确定,而是要取决于选手在之前比赛中的表现。
现给定每个选手的初始分数及其实力值,试计算在 \(R\) 轮比赛过后,排名第 \(Q\) 的选手编号是多少。我们假设选手的实力值两两不同,且每场比赛中实力值较高的总能获胜。\(1 \le N \le 10^5,1 \le R \le 50,1 \le Q \le 2N,0 \le s_1, s_2, …, s_{2N}\le 10^8,1 \le w_1, w_2 , \cdots, w_{2N} \le 10^8\)。
思路
注:为了方便表述,下文中使用 \(n\) 代替 \(2N\),使用 \(m\) 代替 \(M\)。
在每轮结束之后都全部排序一遍显然行不通,因为每次排序的时间复杂度是 \(O(n \log n)\),一共就是 \(O(R \cdot n \log n)\) 会超时。于是考虑手写归并排序,把 winner 和 loser 数组合并一下。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=2e5+7;
int q[N],s[N],w[N],winners[N],losers[N];
int n,rnd,Q;
bool cmp(int a,int b) //a,b均为编号,如果a>b则返回true
{
//如果分数不相同
if(s[a]!=s[b]) return s[a]>s[b];
return a<b;
}
void merge(int x,int y) //合并操作
{
int i=1,j=1;
int p=1; //新数组指针
while(i<=x&&j<=y)
{
// cerr<<i<<' '<<j<<endl;
if(cmp(losers[i],winners[j])) //如果失败的man比成功的man还强
q[p++]=losers[i++];
else q[p++]=winners[j++];
}
while(i<=x) q[p++]=losers[i++];
while(j<=y) q[p++]=winners[j++];
// for(int F=1;F<=n;F++) cout<<q[F]<<" \n"[F==n];
}
int main()
{
// freopen("neuvillette.in","r",stdin);
// freopen("neuvillette.out","w",stdout);
scanf("%d%d%d",&n,&rnd,&Q);
n=n*2;
for(int i=1;i<=n;i++) scanf("%d",&s[i]);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1;i<=n;i++) q[i]=i;
sort(q+1,q+n+1,cmp); //把第一次的编号排好
// for(int i=1;i<=n;i++) cout<<q[i]<<' ';
while(rnd--)
{
//标记获胜人(winner)的编号以及失败人(loser)的编号
int winner=0,loser=0;
for(int i=1;i<=n;i+=2) //处理每个输赢情况
{
int a=q[i],b=q[i+1]; //找两个人PK
if(w[a]>w[b]) //如果a实力比b强
{
s[a]++; //分数+1
winners[++winner]=a; //放置赢家和输家
losers[++loser]=b;
}
else
{
s[b]++; //分数+1
winners[++winner]=b; //放置赢家和输家
losers[++loser]=a;
}
}
merge(loser,winner); //合并
}
printf("%d",q[Q]);
return 0;
}

浙公网安备 33010602011771号