mooc北大算法课第五周:分治

本文深入解析分治算法的基本概念,通过归并排序和快速排序的实例,详细阐述其原理和应用。同时,针对输出前k大的数和求排列逆序数的练习,提供了高效的分治解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ps:无语无语无语,递归不要再搞我了。。。

分治基本概念
把一个任务,分成形式与原任务相同,但规模更小的几个部分任务(通常使两个部分),分别完成,或只需要选一步完成。然后在处理完成后的这一个或几个部分的结果,实现整个任务的完成。

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

  1. 数组排序任务可以如下完成:
    1) 把前一半排序
    2)把后一半排序
    3)把两半归并到一个新的有序数组,然后再拷贝回原数组,排序完成

归并排序的时间复杂度

对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 = log2n),T(n) = 2^k+k*a*n = 2^k*T(1)+k*a*n = 2^k+k*a*n = n+a*(log2n)*n
复杂度O(nlogn)

代码举例:归并排序

#include<iostream>
using namespace std;
int a[10] = { 13,27,19,2,8,12,2,8,30,89};
int b[10];
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]
//归并操作时间复杂度:0(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[])
{ //a[s]到a[e]连续排序,tmp[]做中转
    if(s < e) {// s = e直接 return
        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 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;
}

总结:

  1. 语法问题
    1)sizeof(a)/sizeof(int)
    sizeof(int)计算int型变量占内存单元
    sizeof(a) 计算 整型数组里元素 占用内存多少单元
    所以sizeof(a)/sizeof(int)计算元素个数
  2. 细节问题
    1)merge()函数中,注意排序后要复制回原数组
  3. 逻辑问题
    1)merge()函数负责数组比较归并,mergesort()函数负责整体排序

分治的典型应用:快速排序

  1. 数组排序任务可以如下完成:
    1)设k = a[0],将k挪到适当位置,使得比k小的元素都在k左边,比k大的元素都在k右边,和k相等的,不关心在k左右出现均可(O(n)时间完成)
    2)把k左边的部分快速排序
    3)把k右边的部分快速排序

代码举例:快速排序

#include<iostream>
using namespace std;
int a[] = {93,27,30,2,8,30,89};
void swap(int & a,int & b) //交换变量a,b值
{// 增加引用符,改a,b, c,d也会变化
    int tmp = a;
    a = b;
    b = tmp;
}

void QuickSort(int a[],int s, int e)
{// 数组a[],起点s,终点e
    if(s >= e) //待排序只有一个元素
      return;
    int k = a[s];
    int i = s, j = e;
    while(i != j) { //比较a[i]和a[j]
        while(j > i && a[j] >= k)
          --j;
        swap(a[i],a[j]);
        while(j < i && a[i] <= k)
          ++i;
        swap(a[i],a[j]);
    }//处理完以后,a[i] = k
    QuickSort(a,s,i-1); //k左边进行快速排序
    QuickSort(a,i+1,e); //k右边进行快速排序
}
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;
}
// 运气不坏:两边数目差不多
// 运气坏:左边0个或右边0个

总结

  1. 语法问题
    1)注意swap函数内参数
    2)考虑s>=e的情况

课后练习1:输出前k大的数
描述:
给定一个数组,统计前k大的数并且把这k个数从大到小输出。

输入:
第一行包含一个整数n,表示数组的大小。n < 100000。
第二行包含n个整数,表示数组的元素,整数之间以一个空格分开。每个整数的绝对值不超过100000000。
第三行包含一个整数k。k < n。

输出:
从大到小输出前k大的数,每个数一行。

解题思路:
从大到小输出前k大的数,每个数一行
排序后再输出,复杂度O(nlogn)
用分治处理:复杂度O(n+klogk) //确保m << n
思路:把前k大的都弄到数组最右边,然后对这最右边k个元素排序,再输出
关键:O(n)时间内实现把前k大的都弄到数组最右边
引入操作arrangeRight(k):把数组(或数组的一部分)前k大的都弄到最右边
1)设key = a[0],将key挪到适当位置,使得比key小的元素都在k的左边,比key大的元素都在key右边(线性时间完成)
2)选择数组的前部或后部再进行arrangeRight操作
a = k done
a > k 对此a个元素再进行arrangeRigth(k)
a < k 对左边b个元素再进行arrangeRight(k-a)
将前k大的都弄到数组最右边的时间:

T(n) = T(n/2) + a*n
     = T(n/4) + a*n/2 + a*n
     = T(n/8) + a*n/4 + a*n/2 + a*n
     = ...
     = T(1) + ... + a*n/8 + a*n/4 + a*n/2 + a*n
     < 2*a*n
即O(n)

样例输入:

10
4 5 6 9 8 7 1 2 3 0
5

样例输出:

9
8
7
6
5

代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int a[100010];
void swap(int & a,int & b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
void FindMaxK(int a[],int s,int e,int k)
{
	if( s - e +1 == k )
		return;
	int key = a[s]; //注意key值不是k值
	int i = s,j = e;
	while( i != j ) {
		while( j > i &&  a[j] >= key )
			 --j; //注意符号在前
		swap(a[i],a[j]);
		while( i < j && a[i] <= key ) 
			++i; //注意符号在前
		swap(a[i],a[j]);
	}
	if( e-i+1 == k)
		return;
	else if(  e-i+1 > k)
		FindMaxK(a,i+1,e,k);
	else
		FindMaxK(a,s,i-1,k-e+i-1);
}
int main()
{
	int n;
	scanf("%d",&n);
	for(int i = 0;i < n; ++i)
		scanf("%d",&a[i]); 
	int k;
	scanf("%d",&k);
	FindMaxK(a,0,n-1,k);
	//for(int i = 0;i < n; ++i)
	//	cout << a[i] << ",";
	//cout << endl; 
	sort(a+n-k-1,a+n);
	for(int i = n-1;i >= n-k; --i) //注意打印内容从n-k开始
		printf("%d\n",a[i]);
	return 0;
}

总结:

  1. 语法问题
    1)sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp)
    进行排序的时间复杂度为n*log2n
    first:排序数组的起始地址
    last:结束地址(最后一个数据的后一个数据的地址)
    comp:升序或降序,如果不写,默认从小到大排序
  2. 逻辑问题
    1)虽然但是!这题看似不难然而偶套快速排序还是没套好。。。
    注意这道题和快速排序题最大的区别是这道题只需打印出前k个最大的数,所以不用完全排序。
    如果第一遍分类结束,右边最大值刚好k个,直接输出;
    如果大于k个,再对右边排序;
    如果小于k个,再对左边排序,取最大的k-e+i-1个值

课后练习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个不同的正整数,之间以空格隔开,表示该排列。

输出:
输出该排列的逆序数。

解题思路:
解题思路
笨办法:O(n^2)
分治:O(nlogn)
1)将数组分为两半,分别求出左半边的逆序数和右半边的逆序数
2)再算有多少逆序是由左半边取一个数和右半边取一个数构成(要求O(n)实现)
关键: 左半边和右半边都是排好序的。比如,都是从大到小排序的。这样,左右半边只需要从头到尾各扫一遍,就可以找出由两边各取一个数构成的逆序个数
总结:由归并排序改进得到,加上计算逆序的步骤

样例输入:

6
2 6 3 4 5 1

样例输出:

8

提示:
提示

  1. 利用二分归并排序算法(分治);
  2. 注意结果可能超过int的范围,需要用long long存储。

代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int a[100010];
int b[100010];
void Merge(int a[],int s,int m, int e,int tmp[])
{// 对数组进行排序
	int pb = 0;
	int p1 = s,p2 = m+1;
	/*cout << "merge1" << endl;
	for(int i = s; i <= e; i++)
	  cout << a[i] << ",";
	cout << endl;*/
	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];
	}
	/*cout << "merge2" << endl;
	for(int i = 0; i <= 5; i++)
	  cout << a[i] << ",";
	cout << endl;*/
}

long long Count(int a[],int s,int m, int e)
{// 计算逆序值
	long long result = 0;
	int p1 = s,p2 = m+1;
	/*cout << "count1" << endl;
	for(int i = s; i <= e; i++)
	  cout << a[i] << ",";
	cout << endl;*/
	while( p1 <= m && p2 <= e) {
		if( a[p1] > a[p2]) {
			result += e-p2+1; //如果a[p1]>a[p2],则必然大于p2后面所有值 
			++p1;
		}
		else 
			++p2;
	}
	/*cout << "count2" << endl;
	for(int i = s; i <= 5; i++)
	  cout << a[i] << ",";
	cout << endl;*/
	return result;
}

long long MergeSort(int a[],int s,int e,int  tmp[])
{// 总体方案
	long long result = 0;
	if( s < e) {
		int m = s + (e-s)/2;
		result += MergeSort(a,s,m,tmp); //分别求两边的逆序数 
		//cout<<"左边 result="<<result << endl; 
		result += MergeSort(a,m+1,e,tmp);
		//cout<<"右边 result="<<result << endl; 
		result += Count(a,s,m,e); //然后再o(n)算左边和右边造成的逆序数 。此时要求左边和右边都是从大到小有序的,才能在o(n)时间内算出结果 
		//cout<<"左右 result="<< result << endl; 
		Merge(a,s,m,e,tmp); //从大到小合并,确保排序 
	}
	return result;
}
int main()
{
	int n;
	scanf("%d",&n);
	for(int i = 0;i < n; ++i)
		scanf("%d",&a[i]);
	printf("%lld",MergeSort(a,0,n-1,b));
	return 0;
}
/*
6
2 6 3 4 5 1
左边 result=0
右边 result=0

count1
2,6,
count2
2,6,3,4,5,1,
左右 result=0

merge1
2,6,
merge2
6,2,3,4,5,1,
左边 result=0
右边 result=0

count1
6,2,3,
count2
6,2,3,4,5,1,
左右 result=1
    
merge1
6,2,3,
merge2
6,3,2,4,5,1,
左边 result=1
左边 result=0
右边 result=0

count1
4,5,
count2
4,5,1,
左右 result=0

merge1
4,5,
merge2
6,3,2,5,4,1,
左边 result=0
右边 result=0

count1
5,4,1,
count2
5,4,1,
左右 result=2

merge1
5,4,1,
merge2
6,3,2,5,4,1,
右边 result=3

count1
6,3,2,5,4,1,
count2
6,3,2,5,4,1,
左右 result=8

merge1
6,3,2,5,4,1,
merge2
6,5,4,3,2,1,
8
*/

总结:

  1. 逻辑问题
    1)这道题花了好久去做。。为毛?因为就是想不懂为啥它直接排序难道不需要记住原来的下标嘛,不然怎么知道是逆序?想不通。。把重要结果打印了一遍发现:
    原数组:2 6 3 4 5 1
    即先用count计算2,6的逆序数,然后用merge给2,6排序,当作一个整体再和3比较
    即 count:2|6 result=0 merge: 2 6——> 6 2 count: 6 2|3 result=1
    然后2 6 3计算结束后,进行后半部分 4 5 1的计算同上
    最后进行6 3 2|5 4 1计算
    其实方法思路里已经写的很明确了,但是偶递归学的太差勒。。。。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值