蓝桥杯之二分
- 二分板子?第一次和最后一次出现的位置
- 机器人跳跃问题
- 四平方和
- 分巧克力?典型二分找大的(从右往左找)
- 二分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



















