分治的基本概念:
把一个任务,分成形式和原任务相同,但规模更小的几个部分任务(通常是两个部分),分别完成,或只需要选一部完成。然后再处理完成后的这一个或几个部分的结果,实现整个任务的完成。
分治的典型应用:归并排序:
1) 把前一半排序
2) 把后一半排序
3) 把两半归并到一个新的有序数组,然后再拷贝回原数组,排序完成。
#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,并保证tmp有序,然后再拷贝回a[s,m]
//归并操作时间复杂度:O(e-m+1),即O(n)
int pb = 0;
int p1 = s,p2 = m+1;
while( p1 <= m && p2 <= e) {
if( a[p1] < a[p2])
tmp[pb++] = a[p1++];
else
tmp[pb++] = a[p2++];
}
while( p1 <= m)
tmp[pb++] = a[p1++];
while( p2 <= e)
tmp[pb++] = a[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) {
int m = s + (e-s)/2;
MergeSort(a,s,m,tmp);
MergeSort(a,m+1,e,tmp);
Merge(a,s,m,e,tmp);
}
}
int a[10] = { 13,27,19,2,8,12,2,8,30,89};
int b[10];
int main()
{
int size = sizeof(a)/sizeof(int);
MergeSort(a,0,size-1,b);
for(int i = 0;i < size; ++i)
cout << a[i] << ",";
cout << endl;
return 0;
}
归并排序的时间复杂度:
对n个元素进行排序的时间:
T(n) = 2*T(n/2) + a*n (a是常数,具体多少不重要)
= 2*(2*T(n/4)+a*n/2)+a*n
= 4*T(n/4)+2a*n
= 4*(2*T(n/8)+a*n/4)+2*a*n
= 8*T(n/8)+3*a*n
...
= 2 k *T(n/2 k )+k*a*n
一直做到 n/2 k = 1 (此时 k = log 2 n),
T(n)= 2 k *T(1)+k*a*n = 2 k *T(1)+k*a*n = 2 k +k*a*n= n+a*(log 2 n)*n
复杂度O(nlogn)
分治的典型应用:快速排序
1)设k=a[0], 将k挪到适当位置,使得比k小的元素都在k左边,比k大的元素都在k右边,和k相等的,不关心在k左右出现均可 (O(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)
return;
int k = a[s];
int i = s,j = e;
while( i != j ) {
while( j > i && a[j] >= k )
--j;
swap(a[i],a[j]);
while( i < j && a[i] <= k )
++i;
swap(a[i],a[j]);
} //处理完后,a[i] = k
QuickSort(a,s,i-1);
QuickSort(a,i+1,e);
}
int a[] = { 93,27,30,2,8,12,2,8,30,89};
int main()
{
int size = sizeof(a)/sizeof(int);
QuickSort(a,0,size-1);
for(int i = 0;i < size; ++i)
cout << a[i] << ",";
cout << endl;
return 0;
}
1:输出前k大的数
描述
给定一个数组,统计前k大的数并且把这k个数从大到小输出。
输入
第一行包含一个整数n,表示数组的大小。n < 100000。
第二行包含n个整数,表示数组的元素,整数之间以一个空格分开。每个整数的绝对值不超过100000000。
第三行包含一个整数k。k < n。
输出
从大到小输出前k大的数,每个数一行。
样例输入
10
4 5 6 9 8 7 1 2 3 0
5
样例输出
9
8
7
6
5
我的想法比较浅薄,使用的是对数组直接进行排序,然后输出后K个数即可,emmm,时间复杂度为O(nlogn)
C++程序实现:
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int n;
cin >> n;
int *arr = new int[n];
for(int i = 0; i < n; i++){
cin >> arr[i];
}
int result;
cin >> result;
sort(arr, arr + n);
for(int i = result,m = n-1; i > 0; i--,m--){
cout<<arr[m]<<endl;
}
return 0;
}
用分治处理:复杂度 O(n+mlogm)
思路:把前m大的都弄到数组最右边,然后对这最右边m个元素排序,再输出
关键 :O(n)时间内实现把前m大的都弄到数组最右边
引入操作 arrangeRight(k): 把数组(或数组的一部分)前k大的都弄到最右边
如何将前k大的都弄到最右边
1)设key=a[0], 将key挪到适当位置,使得比key小的元素都在key左边,比key大的元素都在key右边(线性时间完成)
2) 选择数组的前部或后部再进行 arrangeRight操作
C++代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
int n;
void quickSort(int arr[], int s, int e){
if(s >= e)
return;
int temp = arr[s];
int i = s, j = e;
while(i < j){
while(j > i && arr[j] >= temp)
j--;
swap(arr[i], arr[j]);
while(i < j && arr[i] <= temp)
i++;
swap(arr[i], arr[j]);
}
quickSort(arr, s, i-1);
quickSort(arr, i+1, e);
}
void arrangeRight(int a[], int k, int s, int e){
if(s <= e){
int kk = a[s];
int i = s, j = e;
while(i < j){
while( j > i && a[j] >= kk)
--j;
swap(a[i], a[j]);
while( i < j && a[i] <= kk)
++i;
swap(a[i], a[j]);
}
if(e - i + 1 == k) return;
else if(e - i + 1 > k) arrangeRight(a, k, i+1, e);
else arrangeRight(a, k-(e-i+1), s, i-1);
}
}
int main()
{
cin >> n;
int *arr = new int[n];
for(int i = 0; i < n; i++){
cin >> arr[i];
}
int m;
cin >> m;
arrangeRight(arr, m, 0, n-1);
quickSort(arr, n - m,n - 1);
for(int i = n - 1; i >= n - m; --i)
cout << arr[i]<<" ";
cout<<endl;
return 0;
}
2:求排列的逆序数
描述
在Internet上的搜索引擎经常需要对信息进行比较,比如可以通过某个人对一些事物的排名来估计他(或她)对各种不同信息的兴趣,从而实现个性化的服务。
对于不同的排名结果可以用逆序来评价它们之间的差异。考虑1,2,…,n的排列i1,i2,…,in,如果其中存在j,k,满足 j < k 且 ij > ik, 那么就称(ij,ik)是这个排列的一个逆序。
一个排列含有逆序的个数称为这个排列的逆序数。例如排列 263451 含有8个逆序(2,1),(6,3),(6,4),(6,5),(6,1),(3,1),(4,1),(5,1),因此该排列的逆序数就是8。显然,由1,2,…,n 构成的所有n!个排列中,最小的逆序数是0,对应的排列就是1,2,…,n;最大的逆序数是n(n-1)/2,对应的排列就是n,(n-1),…,2,1。逆序数越大的排列与原始排列的差异度就越大。
现给定1,2,…,n的一个排列,求它的逆序数。
输入
第一行是一个整数n,表示该排列有n个数(n <= 100000)。
第二行是n个不同的正整数,之间以空格隔开,表示该排列。
输出
输出该排列的逆序数。
样例输入
6
2 6 3 4 5 1
样例输出
8
提示
1. 利用二分归并排序算法(分治);
2. 注意结果可能超过int的范围,需要用long long存储。
思路一:
不就是计算逆序数个数吗?遍历数组暴力求解很快很容易想到啊,不过会超时!!!!
#include <iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int *arr = new int[n];
for(int i = 0; i < n; i++){
cin >> arr[i];
}
long long result = 0;
for(int i = 0;i <= n-2; i++){
for(int j = i+1;j <= n-1;j++){
if(arr[i] > arr[j])
result++;
}
}
cout<<result<<endl;
return 0;
}
思路二:
根据提示,必须进行分治了,but 怎么进行分治呢。只能想到模仿二路归并了,划分成一个个小区域,在每个区域进行查找逆序数,再归并到一起。
分治O(nlogn):
1) 将数组分成两半,分别求出左半边的逆序数和右半边的逆序数
2) 再算有多少逆序是由左半边取一个数和右半边取一个数构成(要求O(n)实现)的关键:左半边和右半边都是排好序的。比如,都是从大到小排序的。这样,左右半边只需要从头到尾各扫一遍,就可以找出由两边各取一个数构成的
逆序个数
C++程序实现:
#include <iostream>
using namespace std;
int arr[100010],tmp[100010];
long long result = 0;
int n;
void leapmix(int l, int mid, int r){
int y = l;
int z = mid + 1;
int num = l;
while(y <= mid && z <= r){
if(arr[y] <= arr[z]){
tmp[num++] = arr[y++];
}
else{
tmp[num++] = arr[z++];
result += (mid-y+1);
}
}
while(y <= mid){
tmp[num++] = arr[y++];
}
while(z <= r){
tmp[num++] = arr[z++];
}
for(int i = l; i <= r; i++)
arr[i] = tmp[i];
}
void leapSort(int l, int r){
if(l>=r)
return;
int mid = (l + r)/2;
leapSort(l, mid);
leapSort(mid+1, r);
leapmix(l, mid, r);
}
int main()
{
cin >> n;
for(int i = 0; i < n; i++){
cin >> arr[i];
}
leapSort(0, n-1);
cout << result << endl;
return 0;
}
这里计算逆序数使用了 result += (mid-y+1); 这里如何理解呢?
我们以输入2 6 3 4 5 1为例,
归并为 (2,6) (3) || (4,5) (1)
其中(2,6)和(3)分别满足顺序排列,无逆序数,对(2,6)(3)归并,2<3,接着比较6 > 3,此时mid=2,y=2,mid-y+1=1,逆序数+1.
(4,5) 和(1)也分别满足顺序排列,无逆序数,对(4,5) (1)归并,4>1,此时mid=2,y=1,mid-y+1=2,逆序数+2,为什么会加2呢?因为(4,5)序列已经顺序排列,而4>1那么肯定5也大于1,即是说(a1,a2,....am)(an),其中a1<a,但a2>an,则(a2,...am)均会大于an,所以逆序数的个数为(a2,...am)的元素个数,即m-2+1。
最后一次归并为(2,3,6)和(1,4,5)合并,
因为2>1则mid-y+1=3,逆序数+3, 接着2<4 3<4 6>4,逆序数+1,6>5,逆序数+1.
逆序数=8。
快速求幂的应用:
如果用递归的方法求幂, 代码可以是这样的:
double Pow(double x, unsigned int n)
{
if (n == 0)
return 1;
if (n == 1)
return x;
if (n & 1 == true) // 如果n是奇数
return Pow(x * x, n / 2) * x;
else // 如果n是偶数
return Pow(x * x, n / 2);
}
虽然简单, 但是效率并不高. 因为函数调用的代价非常昂贵. 用循环实现的效率更高.
用循环做的话,当然不能直接死乘。举个例子:
3 ^ 999 = 3 * 3 * 3 * … * 3
直接乘要做998次乘法。但事实上可以这样做,先求出2^k次幂:
3 ^ 2 = 3 * 3
3 ^ 4 = (3 ^ 2) * (3 ^ 2)
3 ^ 8 = (3 ^ 4) * (3 ^ 4)
3 ^ 16 = (3 ^ 8) * (3 ^ 8)
3 ^ 32 = (3 ^ 16) * (3 ^ 16)
3 ^ 64 = (3 ^ 32) * (3 ^ 32)
3 ^ 128 = (3 ^ 64) * (3 ^ 64)
3 ^ 256 = (3 ^ 128) * (3 ^ 128)
3 ^ 512 = (3 ^ 256) * (3 ^ 256)
再相乘:
3 ^ 999 = 3 ^ (512 + 256 + 128 + 64 + 32 + 4 + 2 + 1)
= (3 ^ 512) * (3 ^ 256) * (3 ^ 128) * (3 ^ 64) * (3 ^ 32) * (3 ^ 4) * (3 ^ 2) * 3
这样只要做16次乘法。即使加上一些辅助的存储和运算,也比直接乘高效得多(尤其如果这里底数是成百上千位的大数字的话)。
我们发现,把999转为2进制数:1111100111,其各位就是要乘的数。这提示我们利用求二进制位的算法(其中mod是模运算):
所以就可以写出下面的代码:
int Pow(int a,int b)
{ //快速求a^b ,复杂度 log(b)
int result = 1;
int base = a;
while(b) {
if( b & 1) // 等价于 if (n % 2 != 0)
result *= base;
base *= base;
b >>= 1;
}
return result;
}