Leetcode刷题Task01:分治算法(C++)

一、概念

百度百科中分治算法的概念为:

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。

二、思想或策略

分治算法的主要思想是将原问题分成若干个规模较小的子问题,这些子问题互相独立且与原问题形式相同,直到子问题满足边界条件,递归或迭代地解这些子问题(一般可以直接求解或简单计算)。然后将各子问题的解层层合并得到原问题的解,这种思想或策略就叫做分治法。

【理解】分治字面意思为‘分而治之’,即把一个复杂的问题分解为几个相同或相似的子问题,再把子问题分解为更小的子问题,直到子问题可以简单求解,那么原问题就是这些子问题解的合并。

三、步骤

  1. 分:递归或迭代地将原问题分解为各个的子问题(性质相同的、相互独立的子问题)
  2. 治:若子问题规模足够小则直接解决,否则递归解决各子问题
  3. 合并:逐层合并各子问题的解得到原问题的解

在这里插入图片描述

四、分治适用的情况

  1. 原问题的计算复杂度随着问题的规模的增加而增加
  2. 原问题可以分解为相同或相似的子问题(类似递归)
  3. 分解的子问题小到一定程度可以轻易解决
  4. 子问题的解可以合并成整个问题的解
  5. 各个子问题相互独立,即不包含公共子问题

【关键】

  • 能否利用分治法完全取决于问题是否具有第四条特征,如果具备了第一、二和三条特征,而不具备第四条特征,则可以考虑用贪心法或动态规划法。
  • 第五条涉及分治的效率,独立子问题分别求解可以免去很多判断条件以及不必要的工作,如果是重复解决公共子问题还是使用DP算法好。

五、C++实现分治算法的一般模板

void divided_conquer(problem,param1,param2,...){
	if(达到解决小问题的条件){
		//直接返回 或 简单计算得到子问题结果
	}
	mid=pre_proc(problem);  //准备划分数据(一般是中值或者是将原问题分割的部分值)
	subProblem=split_proc(problem,mid);  //根据mid(划分条件)将原问题划分为子问题

	sub_res1=divided_conquer(subProblem[0],p1,p2...)  //递归或迭代处理子问题,得到子问题结果
	sub_res2=divided_conquer(subProblem[1],p1,p2...)
	......

	res=merge_proc(sub_res1,sub_res2,...);  //合并子问题的解得到原问题的最终解
}

六、C++实现LeetCode分治相关例题

6.1 Leetcode50–Pow(x,n)

  • 题目描述
    实现 pow(x, n) ,即计算 x 的 n 次幂函数。
    示例 1:

输入: 2.00000, 10
输出: 1024.00000

示例 2:

输入: 2.10000, 3
输出: 9.26100

示例 3:

输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25

说明:

-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。

  • 解题思路
    1、达到解决最小子问题的条件:n不断除以2直到为0或1,为0时返回1(任何数的0次幂为1)、为1时返回x本身(任何数的一次幂为其本身)。
    2、原问题分解为子问题:n不断取半,即n=n/2;
    3、合并子问题结果:
       (1)n为偶数:计算pow(x,n/2)的平方并返回;
       (2)n为奇数:计算pow(x,n/2)的平方在乘以一个x并返回
    【注】n这里需要先判断正负数,如果为负数,需要n=-n处理。

  • C++实现代码

class Solution {
public:
    double myPow(double x, int n) {   //分治+递归
        if(x==1 || n==0)
            return 1;
        long long N = n;
        return N >= 0 ? helper(x, N) : 1.0 / helper(x, -N);
    }
    double divide_conquer(double x,long long n){ //【注】这里 n要定义成long long 才能通过提交
        if(n == 1)
            return x;
        if(n%2 ==0){  //偶数
            double res=divide_conquer(x,n/2);
            return res*res;
        }
        else{  //奇数
            double res=divide_conquer(x,n/2);
            return res*res*x;
        }
    }
};

6.2 Leetcode169–多数元素

  • 题目描述
    给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
    示例 1:

输入: [3,2,3]
输出: 3

示例 2:

输入: [2,2,1,1,1,2,2]
输出: 2

  • 解题思路
    1、达到解决最小子问题的条件: nums数组长度为1时,最大子段和就是本身,直接返回。
    2、原问题分解为子问题:二分法,每次去nums数组中点,将数组分为左右两个区间,
    3、合并子问题结果:分为三种情况
      (1)长度为 1 的子数组中唯一的数显然是众数,直接返回即可;
      (2)如果它们的众数相同,那么显然这一段区间的众数是它们相同的值;
      (3)如果他们的众数不同,比较两个众数在整个区间内出现的次数来决定该区间的众数。

  • C++实现代码

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        return divide_conquer(nums,0,nums.size()-1);
    }

    //分治法+递归
    int divide_conquer(vector<int>& nums,int i,int j) {
        if(i == j)   //当区间长度为1时,直接返回该元素
            return nums[i];
        
        int mid=(i+j)/2;
        int left=divide_conquer(nums,i,mid);
        int right=divide_conquer(nums,mid+1,j);

        if(count(nums.begin()+i,nums.begin()+mid,left) > (j-i+1)/2)
            return left;
        if(count(nums.begin()+mid+1,nums.begin()+j,right) > (j-i+1)/2)
            return right;
        
        return -1;
    }
    int count_in_range(vector<int>& nums, int target, int i, int j) {
        int count = 0;
        for (int k = i; k <= j; ++k)
            if (nums[k] == target)
                ++count;
        return count;
    }
};

6.3 Leetcode53–最大子序和

  • 问题描述
    给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

  • 解题思路
    1、达到解决最小子问题的条件: nums数组长度为1时,最大子段和就是本身,直接返回。
    2、原问题分解为子问题:二分法,每次去nums数组中点,将数组分为左右两个区间,
    3、合并子问题结果:分为三种情况
      (1)最大子段和在左区间
      (2)最大子段和在右区间
      (3)最大子段和横跨两个区间
    最终返回左区间的元素、右区间的元素、以及整个区间(相对子问题)和的最大值即可

  • C++实现代码

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        return divided_conquer(nums,0,nums.size()-1);  
    }

    // 分治法 求解
    int divided_conquer(vector<int>& nums,int left,int right){
        if(left == right)  //子问题求解条件
            return nums[left];
        
        int mid=(left+right)/2;
        int left_sum=divided_conquer(nums,left,mid);   //左边区间最大子段和
        int right_sum=divided_conquer(nums,mid+1,right);  //右边区间最大子段和

        int mid_sum=crossSubarray(nums,left,mid,right);  //横跨左右两区间的连续最大子段和
        int res=max(left_sum,right_sum);
        return max(res,mid_sum);
    }

    int crossSubarray(vector<int>& nums,int left,int mid,int right){
         //因为mid_sum包含左右两个区间,所以mid一定含有,然后分别(1)左区间从右往左扩展;(2)右区间从左到右扩展
        int left_sum=INT_MIN;
        int sum=0;
        for(int i=mid;i>=left;i--){  //(1)左区间从右往左扩展
            sum+=nums[i];
            left_sum=max(sum,left_sum);
        }
        int right_sum=INT_MIN;
        sum=0;
        for(int j=mid+1;j<=right;j++){  //(2)右区间从左到右扩展
            sum+=nums[j];
            right_sum=max(right_sum,sum);
        }
        return (left_sum+right_sum);
    }
};

这里由于子区间有重叠的情况,我们使用DP算法将两者做一个对比:
DP算法代码为:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        //动态规划:关键在确定状态
        //状态i表示:以i为结尾的最大连续子数组
        //状态转移方程:dp[i]=max(dp[i-1]+nums[i],nums[i])
        vector<int> dp(nums.size(),0);  //初始化为0
        dp[0]=nums[0];
        int max_res=dp[0];
        for(int i=1;i<nums.size();i++){
            dp[i]=max(dp[i-1]+nums[i],nums[i]);
            if(dp[i]>max_res)
                max_res=dp[i];   //遍历nums时,不断更新max_res,直到取得最大连续子段和
        }
        return max_res;   
    }
};

提交结果:
在这里插入图片描述
可见子区间有重叠时,使用分治的效率反而会比较低!

七、总结

 分治算法一般用来解决数据规模比较大、子区间不重叠的情况,用分治算法解决问题的关键在于如何合并子问题,这也是分治区别于DP和贪心的主要特征,像Pow(x,n)和多数元素这两个例题,可以在各自的子区间得到子问题的解然后逐层合并,而最大子序和例题则需要考虑两个子区间重叠的情况,所以她用DP算法来求解效果会好一些!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值