北京大学MOOC 算法基础第五周 分治算法 测试

分治的基本概念:

把一个任务,分成形式和原任务相同,但规模更小的几个部分任务(通常是两个部分),分别完成,或只需要选一部完成。然后再处理完成后的这一个或几个部分的结果,实现整个任务的完成。

分治的典型应用:归并排序:

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;
}

猜你喜欢

转载自blog.csdn.net/qq_25406563/article/details/83052303