【算法】分治法详解

一、分治法介绍

分治法:

即分而治之,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
eg:快速排序

动态规划:(参考:https://blog.youkuaiyun.com/qq_36935691/article/details/113888868)

通常用于求解最优解问题,与分治法类似,其基本思想也是将待求解问题分解成若干子问题,先求解子问题,然后从这些子问题的解得到原问题的解。

与分治法不同的是,适合用动态规划求解的问题,经分解得到的子问题往往不是相互独立的
若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题会被重复计算多次。
而动态规划的做法是将已解决子问题的答案保存下来,在需要子问题答案的时候便可直接获得,而不需要重复计算,这样就可以避免大量的重复计算,提高效率。(记忆化搜索(MEMO)、递归化递推)

算法范式(算法设计的设计模式):
在这里插入图片描述

二、例题介绍:

Question1:寻找多数

在这里插入图片描述
思路:
根据多数元素的定义可知多数元素必定在数组中出现次数最多,而若将原数组分成2部分,则在其中一部分出现次数最多的必定是多数元素,证明如下:
参考:https://blog.youkuaiyun.com/liudehuatw/article/details/116832113

假定数组长度为n,多数元素为a,出现次数为f(a),已知:fn(a) > n/2 将数组分成两部分,长度分别为l,r,其中l+r = n
若假设fl(a) <= l/2, fr(a) <= r/2 则fn(a) <= n/2,与已知条件相矛盾,故假设不成立
故将原数组分成2部分,则在其中一部分出现次数最多的必定是多数元素

#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

int countNumber(int nums[], int num, int left, int right){
	int count = 0;
	for(int i=left; i<=right; i++){
		if(nums[i]==num) count++;
	}
	return count;
} 

int majorityElement(int nums[], int left, int right){
//	数组只有一个数,则这个数是该数组的众数 
	if(left==right) return nums[left];
	int mid = (right-left)/2 + left;
//	获取左半数组的众数 
	int left_majority = majorityElement(nums, left, mid);
//	获取右半数组的众数 
	int right_majority = majorityElement(nums, mid+1, right);
//	若左右两边众数相同,则该数是该数组的众数 
	if(left_majority==right_majority) return left_majority; 
	else{
		int left_count = countNumber(nums, left_majority, left, mid);
		int right_count = countNumber(nums, right_majority, mid+1, right);
		return left_count > right_count ? left_majority : right_majority;
	}
}

int main(){
	int n;
	cin>>n;
	vector<int> nums;
	int num;
	while (cin>>num) {
		nums.push_back(num);
		if (cin.get() == '\n') break;
	}
	int arr[nums.size()];
	for(int i=0;i<nums.size();i++){
		arr[i] = nums[i];
	}
	cout<<majorityElement(arr, 0, nums.size()-1);
	return 0;
}

方法二:sort排序+输出

#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

int main()
{
	int n;
	cin>>n;
	vector<int> nums;
	int num;
	while (cin>>num) {
		nums.push_back(num);
		if (cin.get() == '\n') break;
	}
	sort(nums.begin(), nums.end());
	cout<<nums[n/2]<<endl;
	return 0;
}

Question2:找到 k 个最小数

在这里插入图片描述
思路:(参考:https://blog.youkuaiyun.com/qq_15026111/article/details/105356456)
使用sort函数对整个数组进行排序,然后取前k个最小的数字。目前最高效的排序算法是快速排序,其采用了分治法的思想。这里打算手撕快速排序,而不用algorithm头文件中的sort()函数,借以巩固分治法的基础。

优化点:
题目要求取前 K 个数字,所以快排流程不需要完全执行完,只要判断枢纽元素的位置与 K 的关系即可。

一次快速排序的划分:

int partition(int arr[], int low, int high){
	int i = low;
	int j = high+1;
	int pivot = arr[low]; //设置数组第一位为临时枢纽
	while(true){
		while(arr[++i]<pivot){
			if(i==high) break;
		}
		while(arr[--j]>pivot){
			if(j==low) break;
		}
		if(i>=j) break;
		swap(arr, i, j);
	}
	swap(arr, low, j);
	return j;
}

一次划分后 得到的数组大小与 k 的关系:k与m

if(k==m){ //快排划分一次后,正好找到最小的k个数 
		return;
	}
	else if(k<m){ //快排划分一次后,最小的k个数都在前m个数当中,继续递归划分 
		partitionArray(arr, low, m-1, k);
	}
	else{ //快排划分一次后,还有k-m个数在右侧数组当中,继续递归划分 
		partitionArray(arr, m+1, high, k);
	}

partitionArray(arr, m+1, high, k);
细节:
当 m < k 时,我们需要进行第二种类型的划分,即在剩余数组中寻找剩余的 k-m 个最小数,那么你是否有疑问,既然已经找到了 m个最小数,还差 k-m 个,那么是不是最后一个参数也要传 k-m?
其实这里的 k 的含义并不是剩余需要寻找的数字的个数,而是一个相对于整个数组而言,所需的前 K小的数字的个数,无论你要在数组的哪个范围内进行划分,这个标准不能变。
还有一种解释方法是,因为首指针没有从 0 开始计算,而是 m+1 ,如果首指针从 0 开始计算的话,那么最后一个参数可以为 k-m,但是这样不方便进行递归操作。

完整代码:

#include<iostream>
#include<algorithm> 
using namespace std;

void swap(int arr[], int i, int j){
	int temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}

// partition 函数和快速排序中相同,具体可参考快速排序相关的资料
// 代码参考 Sedgewick 的《算法4》
int partition(int arr[], int low, int high){
	int i = low;
	int j = high+1;
	int pivot = arr[low]; //设置数组第一位为临时枢纽
	while(true){
		while(arr[++i]<pivot){
			if(i==high) break;
		}
		while(arr[--j]>pivot){
			if(j==low) break;
		}
		if(i>=j) break;
		swap(arr, i, j);
	}
	swap(arr, low, j);
	return j;
}

void partitionArray(int arr[], int low, int high, int k){
	int m = partition(arr, low, high);
	if(k==m){ //快排划分一次后,正好找到最小的k个数 
		return;
	}
	else if(k<m){ //快排划分一次后,最小的k个数都在前m个数当中,继续递归划分 
		partitionArray(arr, low, m-1, k);
	}
	else{ //快排划分一次后,还有k-m个数在右侧数组当中,继续递归划分 
		partitionArray(arr, m+1, high, k);
	}
}

void printArr(int arr[], int k){
	sort(arr, arr+k);
	for(int i=0; i<k; i++)
	{
		cout<<arr[i]<<" ";
	}
}

int main(){
	int len, k;
	cin>>len>>k;
	int num;
	int nums[len];
	for(int i=0; i<len; i++){
		cin>>nums[i];
	}
	
	if(k==0) {
        cout<<"0";
        return 0;
    }else if (len <= k) {
//  打印数组
		printArr(nums, k);
		return 0;
    }
//  开始划分数组 
    partitionArray(nums, 0, len-1, k); 
    int res[k] = {0};
    for(int i=0; i<k; i++){
    	res[i] = nums[i];
	} 
    printArr(nums, k);
	return 0;
}

方法二:sort排序+输出

#include<iostream>
#include<vector>
#include<algorithm> 
using namespace std;

int main(){
	int len, k;
	cin>>len>>k;
	int num;
	vector<int> nums;
	while(cin>>num){
		nums.push_back(num);
		if(cin.get()=='\n') break; 
	} 
	sort(nums.begin(),nums.end());
	for(int i=0; i<k-1; i++){
		cout<<nums[i]<<" ";
	}
	cout<<nums[k-1];
	return 0;
}

附:
快速排序算法

void Quick_Sort(int *arr, int begin, int end){
    if(begin > end)
        return;
    int tmp = arr[begin];
    int i = begin;
    int j = end;
    while(i != j){
        while(arr[j] >= tmp && j > i)
            j--;
        while(arr[i] <= tmp && j > i)
            i++;
        if(j > i){
            int t = arr[i];
            arr[i] = arr[j];
            arr[j] = t;
        }
    }
    arr[begin] = arr[i];
    arr[i] = tmp;
    Quick_Sort(arr, begin, i-1);
    Quick_Sort(arr, i+1, end);
}
//原文链接:https://blog.youkuaiyun.com/qq_40941722/article/details/94396010
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值