分治法(divide-and-conquer):将原问题划分成n个规模较小而结构与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。
分治模式在每一层递归上都有三个步骤:
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
分治法的精髓:
分--将问题分解为规模更小的子问题;
治--将这些规模更小的子问题逐个击破;
合--将已解决的子问题合并,最终得出“母”问题的解;
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
上述的第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
1.分治的典型应用之一:归并排序
数组排序任务可以如下完成:
(1)前一半排序
(2)后一半排序
(3)把两半归并到一个新的有序数组,再拷贝回原数组。
对每一半都递归采用上述方法。
即:
#include
using namespace std;
void merge(int a[],int s,int m,int e,int tmp[])//将数组a的局部a[s,m]和a[m+1,e]合并到tmp[],并保证temp有序,然后再拷贝回a[s,e]。注意a[s,m]和a[m+1,e]都各自已经是有序了的
{
int p=0;//p在tmp数组上移动
int p1=s,p2=m+1;//p1在排好序了的前半段数组上移动,p2在排好序了的后半段数组上移动,
while(p1<=m&&p2<=e)
{
if(a[p1]>a[i];
}
int size=sizeof(a)/sizeof(int);//待排序的a数组的元素个数
mergesort(a,0,size-1,b);
for(int i=0;i
2.分治的典型应用之二:快速排序
数组排序任务可以如下完成:
(1)设k=a[0],将k挪到适当位置,使得比k小的元素都在k左边,比k大的元素都在k右边,和k相等的,不关心在k左右出现均可(0 (n) 时间完成)。
(2)把k左边的部分快速排序。
(3)把k右边的部分快速排序。
对左右两边的都递归采用上述方法。
#include
using namespace std;
void swap(int & a,int & b)//交换变量a,b的值,传址方式,参数是引用(注意要加&,这样形参的值改变的话实参的值才会跟着改变)
{
int tmp=a;
a=b;
b=tmp;
}
void Quicksort(int a[],int s,int e)
{
if(s==e)//边界条件,s=e, 也就是头和尾相等,指向的元素是同一个,即数组只有一个元素,此时直接return
return;
if(s=k)
{
j--;
}
swap(a[i],a[j]);//从while循环中出来,可能因为a[j]k,这时要交换一下;
//还可能因为i=j,i和j指向同一个元素,交换一下相当于没交换,无影响
}//做完后a[i]=a[j]=k;
Quicksort(a,s,i-1);
Quicksort(a,i+1,e);
}
}
int main()
{
int a[10];
for(int i=0;i<10;i++)
{
cin>>a[i];
}
int size=sizeof(a)/sizeof(int);//待排序的a数组的元素个数
Quicksort(a,0,size-1);
for(int i=0;i
3.求排列的逆序数
考虑1,2,..,n (n <=,100000) 的排列R1,R2,... Rn, 如果其中存在j,k,满足j < k 且 Rj> Rk, 那么就称(Rj,Rk)是这个排列的一个逆序。
一个排列含有逆序的个数称为这个排列的逆序数。例如排列263451 含有8个逆序(2,1),(6,3), (6,4), (6,5), (6, 1), (3,1),(4,1), (5,1),因此该排列的逆序数就是8。
现给定1,2,...,n的一个排列,求它的逆序数。
输入:
第一行是一个整数n,表示该排列有n个数(n <= 100000)。
第二行是n个不同的正整数,之间以空格隔开,表示该排列。
输出:
输出该排列的逆序数。
样例输入
6
2 6 3 4 5 1
样例输出
8
提示
1. 利用二分归并排序算法(分治);
2. 注意结果可能超过int的范围,需要用long long存储。
解题思路:
1.寻找逆序对的数量,最容易想到的便是双for循环寻找,为O(n^2),效率太低,容易超时,如何将线性查找简化,就是分治,如何利用分治进行查找才是关键。
2.分治O(nlogn)
1.将数组分成两半,分别对左半端和右半段进行求逆序数。
2.再计算有多少个逆序数是由左半段取一个数和右半段取一个数构成(要求O(n)实现)。
步骤2的关键:左半边和右半边都是排好序的。比如,都是从大到小排序的。这样,左右半边只需要从头到尾各扫一遍,就可以找出由两边各取一个数构成的逆序个数。
/*样例输入
6
2 6 3 4 5 1
样例输出
8
样例输入
8
10 3 7 8 12 11 5 2
样例输出
16
*/
#include
using namespace std;
const int maxn=100005;
int a[maxn];
int b[maxn];
long long sum=0;//注意结果可能超过int的范围,需要用long long存储。
void MergeAndCount(int a[],int s,int m,int e,int tmp[])//计算前半段的一个数和后半段的一个数能构成多少个逆序数,并且将数组a的局部a[s,m]和a[m+1,e]合并到tmp[],并保证temp从大到小有序,然后再拷贝回a[s,e]。注意a[s,m]和a[m+1,e]都各自已经是有序了的
{
int p=0;//p在tmp数组上移动
int p1=s,p2=m+1;//p1在排好序了的前半段数组上移动,p2在排好序了的后半段数组上移动
while(p1<=m&&p2<=e)
{
if(a[p1]<=a[p2])//如果左半边的当前这个小于右半边的当前这个(即不能构成逆序数),那么左半边后边的肯定更不能构成(更小)
{
tmp[p]=a[p2];//因为是从大到小排列,故tmp赋值为大的那个
p++;
p2++;
}
else//如果大于
{
tmp[p]=a[p1];
sum+=(e-p2+1);
p++;
p1++;
}
}
while(p1<=m)//若p2走到头了,p1还没有走到头
{
tmp[p]=a[p1];
p++;
p1++;
}
while(p2<=e)//若p1走到头了,p2还没有走到头
{
tmp[p]=a[p2];
p++;
p2++;
}
for(int i=0;i>n;
for(int i=0;i>a[i];
}
MergeSort(a,0,n-1,b);
/*以下是为了看看是否从大到小排列了*/
for(int i=0;i