zoukankan      html  css  js  c++  java
  • 分治

    分治

    所谓分治 简而言之就是分而治之,就是说把任何一个题目分成几段,并逐段去解决,最终达到我们想要的效果。对于任何一个题目来说,“分”俨然成了十分重要的一步,如何分段和能否想到使用分段的思想去解决问题,俨然成了解决分治题目的关键。下面让我们结合题目来看看使用分值的方法。

    1 归并排序

    分段:把一个序列运用递归不断地从中间“劈开”,使其最终成为单个的个体,然后两两合并。
    解决:排序方法如下:

    有一个序列集合: {1 9 7 4 23 6}
    一直分段下去 变成{1}{9}{7}{4}{23}{6}
    两两合并(其实也就是回溯的过程){1,9}{7}{4}{6,23} 并对这几个合并的结合排序变成:{1,9}{7}{4}{6,23}

    下一步我们就要对1 9 7进行排序(变成1 7 9),对4 6 23 排序变成(4,6,23)

    然后你会发现,问题似乎已经转化成了把两个有序序列合并为一个有序序列。

    代码:

    void msort(int l,int r)
    {
    	if(l>=r) return;
    	int mid=l+r>>1,i=l,j=mid+1,h=l;
    	msort(l,mid);msort(mid+1,r);
    	while(i<=mid&&j<=r)
    	{
    		if(a[i]<=a[j]) c[h++]=a[i++];
    		else c[h++]=a[j++];
    	}
    	while(i<=mid) c[h++]=a[i++];
    	while(j<=r) c[h++]=a[j++];
    	for(int q=l;q<=h-1;q++) a[q]=c[q];
    }
    

    利用归并我们可以来解决逆序对问题,题目http://ybt.ssoier.cn:8088/problem_show.php?pid=1237
    相信认真读过代码的读者已经发现,在把两个有序数列合并为一个有序数列时要进行比较,通过比较把合并完的数列复制给c数组,然后再把c数组排序之后的序列将a数组对应位置的值覆盖,即l到r,使得a数组中l到r的序列为有序数列。
    在比较的过程中,可以统计逆序对个数。
    代码如下,不过多做讲解,请读者自行解决

    void msort(int l,int r)
    {
    	if(l>=r) return;
    	int mid=l+r>>1,i=l,j=mid+1,h=l;
    	int t=0;
    	msort(l,mid);msort(mid+1,r);
    	while(i<=mid&&j<=r)
    	{
    		if(a[i]<=a[j]) c[h++]=a[i++],ans+=t;
    		else c[h++]=a[j++],t++;
    	}
    	while(i<=mid) c[h++]=a[i++],ans+=t;
    	while(j<=r) c[h++]=a[j++];
    	for(int q=l;q<=h-1;q++) a[q]=c[q];
    }
    

    2快速排序

    快速排序相对来说比较好理解,其分治方法为:随便取一个序列中的值,把所有小于该值的数放到该值的左边,把所有大于该值的数都放到右边,然后在左边和右边分别进行此操作。

    void qsort(int l,int r)
    {
    	int mid=l+r>>1,i=l,j=r;
    	for(int q=l;q<=r;q++)
    	{
    		if(a[q]<a[mid]) c[i++]=a[q];
    		if(a[q]>a[mid]) c[j--]=a[q];
    	}
    	int now=a[mid];
    	for(int q=l;q<=r;q++) a[q]=c[q];
    	if(l<=i-1) qsort(l,i-1);
    	if(j+1<=r) qsort(j+1,r);
    }
    

    至于中间的for(int q=l;q<=r;q++) a[q]=c[q];是为了处理和该值相同的情况,这里博主随便取的值为a[mid]。
    那么和逆序对可以用归并解决一样,对于快速排序也有题目可以用它解决。

    题目1:输出前K大的数http://ybt.ssoier.cn:8088/problem_show.php?pid=1235
    思路稍有变化,在我们取了a[mid]后,如果发现其正是第k大的数,那把a[mid]和比他大的数全部输出,
    否则,如果该值是第n大的数,若n<k,则往左边找,否则往右边找。
    注意:以上思路并没有谈到有数值相等的请况,读者在尝试时不要忘记处理数值相等的情况。

    当然这个题也可以sort拍完序后一遍过

    题目2:统计数字http://ybt.ssoier.cn:8088/problem_show.php?pid=1239
    这个题本可以map一遍过,但题目说不让用STL,只得用快速排序。
    思路大致如下,在原先的快速排序模板中,我们看到for(int q=l;q<=r;q++) a[q]=c[q];,其实l到r之间(如果还有数值的话)都是和a[mid]相等的,那么我们就可以很轻松地只得a[mid]这个值的出现次数。
    代码

    #include<iostream>
    #include<cstdio>
    #define dd double
    #define ll long long
    #define N 200001
    #define M number
    using namespace std;
    
    ll n;
    ll a[N],c[N];
    
    void qsort(int l,int r)
    {
    	int mid=l+r>>1,i=l,j=r;
    	for(int q=l;q<=r;q++)
    	{
    		if(a[q]<a[mid]) c[i++]=a[q];
    		if(a[q]>a[mid]) c[j--]=a[q];
    	}
    	int now=a[mid];
    	for(int q=l;q<=r;q++) a[q]=c[q];
    	if(l<=i-1) qsort(l,i-1);
    	cout<<now<<" "<<j-i+1<<endl;
    	if(j+1<=r) qsort(j+1,r);
    //这个题也要注意一下顺序,一定要先qsort左边,在输出当前的统计,在qsort右边,以顺应题目要求。
    }
    
    int main()
    {
    	cin>>n;
    	for(int i=1;i<=n;i++) cin>>a[i];
    	qsort(1,n);
    }
    

    那么根据分治的定义“分而治之”,其实我们很常用的二分法也是一个分治,有些题目更是分段后再分段。我们先来看一道浮点型二分题
    题目:二分法求函数的零点http://ybt.ssoier.cn:8088/problem_show.php?pid=1241
    那么这道题就是一道裸的二分,思路不多讲,我们直接来看程序:

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<algorithm>
    #include<cstring>
    #include<sstream>
    #include<queue>
    #include<map>
    #include<vector>
    #include<set>
    #include<deque>
    #include<cstdlib>
    #include<ctime>
    #define dd double
    #define ll long long
    #define N number
    #define M number
    using namespace std;
    
    dd l=1.5;
    dd r=2.4;
    dd egp=1e-8;
    
    dd ksm(dd a,int b)
    {
    	dd re=1;
    	while(b>0)
    	{
    		if(b&1) re*=a;
    		a=a*a;
    		b>>=1;
    	}
    	return re;
    }
    
    bool check(dd x)
    {
    	if(ksm(x,5)-ksm(x,4)*15+ksm(x,3)*85-ksm(x,2)*225+x*274-121<0) return 1;
    	else return 0;
    }
    
    int main()
    {
    	dd mid;
    	while(r-l>egp)
    	{
    		mid=(r+l)/2;
    		if(check(mid)) r=mid;
    		else l=mid+egp;
    	}
    	printf("%0.6lf",mid);
    }
    

    这里需要注意一个地方,题目虽然说四舍五入到小数点之后6位,但我们在确定精度时却至少要高上两位,即1e-8(10的-8次方)不然会出错。并且要在else的后面+-egp。二分的关键就是要保证l到r中全是合法值并且正确答案就在l到r中。

    在看一道暴力二分题:一元三次方程求解http://ybt.ssoier.cn:8088/problem_show.php?pid=1238
    很多小伙伴看到这个题都无处下手,三次函数交x轴三次,那就要把这个函数劈成三段,使得每一段中都只有一个值,怎么分呢?
    其实枚举是个不错的想法,根的取值已经给出,我们在其中枚举两个相邻的整数,看其中是否有根,如果有的话,这两个整数的函数值正负一定是不同的(题目中说根与根之间差的绝对值≥1)
    所以如果遇到符合上述条件的整数,在其中二分就好了。
    代码:

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<vector>
    #include<queue>
    #include<map>
    #include<sstream>
    #define N 100001
    #define ll long long
    #define dd double
    using namespace std;
    
    dd eps=1e-3,a,b,c,d;
    
    dd f(dd x)
    {
    	return a*x*x*x+b*x*x+c*x+d;
    }
    
    int main()
    {
    	
    	cin>>a>>b>>c>>d;
    	for(int i=-100;i<=99;i++)
    	{
    		dd l=i,r=i+1;
    		dd x1=f(l),x2=f(r);
    		if(!x1) printf("%.2lf ",l);
    		if(x1*x2<0)
    		{
    			while(r-l>eps)
    		  {
    			  dd mid=(l+r)/2;
    			  if(f(mid)*f(r)<=0) l=mid;
    			  else r=mid-eps;
    		  }
    		  printf("%.2lf ",r);
    		}
    	}
    }
    

    我们最后在看一道不寻常的分治题目:
    题目:黑白棋子的移动http://ybt.ssoier.cn:8088/problem_show.php?pid=1327

    这个题许多小伙伴看了都无从下手,但实际上,我们发现在黑白棋子各只剩下4个之前的移动策略是相同的,那么我们就可以递归模拟这些策略,当只剩下4个时我们又发现无论之前一共有多少黑白棋子,这时的策略都不会变,所以就有以下程序:

    程序:

    #include<iostream>
    #include<cstdio>
    #include<cmath>
    #include<algorithm>
    #include<cstring>
    #include<sstream>
    #include<queue>
    #include<map>
    #include<vector>
    #include<set>
    #include<deque>
    #include<cstdlib>
    #include<ctime>
    #define dd double
    #define ll long long
    #define N 100000
    #define M number
    using namespace std;
    
    char a[N];
    int n,t;
    int where;
    
    void print()
    {
    	printf("step%2d:",t);
    	for(int i=1;i<=2*n+2;i++) cout<<a[i];
    	printf("
    ");
    	t++;
    }
    
    void kaiban(int c)
    {
    	int b=where;
    	for(int i=0;i<2;i++)
    	{
    		a[b+i]=a[c+i];
    		a[c+i]='-';
    	}
    	where=c;
    	print();
    }
    
    int banjia(int n)//the last o
    {
    	if(n!=4)
    	{
    		kaiban(n);kaiban(n*2-1);
    		banjia(n-1);
    	}
    	else
    	{
    		kaiban(4);kaiban(8);kaiban(2);kaiban(7);kaiban(1);
    	}
    }
    
    int main()
    {
    	
    	cin>>n;
    	for(int i=1;i<=n;i++) a[i]='o';
    	for(int i=n+1;i<=2*n;i++) a[i]='*';
    	for(int i=2*n+1;i<=2*n+2;i++) a[i]='-';
    	where=2*n+1;
    	print();
    	
    	banjia(n);
    	
    }
    

    这周通过刷题发现,自己的思维似乎已经固化了,一些简单的题却不能很好地运用分治的思想去解决。编程做题一半在码力,一半在思想,要注重每个算法的思想,掌握用思想解决问题的能力。

  • 相关阅读:
    Kotlin之类属性延迟初始化
    Android 之ANR
    Android之Handler基础篇
    Android Handler进阶篇
    Android 进程与线程管理
    Android 启动模式LaunchMode详解(LaunchMode四种模式详解)
    Android 应用版本号配置修改
    Android ViewGroup
    Android app与Activity主题配置
    Android 本地序列化
  • 原文地址:https://www.cnblogs.com/TianMeng-hyl/p/13413296.html
Copyright © 2011-2022 走看看