目录
数的范围(lower_bound和upper_bound的原理)
分巧克力(数学+枚举含义)
递增三元组(数学+构造二分)
中位数(大佬写法)
楼梯(数学+枚举含义)
奶牛棒球(数学+枚举的含义)
工资计算(数学+枚举含义)
愤怒的牛(枚举含义+前缀和)
数列分段 II(枚举含义+前缀和)
关于(整数)二分的模板:
我的一些小经验总结:
(1)<判断题意>
首先是要判断该题目是否可以用二分,一般来说具有二段性的就可以用二分
何为二段性?
在一整个长串中,可被分为两个连续的段,且这两段的性质存在差异即可
典型的例子为:前一段满足,后一段不满足,那么就可以对该性质进行二分
(2)<判断类别>
对于二分,(y总)总结了两套方法:对于上面的二段性
<1>最后一个满足第一段性的位置:
更新区间的方式为:
int left = 0; int right = nums.size() - 1; while (left < right) { int mid = (left + right + 1) >> 1; if (check(mid)) left = mid; else right = mid - 1; }
<2>第一个满足第二段性的位置:
更新区间的方式为:
int left = 0; int right = nums.size() - 1; while (left < right) { int mid = (left + right) >> 1; if (check(mid)) right = mid; else left = mid + 1; }
(3)<弄懂枚举的意义>
这点是二分运用在综合题最难的地方,为什么要二分,因为要优化枚举,这时候就要知道枚举的这个是什么东西,才能写对正确的check函数,这点会在下面的题目中重点介绍
二分的数字简单应用(剑指Offer)
不修改数组找出重复的数字
抽屉原理:
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int l = 1, r = nums.size() - 1;
while (l < r)//寻找第一个满足的第二段性
{
int mid = l + r >> 1; // 划分的区间:[l, mid], [mid + 1, r]
int s = 0;
for (auto x : nums) s += x >= l && x <= mid;//重点:如果有多个在这个区间
//那么就证明这个区间有重复元素
if (s > mid - l + 1) r = mid;//在这个区间:符合check
else l = mid + 1;
}
return r;
}
};
数字在排序数组中出现的次数
class Solution {
public:
int getNumberOfK(vector<int>& nums , int k) {
auto l=lower_bound(nums.begin(),nums.end(),k);//第一个满足的
auto r=upper_bound(nums.begin(),nums.end(),k);//最后一个满足的
return r-l;//最后一个满足的-第一个满足的==满足的元素个数
}
};
0到n-1中缺失的数字
class Solution {
public:
int getMissingNumber(vector<int>& nums) {
if (nums.empty()) return 0;
int left = 0;//枚举的意义是:0~n的数字
int right = nums.size();//预防处于边界的情况
while (left < right)//二段性:不满足证明该数字小了,需要向右寻找
{
int mid = left + right >> 1;
if (nums[mid] != mid) right = mid;
else left = mid + 1;
}
return right;
}
};
数组中数值和下标相等的元素
class Solution {
public:
int getNumberSameAsIndex(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
while(left<right)
{
int mid=left+right>>1;
if(nums[mid]>=mid) right=mid;//满足条件,缩小区间
else left=mid+1;
}
if(nums[left]==left) return left;//特判
else return -1;
}
};
旋转数组的最小数字(中等)
class Solution {
public:
int findMin(vector<int>& nums) {
if(nums.empty()) return -1;
int n=nums.size();
while(nums[0]==nums[n-1]) n--;//防止重复元素影响单调性
int left=0;
int right=n-1;
while(left<right)//二段性:单调性
{
int mid=left+right>>1;
if(nums[mid]>nums[right]) left=mid+1;//证明还在左半区:‘=’不可删除:因为是严格单调
else right=mid;
}
return nums[left];
}
};
二分的综合应用
数的范围(lower_bound和upper_bound的原理)
#include <iostream>
using namespace std;
const int maxn = 100005;
int n, q, x, a[maxn];
int main()
{
scanf("%d%d", &n, &q);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
while (q--)
{
scanf("%d", &x);
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (a[mid] < x) l = mid + 1;
else r = mid;
}
if (a[l] != x)
{
printf("-1 -1\n");
continue;
}
int l1 = l, r1 = n;//l1为l,加快运行效率,因为lower_bound一定在upper_bound的位置或者左边
while (l1 + 1 < r1)
{
int mid = l1 + r1 >> 1;
if (a[mid] <= x) l1 = mid;
else r1 = mid;
}
printf("%d %d\n", l, l1);
}
return 0;
}
四平方和(重载比较)
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=2500010;
int n,m;
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;
}
}sum[N];
int main()
{
cin>>n;
for(int c=0;c*c<=n;c++)
for(int d=c;c*c+d*d<=n;d++)
{
sum[m++]={c*c+d*d,c,d};
}
sort(sum,sum+m);
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;
int left=0;
int right=m-1;
while(left<right)
{
int mid=left+right>>1;
if(sum[mid].s>=t) right=mid;
else left=mid+1;
}
if(sum[left].s==t)
{
printf("%d %d %d %d\n",a,b,sum[left].c,sum[left].d);
return 0;
}
}
return 0;
}
分巧克力
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=100010;
int h[N],w[N];
int n,k;
bool check(int mid)
{
int res=0;
for(int i=0;i<n;i++)
{
res+=(h[i]/mid)*(w[i]/mid);//分巧克力的公式
if(res>=k) return true;
}
return false;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++) scanf("%d%d",&h[i],&w[i]);
int left=1;
int right=1e5;
while(left<right)
{
int mid=left+right+1>>1;
if(check(mid)) left=mid;//比较的是边长,大于这个边长的不满足,所以取的是最后一个满足的,也就是第一段的末尾
else right=mid-1;
}
cout<<right<<endl;
}
递增三元组
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N], b[N], c[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = 0; i < n; i++) scanf("%d", &b[i]);
for (int i = 0; i < n; i++) scanf("%d", &c[i]);
sort(a, a + n); //二分需要满足单调性
//sort(b, b + n);
sort(c, c + n);
LL res = 0; //答案可能会很大,会爆int
for (int i = 0; i < n; i++)
{
int l = 0, r = n - 1; //二分查找a数组中最后一个小于b[i]的数的下标
while (l < r)
{
int mid = (l + r + 1) / 2;
if (a[mid] < b[i]) l = mid;
else r = mid - 1;
}
if (a[l] >= b[i]) //如果未找到小于b[i]的数,将x标记为-1,后续计算时 x+1==0
{
l = -1;
}
int x = l;
l = 0, r = n - 1;
while (l < r)
{
int mid = (l + r) / 2;
if (c[mid] > b[i]) r = mid;
else l = mid + 1;
}
if (c[l] <= b[i]) //如果未找到大于b[i]的数,将y标记为n,后续计算时 n-y==0;
{
r = n;
}
int y = r;
res += (LL)(x + 1) * (n - y);
}
printf("%lld\n", res);
return 0;
}
我在哪?(字符串哈希)
整理题意:
要求的以枚举区间长度,遍历整个数组,是否存在相同的字符串即可
#include <iostream>
#include <unordered_set>
using namespace std;
int n;
string s;
int main()
{
cin >> n >> s;
for (int i = 1; i <= n; i++)//枚举区间长度
{
unordered_set<string> st;//自动去重
bool flag = true;
for (int j = 0; j + i <= n; j++)//枚举左端点
{
string t = s.substr(j, i);//截取片段
if (st.count(t) != 0)//如果已经出现过了
{
flag = false;
break;
}
st.insert(t);
}
if (flag == true)
{
cout << i << endl;
break;
}
}
return 0;
}
中位数
暴力思路:
合并后排序,输出中位数即可
大佬写法学习一下:
#include <iostream>
#include <vector>
using namespace std;
const int N = 2e5;
const int INF = 1e7;
vector<int> a(N), b(N);
int main()
{
ios::sync_with_stdio(false);
int n, m;
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
cin >> m;
for (int i = 0; i < m; i++)
cin >> b[i];
a[n] = INF, b[m] = INF; //避免数字序列长度不一致发生越界错误
int median = n + m - 1 >> 1;
int i = 0, j = 0, cnt = 0;
while (cnt < median) //双指针寻找中位数,若a[i]<b[j],则a[i]排在前面,i++,反之b[i]排在前面,j++
{
a[i] < b[j] ? i++ : j++;
cnt++;
}
cout << (a[i] < b[j] ? a[i] : b[j]) << endl;
return 0;
}
楼梯
核心:相似三角形 的数学运用推导公式
大佬思路:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
double x, y, c;
bool check(double mid)
{
double ry = sqrt(y * y - mid * mid);
double rx = sqrt(x * x - mid * mid);
double h = rx * ry / (rx + ry);
if (h < c) return true; //h<c,间距取大了,会让梯子下滑,这样交点高度就小了
else return false;
}
int main()
{
scanf("%lf%lf%lf", &x, &y, &c);
double l, r;
if (x < y) l = 0, r = x; //取小的边长,作为右边界,因为梯子不能趴地上吧
else l = 0, r = y;
while (r - l > 1e-5) //答案保留小数点后3位,这里多取2位即可 POJ原题数据1e-4也能过
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
printf("%.3lf\n", l);
return 0;
}
奶牛棒球
简单的数学推导关系:
枚举前两个数,根据范围二分出第三个数
#include <bits/stdc++.h>
using namespace std;
int main ()
{
int n, ret = 0; cin >> n;
vector<int> a(n); for (int & x : a) cin >> x;
sort(a.begin(), a.end());
for (int i = 0; i < n; i ++ )
for (int j = i + 1; j < n; j ++ )
ret += upper_bound(a.begin(), a.end(), 3 * a[j] - 2 * a[i]) - lower_bound(a.begin(), a.end(), 2 * a[j] - a[i]);
cout << ret << endl;
return 0;
}
工资计算
数学反推导
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int T;
int get(int k){
int a[] = {0, 1500, 4500, 9000, 35000, 55000, 80000, 1000000};
double r[] = {0, 0.03, 0.1, 0.2, 0.25, 0.3, 0.35, 0.45};
if(k <= 3500) return k;
int tax = k - 3500;
double sum = 0;
for(int i = 1; i <= 7; i ++){
if(tax >= a[i]){
sum += (a[i] - a[i - 1]) * r[i];
}
else{
sum += (tax - a[i - 1]) * r[i];
break;
}
}
return k - sum;
}
int main(){
cin >> T;
int l = T, r = T * 2;
while (l < r){
int mid = (l + r) >> 1;
if(get(mid) >= T) r = mid;
else l = mid + 1;
}
cout << l;
return 0;
}
愤怒的牛
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5+10;
int x[N];
int n,m;
bool check(int mid)
{
int cur = x[0];
int res = 1;
for(int i = 1; i < n; i++)
{
if(x[i] - cur >= mid)
{
res ++;
cur = x[i];
}
}
if(res >= m) return true;
return false;
}
int main()
{
cin>>n>>m;
for(int i = 0;i < n;i++) cin>>x[i];
sort(x,x+n);
int l = 1,r = x[n-1];
while(l < r)//枚举的是最大的最小距离:因此是二段性的第一段的末尾
{
int mid = (l + r + 1) / 2;
if(check(mid)) l = mid;
else r = mid - 1;
}
cout<<r<<endl;
return 0;
}
数列分段 II
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 1e5+10;
int a[MAXN];
int N,M;
int l,r,mid;
bool check(int x){
int tot=0;
int sum=0;
for(int i=1;i<=N;i++){
if(sum+a[i]<=x) sum+=a[i];
else{
sum=a[i];
tot++;
}
}
return tot<M;
}
int main()
{
cin>>N>>M;
for (int i = 1; i <= N; i ++ ){
scanf("%d", &a[i]);
l=max(l,a[i]);
r+=a[i];
}
while(l<r){
mid=l+r>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
cout<<l<<endl;
return 0;
}
特别注意浮点数的二分
数的三次方根
#include<iostream>
#include<stdio.h>
using namespace std;
int main()
{
double x;
cin >> x;
double left = -10000;
double right = 10000;
while (right - left > 1e-8)
{
double mid = (right + left) / 2;
if (mid * mid * mid > x) right = mid;
else left = mid;
}
printf("%.6lf", left);
return 0;
}