分治法
分治问题由“分”(divide)和“治”(conquer)两部分组成,通过把原问题分为子问题,再将子问题进行处理合并,从而实现对原问题的求解。我们在排序章节展示的归并排序就是典型的分治问题,其中“分”即为把大数组平均分成两个小数组,通过递归实现,最终我们会得到多个长度为 1 的子数组;“治”即为把已经排好序的两个小数组合成为一个排好序的大数组,从长度为 1 的子数组开始,最终合成一个大数组。
自上而下的分治可以和 memoization 结合,避免重复遍历相同的子问题。如果方便推导,也可以换用自下而上的动态规划方法求解。
表达式问题
241. 为运算表达式设计优先级
给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。
思路:
利用分治思想,我们可以把加括号转化为,对于每个运算符号,先执行处理两侧的数学表达式,再处理此运算符号(递归)。注意边界情况,即字符串内无运算符号,只有数字。(不断分为两个小表达式)
代码:
class Solution {
public:
vector<int> Compute(string expression) {
vector<int> re, m, n;
string str;
if (expression.size() % 2)
expression = "+" + expression;
if (expression.size() == 2)
return { expression[1]};
for (int i = 2; i < expression.size(); i += 2) {
str.assign(expression.begin(), expression.begin() + i);
m = Compute(str);
str.assign(expression.begin() + i + 1, expression.end());
n = Compute(str);
for (int o = 0; o < m.size(); o++) {
for (int j = 0; j < n.size(); j++) {
if (expression[i] == '+')
re.push_back(m[o] + n[j]);
else if (expression[i] == '-')
re.push_back(m[o] - n[j]);
else
re.push_back(m[o] * n[j]);
}
}
}
return re;
}
vector<int> diffWaysToCompute(string expression) {
string str="";
int n;
str += expression[0] - '0';
for (int i = 1; i < expression.size(); i++) {//先转换为1位数存储
if (expression[i] >= '0') {
n = str[str.size() - 1];
if (n <= 9)
str[str.size() - 1] = n * 10 + expression[i] - '0';
else
str += expression[i] - '0';
}
else
str += expression[i];
}
return Compute(str);
}
};
932. 漂亮数组
对于某些固定的 N,如果数组 A 是整数 1, 2, …, N 组成的排列,使得:
对于每个 i < j,都不存在 k 满足 i < k < j 使得 A[k] * 2 = A[i] + A[j]。
那么数组 A 是漂亮数组。
给定 N,返回任意漂亮数组 A(保证存在一个)。
思路:
题目要求不能让该等式成立:A[k] * 2 = A[i] + A[j],i < k < j,可知等式左边必为偶数,只要右边和为奇数即可保证不成立
可知 奇数 + 偶数 = 奇数,那就让A[i]和A[j]一个为奇数一个为偶数即可,不妨让A[i]为奇数,A[j]为偶数
对于从1到N的所有整数,奇数个数为 (N + 1) / 2,偶数个数为 N / 2
对于从1到N的所有整数,奇数个数为 (N + 1) / 2,偶数个数为 N / 2
如果[x1, x2, x3]是一个漂亮数组,则[k * x1 + b, k * x2 + b, k * x3 + b] 也一定是漂亮数组
对于从1到(N + 1)/2的所有整数x,得出其漂亮数组,并映射成1到N范围的所有奇数 2 * x - 1
对于从1到N/2的所有整数x,得出其漂亮数组,并映射成1到N范围的所有偶数 2 * x
一直到子问题规模为3
代码:
class Solution {
public:
vector<int> beautifulArray(int n) {
vector<int> a, b;
if (n == 1)
return { 1 };
if (n == 2)
return { 1,2 };
if (n == 3)
return { 1,3,2 };
a = beautifulArray((n + 1) / 2);//n个数里有(n + 1) / 2个奇数
for (int i = 0; i < a.size(); i++)//(n + 1) / 2个数映射成奇数
a[i] = a[i] * 2 - 1;
b = beautifulArray(n / 2);
for (int i = 0; i < b.size(); i++)
b[i] = b[i] * 2;
a.insert(a.end(), b.begin(), b.end());//奇数在左,偶数在右
return a;
}
};
312. 戳气球
有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。
求所能获得硬币的最大数量。
思路:
由题,气球没有边界就是1,那么我们倒推,给两边加上1 的边界,每次在中间加上气球,表示被戳的气球,这样分析,我们设置左右边界为i,j,然后选取一个i、j的中间节点mid,总的价值就是:
val[i]×val[mid]×val[j]+solve(i,mid)+solve(mid,j)
简单来说,就是中间节点mid的气球在最后被戳破前,左右区间的气球已经被戳完了,直接相加就行,这样就能不断对区间细化为子问题,最后取最大值就行。
为避免重复,还可以用动态规划:
令 dp[i][j] 表示填满开区间 (i,j) 能得到的最多硬币数
for (int i = numsSize - 1; i >= 0; i--) {
for (int j = i + 2; j <= numsSize + 1; j++) {
for (int k = i + 1; k < j; k++) {
int sum = val[i] * val[k] * val[j];
sum += rec[i][k] + rec[k][j];
rec[i][j] = fmax(rec[i][j], sum);
}
}
}
代码:
class Solution {
public:
int maxCoins(vector<int>& nums) {
vector<int> n{ 1 };
n.insert(n.end(), nums.begin(), nums.end());
n.push_back(1);
vector<vector<int>> rec(n.size(), vector<int>(n.size(), 0));
for (int i = n.size() - 1; i >= 0; i--) {//i倒序,保证每次计算用到的rec都是遍历过的
for (int j = i + 2; j < n.size(); j++) {
for (int k = i + 1; k < j; k++) {
int sum = n[i] * n[k] * n[j];
sum += rec[i][k] + rec[k][j];
rec[i][j] = fmax(rec[i][j], sum);
}
}
}
return rec[0][n.size() - 1];
}
};