分治法
一、算法解释
顾名思义,分治问题由“分”(divide
)和“治”(
conquer
)两部分组成,通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。我们在排序章节展示的归并排序就是典型的分治问题,其中“分”即为把大数组平均分成两个小数组,通过递归实现,最终我们会得到多个长度为 1
的子数组
;
“治”即为把已经排好序的两个小数组合成为一个排好序的大数组,从长度为 1
的子数组开始,最终合成一个大数组。
我们也使用数学表达式来表示这个过程。定义 T(n
)
表示处理一个长度为
n
的数组的时间复杂度,则归并排序的时间复杂度递推公式为 T
(
n
)
=
2
T
(
n
/
2
)
+
O
(
n
)
。其中
2
T
(
n
/
2
)
表示我们分成了两个长度减半的子问题,O
(
n
)
则为合并两个长度为
n
/
2
数组的时间复杂度。
那么怎么利用这个递推公式得到最终的时间复杂度呢?这里我们可以利用著名的主定理(Master theorem)求解:
考虑 T(n ) = aT ( n / b ) + f ( n ) ,定义 k = log b a1. 如果 f ( n ) = O ( n p ) 且 p < k ,那么 T ( n ) = O ( n K )2. 如果存在 c ≥ 0 满足 f ( n ) = O ( n k log c n ) ,那么 T ( n ) = O ( n k log c + 1 n )3. 如果 f ( n ) = O ( n p ) 且 p > k ,那么 T ( n ) = O ( f ( n ))
通过主定理我们可以知道,归并排序属于第二种情况,且时间复杂度为 O
(
n
log
n
)
。其他的分治问题也可以通过主定理求得时间复杂度。
另外,自上而下的分治可以和 memoization
结合,避免重复遍历相同的子问题。如果方便推 导,也可以换用自下而上的动态规划方法求解。
二、经典问题
1. 表达式问题
241. Different Ways to Add Parentheses
给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
生成的测试用例满足其对应输出值符合 32 位整数范围,不同结果的数量不超过 10^4 。
利用分治思想,我们可以把加括号转化为,对于每个运算符号,先执行处理两侧的数学表达式,再处理此运算符号。注意边界情况,即字符串内无运算符号,只有数字。
class Solution {
public:
vector<int> diffWaysToCompute(string expression) {
vector<int> ways;
for(int i=0; i<expression.length(); ++i){
char c = expression[i];
if(c == '+' || c == '-' || c == '*'){
vector<int> left = diffWaysToCompute(expression.substr(0, i));
vector<int> right = diffWaysToCompute(expression.substr(i + 1));
for(const int & l: left){
for(const int & r: right){
switch(c){
case '+': ways.push_back(l + r); break;
case '-': ways.push_back(l - r); break;
case '*': ways.push_back(l * r); break;
}
}
}
}
}
if(ways.empty()) ways.push_back(stoi(expression));
return ways;
}
};
我们发现,某些被 divide 的子字符串可能重复出现多次,因此我们可以用 memoization 来去重。我们可以从上到下用分治处理 +memoization ,当然也可以直接从下到上用动态规划处理。
class Solution {
public:
unordered_map<string, vector<int>> memo;
vector<int> diffWaysToCompute(string expression) {
// 之前计算过这个式子了,直接返回
if(memo.find(expression) != memo.end()){
return memo[expression];
}
vector<int> ways;
for(int i=0; i<expression.length(); ++i){
char c = expression[i];
if(c == '+' || c == '-' || c == '*'){
vector<int> left = diffWaysToCompute(expression.substr(0, i));
vector<int> right = diffWaysToCompute(expression.substr(i + 1));
for(const int & l: left){
for(const int & r: right){
switch(c){
case '+': ways.push_back(l + r); break;
case '-': ways.push_back(l - r); break;
case '*': ways.push_back(l * r); break;
}
}
}
}
}
// 字符串是纯数字,直接转换
if(ways.empty()) ways.push_back(stoi(expression));、
// 记忆化存储
memo[expression] = ways;
return memo[expression];
}
};
三、巩固练习
欢迎大家共同学习和纠正指教