蓝桥杯之二分
- 二分板子?第一次和最后一次出现的位置
- 机器人跳跃问题
- 四平方和
- 分巧克力?典型二分找大的(从右往左找)
- 二分upper_bound(a+1,a+n+1,x)-a?递增三元组
- 前缀和取余?K倍区间
- 二维前缀和?激光炸弹
二分板子?第一次和最后一次出现的位置
目的总是取到一个最合适的值。
首先,找到取值的范围,在该范围内进行二分。
判断取值是否满足题意条件
……
整数二分模板一共有两个,分别适用于不同情况。
算法思路:假设目标值在闭区间
[
l
,
r
]
[l, r]
[l,r]中, 每次将区间长度缩小一半,当
l
=
r
l = r
l=r时,我们就找到了目标值。
模板一:(再也不用为边界的处理而头痛了)
当我们将区间
[
l
,
r
]
[l, r]
[l,r]划分成
[
l
,
m
i
d
]
[l, mid]
[l,mid]和
[
m
i
d
+
1
,
r
]
[mid + 1, r]
[mid+1,r]时,其更新操作是
r
=
m
i
d
r = mid
r=mid或者
l
=
m
i
d
+
1
l = mid + 1
l=mid+1;,计算
m
i
d
mid
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;
}
模板二:
当我们将区间
[
l
,
r
]
[l, r]
[l,r]划分成
[
l
,
m
i
d
−
1
]
[l, mid - 1]
[l,mid−1]和
[
m
i
d
,
r
]
[mid, r]
[mid,r]时,其更新操作是
r
=
m
i
d
−
1
r = mid - 1
r=mid−1或者
l
=
m
i
d
l = mid
l=mid;,此时为了防止死循环,计算
m
i
d
mid
mid时需要加1。
边界的处理:
死循环的解释是:例如二分区间缩小到 l=3,r=4 时,l=mid ,mid的算法一定是向上取整,否则一直取的是3
或者解释为,要保持一致的区间分割方式
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;
}
这两种情况就是一个是从小到大找,一个是从大到小找。因为二分的前提是要有序(一般都是,搜索的范围中值是升序的,寻找满足条件的最小值,那么当adequate(mid)
,对应的就是 模板一 r=mid
,
相反,如果要找满足条件的最大值,那么当adequate(mid)
,对应的就是 模板二 l=mid
浮点数二分
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;//不能是mid减1噢,这可是浮点数,1是精度的巨大倍
}
return l;
}
#include <iostream>
#include <stdlib.h>
#include <algorithm>
using namespace std;
int n,q;
const int N=1e5+5;
int a[N];
void search(int t){
int l=0;int r=n-1;
while(l<r){
int mid=l+(r-l)/2;
if(a[mid]<t){
l=mid+1;
}
else r=mid;
};
if(a[l]!=t){
cout<<"-1 -1";
return ;
}
cout<<l<<" ";
r=n-1;
while(l<r){
int mid=l+(r-l+1)/2;
if(a[mid]<=t){
l=mid;
}
else r=mid-1;
}
cout<<l;
}
int main(){
cin>>n>>q;
for(int i=0;i<n;i++){
cin>>a[i];
}
sort(a,a+n);//题外话:第3个参数 greater<int>()
int t;
while(q--){
cin>>t;
search(t);
if(q)cout<<endl;
}
return 0;
}
机器人跳跃问题
根据题意可知,每过一个建筑,能量
E
E
E 会转变成
2
E
−
H
[
i
]
2E-H[i]
2E−H[i]
随初始能量
E
E
E 单调增长,可以对初始能量
E
E
E 进行二分
if(x>=1e5)return true;
在判断二分值是否合法时,要防止爆int变成负值,那么将永远不合法,永远返回二分的最右边的值
n最大可达1e5,按照x的计算方式(指数爆增),
2^(1e5)早就爆了longlong变成一个负值 ,于是返回二分的最右边的值
#include <iostream>
#include <stdlib.h>
#include <string.h>
using namespace std;
int n;
const int N=1e5+5;
int a[N];
bool adequate(int x){
for(int i=0;i<n;i++){
x=x*2-a[i];
if(x<0)return false;
if(x>=1e5)return true;//n最大可达1e5,按照x的计算方式,
// 2^(1e5)早就爆了longlong变成一个负值 ,于是返回二分的最右边的值
}
return true;
}
int main(){
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
int l=0,r=1e5;
while(l<r){
int mid=l+(r-l)/2;
if(adequate(mid))r=mid;
else l=mid+1;
}
cout<<l;
return 0;
}
四平方和
思路速递
数量级
1
e
6
1e6
1e6,复杂度应该为
n
n
n 或者
n
l
o
g
n
nlogn
nlogn
1、abcd皆小于
n
\sqrt{n}
n ,暴力枚举abc,时间复杂度为
n
∗
n
∗
n
=
n
n
\sqrt{n}*\sqrt{n}*\sqrt{n}=n\sqrt{n}
n∗n∗n=nn,不可行
2、先枚举cd所有可能的组合的和,存放在哈希表中,接下来枚举ab,在查找
n
−
(
a
+
b
)
n-(a+b)
n−(a+b) 是否在哈希表中存在
存放 这一步非常有门道,首先 “输出第一个表示法”,只需保存字典序最小的
c
d
cd
cd 组合, 故想用map容器存放,只需要map 而不需要 unordered_map
或者 直接将
c
d
cd
cd 组合存放在数组中,直接
O
(
1
)
O(1)
O(1) 查询,时间复杂度为
O
(
n
)
O(n)
O(n)
或者将所有
c
d
cd
cd 组合(和相同的也全部存下)存放在 vector 数组中,则需要根据 cd和排序,内部按字典序排序(cd和相同按cd字典序),再二分,时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 如此
存放在map容器中,超时
#include <iostream>
#include <stdlib.h>
#include <algorithm>
using namespace std;
int n;
const int N=1e5+5;
int a[N];
#define PII pair<int,int>
#include <map>
map<int,PII> mp;
int main(){
cin>>n;
for(int c=0;c*c<n;c++){
for(int d=c;d*d<=n;d++){
int t=c*c+d*d;
if(mp.find(t)==mp.end()){
mp.insert({t,{c,d}});
}
}
}
for(int a=0;a*a*4<=n;a++){
for(int b=a;a*a+b*b<=n/2;b++){
int t=n-a*a-b*b;
if(mp.find(t)!=mp.end()){
cout<<a<<" "<<b<<" "<<mp[t].first<<" "<<mp[t].second<<endl;
return 0;
}
}
}
return 0;
}
像只枚举ab,借助
n
−
a
∗
a
−
b
∗
b
n-a*a-b*b
n−a∗a−b∗b 搜索剩余两个一样
存放时,可以存放部分信息,剩下的部分通过条件计算得到
#include <iostream>
#include <stdlib.h>
#include <math.h>
#include <algorithm>
using namespace std;
int n;
const int N=1e5+5;
int a[N];
#define PII pair<int,int>
#include <map>
map<int,int> mp;
int main(){
cin>>n;
for(int c=0;c*c<n;c++){
for(int d=c;d*d<=n;d++){
int t=c*c+d*d;
if(mp.find(t)==mp.end()){
mp.insert({t,c});
}
}
}
for(int a=0;a*a*4<=n;a++){
for(int b=a;a*a+b*b<=n/2;b++){
int t=n-a*a-b*b;
if(mp.find(t)!=mp.end()){
cout<<a<<" "<<b<<" "<<mp[t]<<" "<<sqrt(t-mp[t]*mp[t])<<endl;
return 0;
}
}
}
return 0;
}
进而不需要利用map容器,直接存放在数组中(cd 组合的值、c值)
#include <iostream>
#include <stdlib.h>
#include <math.h>
#include <algorithm>
using namespace std;
int n;
const int N=5e6+5;
int p[N];
#define PII pair<int,int>
#include <map>
map<int,int> mp;
int main(){
cin>>n;
for(int c=0;c*c<n;c++){
for(int d=c;d*d<=n;d++){
int t=c*c+d*d;
if(t>n)continue;//否则t值很大,数组越界
if(!p[t]){
p[t]=c+1;//存放的值与0错开,0默认为之前未出现过,真正的0存的是1
}
}
}
for(int a=0;a*a*4<=n;a++){
for(int b=a;a*a+b*b<=n/2;b++){
int t=n-a*a-b*b;
if(p[t]){
int c=p[t]-1;
cout<<a<<" "<<b<<" "<<c<<" "<<sqrt(t-c*c)<<endl;
return 0;
}
}
}
return 0;
}
分巧克力?典型二分找大的(从右往左找)
#include <iostream>
#include <stdlib.h>
#include <math.h>
#include <algorithm>
using namespace std;
int n,k;
const int N=1e5+5;
int h[N];
int w[N];
bool adequate(int x){
int cnt=0;
for(int i=0;i<n;i++){
if(h[i]<x||w[i]<x)continue;
cnt+=(h[i]/x)*(w[i]/x);
}
if(cnt>=k)return true;
else return false;
}
int search(int l,int r){
while(l<r){
int mid=l+(r-l+1)/2;
if(adequate(mid)){
l=mid;
}
else r=mid-1;
}
return l;
}
int main(){//19
cin>>n>>k;
int l=1;
int r=0;
for(int i=0;i<n;i++){
cin>>h[i]>>w[i];
r=max(r,h[i]);
r=max(r,w[i]);
}
cout<<search(l,r);
return 0;
}
二分upper_bound(a+1,a+n+1,x)-a?递增三元组
xy也要取longlong,否则xy相乘就会强制转换为int,导致错误
#include <iostream>
#include <stdlib.h>
#include <algorithm>
using namespace std;
#define ll long long int
int n;
const int N=1e5+5;
int a[N];
int b[N];
int c[N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
for(int i=1;i<=n;i++){
cin>>c[i];
}
sort(a + 1, a + 1 + n);
sort(b + 1, b + 1 + n);
sort(c + 1, c + 1 + n);
ll res=0;
for(int i=1;i<=n;i++){
ll x=(lower_bound(a+1,a+n+1,b[i])-a)-1;
ll y=n-(upper_bound(c+1,c+n+1,b[i])-c)+1;
res+=x*y;
}
cout<<res;
return 0;
}
前缀和取余?K倍区间
区间和等于k的倍数无非两中情况
一:区间 1 ~ i,这种只需要计算 求余前缀和为0的次数
二:区间 i ~ j,这种处于里段的区间段,在 两个相等的前缀和 之间
对前缀和取模之后,两个相等的前缀和就能组成一个k倍区间。
注意,res 是区间个数,n的平方级别,会爆int
#include <iostream>
#include <stdlib.h>
#include <algorithm>
using namespace std;
#define ll long long int
int n,k;
const int N=1e5+5;
int s[N];
int cnt[N];
signed main(){
cin>>n>>k;
ll res=0;
int x;
for(int i=1;i<=n;i++){
cin>>x;
s[i]=(s[i-1]+x)%k;
res+=cnt[s[i]];//两个相同的前缀和
cnt[s[i]]++;
}
cout<<res+cnt[0];
return 0;
}
上来就一个TLE,真难过
#include <iostream>
#include <stdlib.h>
#include <algorithm>
using namespace std;
//#define ll long long int
int n,k;
const int N=1e5+5;
int a[N];
int s[N];
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
s[i]=s[i-1]+a[i];
}
int cnt=0;
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
if((s[j]-a[i-1])%k==0)cnt++;
}
}
cout<<cnt;
return 0;
}
二维前缀和?激光炸弹
坑
1、不能开两个数组(直接在原数组上求二维前缀和就好),因为每个数组是5000x5000这么大,就是5000x5000x4bytes,5000x5000x4/1024/1024=95MB,这题的空间是168MB,所以两个数组会MLE。
一个数组就行了,在自己身上求前缀和。
2、 xy坐标是从0开始的。
边界问题:
r=min(r,max(mx,my));❌
r是不能减小的,比如 4 5 3,
不能因为最后一个双重循环从r开始,r必须小于mx,my,就随意减小r
且 为了得到最佳res,极有可能出现r的范围 圈住了空地(即超出了5*3范围)
故而,只能选择将mx,my搞大,一定要都大于r,才能让r肆意圈地
这样一来,4 5 4
求前缀和时不能只求 行1~ 5,列1~ 3 的,否则 例如列1~4的前缀和就会误以为0,实际上等于3列所有
r=min(r,5001); ✔
mx=max(mx,r);
my=max(my,r);
但 r 超过a数组的大小也没有意义
#include <iostream>
#include <stdlib.h>
#include <math.h>
#include <algorithm>
using namespace std;
//#define ll long long int
int n,r;
const int N=5005;
int a[N][N];
//int sum[N][N];
signed main(){//30
cin>>n>>r;
int x,y,w;
int mx=0,my=0;
for(int i=1;i<=n;i++){
cin>>x>>y>>w;
a[x+1][y+1]+=w;
mx=max(mx,x+1);
my=max(my,y+1);
}
r=min(r,5001);
mx=max(mx,r);
my=max(my,r);
int res=0;
for(int i=1;i<=mx;i++){//二维矩阵前缀和
for(int j=1;j<=my;j++){
a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
}
}
for(int i=r;i<=mx;i++){//二维矩阵前缀和
for(int j=r;j<=my;j++){
int tmp=a[i][j]-a[i-r][j]-a[i][j-r]+a[i-r][j-r];
res=max(res,tmp);
}
}
cout<<res;
return 0;
}
//边界问题,求边长为r正方形内总和,借助矩阵前缀和
// 1 2 3 4 5
// 1 2 3 4 5
// 1 2 3 4 5