一次性弄懂到底什么叫做分治思想(含有大量经典例题,附带详细解析)
Posted yinbiao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一次性弄懂到底什么叫做分治思想(含有大量经典例题,附带详细解析)相关的知识,希望对你有一定的参考价值。
期末了,通过写博客的方式复习一下算法,把自己知道的全部写出来
分治:分而治之,把一个复杂的问题分解成很多规模较小的子问题,然后解决这些子问题,把解决的子问题合并起来,大问题就解决了
但是我们应该在什么时候用分治呢?这个问题也困扰了我很久,做题的时候就不知道用什么算法
能用分治法的基本特征:
1.问题缩小到一定规模容易解决
2.分解成的子问题是相同种类的子问题,即该问题具有最优子结构性质
3.分解而成的小问题在解决之后要可以合并
4.子问题是相互独立的,即子问题之间没有公共的子问题
第一条大多数问题都可以满足
第二条的大多数问题也可以满足,反应的是递归的思想
第三条:这个是能分治的关键,解决子问题之后如果不能合并从而解决大问题的话,那凉凉,如果满足一,二,不满足三,即具有最优子结构的话,可以考虑贪心或者dp
第四条:如果不满足第四条的话,也可以用分治,但是在分治的过程中,有大量的重复子问题被多次的计算,拖慢了算法效率,这样的问题可以考虑dp(大量重复子问题)
了解了什么问题可以采用分治,那么分治到达怎么用?步骤是什么呢
三个步骤:
1.分解成很多子问题
2.解决这些子问题
3.将解决的子问题合并从而解决整个大问题
化成一颗问题树的话,最底下的就是很多小问题,最上面的就是要解决的大问题,自底向上的方式求解问题
说的再多不如看经典的样例,更好的体会分治的思想
样例1:二分查找
条件:数组有序,假设是升序数组
虽然二分很容易,但是我还是要具体从算法思想分治的方向分析一下
现在我们要在一个有序的升序数组里面查找一个数x有没有
暴力的做法就是拿跟数组里面每个数比较一下,有的话就返回下标,这个是大问题
仔细想一下,就知道这个大问题是由很多小问题组成的,小问题:在数组的一部分里面找x
那么我们可以把数组分成很多部分,在很多部分里面找x,如果在这些部分里面没有找到x,那么把这些子问题合并起来,就是大数组里面没有x,否则就是有x
这个真的很好的反应了分治的思想,先分解成很多小问题,解决这些小问题,把解决的小问题合并起来,大问题就解决了,二分具体的做法我就不多说了,都知道,贴个代码
#include<string.h> #include<stdio.h> int k; int binarysearch(int a[],int x,int low,int high)//a表示需要二分的有序数组(升序),x表示需要查找的数字,low,high表示高低位 { if(low>high) { return -1;//没有找到 } int mid=(low+high)/2; if(x==a[mid])//找到x { k=mid; return x; } else if(x>a[mid]) //x在后半部分 { binarysearch(a,x,mid+1,high);//在后半部分继续二分查找 } else//x在前半部分 { binarysearch(a,x,low,mid-1); } } int main() { int a[10]={1,2,3,4,5,6,7,8,9,10}; printf("请输入需要查找的正数字: "); int x; scanf("%d",&x); int r=binarysearch(a,x,0,9); if(r==-1) { printf("没有查到 "); } else { printf("查到了,在数列的第%d个位置上 ",k+1); } return 0; }
经典样例二:全排列问题
有1,2,3,4个数,问你有多少种排列方法,输出来
仔细想想,采用分治的话,我们就要把大问题分解成很多的子问题,大问题是所有的排列方法
那么我们分解得到的小问题就是以1开头的排列,以2开头的排列,以3开头的排列,以4开头的排列
现在这些问题有能继续分解,比如以1开头的排列中,只确定了1的位置,没有确定2,3,4的位置,把2
3,4三个又看成大问题继续分解,2做第二个,3做第二个,或者4做第二个
一直分解下去,直到分解成的子问题只有一个数字的时候,不再分解
因为1个数字肯定只有一种排列方式啊,现在我们分解成了很多的小问题,解决一个小问题就合并,合并成
一个大点的问题,合并之后这个大点的问题也解决了,再将这些大点的问题合并成一个更大的问题,那么这
个更大点的问题也解决了,直到最大的问题解决为止
这个就是用分治的思想解决全排列问题,我主要想分析的是分治的思想者全排列问题上是怎么用的,不想分析具体全排列的做法,因为我觉得思想比方法更重要,在解题的时候深有体会,因为又的时候没有题是你做过的原题,全排列问题的具体做法参考我的这篇博客:https://www.cnblogs.com/yinbiao/p/8684313.html,也贴一下代码
#include<string.h> #include<stdio.h> int k=0; char a[100]; long long count=0;//全排列个数的计数 void s(char a[],int i,int k)//将第i个字符和第k个字符交换 { char t=a[i]; a[i]=a[k]; a[k]=t; } void f(char a[],int k,int n) { if(k==n-1)//深度控制,此时框里面只有一个字符了,所以只有一种情况,所以输出 { puts(a); count++; } int i; for(i=k;i<n;i++) { s(a,i,k); f(a,k+1,n); s(a,i,k);//复原,就将交换后的序列除去第一个元素放入到下一次递归中去了,递归完成了再进行下一次循环。这是某一次循环程序所做的工作,这里有一个问题,那就是在进入到下一次循环时,序列是被改变了。可是,如果我们要假定第一位的所有可能性的话,那么,就必须是在建立在这些序列的初始状态一致的情况下,所以每次交换后,要还原,确保初始状态一致。 } } int main() { gets(a); int l=strlen(a);//字符串长度 f(a,k,l); printf("全排列个数:%lld ",count); return 0; }
经典样例三:整数划分问题
给你一个数,问你所有的划分方式,比如4,4=1+3,4=1+1+2,4=2+2,4=1+1+1+1
我们来分析一下,我们想用分治的话,就要找子问题,假设n是要划分的数,m说最大的加数,n=4,m=3
分解成两类的子问题,一个是:一个是有m的情况,一个是没有m的情况,然后将有m的情况继续划分,分
解成有m-1和没有m-1的情况,一直划分下去,直到m=1,比如n=4,m=3,划分成的子问题:有3,无
3,有2,无2,有1,无1(没有意义,除非0+4=4),将这些子问题合并起来大问题就解决了,比如有
3:1+3,没有3分成有2,和无2,有2:1+1+2,2+2,无2分成有1:1+1+1+1,一共四种解决方案
我们来理一下思路:划分成子问题,解决这些子问题,合并
但是注意:这个问题里面的子问题有很多是重复的,大量重复子问题,比如n=5,m=4,1+4=5,1+1+
3=5,2+3=5,求3有几种划分方法的时候求了2次,如果n很大的话,那么就会有大量的重复子问题,这个时候可以采用dp(自己有点不理解重复子问题重复在哪里,觉得哪里有点不对劲)
分析了一下题中分治的思想,具体做法参考我的这篇博客:https://www.cnblogs.com/yinbiao/p/8672198.html,也贴个代码
/* 整数划分问题 :将一个整数划分为若干个数相加 例子: 整数4 最大加数 4 4=4 1+3=4 1+1+2=4 2+2=4 1+1+1+1=4 一共五种划分方案 注意:1+3=4,3+1=4被认为是同一种划分方案 */ #include<stdio.h> int q(int n,int m)//n表示需要划分的数字,m表示最大的家数不超过m { if(m==1||n==1)//只要存在一个为1,那么划分的方法数肯定只有一种,那就是n个1相加 { return 1; }else if(n==m&&n>1)//二者相等且大于1的时候,问题等价于:q(n,n-1)+1;意味着将最大加数减一之后n的划分数,然后加一,最后面那个一代表的是:0+n,这个划分的方案 { return q(n,n-1)+1; }else if(n<m)//如果m>n,那么令m=n就ok,因为最大加数在逻辑上不可能超过n { return q(n,n); }else if(n>m) { return q(n,m-1)+q(n-m,m);//分为两种:划分方案没有m的情况+划分方案有m的情况 } return 0; } int main() { printf("请输入需要划分的数字和最大家数: "); int n,m; scanf("%d %d",&n,&m); int r=q(n,m); printf("%d ",r); return 0; }
经典样例4:归并排序
把一个无序的数组,变成一个有序的数组,这个是大问题,根据分治的思想,要分解成很多的小问题,比如
无序数组8个数,要使得数组有序,即使得这8个数有序,分解成两个子问题:使得前面4个数有序,使得后
面的四个数有序,然后继续分解,在前面的4个数字中,又把它看成一个大问题,继续分解成两个小问题:
使得前面两个数有序,使得后面两个数有序,直到小问题数组中只有一个数为止,因为一个数的数组肯定是
有序的,小问题解决之后,还需要合并成一个大一点的问题,这样这个大一点的问题就也解决了,然后将两
个大一点的问题继续合并成一个更大一点的问题,这样这个更大一点的问题也解决了,直到最后,最大的问
题也解决了,这个就是分治思想在归并排序中的应用
也贴个代码,附带详细的解析
/* 归并排序 思想: 1.分而治之,将一个无序的数列一直一分为二,直到分到序列中只有一个数的时候,这个序列肯定是有序的,因为只有一个数,然后将两个只含有一个数字的序列合并为含有两个数字的有序序列,这样一直进行下去,最后就变成了一个大的有序数列 2.递归的结束条件是分到最小的序列只有一个数字的时候 时间复杂度分析: 最坏情况:T(n)=O(n*lg n) 平均情况:T(n)=O(n*lg n) 稳定性:稳定(两个数相等的情况,不用移动位置 辅助空间:O(n) 特点总结: 高效 耗内存(需要一个同目标数组SR相同大小的数组来运行算法) */ #include<stdio.h> #define max 1024 int SR[max],TR[max]; int merge(int SR[],int TR[],int s,int m,int t)//SR代表两个有序序列构成的序列,s表示起始位置,m表示两个序列的分解位置,但是SR[m]仍是属于前面一个序列,t表示结束位置 {//TR是一个空数组,用来存放排序好之后的数字 int i=s,j=m+1,k=s; while(i<=m&&j<=t) { if(SR[i]<SR[j]) { TR[k++]=SR[i++]; }else { TR[k++]=SR[j++]; } } while(i<=m)//当前面一个序列有剩余的时候,直接把剩余数字放在TR的后面 { TR[k++]=SR[i++]; } while(j<=t)//当后面一个序列有剩余的时候,直接把剩余数字放在TR的后面 { TR[k++]=SR[j++]; } return 0; }//该函数要求SR是由两个有序序列构成 void copy(int SR[],int TR[],int s,int t)//把TR赋给SR { int i; for(i=s;i<=t;i++) { SR[i]=TR[i]; } } int mergesort(int SR[],int s,int t) { if(s<t)//表示从s到t有多个数字 { int m=(s+t)/2;//将序列一分为二 mergesort(SR,s,m);//前一半序列继续进行归并排序 mergesort(SR,m+1,t);//后一半序列同时进行归并排序, //以上递归调用的结束条件是s!<t,也就是进行分到只有一个数字进行归并排序的时候,一个序列只有一个数字,那么这个序列肯定是有序的 //以上都是属于“分”的阶段,目的是获得两个有序的数列 merge(SR,TR,s,m,t);//对这两个有序的数列,进行排序,变成一个同样大小但是有序的数列 copy(SR,TR,s,t);//将在TR中排序好的数列给SR,方便SR递归调用归并排序,因为每次两个归并排序的结果都是保存在TR中的,现在要进行下一步就必须在TR数列的基础上面=进行,所以我们把TR给SR }else//表示从s到t只有一个数字(s==t),或者没有数字(s>t) { ;//空,也可省略,加一个else只是为了更好的理解程序 } return 0; } int main() { int n; printf("请输入排序数字的个数: "); scanf("%d",&n); int i; for(i=0;i<n;i++) { scanf("%d",&SR[i]); } mergesort(SR,0,n-1);//升序排列 for(i=0;i<n;i++) { printf("%d ",SR[i]); } printf(" "); return 0; }
经典样例五:棋盘覆盖问题
不知道棋盘覆盖问题的请自行百度
在棋盘的某个位置给了你一个不可覆盖点,现在大问题是问我们怎么用L形状块覆盖整个棋盘,现在我们要把大问题分解成很多的子问题:把整块大棋盘分成同样大小的四个棋盘,直到分解成的棋盘大小为1,就是只有一个格子的时候,不再分解,所以最小的子问题就是四个格子的棋盘,如果这个四个格子的棋盘有不可覆盖点的话,那么就进行棋盘覆盖,如果没有的话就进行覆盖点的构造然后在覆盖(先不讲怎么判断,怎么构造,只讲思想,具体做法我有专门的博客),所以这样我们就解决了这个四个格子的棋盘,把所有的这样的小问题解决的,也就是把解决好的小棋盘合并起来不就构成了我们需要的大棋盘吗?
理清一下思路:
分解棋盘(分解成四个小棋盘,一直分解下去,直到棋盘大小为1)
解决问题(是直接覆盖还是先构造再覆盖)
合并已经解决的问题(将已经解决的所有小问题合并起来就构成了我们需要覆盖的大棋盘,且此时大棋盘也
已经覆盖好了)
棋盘问题具体做法请参考我的这篇博客:https://www.cnblogs.com/yinbiao/p/8666209.html
也贴一下代码吧
#include<stdio.h> #define max 1024 int cb[max][max];//最大棋盘 int id=0;//覆盖标志位 int chessboard(int tr,int tc,int dr,int dc,int size)//tr,tc代表棋盘左上角的位置,dr ,dc代表棋盘不可覆盖点的位置,size是棋盘大小 { if(size==1)//如果递归到某个时候,棋盘大小为1,则结束递归 { return 0; } int s=size/2;//使得新得到的棋盘为原来棋盘大小的四分之一 int t=id++; if(dr<tr+s&&dc<tc+s)//如果不可覆盖点在左上角,就对这个棋盘左上角的四分之一重新进行棋盘覆盖 { chessboard(tr,tc,dr,dc,s); }else//因为不可覆盖点不在左上角,所以我们要在左上角构造一个不可覆盖点 { cb[tr+s-1][tc+s-1]=t;//构造完毕 chessboard(tr,tc,tr+s-1,tc+s-1,s);//在我们构造完不可覆盖点之后,棋盘的左上角的四分之一又有了不可覆盖点,所以就对左上角棋盘的四分之一进行棋盘覆盖 } if(dr<tr+s&&dc>=tc+s)//如果不可覆盖点在右上角,就对这个棋盘右上角的四分之一重新进行棋盘覆盖 { chessboard(tr,tc+s,dr,dc,s); }else//因为不可覆盖点不在右上角,所以我们要在右上角构造一个不可覆盖点 { cb[tr+s-1][tc+s]=t; chessboard(tr,tc+s,tr+s-1,tc+s,s);//在我们构造完不可覆盖点之后,棋盘的右上角的四分之一又有了不可覆盖点,所以就对右上角棋盘的四分之一进行棋盘覆盖 } if(dr>=tr+s&&dc<tc+s)//如果不可覆盖点在左下角,就对这个棋盘左下角的四分之一重新进行棋盘覆盖 { chessboard(tr+s,tc,dr,dc,s); }else//因为不可覆盖点不在左下角,所以我们要在左下角构造一个不可覆盖点 { cb[tr+s][tc+s-1]=t; chessboard(tr+s,tc,tr+s,tc+s-1,s);//在我们构造完不可覆盖点之后,棋盘的左下角的四分之一又有了不可覆盖点,所以就对左下角棋盘的四分之一进行棋盘覆盖 } if(dr>=tr+s&&dc>=tc+s)//如果不可覆盖点在右下角,就对这个棋盘右下角的四分之一重新进行棋盘覆盖 { chessboard(tr+s,tc+s,dr,dc,s); }else//因为不可覆盖点不在右下角,所以我们要在右下角构造一个不可覆盖点 { cb[tr+s][tc+s]=t; chessboard(tr+s,tc+s,tr+s,tc+s,s);//在我们构造完不可覆盖点之后,棋盘的右下角的四分之一又有了不可覆盖点,所以就对右下角棋盘的四分之一进行棋盘覆盖 } //后面的四个步骤都跟第一个类似 } int main() { printf("请输入正方形棋盘的大小(行数): "); int n; scanf("%d",&n); printf("请输入在%d*%d棋盘上不可覆盖点的位置: ",n,n); int i,j,k,l; scanf("%d %d",&i,&j); printf("不可覆盖点位置输入完毕,不可覆盖点的值为-1 "); cb[i][j]=-1; chessboard(0,0,i,j,n); for(k=0;k<n;k++) { printf("%2d",cb[k][0]); for(l=1;l<n;l++) { printf(" %2d",cb[k][l]); } printf(" "); } return 0; }
经典样例六:快速排序
快速排序中分治的思想体现在哪里呢?
首先我们要了解快速排序的思想,选择一个基准元素,比基准元素大的放基准元素后面,比基准元素小的放
基准元素前面,这个叫做分区,每次分区都使得一个元素有序,进行很多次分区以后,数组就是有序数组
了,为什么是这样呢?因为每次分区,我们都使得了基准元素有序,以比基准元素小的为例,这些元素都比
基准元素小,放在基准元素前面,但这些比基准元素小的元素自己是无序的,确定的位置只有基准元素位
置,有序之后这些元素与基准元素的相对位置是不会变的,变的只有这些元素自己内部的位置,因为进行一
次分区就可以使得一位元素有序,所以进行很夺次分区以后,数组就是有序的了,
那么分治的思想到底体现在哪里呢/
第一步:把大问题分解成很多子问题(每次使得一位元素有序,分区操作可以做到)
第二步:解决子问题(进行分区操作,每次使得一位元素有序)
第三步:所有子问题解决了那么最大的问题也解决了
再简单分析一下:第一次分区是对整个数组进行分区,确定了第一个基准元素的位置,然后对比基准元素大
的和比基准元素小的进行分区,确定第二个和第三个基准元素的位置,如果序列够好的话(每次分区时,比
基准元素大的元素和比基准元素小的元素每次都一样多)n*logn时间可解决
关于快排的具体做法请参考我的这篇博客:https://www.cnblogs.com/yinbiao/p/8805233.html
也贴个代码吧(随机化快排,基准元素选择是随机的)
#include<bits/stdc++.h> using namespace std; #define n 5 int a[n]; void swap_t(int a[],int i,int j) { int t=a[i]; a[i]=a[j]; a[j]=t; } int par(int a[],int p,int q) { int i=p;//p是轴 int x=a[p]; for(int j=p+1;j<=q;j++) { if(a[j]<=x) { i++; swap_t(a,i,j); } } swap_t(a,p,i); return i;//轴位置 } int Random(int p,int q) { return rand()%(q-p+1)+p; } int Randomizedpar(int a[],int p,int q) { int i=Random(p,q); swap_t(a,p,i);//第一个和第i个交换,相当于有了一个随机基准元素 return par(a,p,q); } void RandomizedQuickSort(int a[],int p,int q) { if(p<q) { int r=Randomizedpar(a,p,q); printf("%d到%d之间的随机数:%d ",p,q,r); RandomizedQuickSort(a,p,r-1); RandomizedQuickSort(a,r+1,q); } } int main() { int i; for(i=0;i<n;i++) { scanf("%d",&a[i]); } RandomizedQuickSort(a,0,n-1); for(i=0;i<n;i++) { printf("%d ",a[i]); } return 0; }
经典样例七:求第k小/大元素
这是快排分区思想的应用,也要进行分区操作,和快排不同的是,快排分区之后还有继续处理基准元素
两边的数据,而求k小/大不用,只用处理一边即可
假如现在这里5个元素,分为1,2,3,4,5号位置
第一种情况:假设求第3小元素,假设第一次分区的基准元素完成分区后在第2号位置,那么我们知道3>2
所以只要对基准元素后面的元素继续分区就可以(注意k的值要变了,k代表的是在升序有序数组的1相对位
置,现在对第一次分区的基准元素后面的元素进行分区操作,区间大小是变小了的,所以k值是要跟着变的)
讲了这么多,所以分治的思想到底体现在哪里呢?
跟快排一样,有分区操作,所以分治的思想在这里的体现和在快排的体现都是一样的,不同的是这里只要对
基准元素前面元素或者后面元素进行继续分区(如果需要继续分区的话),而快排是基准元素两边都要继续
分区的
贴个代码(采用的是随机分区)
#include<bits/stdc++.h> using namespace std; void swap_t(int a[],int i,int j) { int t=a[i]; a[i]=a[j]; a[j]=t; } int par(int a[],int p,int q)//p是轴,轴前面是比a[p]小的,后面是比a[p]大的 { int i=p,x=a[p]; for(int j=p+1;j<=q;j++) { if(a[j]>=x) { i++; swap_t(a,i,j); } } swap_t(a,p,i); return i;//返回轴位置 } int Random(int p,int q)//返回p,q之间的随机数 { return rand()%(q-p+1)+p; } int Randomizedpar(int a[],int p,int q) { int i=Random(p,q); swap_t(a,p,i);//第一个和第i个交换,相当于有了一个随机基准元素 return par(a,p,q); } int RandomizedSelect(int a[],int p,int r,int k) { if(p==r) return a[p]; int i=Randomizedpar(a,p,r); int j=i-p+1; printf("i=%d j=%d ",i,j); if(k<=j) return RandomizedSelect(a,p,i,k); else return RandomizedSelect(a,i+1,r,k-j); } int main() { int n; scanf("%d",&n); int a[n]; for(int i=0;i<n;i++) { scanf("%d",&a[i]); } int x=RandomizedSelect(a,0,n-1,2); printf("%d ",x); }
样例大概就是这些
还有一个很重要的知识点差点忘了复习,分治的主定理
分治的一般形式:
T(N)=aT(N/b)+f(n)
1.a==1 T(n)=O(logn)
2.a!=1 T(n)=O(n的logb a)次方
3. a==b T(n)=O(n*log b a)
4 a<b T(n)=O(n)
5. a>b T(n)=O(n的log b a次方)
用于估算分治算法的时间复杂的(数学log 的指数和底数不好表示。。。)
以上是关于一次性弄懂到底什么叫做分治思想(含有大量经典例题,附带详细解析)的主要内容,如果未能解决你的问题,请参考以下文章