@(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}
x→x2→x4→x8→x16→x32→x64
的顺序,从 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} x→x2→x4→x9→x19→x38→x77
的顺序,在 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} x→x2,x2→x4,x19→x38这些步骤中,我们直接把上一次的结果进行平方,而在 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} x4→x9,x9→x19,x38→x77这些步骤中,我们把上一次的结果进行平方后,还要额外乘一个 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);
}
};