本人由于水平有限,笔记难免有疏漏与不严谨的地方,请大家给予批评指正,谢谢!
二、分治
分治的本质就是把原问题划分成若干个与原问题结构相似的子问题进行递归求解,并同时在回溯时解出原问题。
具体来说,分治的过程分为以下三个步骤:
1.划分问题:将原问题划分成若干子问题;
2.递归求解:分别求解若干个子问题;
3.合并问题:通过求解若干子问题,解出原问题;
例题:归并排序
归并排序所运用的原理就是分治。
我们对于原序列划分成左右两个等数量的连续序列,对这些子序列进行排序,最终通过对有序的子序列处理,从而对该序列进行排序。
具体来说,我们考虑递归的边界情况:给只有两个数的序列排序。
显然,仅需比较大小即可。
对于有四个数的序列呢?
例如:9,1,6,4
先将此序列划分左右两个子序列,对它们进行排序;
此时,9>1,因此,左边子序列为1,9;同样的,右边的序列为4,6。
原序列就变成:1,9,4,6。
这时我们只需要分别从左右两个序列中从小到大依次遍历,比较大小即可。
1和4,1<4,原序列第一个为1;
9>4,原序列第二个为4;
同理,6是第三个;
那最后剩下的怎么办?直接放进排过序的新序列后面,因为此时剩下的一定同时属于左右子序列其中一个,而左右子序列都已经有序,故剩下的一定也有序。
#include<iostream> #include<cstdio> #define maxn 500001 using namespace std; int n,a[maxn]={},x[maxn]={}; void merge_sort(int s,int t){ //边界 if(s==t)return; int mid=s+(t-s)/2,i=s,j=mid+1,k=s; //求解子序列,分治 merge_sort(s,mid); merge_sort(mid+1,t); //合并,算法之精髓 while(i<=mid||j<=t){ if(j>t||(i<=mid&&a[i]<a[j]))x[k++]=a[i++]; else x[k++]=a[j++]; } //将排好序的x数组一一对应赋值a数组 for(k=s;k<=t;k++)a[k]=x[k]; return; } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d",&a[i]); merge_sort(1,n); for(int i=1;i<=n;i++)printf("%d ",a[i]); return 0; }
练习:洛谷P1908 逆序对
题目描述
对于给定的一段正整数序列,逆序对就是序列中ai>aj且i<j的有序对,求给定的一段正整数序列中逆序对的数目。
输入格式
第一行,一个数n,表示序列中有n个数。
第二行n个数,表示给定的序列。序列中每个数字不超过10^9109
输出格式
给定序列中逆序对的数目。
输入输出样例
5 4 2 6 3 1
说明/提示
对于25%的数据,n≤2500
对于50%的数据,n≤4×104。
对于所有数据,n≤5×105
这道题可以在归并排序同时,顺便记一下数量。
为什么可以这样呢?
对于一个序列来讲,可以统计划分成的左右子序列各自的逆序对个数。
但是,对于左子序列而言,序列中每一个数的原下标都是小于右子序列的。
因此,在对于两子序列的合并时,由于我们在排序时需要比较大小,所以当右边的数小于左边的数时,这时逆序对数就又加了mid-i+1个。
#include<iostream> #include<cstdio> using namespace std; long long num=0,a[1000000]={},t1[1000000]={}; void T(int s,int t){ if(s==t)return; int mid=(s+t)/2,i=s,j=mid+1,k=s; T(s,mid); T(mid+1,t); while(i<=mid||j<=t){ if(j>t||(i<=mid&&a[i]<=a[j]))t1[k++]=a[i++]; else{ t1[k++]=a[j++]; num+=mid-i+1; } } for(i=s;i<=t;i++)a[i]=t1[i]; } int main(){ int n; cin>>n; for(int i=0;i<n;i++)cin>>a[i]; T(0,n-1); cout<<num<<endl; return 0; }
例题:循环日程表问题
输入正整数k表示有n=2^k个运动员进行循环比赛,需要设计比赛日程表。每个选手与其他n-1个选手各赛一次;每个选手一天只能赛一次;循环赛一共进行n-1天。
按此要求设计一张比赛日程表,该表有n行和n-1列,第i行第j列表示第i个选手第j天遇到的选手。
我们讨论k=1的情况,只有两个运动员;
再看k=2的情况,可先安排1和2比赛,3和4比赛,划分成两个更小的问题进行求解。
然后将左上复制到右下,右上复制到左下,即可得到k=2的循环表
以此类推,对于任意一个正整数k,都可以考虑它的k/2的循环表,再进行相应的复制。
这题就迎刃而解了。
例题:洛谷P1498 南蛮图腾
题目描述
自从到了南蛮之地,孔明不仅把孟获收拾的服服帖帖,而且还发现了不少少数民族的智慧,他发现少数民族的图腾往往有着一种分形的效果,在得到了酋长的传授后,孔明掌握了不少绘图技术,但唯独不会画他们的图腾,于是他找上了你的爷爷的爷爷的爷爷的爷爷……帮忙,作为一个好孙子的孙子的孙子的孙子……你能做到吗?
输入格式
每个数据一个数字,表示图腾的大小(此大小非彼大小) n<=10
输出格式
这个大小的图腾
输入输出样例
输入
3
输出
/\
/__\
/\ /\
/__\/__\
/\ /\
/__\ /__\
/\ /\ /\ /\
/__\/__\/__\/__\
这道题和上一题一样,将大图形划分成三个小图形进行求解,注意边界条件以及二维数组的横纵坐标之间的关系
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
char a[2049][2049]={};
int n;
inline void _read(int &x){
char ch=getchar();bool mark=false;
for(;!isdigit(ch);ch=getchar())if(ch=='-')mark=true;
for(x=0;isdigit(ch);ch=getchar())x=(x<<1)+(x<<3)+ch-'0';
if(mark)x=-x;
}
void solve(int x,int y,int k){
if(k==4){
a[x][y]=a[x+1][y-1]='/';
a[x+1][y]=a[x+2][y]='_';
a[x+2][y-1]=a[x+3][y]='\\';
return;
}
solve(x,y,k>>1);
solve(x+(k>>2),y-(k>>2),k>>1);
solve(x+(k>>1),y,k>>1);
return;
}
int main(){
memset(a,' ',sizeof(a));
_read(n);
n=1<<(n+1);
solve(1,n>>1,n);
for(int j=1;j<=(n>>1);j++){
for(int i=1;i<=n;i++)printf("%c",a[i][j]);
puts("");
}
return 0;
}