Loading

分治

分治

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

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("\n");
	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);
	
}

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

posted @ 2020-08-01 12:17  hyl天梦  阅读(406)  评论(0编辑  收藏  举报