分治法:
分治法(divide-and-conquer):将原问题划分成n个规模较小而结构与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。
分治模式在每一层递归上都有三个步骤:
- 分解(Divide):将原问题分解成一系列子问题;
- 解决(conquer):递归地解各个子问题。若子问题足够小,则直接求解;
- 合并(Combine):将子问题的结果合并成原问题的解。
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
分治法的精髓:
分--将问题分解为规模更小的子问题;
治--将这些规模更小的子问题逐个击破;
合--将已解决的子问题合并,最终得出“母”问题的解;
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
上述的第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
1.分治的典型应用之一:归并排序
数组排序任务可以如下完成:
(1)前一半排序
(2)后一半排序
(3)把两半归并到一个新的有序数组,再拷贝回原数组。
对每一半都递归采用上述方法。
即:
- 分解:将n个元素分成各含n/2个元素的子序列;
- 解决:用合并排序法对两个子序列递归地排序;
- 合并:合并两个已排序的子序列以得到排序结果。
#include<iostream>
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[p2])
{
tmp[p]=a[p1];
p++;
p1++;//上面三行可以简写为tmp[p++]=a[p1++];
}
else
{
tmp[p]=a[p2];
p++;
p2++;
}
}
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<e-s+1;i++)//将这两部分排好序放在tmp里了后,再拷贝回a[s,e]注意是a[s,e](一部分),而不是全部a[0,size-1]
{
a[s+i]=tmp[i];
}
}
void mergesort(int a[],int s,int e,int tmp[])
{
if(s==e)//边界条件,s=e, 也就是头和尾相等,指向的元素是同一个,即数组只有一个元素,此时直接return
return;
if(s<e)
{
int m=s+(e-s)/2;//数组中间元素
mergesort(a,s,m,tmp);
mergesort(a,m+1,e,tmp);
merge(a,s,m,e,tmp);
}
}
int main()
{
int a[10],b[10];
for(int i=0;i<10;i++)
{
cin>>a[i];
}
int size=sizeof(a)/sizeof(int);//待排序的a数组的元素个数
mergesort(a,0,size-1,b);
for(int i=0;i<size;i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
return 0;
}
2.分治的典型应用之二:快速排序
数组排序任务可以如下完成:
(1)设k=a[0],将k挪到适当位置,使得比k小的元素都在k左边,比k大的元素都在k右边,和k相等的,不关心在k左右出现均可(0 (n) 时间完成)。
(2)把k左边的部分快速排序。
(3)把k右边的部分快速排序。
对左右两边的都递归采用上述方法。
#include<iostream>
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<e)
{
int k=a[s];//a数组一部分的第一个元素
int i=s,j=e;//i从头开始,负责前一半;j从尾开始,负责后一半
while(i!=j)
{
while(i<j&&a[j]>=k)
{
j--;
}
swap(a[i],a[j]);//从while循环中出来,可能因为a[j]<k,这时要交换一下;
//还可能因为i=j,i和j指向同一个元素,交换一下相当于没交换,无影响
while(i<j&&a[i]<=k)
{
i++;
}
swap(a[i],a[j]);//从while循环中出来,可能因为a[i]>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<size;i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
return 0;
}
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<iostream>
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<e-s+1;i++)
{
a[s+i]=tmp[i];
}
}
void MergeSort(int a[],int s,int e,int tmp[])
{
if(s==e)
{
return;
}
if(s<e)
{
int m=s+(e-s)/2;
MergeSort(a,s,m,tmp);
MergeSort(a,m+1,e,tmp);
MergeAndCount(a,s,m,e,tmp);
}
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
MergeSort(a,0,n-1,b);
/*以下是为了看看是否从大到小排列了*/
for(int i=0;i<n;i++)
{
cout<<a[i];
}
cout<<endl;
cout<<sum<<endl;
return 0;
}