LeetCode打卡——分治算法

@(Aaron) [LeetCode, C++]

主要内容包括:

  • 分治算法

  • 实例讲解


1、分治算法

  分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。

  基本思想:

当我们求解某些问题时,由于这些问题要处理的数据相当多,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个问题的解法。如果这些子问题还较大,难以解决,可以再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治策略的基本思想。一般情况下,还会用到二分法。

  二分法:

利用分治策略求解时,所需时间取决于分解后子问题的个数、子问题的规模大小等因素,而二分法,由于其划分的简单和均匀的特点,是经常采用的一种有效的方法,例如二分法检索。

  基本算法:

  分治法解题的一般步骤:

  1. 分解,将要解决的问题划分成若干规模较小的同类问题;
  2. 求解,当子问题划分得足够小时,用较简单的方法解决;
  3. 合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

2、实例打卡

2.1 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/(2*2) = 1/4 = 0.25
说明:

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

快速幂算法的本质是分治算法。举个例子,如果我们要计算 x 64 x^{64} x64,我们可以按照:
x → x 2 → x 4 → x 8 → x 16 → x 32 → x 64 x \rightarrow x^{2} \rightarrow x^{4} \rightarrow x^{8} \rightarrow x^{16} \rightarrow x^{32} \rightarrow x^{64} xx2x4x8x16x32x64

的顺序,从 x 开始,每次直接把上一次的结果进行平方,计算 66次就可以得到 x 64 x^{64} x64的值,而不需要对 x 乘 63次 x。这是比较巧合的例子,全是偶数次幂。

再举一个例子,如果我们要计算 x 77 x^{77} x77 ,我们可以按照:

x → x 2 → x 4 → x 9 → x 19 → x 38 → x 77 x \rightarrow x^{2} \rightarrow x^{4} \rightarrow x^{9} \rightarrow x^{19} \rightarrow x^{38} \rightarrow x^{77} xx2x4x9x19x38x77

的顺序,在 x → x 2 , x 2 → x 4 , x 19 → x 38 x \rightarrow x^{2}, x^{2} \rightarrow x^{4}, x^{19} \rightarrow x^{38} xx2,x2x4,x19x38这些步骤中,我们直接把上一次的结果进行平方,而在 x 4 → x 9 , x 9 → x 19 , x 38 → x 77 x^{4} \rightarrow x^{9}, x^{9} \rightarrow x^{19}, x^{38} \rightarrow x^{77} x4x9,x9x19,x38x77这些步骤中,我们把上一次的结果进行平方后,还要额外乘一个 x。

class Solution {
public:
    double quickMul(double x, long long N) {
        if (N == 0) {
            return 1.0;
        }
        double y = quickMul(x, N / 2);
        return N % 2 == 0 ? y * y : y * y * x;
    }

    double myPow(double x, int n) {
        long long N = n;
        return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
    }
};

另一种迭代的解法:

class Solution {
public:
    double quickMul(double x, long long N) {
        double ans = 1.0;
        long long flag = 1;
        double x_contribute = x;
        while (flag <= N) {
            if (N & flag) {
                ans *= x_contribute;
            }
            x_contribute *= x_contribute;
            flag = flag << 1;
        }
        return ans;
    }

    double myPow(double x, int n) {
        long long N = n;
        return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
    }
};

2.2 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

先给出动态规划的解法:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
       int pre = 0, maxRes = nums[0];
       for(auto x:nums)
       {
           pre = max(pre+x, x);
           maxRes = max(pre, maxRes);
       }
       return maxRes;
    }
};
  • 时间复杂度:O(n),其中 n 为 nums 数组的长度。我们只需要遍历一遍数组即可求得答案。
  • 空间复杂度:O(1)。我们只需要常数空间存放若干变量。

分治解法:
我们定义一个操作 g e t ( a , l , r ) get(a, l, r) get(a,l,r) 表示查询 a 序列 [ l , r ] [l, r] [l,r] 区间内的最大子段和,那么最终我们要求的答案就是 g e t ( n u m s , 0 , n u m s . s i z e ( ) − 1 ) get(nums, 0, nums.size() - 1) get(nums,0,nums.size()1)。如何分治实现这个操作呢?对于一个区间 [ l , r ] [l, r] [l,r],我们取 m = ⌊ l + r 2 ⌋ m=\left\lfloor\frac{l+r}{2}\right\rfloor m=2l+r,对区间 [ l , m ] 和 [ m + 1 , r ] [l, m] 和 [m + 1, r] [l,m][m+1,r] 分治求解。当递归逐层深入直到区间长度缩小为 1 的时候,递归「开始回升」。这个时候我们考虑如何通过 [ l , m ] [l, m] [l,m] 区间的信息和 [ m + 1 , r ] [m + 1, r] [m+1,r] 区间的信息合并成区间 [ l , r ] [l, r] [l,r] 的信息。最关键的两个问题是:

  • 我们要维护区间的哪些信息呢?
  • 我们如何合并这些信息呢?

对于一个区间 [ l , r ] [l, r] [l,r],我们可以维护四个量:

  • lSum 表示 [ l , r ] [l, r] [l,r] 内以 ll 为左端点的最大子段和
  • rSum 表示 [ l , r ] [l, r] [l,r] 内以 rr 为右端点的最大子段和
  • mSum 表示 [ l , r ] [l, r] [l,r]内的最大子段和
  • iSum 表示 [ l , r ] [l, r] [l,r] 的区间和

以下简称 [ l , m ] 为 [ l , r ] [l, m] 为 [l, r] [l,m][l,r] 的「左子区间」, [ m + 1 , r ] 为 [ l , r ] [m + 1, r] 为 [l,r] [m+1,r][l,r] 的「右子区间」。我们考虑如何维护这些量呢?对于长度为 1 的区间 [i, i],四个量的值都和 a i a_i ai相等。对于长度大于 1 的区间:

  • 首先最好维护的是 iSum,区间 [l, r]的 iSum 就等于「左子区间」的 iSum 加上「右子区间」的 iSum。
  • 对于 [l, r]的 lSum,存在两种可能,它要么等于「左子区间」的 lSum,要么等于「左子区间」的 iSum 加上「右子区间」的 lSum,二者取大。
  • 对于 [l, r] 的 rSum,同理,它要么等于「右子区间」的 rSum,要么等于「右子区间」的 iSum 加上「左子区间」的 rSum,二者取大。
  • 当计算好上面的三个量之后,就很好计算 [ l , r ] [l, r] [l,r]的 mSum 了。我们可以考虑 [ l , r ] [l, r] [l,r]的 mSum 对应的区间是否跨越 mm——它可能不跨越 mm,也就是说 [ l , r ] [l, r] [l,r] 的 mSum 可能是「左子区间」的 mSum 和 「右子区间」的 mSum 中的一个;它也可能跨越 mm,可能是「左子区间」的 rSum 和 「右子区间」的 lSum 求和。三者取大。
    这样问题就得到了解决。
class Solution {
public:
    struct Status {
        int lSum, rSum, mSum, iSum;
    };

    Status pushUp(Status l, Status r) {
        int iSum = l.iSum + r.iSum;
        int lSum = max(l.lSum, l.iSum + r.lSum);
        int rSum = max(r.rSum, r.iSum + l.rSum);
        int mSum = max(max(l.mSum, r.mSum), l.rSum + r.lSum);
        return (Status) {lSum, rSum, mSum, iSum};
    };

    Status get(vector<int> &a, int l, int r) {
        if (l == r) return (Status) {a[l], a[l], a[l], a[l]};
        int m = (l + r) >> 1;
        Status lSub = get(a, l, m);
        Status rSub = get(a, m + 1, r);
        return pushUp(lSub, rSub);
    }

    int maxSubArray(vector<int>& nums) {
        return get(nums, 0, nums.size() - 1).mSum;
    }
};


2.3 多数元素

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

先给一个哈希表解法:

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        unordered_map<int, int> map;
        for(auto num:nums)
        {
            map[num]++;
        }
        int res = nums[0];
        int max_count = map[res];
        for(auto x:map)
        {
            if(x.second>max_count)
            {
                max_count = x.second;
                res = x.first;
            }
        }
        return res;
    }
};

分治解法:
分治解法的思想就是如果数 a 是数组 nums 的众数,如果我们将 nums 分成两部分,那么 a 必定是至少一部分的众数。

class Solution {
    int count_in_range(vector<int>& nums, int target, int lo, int hi) {
        int count = 0;
        for (int i = lo; i <= hi; ++i)
            if (nums[i] == target)
                ++count;
        return count;
    }
    int majority_element_rec(vector<int>& nums, int lo, int hi) {
        if (lo == hi)
            return nums[lo];
        int mid = (lo + hi) / 2;
        int left_majority = majority_element_rec(nums, lo, mid);
        int right_majority = majority_element_rec(nums, mid + 1, hi);
        if (count_in_range(nums, left_majority, lo, hi) > (hi - lo + 1) / 2)
            return left_majority;
        if (count_in_range(nums, right_majority, lo, hi) > (hi - lo + 1) / 2)
            return right_majority;
        return -1;
    }
public:
    int majorityElement(vector<int>& nums) {
        return majority_element_rec(nums, 0, nums.size() - 1);
    }
};


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值