【介绍】
二分就是在有序序列中,通过不断的二分,进而不断地缩小范围去寻找满足我们条件的解。这只是对二分一个狭义上的理解,广义二分其实是如果有一个临界值使得临界值一边的数据满足一种性质,另一边满足另一种性质,即使不是有序的但也可以利用二分去寻找这个临界值。
二分的本质是二段性不是单调性。
【用处】
1.找大于等于数的第一个位置 (满足某个条件的第一个数)
2.找小于等于数的最后一个数 (满足某个条件的最后一个数)
3.查找最大值 (满足该边界的右边界)
4.查找最小值 (满足该边界的左边界)
【模板】
1.整数二分1
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
2.整数二分2
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
关于为什么要 +1
原因是如果不加上1,那么mid得到的是下取整的数,那么有可能 [m,r] 更新过后m会一直等于m(m+1==r的情况)会陷入死循环。(取左边界用)
3.浮点数二分
while(r-l>1e-5) //注意精度,最好比所求位数多两位
{
double mid = (l+r)/2;
if(check(mid)) l=mid; //或r=mid;
else r=mid; //或l=mid;
}
一.快速排序
【分析】
1.首先选定一个数x(首尾中间都可以),指针ij分别从首尾出发相向而行,当i碰到比x更小的数时,继续移动,直到遇到一个大于x的数,停下;同理,当j碰到比x更大的数时,继续移动,直到遇到一个小于x的数,停下。然后,交换ij指向的数的位置(注意不是交换ij)
2.然后递归完成后续排序( l ~ j , j+1 ~ r )
【代码】
#include <iostream>
using namespace std;
const int N = 100010;
int q[N];
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[(l+r)/2];
while (i < j){
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}//模板
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
quick_sort(q, 0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
【分析】
总体思路就是排序之后遍历数组找到第k个数。
快排的每一趟,数轴的左边都是 小于等于x 的, 右边都是 大于等于 x 的。
左边元素的个数是 j - l + 1, 如果j=l+1<= s1 的话,那么下次递归的区间就是左边,否则右边。
直到 l == r 时返回q[l]。
【代码】
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int q[N];
int qsort(int q[],int l,int r,int k){
if(l>=r) return q[l];
int i=l-1,j=r+1,x=q[(l + r)>> 1];
while(i<j){
do i++;while(q[i]<x);
do j--;while(q[j]>x);
if(i<j) swap(q[i],q[j]);
}
if(j-l+1>=k) return qsort(q,l,j,k);
else return qsort(q,j+1,r,k-(j-l+1));
}
int main(){
int n,k;
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++){
scanf("%d",&q[i]);
}
cout<<qsort(q,0,n-1,k);
return 0;
}
二.归并排序
【分析】
1.主要思路
分成子问题(l ~ mid , mid+1 ~ r ) -> 递归处理子问题 -> 合并
一是将数组一分为二,一个无序的数组成为两个数组;
最后合二为一,将两个有序数组合并成为一个有序数组。
2.图示
【代码】
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N],tmp[N];
int n;
void merge_sort(int a[],int l,int r){
if(l>=r) return ;
int mid= l + r >> 1;
merge_sort(a,l,mid),merge_sort(a,mid+1,r);//假定左边和右边都已经排好序
int k=0,i=l,j=mid+1;//k指向tmp里的数
while(i<=mid&&j<=r){
if(a[i]<=a[j]) tmp[k++]=a[i++];
else tmp[k++]=a[j++];
}//归并操作,比较两个排好序的数列的第一个元素大小,将较小数放进tmp,相应i或j向后移,然后继续比较
while(i<=mid) tmp[k++]=a[i++];
while(j<=r) tmp[k++]=a[j++];//处理左右还有剩余的数
for(int i=l,j=0;i<=r;i++,j++) a[i]=tmp[j];//该还的总要还
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
merge_sort(a,0,n-1);
for(int i=0;i<n;i++){
printf("%d ",a[i]);
}
return 0;
}
【分析】
1.首先分三种情况,1逆序对在数轴的左边,2逆序对在数轴的右边,3逆序对跨数轴左右。
2.在合并前,若 a[i] > a[j],则a[i] 和它后面的元素都大于 a[j],a[i] 构成逆序对数量:res += mid - i + 1
3. if(a[i] <= a[j]) tmp[k++] = a[i++] 这里和之前归并排序不一样。
这里是先判断 (a[i] <= a[j] ,表示当前的a[i]和a[j]不是逆序对。不是的话res就不用累加。
【代码】
#include <bits/stdc++.h>
typedef long long LL;
using namespace std;
const LL N=1e5+10;
int a[N],tmp[N];
int n;
LL merge_sort(int l,int r){
if(l>=r) return 0;
int mid= (l + r) >> 1;
LL res=merge_sort(l,mid)+merge_sort(mid+1,r);
int k=0,i=l,j=mid+1;
while(i<=mid && j<=r){
if (a[i] <= a[j]) tmp[k ++ ] = a[i ++ ];
else{
res+= mid - i + 1;//数量公式
tmp[k ++ ]=a[j ++ ];
}
}
while (i <= mid) tmp[k ++ ] = a[i ++ ];
while (j <= r) tmp[k ++ ] = a[j ++ ];
for (i = l, j = 0; i <= r; i ++, j ++ ) a[i] = tmp[j];
return res;
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
cout<<merge_sort(0,n-1)<<endl;
}
三. 啦啦啦
【分析】
本题大意是:在数组中查找某元素,找不到就输出 "−1 −1",找到了就输出不小于该元素的最小位置和不大于该元素的最大位置。所以,需要写两个二分,一个需要找到>=x的第一个数,另一个需要找到<=x的最后一个数。
【代码】
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N];
int n,q;
int main(){
scanf("%d%d",&n,&q);
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
while(q--){
int k;
scanf("%d",&k);;
int l=0,r=n-1;
while(l<r){
int mid=l + r >> 1;
if(a[mid]>=k) r=mid;
else l=mid+1;
}//查找终止位置
if(a[l]!=k) cout<<"-1 -1"<<endl;//不存在k元素
else{
cout<<l<<" ";
int l=0,r=n-1;
while(l<r){
int mid=l + r + 1 >> 1;
if(a[mid]<=k) l=mid;
else r=mid-1;
}//查找起始位置
cout<<l<<endl;
}
}
return 0;
}
【分析】
比较简单,,,浮点数二分,注意一下边界和精度就好啦
【代码】
#include <bits/stdc++.h>
using namespace std;
int main(){
double x;
cin>>x;
double l=-10000,r=10000;
while(r-l>=0.00000001){
double mid=(l+r)/2;
if(mid*mid*mid>=x) r=mid;
else l=mid;
}
printf("%lf\n",l);
return 0;
}
四.练习
【分析】
可以使用二分的原因
(1) 当伐木机锯片升高,相应得到的木材长度减少;相反,得到的木材长度增加。因而可以发现,答案具有单调性。
(2)如果暴力枚举,时间复杂度大概率会超,但是二分就不会啦。
思路
找到最高的树,然后遍历直到得到木材总长度为m。
【代码】
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
long long trees[N];
long long n,m,h,l,r;
int main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&trees[i]);
if(trees[i]>r) r=trees[i];
}//找到最高的树作为右边界
while(l<=r){
int mid=l+r>>1;
h=0;
for(int i=1;i<=n;i++){
if(trees[i]>mid) h+=trees[i]-mid;
}
if(h<m) r=mid-1;//还没有达到需要的长度,说明需要继续向右找
else l=mid+1;
}
printf("%lld",r);
return 0;
}
【代码】
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
long long n,k;
long long a[N];
bool f(long long x){
long long ans=0;
for(int i=1;i<=n;i++){
ans+=a[i]/x;
}
return ans>=k;
}//判断一根原木的长度分成每段x能分多少段
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
long long l=0,r=100000001;
long long mid;
while (l + 1 < r) {
mid = (l + r) / 2;
if (f(mid)) l = mid;//大于k,说明答案在右区间
else r = mid;//小于k,说明答案在左区间
}
cout << l << endl;
return 0;
}