zoukankan      html  css  js  c++  java
  • 正经分治(一)

    Part 1:简单总结分治应用

    分治算法不是一种固定的算法,确切的说,它是一种思想

    总结一下几种常用的分治算法

    二分法求解

    注意事项

    二分法在之前的分治博客中已经提到过了,这里仅作简单的补充描述

    首先,二分法在求最优解问题上有广泛的应用,如果一个题目提到了“存在多个答案输出最优解”,那么有很大的概率要运用二分

    其次,也是使用二分法需要注意的点:对于一个有单调性的答案区间,我们才能使用二分法进行求解

    单调性:对于有解区间内的任意值(x_1,x_2(x_1<x_2)),带入求解得到解(y_1,y_2),那么(y_1,y_2)的大小关系一定可以确定

    换句话说,就是解(y)关于自变量(x)的函数,在定义域(有解区间)内,具有单调性

    二分法求解模板

    使用(while)或者递归实现二分比较方便,这里因为个人习惯(while)就发一个(while)版本的(PS:递归有常数)

    int l=/*二分下界,答案可能出现的最小值*/,r=/*二分上界,答案可能出现的最大值*/
    while(l<=r){
    	int Mid=(l+r)>>1;
    	if(check(Mid)){
    		r=mid-1;//或者l=mid+1,看情况而定 
    		//更新答案 
    	}else l=Mid+1;
    }
    //最后的最优解是l 
    

    这里的(check())函数是精髓,需要根据题目来编写,用来判断这个解合不合法,然后缩小答案所在区间的范围,逐步求解

    时间复杂度

    一般是(O(klogn)),其中(k)(check())函数的复杂度

    分治法求解

    其实二分属于分治,但是二分需要具有单调性,分治却不需要,所以这里才分开来讲

    分治的应用范围非常广泛,对于一个大问题可以不断按照某种规则划分成几个小问题,而一些小问题可以直接求解的模型,可以考虑使用分治法

    分治的典型应用就是各种(ex)数据结构(比如线段树、(st)表、二叉堆等等)

    分治法模板

    这个真的没啥模板了,就像(DP)方程一样,需要自己思考实现

    Part 2:例题梳理

    洛谷P1281书的复制

    传送门

    洛谷P1281书的复制

    (Solution)

    首先拿到了题面,扫了一眼之后发现了这样一句话很扎眼,而且出题人还单独分了一行出来给我们看:

    嗯,求最大时间最少,有二分内味了

    但是不能妄下结论,先判断一手单调性:

    假设抄写页数最多的人花的时间是(x),那么显然其他人的抄写数量要(leq x)
    因为其他人抄书数量在(leq x)时,不对答案造成影响,所以设所有人最多能抄书的页数为(y),那么(y)在当(x)增大时一定增大,当(x)减小时一定减小
    换句话说,只要抄的总量不超过(x),每个人想抄多少抄多少,对答案没有影响。
    为了所有人抄的总量最大,贪心的想,每个人要尽可能多抄,假设每个人都抄了(x),那么有(y=kx)(k)是人数),这个函数显然具有单调性
    综上所述,本题可以使用二分法求解

    明确了(y)(x)之间的关系具有单调性,现在要求(x)的最小值,考虑二分(x)的值

    每二分一次,我们都要检查一下这个答案是不是合法(也就是设计(check())函数)

    题目中要求(k)个人去抄(m)本书,因为我们二分了(x)的值,所以每个人就最多抄(x)

    扫一遍整个代表每本书的页数的数组,如果一个人已经抄了(i)页,下一本书是(j)

    如果(i+jleq x)说明这个人再抄一本书也不会超过(x),根据每个人多抄的思路,我们把下一本书也给这个人抄

    如果(i+j>x)说明这个人再抄下一本书,他就超过那个每个人最多抄(x)的限制了,此时我们要再新找一个人来抄这(j)

    扫到最后,看看这(k)个人在每个人抄写不超过(x)页的情况下能不能把所有书都抄完

    如果抄不完,根据单调性,说明(x)取小了,如果抄完了,说明(x)可能取大了,更新答案,继续二分(x)

    题目还要求后面的人多抄,那么我们只要在统计答案的时候,改为从后向前扫描,依旧贪心地分配任务(尽可能多抄)

    二分结束后求出了最优的(x),再扫一遍整个数组,记录下每个人从第几本抄到第几本即为最终答案

    (Code)

    #include<cstdio>
    #include<cstring>
    #include<queue>
    #include<stack>
    #include<algorithm>
    #include<set>
    #include<map>
    #include<utility>
    #include<iostream>
    #include<list>
    #include<ctime>
    #include<cmath>
    #include<cstdlib>
    #include<iomanip>
    typedef long long int ll;
    inline int read(){
    	int fh=1,x=0;
    	char ch=getchar();
    	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
    	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
    	return fh*x;
    }
    inline int _abs(const int x){ return x>=0?x:-x; }
    inline int _max(const int x,const int y){ return x>=y?x:y; }
    inline int _min(const int x,const int y){ return x<=y?x:y; }
    inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
    inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }
    const int maxn=505;
    int seq[maxn];
    struct Node{
    	int st,ed;
    }wk[maxn];
    int n,k,l,r; //l,r二分最快需要多少时间完成工作 
    
    inline bool check(const int Mid){
    	int t=0,cnt=0;//t记录这个人抄了t页,cnt记录用了cnt个人
    	for(int i=1;i<=n;i++){
    		if(t+seq[i]<=Mid) t+=seq[i];//合法,分配
    		else t=seq[i],cnt++;//超过了二分的值,新找一个人
    		if(cnt>k) return false;//如果用人数已经大于k个了,直接返回false
    	}
    	cnt++;//最后一点任务要分配一个人来抄
    	if(cnt<=k) return true;
    	else return false;
    }
    
    inline void make_ans(const int ans){//最终答案是ans,从后向前贪心分配每个人的工作
    	int t=0,cnt=k;//从后往前安排
    	wk[cnt].ed=n;//最后一个人的最后一本书是n
    	wk[1].st=1;//第一个人的第一本书是1
    	for(int i=n;i>=1;i--){
    		if(t+seq[i]<=ans) t+=seq[i];//抄第i本没有超过ans,贪心分配
    		else{
    			wk[cnt].st=i+1;//记录这个人抄到了第i+1本(因为是倒序枚举的)
    			cnt--;//找一个新的人
    			wk[cnt].ed=i;//新的人的最后一本书是第i本
    			t=seq[i];//新的人已经抄了i页
    		}
    	} 
    }
    
    int main(){
    	n=read(),k=read();
    	for(int i=1;i<=n;i++){
    		seq[i]=read();
    		l=_max(l,seq[i]);
    		r+=seq[i];//更新答案区间上下界
    	}
    	while(l<=r){
    		int x=(l+r)>>1;//套板子
    		if(check(x)) r=x-1;
    		else l=x+1;
    	}
    	make_ans(l);//统计答案
    	
    	for(int i=1;i<=k;i++)
    		printf("%d %d
    ",wk[i].st,wk[i].ed);
    	return 0;
    }
    

    平面最接近点对

    传送门(1)洛谷P1429平面最接近点对(爸爸数据)

    传送门(2)洛谷P1257平面最接近点对(儿子数据)

    当然了这里教大家“打人先打脸,擒贼先擒王,骂人先骂娘”,我们直接拿爸爸开刀,如果切了爸爸,儿子就是双倍经验(爽)

    (Solution)

    不正确的做题姿势(1)

    某数据结构巨佬:我喜欢暴力数据结构,所以我用(KD-Tree)的板子一边喝水一边过了此题(我要是出题人我马上给你卡成(O(n^2))的,让你装13)

    不正确的做题姿势(2)

    俗话说的好:“(n)方过百万,暴力碾标算,管他什么(SA)(rand()),能(AC)的算法就是好算法”

    于是某玄学大师:“啊?我就按照(x)排了个序,如果要求最短,肯定横坐标差不会太大,所以我枚举每个点之前,之后的(10)个点,更新答案,然后就(AC)了……”

    正确的做题姿势:

    好习惯:打开题目看到数据范围——(nleq 2*10^5),猜到正解大概是一个复杂度为(O(nlogn))的算法

    首先,题目随机给定(n)个点的坐标,先不管三七二十一,拿个结构体存下来一点也不亏

    再考虑,如何更快的求解最小距离?突然想到了最小值具有结合律——即整个区间的最小值等于把它分成两个子区间,这两个子区间的最小值(比如线段树(RMQ)

    但是现在手里的数组是无序的,这样没法划分区域,所以我们需要先排序,为了更直观些,我选择了按照横坐标(x)从小到大排序

    假设我们有这样(5)个点,可以按照这(5)个点组成平面中,最中间的那个点,划分成左平面和右平面(中间点归左平面管)

    这样一直递归的划分下去,直到一个平面里只有(2)(3)个点,此时我们暴力求出点距,然后向上合并平面统计(ans=min(ans,min(left,right));)

    简直(Perfect),难道这题就这吗?显然不是,有一种情况被我们忽略了——产生最小点距的两个点可能来自不同的平面

    现在处理这种特殊情况,设左平面和右平面中最接近点对的距离为(d),显然,只有横坐标与中间点横坐标距离不超过(d)的点才可能更新答案

    形象点说,就是这样:

    已知(d)的大小,如果一个左平面的点到中间点横坐标的距离已经(>d),那么它想和右边的点结合,此时两点距离一定(>d),不可能更新答案,对于右平面也是这样

    所以对于上图,我们只用枚举(3、4、5),也就是与中间点(4)横坐标距离小于等于(d)的点即可

    但是这么做,复杂度仍然有可能退化到(O(n^2)),因为可能有这种数据:

    当卡在判断距离之内的点过多时,这么做其实和纯种的暴力没有区别了,怎么办呢?(但是对于这个题来说已经足够了,吸个氧就过掉了)

    发动人类智慧精华:既然我们可以按照横坐标排序划分,为什么不能按纵坐标排序划分呢?

    我们先(O(n))把距离中间点横坐标距离(leq d)的点全都统计出来到一个数组(T)里,然后把数组(T)按照纵坐标排序

    那么我们找可能更新答案的点对时,还可以以纵坐标作为约束,这样,根据某玄学的数学证明方法(鸽巢原理),枚举次数不会超过(6)

    这样再更新一次答案,不停合并直到得到整个平面的最接近点对即可

    Code

    #include<cstdio>
    #include<cstring>
    #include<queue>
    #include<stack>
    #include<algorithm>
    #include<set>
    #include<map>
    #include<utility>
    #include<iostream>
    #include<list>
    #include<ctime>
    #include<cmath>
    #include<cstdlib>
    #include<iomanip>
    typedef long long int ll;
    inline int read(){
    	int fh=1,x=0;
    	char ch=getchar();
    	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
    	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
    	return fh*x;
    }
    inline int _abs(const int x){ return x>=0?x:-x; }
    inline int _max(const int x,const int y){ return x>=y?x:y; }
    inline int _min(const int x,const int y){ return x<=y?x:y; }
    inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
    inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }
    
    inline double _dis(const double x1,const double y1,const double x2,const double y2){
    	return std::sqrt(std::pow(x1-x2,2)+std::pow(y1-y2,2));
    }
    inline double _smin(const double a,const double b,const double c){ return std::min(a,std::min(b,c)); }
    
    const int maxn=200005;
    
    struct Node{
    	double x,y;
    }poi[maxn];
    inline bool cmd1(const Node a,const Node b){ return a.x<b.x; }
    inline bool cmd2(const Node a,const Node b){ return a.y<b.y; }
    
    
    int n;
    
    double find(const int L,const int R){
    	if(R==L+1) return _dis(poi[L].x,poi[L].y,poi[R].x,poi[R].y);
    	if(R==L+2) return _smin(_dis(poi[L].x,poi[L].y,poi[L+1].x,poi[L+1].y),_dis(poi[L+1].x,poi[L+1].y,poi[R].x,poi[R].y),_dis(poi[L].x,poi[L].y,poi[R].x,poi[R].y));
    	//点数<=3暴力统计
    	int Mid=(L+R)>>1;//按x二分平面
    	double d=std::min(find(L,Mid),find(Mid+1,R));//找最小值
    	
    	int cnt=0;
    	Node T[1005];
    	for(int i=L;i<=R;i++)
    		if(poi[i].x>=poi[Mid].x-d&&poi[i].x<=poi[Mid].x+d){
    			T[++cnt].x=poi[i].x;
    			T[cnt].y=poi[i].y;
    		}//O(n)统计所有可能更新答案的点
    		
    	std::sort(T+1,T+cnt+1,cmd2);//按y排个序
    	
    	for(int i=1;i<=cnt;i++)//枚举所有可能更新答案的点
    		for(int j=i+1;j<=cnt;j++){
    			if(T[j].y-T[i].y>=d) break;//如果纵坐标之差超过d,显然不可能更新答案,跳出
    			d=std::min(d,_dis(T[i].x,T[i].y,T[j].x,T[j].y));//尝试更新答案
    		}
    		
    	return d;//返回平面最小值d
    }
    
    int main(){
    	n=read();
    	for(int i=1;i<=n;i++)
    		poi[i].x=read(),poi[i].y=read();//读入点对
    	
    	std::sort(poi+1,poi+n+1,cmd1);//按照x排序
    	
    	double ans=find(1,n);
    	printf("%.4lf",ans);//lf精度别忘了
    	return 0;
    }
    

    感谢您的阅读,给个三连球球辣!(OvO)

  • 相关阅读:
    洛谷—— P2234 [HNOI2002]营业额统计
    BZOJ——3555: [Ctsc2014]企鹅QQ
    CodeVs——T 4919 线段树练习4
    python(35)- 异常处理
    August 29th 2016 Week 36th Monday
    August 28th 2016 Week 36th Sunday
    August 27th 2016 Week 35th Saturday
    August 26th 2016 Week 35th Friday
    August 25th 2016 Week 35th Thursday
    August 24th 2016 Week 35th Wednesday
  • 原文地址:https://www.cnblogs.com/zaza-zt/p/13519836.html
Copyright © 2011-2022 走看看