分治法刷题总结

写在前面

  分治法简单理解就是分而治之,将一个复杂的问题通过一定的方式分解成若干个类似的小问题。其实,从字里行间便能体会到递归的含义。没错,本质上来说,我们还是通过分治法求解去体会递归的魅力。至少接下来的三道题,我是这样做的~~
  前排提醒,一开始遇到递归的问题,私以为不要过于追求细节,这样很容易迷失在递归过程中,造成自我怀疑。有一定基础的可以自己画棵树体会过程,或者直接翻题解找到类似的图也可。重要的是体会思想,剩下的就是重复练习。


53.最大自序和

题目描述

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

  示例:

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

  进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

分治法

   暴力法比较esay,不再赘述,双循环即可。
   题目要求我们求连续数组的最大和,并尝试采用分治法。为了便于分解,我们可以递归地从数组中间将其分成左右两子区间。我们现在就将目光集中到如何求当前区间的最大和?三种情况奉上~

  1. 左区间最大和
  2. 右区间最大和
  3. 跨越中点的区间最大和

   注意:这里第三种情况是容易被忽略的,当然也是比较难实现的一点,不妨用二叉树的递归模型来理解这一过程。

void PostOrder(BiTree T){
	if(T != NULL){
		PostOrder(T->lchild);
		PostOrder(T->rchild);
		visit(T);
	}
}

   这是后序遍历的递归套路,简单理解就是左右根,我们可以将重点放在对根节点的操作上,而左右交给递归过程。我们将其类比成本题的分治法:左右递归子树就是求左右区间的最大连续和,而根节点操作就是求中间衔接段的最大和并比较三者最大和。
   还有一点,当我们发现题目所给的函数的参数似乎不够用时,我们可以添加辅助函数helper。比如本题,显然我们在递归过程需要区间的左右端点,而maxSubArray的参数只有一个容器nums,这时候我们可以添加辅助函数helper(vector<int>& nums, int l, int r)即可。

题目代码

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if(nums.size() == 0)                    //判空
            return 0;
        return helper(nums, 0, nums.size() - 1);
    }
private:
    int helper(vector<int>& nums, int l, int r){
        if(l > r)                               //递归出口 -> 左端点大于右端点
            return INT_MIN;

        int mid = (l + r) >> 1;                 //位运算 -> 求数组中点
        int left = helper(nums, l, mid - 1);    //左区间最大和
        int right = helper(nums, mid + 1, r);   //右区间最大和

        //中间衔接段最大和(越过中点)
        int leftMax = 0, rightMax = 0, sum = 0; //由中点向左发散最大值,由中点向右发散最大值,当前区间和
        for(int i = mid - 1; i >= l; i--){      //求由中点向左发散最大值
            sum += nums[i];
            leftMax = max(leftMax, sum);
        }
        sum = 0;                                //重新置0
        for(int i = mid + 1; i <= r; i++){      //求由中点向右发散最大值
            sum += nums[i];
            rightMax = max(rightMax, sum);
        }
        //左区间最大和,右区间最大和,中间衔接段最大和,三者取最大值
        return max(max(left, right), leftMax + rightMax + nums[mid]);
    }
};

169.多数元素

题目描述

   给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

   你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:
输入: [3,2,3]
输出: 3

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

暴力法

   暴力解千愁,看见题目要求收集数组元素出现次数就应当想到空间换时间,即采用哈希表unordered_map

题目代码

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        if(nums.size() == 0)
            return 0;
        
        unordered_map<int,int> record;
        for(auto num : nums)
            record[num]++;
        
        int len = nums.size() / 2;
        int result = 0;
        for(auto rec : record){
            if(rec.second > len){
                result = rec.first;
                break;
            }
        }

        return result;
    }
};

分治法

   这次用分治法的思想非常巧妙,只机械地记忆模版(取数组中间位置)可能很难想到这样做的理由。那么为什么还是要去中间呢?本题要求我们求多数元素——出现次数大于⌊ n/2 ⌋的数。这个前提条件很重要,没有了它,后面的分治法将无从下手
   好了,我们是否可以这样想,如果我们将数组一分为二,那么这个多数元素至少是一个部分的多数元素。具体的反证法见leetcode官方题解,私以为这更想是一个脑筋急转弯~所以现在知道为什么⌊ n/2 ⌋是如此的重要吧!因为没有这个“超过半数”,我们就无法反证刚才的猜想的正确性,没有这个猜想,我们就无法“堂而皇之”的取中间位置一分为二。
   剩下的工作就又回到了类似于二叉树后序递归模型中来,当然这次我们需要添加一个“工具人”函数count,让它帮我们计算每一区间内的多数元素。这样我们就可以将目光集中到对“根节点”的操作,也就是比较两个区间的多数元素。如果两区间多数元素相同,那么return这个多数元素;如果两区间多数元素不相同,那么需要比较这两个多数元素在合并的整个区间里出现的次数来决定return哪个值。

初始代码

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        if(nums.size() == 0)
            return 0;
        return helper(nums, 0, nums.size() - 1);
    }
private:
    int helper(vector<int>& nums, int l, int r){
        if(l == r)                                //递归出口,区间内仅有一个元素
            return nums[l];
        
        int mid = (r - l) / 2 + l;
        int leftMax = helper(nums, l, mid);       //左区间多数元素
        int rightMax = helper(nums, mid + 1, r);  //右区间多数元素
        if(leftMax == rightMax)                   //左右区间多数元素相同
            return leftMax;
        //左右区间多数元素不同,比较在合并区间的次数
        int leftCount = count(nums, leftMax);
        int rightCount = count(nums, rightMax);
        return leftCount > rightCount ? leftMax : rightMax;
    }

    int count(vector<int>& nums, int target){
        int c = 0;
        for(auto num : nums)
            if(num == target)
                c++;
        
        return c;
    }
};

  :这个c++版本会在倒数第二个测试用例栽跟头,不过思路是正确的。
测试用例

改进代码

  经过排查发现,count函数还是需要加上左右区间边界的,显然想利用c++ STL的偷懒想法破灭啦~如果没有左右边界,每次执行count函数都要对整个数组进行循环,这个时间复杂度肯定是无法接受的~

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        if(nums.size() == 0)
            return 0;
        return helper(nums, 0, nums.size() - 1);
    }
private:
    int helper(vector<int>& nums, int l, int r){
        if(l == r)                                //递归出口,区间内仅有一个元素
            return nums[l];
        
        int mid = (r - l) / 2 + l;
        int leftMax = helper(nums, l, mid);       //左区间多数元素
        int rightMax = helper(nums, mid + 1, r);  //右区间多数元素
        if(leftMax == rightMax)                   //左右区间多数元素相同
            return leftMax;
        //左右区间多数元素不同,比较在合并区间的次数
        int leftCount = count(nums, leftMax, l, r);
        int rightCount = count(nums, rightMax, l, r);
        return leftCount > rightCount ? leftMax : rightMax;
    }

    int count(vector<int>& nums, int target, int l, int r){
        int c = 0;
        for(int i = l; i <= r; i++)
            if(nums[i] == target)
                c++;

        return c;
    }
};

50.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. x的正负性
  3. return的类型

   先给出两个计算过程:
x 64 − > x 32 − > x 16 − > x 8 − > x 4 − > x 2 − > x 1 x^{64}->x^{32}->x^{16}->x^{8}->x^{4}->x^{2}->x^{1} x64>x32>x16>x8>x4>x2>x1
x 77 − > x 38 − > x 19 − > x 9 − > x 4 − > x 2 − > x 1 x^{77}->x^{38}->x^{19}->x^{9}->x^{4}->x^{2}->x^{1} x77>x38>x19>x9>x4>x2>x1

   我们发现,这其实和之前两道题的分治法并无太大区别。依旧是⌊n/2⌋,依旧是将计算交给递归(其实更想树的递归)。我们要将目光集中在当前节点也就是计算这一轮结果的操作上。
   以 x 64 < − x 32 x^{64} <- x^{32} x64<x32为例,此时N == 64,如何从 x 32 x^{32} x32 x 64 x^{64} x64呢?不妨假设我们已经递归计算出上一轮结果 x 32 x^{32} x32,也就是double y = helper(x, N / 2);的意思。这时候,我们只需要判断N的奇偶性即可。由于是偶数,所以我们只需要y*y即可得出这一轮结果。
   再以 x 77 < − x 38 x^{77}<-x^{38} x77<x38为例,此时N == 77,如何从 x 77 x^{77} x77 x 38 x^{38} x38呢?照猫画虎,已经递归计算出上一轮结果 x 38 x^{38} x38。显然N为奇数,如果y*y,相当于只计算出 x 76 x^{76} x76的结果,这是不够的。此时,我们只需要再乘一个x即可,即y*y*x注意x这个参数始终不变。

题目代码

class Solution {
public:
    double myPow(double x, int n) {
        long long N = n;      //防止n越界
        return N >= 0 ? helper(x, N) : 1.0 / helper(x, -N);   //幂次为负
    }

private:
    double helper(double x, long long N){
        if(N == 0)
            return 1.0;                           //递归出口,x^0 = 1
        
        double y = helper(x, N / 2);              //上一轮结果
        
        return N % 2 == 0 ? y * y : y * y * x;    //奇数,多乘一个x
    }
};

总结

  其实,理解分治算法容易,灵活运用分治算法并不容易。通过这三道题,我发现本质上都是在围绕“分隔区间再合并区间”做文章,当然,目前都是以中点为分隔点。在代码构成上,类似于二叉树的遍历递归模型。实际上,我认为可以先从二叉树相关题目入手,理解递归。弄懂了递归的奥妙,再取碰这些分治法会更轻松一些。
  另外,最基础的二分查找也是分治法的一种。从它入手,也可以更好地理解分治思想。以下是我写的一个简单的二分查找方法~

int binarySearch(T arr[], int n, T target){
    int l = 0, r = n; //左右边界,在[l...r)的范围里寻找target
    while(l < r){        //当l==r时,区间[l...r)无效
        //int mid = (l+r) / 2;
        int mid = l + (r - l)/2;  //l,r均为int型,当l,r均很大时,l+r可能会产生整型溢出问题。c++不报错
        if(arr[mid] == target)
            return target;
        if(target > arr[mid])
            l = mid + 1;    //target在[mid+1...r)中
        else                //target<arr[mid]
            r = mid;    //target在[l...mid)中
    }

    return -1;
}

  以上是我对于分治算法的初步认识~


如果有错误或者不严谨的地方,请务必给予指正,十分感谢。
本人blog:http://breadhunter.gitee.io

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值