算法(二) 从二分到分治(含复杂度分析)

算法(二):从二分到分治

1.二分:

二分可以看作是特殊的分治,其内容与分治极其像。顾名思义,二分通过将序列分为左右两段进行解题,如此递归(通过递归实现)。那么,像这样解题显然是需要序列的 单调性

I . I. I.二分查找

引入:

猜数游戏:给出一个取值范围为 [ 1 , 100 ] [1,100] [1,100] 的数,请你用平均次数最少的方法猜这个数。每次仅给出你说出的答案与这个数的大小关系。

显然从中间猜次数最少吧。因为期望的次数是 1 次,而最坏次数为 100 次。朴素方法的平均为 n 次。而每次都排除一半肯定稳定性高且每次都能在平均上浮动,最差每次也能排除一半。

介绍:

二分查找是二分的一种利用,其复杂度为 Θ ( l o g n ) \Theta(logn) Θ(logn)

那么为什么有这个复杂度呢?

由于每次二分都有单调性,那么每次将数列从中一分为二,其中一半一定可以排除。于是就每次都除以2,就可记作 l o g n log n logn , 就如猜数游戏一样。

那么,有了以上思路,便可给出模板了

题目:请在给定的含 n n n 个数的单调序列 a a a 中找到 m m m 的位置(下标从1开始)

输入格式:第一行,一个数 n n n( 1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1n105),第二行 n n n互不相同的正整数(保证不会爆 i n t int int ),第三行给出一个数 T T T ( 1 ≤ T ≤ 1 0 3 1 \leq T \leq 10^3 1T103),表示询问的次数。下面 T T T 行, 每行一个数 m m m (保证不会爆 i n t int int 且一定有这个数)

样例输入:
5
1 2 3 4 5
2
2
3

样例输出:
2
3

根据我们刚刚说的,可以得到的代码如下(尤其注意二分时的上下界更新情况,可以自己推导,亦可以多试是小于还是要取得等)

#include<cstdio>
const int N=1e5+5;
int n,m,T;
int a[N];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    scanf("%d",&T);
    while(T--){
        scanf("%d",&m);
        int l=1,r=n;
        while(l<r){
            int mid=(l+r)>>1;
            if(a[mid]>m)
                r=mid-1;
            else
                l=mid+1;
        }
        printf("%d\n",r);
    }
    return 0;
}

请尤其注意

while(l<r){
     int mid=(l+r)>>1;
     if(a[mid]>m)
          r=mid-1;
     else
          l=mid+1;
}

中的更新,并非每次都要 -1 或 +1 ,而是根据实际情况决定的。

I I . II. II. 二分答案

介绍:

顾名思义,二分答案就是在一定范围内通过二分法的优异效率来枚举答案的。

与二分查找很相似,以下直接给出两道题:

1.luogu:P1873:砍树

伐木工人米尔科需要砍倒 M M M 米长的木材。这是一个对米尔科来说很容易的工作,因为他有一个漂亮的新伐木机,可以像野火一样砍倒森林。不过,米尔科只被允许砍倒单行树木。
米尔科的伐木机工作过程如下:米尔科设置一个高度参数 H H H(米),伐木机升起一个巨大的锯片到高度 H H H,并锯掉所有的树比H高的部分(当然,树木不高于 H 米的部分保持不变)。米尔科就获得树木被锯下的部分。
例如,如果一行树的高度分别为 20,15,10 和 17,米尔科把锯片升到 15 米的高度,切割后树木剩下的高度将是 15,15,10 和 15,而米尔科将从第 1 棵树得到 5 米,从第 4 棵树得到 2 米,共得到 7 米木材。
米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度 H H H,使得他能得到木材至少为 M M M 米。换句话说,如果再升高 1 米,则他将得不到 M M M 米木材。

输入格式
第一行 2 个整数 N N N ( 1 ≤ N ≤ 1 0 5 ) (1\le N \le 10^5) (1N105) M M M ( 1 ≤ M ≤ 2 ⋅ 1 0 9 ) (1\le M \le 2\cdot 10^9) (1M2109)分别表示树木的数量和需要的木材总长度。
第二行 N 个整数表示每棵树的高度,值均不超过 1 0 9 10^9 109。所有木材长度之和不低于 M,因此必有解。
输出格式
一个整数,表示木机锯片的最大的高度 H H H

Sample Input
5 20
4 42 40 26 46
Sample Output
36

代码如下:

#include<cstdio>
const int N=1e5+5;
int n,m,T;
int a[N];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    scanf("%d",&T);
    while(T--){
        scanf("%d",&m);
        int l=1,r=n;
        while(l<r){
            int mid=(l+r)>>1;
            if(a[mid]>m)
                r=mid-1;
            else
                l=mid+1;
        }
        printf("%d\n",r);
    }
    return 0;
}

那么这就是二分的全部内容了!以下我将会给出一组比较有代表性的题的地址:

P1024 [NOIP2001 提高组] 一元三次方程求解

P2678 [NOIP2015 提高组] 跳石头

那么接下来:

2.分治:

基本思路:

分治,顾名思义,分而治之,那么就应该有类似于二分的过程。先将一个母问题拆分成几个子问题,分别解决;其中子问题也一定能分到一个基本的可以手动求解的问题。此时再把几个问题合并起来,就可以得到母问题的解。

同样的,单向基本操作时间复杂度也为 Θ ( l o g n ) \Theta(logn) Θ(logn) ,为什么要说“基本”呢?因为我们发现,如果额外地进行操作,即进行合并,一定会有 ( n l o g n ) (nlogn) (nlogn) 的时间,如归并排序。

特殊的,分治不需要单调性,因为每次都会按基准数进行分组,相当于是把左右看作两个数,具备单调性了。

实现与例题:

1.快速排序 / 归并排序:
这个很好理解,不给题面了。快排代码与图示如下。
请添加图片描述

#include<cstdio>
#include<cstdlib>
#include<iostream>
const int N=(1e5)+9;

int a[N];
void qsort(int l,int r){
	if(l>r)	return ;
	int i=l,j=r,T=a[(l+r)/2];
	while(i<=j){
		while(a[i]<T) i++;
        while(a[j]>T) j--;
        if(i<=j){
            std::swap(a[i],a[j]);
            i++;
            j--;
        }
	}
	qsort(l,j);
	qsort(i,r);
	return ;
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	qsort(1,n);
	for(int i=1;i<=n;i++)
		printf("%d ",a[i]);
	return 0;
} 

其中,T为基准数。

而比较麻烦但好用的归并 (merge) 排序如下。
请添加图片描述

#include<iostream>
#include<cstring>
const int N=1e5+5;
int a[N],tmp[N];
int n;
void merge(int l,int r){
	int mid=(l+r)>>1;
	if(l==r)
		return ;
	merge(l,mid);
	merge(mid+1,r);
	int i=l,j=mid+1,flag=l;
	while(i<=mid&&j<=r){
		if(a[i]>a[j])
			tmp[flag++]=a[j++];
		else
			tmp[flag++]=a[i++];
	}
	while(i<=mid)
		tmp[flag++]=a[i++];
	while(j<=r)
		tmp[flag++]=a[j++];
	//update
	for(int i=l;i<=r;i++)
		a[i]=tmp[i];
	return ;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    merge(1,n);
    for(int i=1;i<=n;++i)
        printf("%d ",a[i]);
    return 0;
}

代码全程高能,请注意。

2.经典题:P1908:逆序对

肯定不用我多说了,只需要在源代码上加上ans的更新即可:

#include<cstdio>
const int N=1000005;
int n;
long long ans=0;
int a[N],tmp[N];//tmp存排完序的数 
void merge(int l,int r){
	int mid=(l+r)>>1;
	if(l==r)
		return ;
	merge(l,mid);
	merge(mid+1,r);
	int i=l,j=mid+1,flag=l;
	while(i<=mid&&j<=r){
		if(a[i]>a[j]){
			ans+=mid-i+1;
			tmp[flag++]=a[j++];
		}else{
			tmp[flag++]=a[i++];
		}
	}
	while(i<=mid){
		tmp[flag++]=a[i++];
	}
	while(j<=r){
		tmp[flag++]=a[j++];
	}
	//update
	for(int i=l;i<=r;i++)
		a[i]=tmp[i];
	return ;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	merge(1,n);
	printf("%lld",ans);	
	return 0;
}

紧接着推荐去做:
P1115 最大子段和
P2345 [USACO04OPEN]MooFest G

然后就是汉诺塔问题,其实也没那个必要。。。

那么,介绍一个小技巧:可以用前缀和来减少额外求和带来的时间复杂度;然后还可以在一些特定的题中套merge或数据结构,如这道题:

P3810 【模板】三维偏序(陌上花开)

题目背景
这是一道模板题,可以使用 bitset,CDQ 分治,KD-Tree 等方式解决。

题目描述
n n n 个元素,第 i i i 个元素有 a i , b i , c i a_i,b_i,c_i ai,bi,ci
三个属性,设 f ( i ) f(i) f(i) 表示满足 a j ≤ a i a_j \leq a_i ajai b j ≤ b i b_j \leq b_i bjbi c j ≤ c i c_j \leq c_i cjci

对于 d ∈ [ 0 , n ) d \in [0, n) d[0,n),求 f ( i ) = d f(i) = d f(i)=d 的数量。

输入格式
第一行两个整数 n , k n,k n,k,表示元素数量和最大属性值。
接下来 n n n 行,每行三个整数 a i , b i , c i a_i ,b_i,c_i ai,bi,ci,分别表示三个属性值。

输出格式
n n n 行,第 d + 1 d + 1 d+1 行表示 f ( i ) = d f(i) = d f(i)=d i i i 的数量。

那么这里只讲 CDQ 分治的解法,以后更新到“数据结构”可能会具体讲:

#include<cstdio>
#include<algorithm> 
const int N=2*(1e+5)+1;
struct node{
	int x,y,z;
	int num;
}a[N];
int c[4*N],b[N],tmp[N],f[N];
int n,k;

void updata(int x,int v){
	while(x<=k)	c[x]+=v,x+=x&(-x);
	return ;
}
int sum(int x){
	int s=0;
	while(x)	s+=c[x],x-=x&(-x);
	return s;
}
//1sort
bool cmp1(node a,node b){
	if(a.x!=b.x)	return a.x<b.x;
	if(a.y!=b.y)	return a.y<b.y;
	return a.z<b.z;
}
//2sort
bool cmp2(node a,node b){
	if(a.y!=b.y)	return a.y<b.y;
	if(a.z!=b.z)	return a.z<b.z;
	return a.x<b.x;
}
void merge(int l,int r){
	//普通分治写法 
	if(l==r)
		return ;
	int mid=(l+r)>>1,flag;
	merge(l,mid);
	merge(mid+1,r);
	//排序维护(如末尾所说,此处针对二三维进行维护) 
	std::sort(a+l,a+r+1,cmp2);
	//求和更新 
	for(int i=l;i<=r;i++)
		(a[i].x<=mid)? updata(a[i].z,1),flag=i : b[a[i].num]+=sum(a[i].z);
	for(int i=l;i<=r;i++)
		if(a[i].x<=mid)
			updata(a[i].z,-1);
	return ;
}
inline int read(){
	char c=getchar();
	int x=0;
	while(c<=32)	c=getchar();
	while(c<='9'&&c>='0'){
		x=x*10+(int)(c-'0');
		c=getchar();
	}
	return x;
}
int main(){
	//实测read()仅比scanf快20ms左右,不值得 
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++){
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
//		a[i].x=read(),a[i].y=read(),a[i].z=read(); 
		a[i].num=i;
	}
	
	//分治前维护 
	std::sort(a+1,a+1+n,cmp1);
	//printf("!");
	for(int i=1;i<=n;){
		int j=i+1;
		while(j<=n&&a[j].x==a[i].x&&a[j].y==a[i].y&&a[j].z==a[i].z)//不能写if,分组时应可停下来 
			j++;
		while(i<j)
			tmp[a[i].num]=a[j-1].num,i++;
	}
	for(int i=1;i<=n;i++)
		a[i].x=i;
	//printf("!");
	merge(1,n);
	//统计 
	for(int i=1;i<=n;i++)
		f[b[tmp[a[i].num]]]++;
	for(int i=0;i<n;i++)
		printf("%d\n",f[i]);
	return 0;
}

以下引用洛谷题解

luogu大佬说这道题要把三维降到二维,并把第三元素用数据结构维护期间需用CDQ分治,如:(luogu题解)

逆序对的问题是二维的,我们只需要对一维排序,然后在用树状数组维护即可。
那么对于三维的陌上花开呢?我们还是可以用这个方法,首先先将数列按第一位排序,这样我们只需要考虑两维的情况。于是我们可以分治做了,将某一个序列 [ 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],然后在对 [ l , r ] [l,r] [l,r] 这段区间的
第二维进行排序。若点在排序前属于 [ l , m i d ] [l,mid] [l,mid],树状数组单点修改;否则该点在排序前属于 [ m + 1 , r ] [m+1,r] [m+1,r] ,便统计一次。(其实就是类似于树状数组求逆序对的操作)

cdq分治是一种神奇的分治算法

例如本题,将第二维的区间采取二分,首先递归计算左边内部对自己的贡献,然后计
算左边对右边的贡献,最后递归计算右边自己内部贡献

本人不太会,也不太喜欢树状数组,所以害怕讲不清楚,便引用洛谷题解。
本题还可以用merge嵌套来做,感兴趣的请移步 luogu 。

至此,本节完。

特别感谢 luogu … 各位也可以去那里刷题,一定要保证信用!!!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值