1. 回顾
回顾笔者以前记录的文章,DSAA之最大子序列之和问题(三)第一次遇到分治的思想,对最大子序列求和问题使用分治可以将 O ( n 3 ) O(n^3) O(n3)降低到 O ( n l o g n ) O(nlogn) O(nlogn);然后在 DSAA之合并排序学习到了归并排序,使用分治的思想可以将排序的时间复杂度稳定在 O ( n l o g n ) O(nlogn) O(nlogn);之后DSAA之快速排序(一)篇中也使用了分治策略,并且进阶的分析到了如果**治阶段不是线性复杂度,则采取分治可能并不改善算法整体复杂度。**本篇根据DSAA将阐述几个分治思想的例子,虽然并不实用,然后再实战一个leetcode分治题结束。
2. 分治时间复杂度
定理1
The solution to the equation T ( n ) = a T ( n / b ) + Θ ( n k ) T(n) = aT(n/b) + \Theta (n^k) T(n)=aT(n/b)+Θ(nk), where a > 1 a > 1 a>1 and b > 1 b > 1 b>1, is:
T ( n ) = { O ( n l o g b a ) i f a > b k O ( n k l o g n ) i f a = b k O ( n k ) i f a < b k T(n) =\left\{ \begin{aligned} O(n^{log_{b}a}) && if&&a>b^k\\ O(n^k{logn}) &&if&&a=b^k \\ O(n^k) &&if&&a<b^k \end{aligned} \right. T(n)=⎩ ⎨ ⎧O(nlogba)O(nklogn)O(nk)ifififa>bka=bka<bk
定理2
The solution to the equation T ( n ) = a T ( n / b ) + ( n k l o g p n ) T(n) = aT(n/b) + (n^klog^pn) T(n)=aT(n/b)+(nklogpn), where a ≥ 1 a\geq1 a≥1, b > 1 b > 1 b>1,
and p ≥ 0 p\geq 0 p≥0 is
T ( n ) = { O ( n l o g b a ) i f a > b k O ( n k l o g p + 1 n ) i f a = b k O ( n k l o g p n ) i f a < b k T(n) =\left\{ \begin{aligned} O(n^{log_{b}a}) && if &&a>b^k\\ O(n^k{log^{p+1}n}) &&if &&a=b^k \\ O(n^klog^pn) &&if &&a<b^k \end{aligned} \right. T(n)=⎩ ⎨ ⎧O(nlogba)O(nklogp+1n)O(nklogpn)ifififa>bka=bka<bk
定理3
∑ i = 1 k a i < 1 \sum_{i=1}^{k}a_{i}<1 ∑i=1kai<1, then the solution to the equation T ( n ) = ∑ i = 1 k T ( a i n ) + O ( n ) a i < 1 T(n)=\sum_{i=1}^{k}T(a_{i}n)+O(n) a_{i}<1 T(n)=∑i=1kT(ain)+O(n) ai<1 is T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n).
这些公式不需要记忆,一般碰到的分治都是 O ( n l o g n ) O(nlogn) O(nlogn),但是一些具体的问题可能并不满足这个界,需要按照仔细查找上面的公式。
3. 两个经典的例子
本部分将是高度概括和总结,只将例子中重点的部分拿出来讨论下。
最近点问题
一般很容易想到暴力解法
O
(
n
2
)
O(n^2)
O(n2),但是如果采用分治策略:每次将点的集合一分为二,然后分别找距离最短的点
d
L
、
d
R
d_{L}、d_{R}
dL、dR。之后再从两个集合各取一个点寻找是否有更短的距离的存在。
在该问题中治的环节为了保证是
O
(
n
)
O(n)
O(n)的复杂度,将预先建立关于所有点的两个表P和Q,分别是以x排序和y排序的。每次治的时候先筛选横坐标满足
∣
x
∣
<
m
i
n
(
d
L
,
d
R
)
|x|<min(d_{L},d_{R})
∣x∣<min(dL,dR)的点,然后再从中筛选
∣
y
∣
<
m
i
n
(
d
L
,
d
R
)
|y|<min(d_{L},d_{R})
∣y∣<min(dL,dR)的点,计算它们的之间距离,并更新最小距离。**这两个条件其实包含了多余的点,但是保证一定不会漏点。**这样问题保证以
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)解决。
选择问题
以前在DSAA之快速选择排序(二)记录过这个问题,快速选择算法是对快速排序的一种妙用,同过枢纽元的选择,将问题的规模减少到原来的一半(理想),最终的平均时间复杂度为
O
(
n
)
O(n)
O(n),但是最坏时间复杂度依然为
O
(
n
2
)
O(n^2)
O(n2)。
可以发现快速选择和快速排序都十分依赖枢纽元的好坏,一种五划分中项的中项的枢纽元选择方法可以让快速选择算法的运行时间为
O
(
n
)
O(n)
O(n),但是该方式的实现却没有那么容易,涉及的系统开销让该算法并不实用,所以这里不赘述。
4. 实战 Maximum Subarray
这个题以前分析过,但是今天重新做的时候,发现有些地方逻辑不熟练了。特别是治的环节,我一开始想one pass
处理,但是犯了严重的逻辑错误:不是所有的case
都是两边同时增长得到最大值。
#define MAX -99999
class Solution {
public:
int maxSubArray(vector<int>& nums) {
return divide_conquer(nums,0,nums.size()-1);
}
int divide_conquer(vector<int>& nums,int left,int right){
int leftcross_max=MAX,rightcross_max=MAX,medium,leftmax,rightmax,conquer_max=0,max;
if(left == right)
return nums[left];
medium=(left+right)/2;
leftmax=divide_conquer(nums,left,medium);
rightmax=divide_conquer(nums,medium+1,right);
max=leftmax<rightmax? rightmax:leftmax;
//conquer
for(int i=medium,tmp=0;i>=left;--i){
tmp+=nums[i];
if(tmp > leftcross_max)
leftcross_max=tmp;
}
for(int i=medium+1,tmp=0;i<=right;++i){
tmp+=nums[i];
if(tmp > rightcross_max)
rightcross_max=tmp;
}
return max>rightcross_max+leftcross_max? max:rightcross_max+leftcross_max;
}
};
5. 总结
一般分治算法都要遵循治的环节不大于线性时间复杂度。在一般解题的时候,拆分问题不是难点,如何治才是分治的难点。在编程上调用不少于两次递归,且将问题拆分成不重合的子问题属于标准的分治模板,但是时间复杂度不一定都遵循 O ( n l o g n ) O(nlogn) O(nlogn)。