分治思想
这里思想很简单,但是真正涉及到题目的时候,会发现使用的时候还是很难的。
但是基本思想还是类似的,基本上就是把大问题拆成小问题,分别解决,然后再组合起来得到结果
241. 为运算表达式设计优先级
题目描述
给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。
例子1
Input: "2-1-1"
output: [0, 2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2
例子2
Input: "23-45"
output: [-34, -14, -10, -10, 10]
解释:
(2*(3-(4 * 5))) = -34
((2 * 3)-(4 * 5)) = -14
((2*(3-4)) * 5) = -10
(2*((3-4) * 5)) = -10
(((2 * 3)-4)* 5) = 10
思考
1 题目难度是中等的,但是感觉难度是hard
刚开始想对于不同运算符进行处理,可是思路一直没有理清楚,后来看了下题解,思路差不多,也是对于不同运算符进行处理,不过这里处理完了之后,采用相互乘得到结果
比如这里"23-45",可以根据对不同字符进行处理,可以获得以下结果
(2*(3-(45))) = -34
(2((3-4)*5)) = -10
((23)-(45)) = -14
((2*(3-4))5) = -10
(((23)-4)*5) = 10
使用递归,可以参考实现1
可以发现实现1中有些字符是重复的,可以使用缓存,参考实现2
不使用递归,使用自底向上,采用动态规划解决,这里的dp[i][j] 表示在字符串中数字组成的数组中nums[i]到nums[j]的可以组成的不同结果,那么动态转移方程就是
dp[i][j] = dp[i][k] * dp[k+1][j] (k>=i&&k<j)
参考实现3
实现1
/**
* @param {string} input
* @return {number[]}
*/
// Runtime: 84 ms, faster than 61.84% of JavaScript online submissions for Different Ways to Add Parentheses.
// Memory Usage: 40.7 MB, less than 42.11% of JavaScript online submissions for Different Ways to Add Parentheses.
const diffWaysToCompute = (input) => {
const ret = [];
for (let i = 0; i < input.length; i++) {
const charI = input.charAt(i);
if (charI === "-" || charI === "*" || charI === "+") {
const part1 = input.substring(0, i);
const part2 = input.substring(i + 1);
const part1Ret = diffWaysToCompute(part1);
const part2Ret = diffWaysToCompute(part2);
// console.log(part1, part1Ret);
// console.log(part2, part2Ret);
for (let p1 of part1Ret) {
for (let p2 of part2Ret) {
let c = 0;
switch (charI) {
case "+":
c = p1 + p2;
break;
case "-":
c = p1 - p2;
break;
case "*":
c = p1 * p2;
break;
}
ret.push(c);
}
}
console.log(ret);
}
}
if (ret.length === 0) {
ret.push(+input);
}
return ret;
};
export default diffWaysToCompute;
实现2
/**
* @param {string} input
* @return {number[]}
*/
// Runtime: 80 ms, faster than 71.05% of JavaScript online submissions for Different Ways to Add Parentheses.
// Memory Usage: 41.1 MB, less than 34.21% of JavaScript online submissions for Different Ways to Add Parentheses.
const computeWithDP = (input, map) => {
const res = [];
const len = input.length;
for (let i = 0; i < len; i++) {
const charI = input.charAt(i);
if (charI == "+" || charI == "-" || charI == "*") {
const part1Res = [];
const part2Res = [];
const part1 = input.substring(0, i);
const part2 = input.substring(i + 1);
if (map.has(part1)) {
part1Res = map.get(part1);
} else {
part1Res = computeWithDP(part1, map);
map.set(part1, part1Res);
}
if (map.has(part2)) {
part2Res = map.get(part2);
} else {
part2Res = computeWithDP(part2, map);
map.set(part2, part2Res);
}
for (let res1 of part1Res) {
for (let res2 of part2Res) {
switch (charI) {
case "+":
res.push(res1 + res2);
break;
case "-":
res.push(res1 - res2);
break;
case "*":
res.push(res1 * res2);
break;
default:
break;
}
}
}
}
}
if (res.length === 0) {
res.push(+input);
}
return result;
};
export default (input) => {
const map = new Map();
return computeWithDP(input, map);
};
实现3
/**
* @param {string} input
* @return {number[]}
*/
// Runtime: 72 ms, faster than 94.74% of JavaScript online submissions for Different Ways to Add Parentheses.
// Memory Usage: 39 MB, less than 86.84% of JavaScript online submissions for Different Ways to Add Parentheses.
const diffWaysToCompute = (input, map) => {
if (input.length === 0 || !input) return [];
const oprs = [];
const nums = [];
let begin = 0;
// 计算出有input中有多少个操作符和多少个数字
for (let i = 0; i < input.length; i++) {
const charI = input.charAt(i);
if (charI == "+" || charI == "-" || charI == "*") {
oprs.push(charI);
nums.push(+input.substring(begin, i));
begin = i + 1;
}
}
// 把最后一个数字加入
nums.push(+input.substring(begin));
const numsLen = nums.length;
const dp = [];
// dp[i][j]表示input中数字nums[i]到数字nums[j]的之间的结果
for (let i = 0; i < numsLen; i++) {
dp[i] = [];
for (let j = 0; j < numsLen; j++) {
dp[i][j] = [];
}
}
// 遍历已经发现的所有数字
for (let i = 0; i < numsLen; i++) {
// 计算0到i的结果
for (let j = i; j >= 0; j--) {
// 如果只是一个数字,直接加入
if (i === j) {
dp[j][i].push(nums[i]);
} else {
// dp[j][i] 等于dp[j][k]和dp[k+1][i]相乘
for (let k = j; k < i; k += 1) {
for (let left of dp[j][k]) {
for (let right of dp[k + 1][i]) {
let val = 0;
switch (oprs[k]) {
case "+":
val = left + right;
break;
case "-":
val = left - right;
break;
case "*":
val = left * right;
break;
}
dp[j][i].push(val);
}
}
}
}
}
}
if (dp[0][numsLen - 1].length === 0) {
dp[0][numsLen - 1].push(+input);
}
return dp[0][numsLen - 1];
};
export default diffWaysToCompute;
932. 漂亮数组
题目描述
对于某些固定的 N,如果数组 A 是整数 1, 2, ..., N 组成的排列,使得:
对于每个 i < j,都不存在 k 满足 i < k < j 使得 A[k] * 2 = A[i] + A[j]。
那么数组 A 是漂亮数组。
给定 N,返回任意漂亮数组 A(保证存在一个)。
例子1
Input: 4
output: [2,1,4,3]
例子2
Input: 5
output: [3,1,2,5,4]
提示:
1 <= N <= 1000
思考
1 题目本身感觉如果第一次接触到,估计应该不会想到如何拆分和如何合并。
这里比较巧妙的是把n给拆分成偶数和奇数,这样如果从偶数集合和奇数集合里边各自取一个数的时候,则肯定不会出现
A[k] * 2 = A[i]+A[j], 因为偶数和奇数相加肯定是奇数,而A[k] * 2肯定是偶数,所以永远不会相等。
这里假设有一个漂亮数组
1.1 删除
如果删除漂亮数组中的任何一个值,到最后肯定还是漂亮数组
1.2 漂亮数组加或者减去同一个值
因为我们有 A[k] * 2 != A[i] + A[j],
(A[k] + x) * 2 = A[k] * 2 + 2x != A[i] + A[j] + 2x = (A[i] + x) + (A[j] + x)
比如: [1,3,2] + 1 = [2,4,3].
1.3 漂亮数组相乘或者相除同一个值
因为我们有 A[k] * 2 != A[i] + A[j],
对于任何x!=0
(A[k] * x) * 2 = A[k] * 2 * x != (A[i] + A[j]) * x = (A[i] * x) + (A[j] * x)
比如: [1,3,2] * 2 = [2,6,4]
通过上面可以得出,如果一个数组是漂亮数组,可以删除或者加上同一个数或者相乘同一个数都还是漂亮数组
现在假设有个漂亮数组A
A1 = A * 2 - 1
A2 = A * 2
B = A1 + A2
比如
A = [2, 1, 4, 5, 3]
A1 = [3, 1, 7, 9, 5]
A2 = [4, 2, 8, 10, 6]
B = A1 + A2 = [3, 1, 7, 9, 5, 4, 2, 8, 10, 6]
A1肯定是漂亮数组,如果从A1中任意选择一个,那么从A1中选择一个和从A2中选择一个,肯定还是不存在
A[k] * 2 === A[i] + A[j]
分治参考实现1
自顶向下参考实现2
字底向上参考实现3
实现1
/**
* @param {number} N
* @return {number[]}
*/
// Runtime: 84 ms, faster than 81.25% of JavaScript online submissions for Beautiful Array.
// Memory Usage: 40.1 MB, less than 87.50% of JavaScript online submissions for Beautiful Array.
export default (N) => {
let res = [];
res.push(1);
while (res.length < N) {
const tmp = [];
for (let odd of res) {
if (odd * 2 - 1 <= N) {
tmp.push(odd * 2 - 1);
}
}
for (let even of res) {
if (even * 2 <= N) {
tmp.push(even * 2);
}
}
res = tmp;
}
return res;
};
时间复杂度O(n),空间复杂度O(n)
实现2
/**
* @param {number} N
* @return {number[]}
*/
// Runtime: 84 ms, faster than 81.25% of JavaScript online submissions for Beautiful Array.
// Memory Usage: 40.6 MB, less than 43.75% of JavaScript online submissions for Beautiful Array.
const getBeautifulArray = (N, map) => {
if (N === 1) {
map.set(1, [1]);
return [1];
}
if (N === 2) {
map.set(2, [1, 2]);
return [1, 2];
}
const left = Math.floor(N / 2);
let leftArr = [];
if (map.has(left)) {
leftArr = map.get(left);
} else {
leftArr = getBeautifulArray(left, map);
map.set(left, leftArr);
}
let rightArr = [];
const right = N - left;
if (map.has(right)) {
rightArr = map.get(right);
} else {
rightArr = getBeautifulArray(right, map);
map.set(right, rightArr);
}
leftArr = leftArr.map((x) => x * 2);
rightArr = rightArr.map((x) => x * 2 - 1);
const temp = [...leftArr, ...rightArr];
map.set(N, temp);
return temp;
};
export default (N) => {
if (N === 1) return [1];
const map = new Map();
return getBeautifulArray(N, map);
};
实现3
/**
* @param {number} N
* @return {number[]}
*/
// Runtime: 156 ms, faster than 6.25% of JavaScript online submissions for Beautiful Array.
// Memory Usage: 57.3 MB, less than 6.25% of JavaScript online submissions for Beautiful Array.
export default (N) => {
if (N === 1) return [1];
const dp = [];
dp[1] = [1];
dp[2] = [1, 2];
for (let i = 3; i <= N; i++) {
const left = Math.floor(i / 2);
const leftArr = dp[left].map((x) => x * 2);
const right = i - left;
const rightArr = dp[right].map((x) => x * 2 - 1);
dp[i] = [...leftArr, ...rightArr];
}
return dp[N];
};
312. 戳气球
题目描述
有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。
求所能获得硬币的最大数量。
说明:
1 你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
2 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
例子1
Input: [3,1,5,8]
output: 167
解释:nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 315 + 358 + 138 + 181 = 167
例子2
Input: nums = [1,5]
output: 10
思考
1 题目本身还是有点难度的,如果第一次接触,考虑下直接看题解就可以
这里比较难的是如何把数组长度为n,拆分成两个数组。
因为这里需要我们每次戳破一个气球,然后得到相连的三个数组的硬币
如果采用拆分和组合的方式,可以按照下面考虑
1.1 首先考虑一下扎[0,nums.length-1] 这个区间可以得到的最大硬币,也就是最后结果
1.2 因为我们需要每次扎一个气球,假设我们从正面考虑,第一次先扎第i位置的气球,这个时候可以拆分成[0, i-1],nums[i],[i+1, nums.length-1] 这三个区间,但是很明显这里不能这么划分,因为如果按照这样划分,那么nums[i]左边和右边相连的数字是不确定的。这个时候如果逆向思考一下,如果是最后扎i位置的气球呢,很明显这时候也拆分成了三个区间
[0, i-1],nums[i],[i+1, nums.length-1],但是这时候因为是最后扎i位置的气球,可以看到nums[i]左边和右边的位置是确定的
1.3 剩下的就是定义区间的结果,假设dp[i][j]表示从i到j扎气球的可以得到的最大硬币数目(不包括i和j),但是这里可以注意到提示
你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
这时候我们可以重新组合下nums成为copynums,假设nums的长度是len,那么copynums的长度是len+2
copynums[0]=1,copynums[len+1]=1,copynums[1...len] = nums[0...len-1]
根据前面dp[i][j]的定义,那么我们的结果就变成了求copynums的dp[0][len+1]了
1.4 最后就剩下如何拆分再如何合并了,因为要求dp[0][len+1]的值,因为在copynums从1到len任何一个位置都可能是最后一次扎破,所以肯定得循环从1到len。假设其中一次我们选择扎破i位置的气球,那么此时的dp[0][len+1]等于什么呢?
此时dp[0][len+1] = dp[0][i]+dp[i][len+1]+copynums[0]* copynums[i]* copynums[len+1]
因为i位置的气球是最后扎破的,那么可以肯定此时的得到的最大硬币是copynums[0]* copynums[i]* copynums[len+1]
此时左边的最大硬币数是dp[0][i],因为可以选择从1到i-1的任何一个气球最后扎破,所以i位置的气球可以作为dp[0][i]的任何一个位置的最右边。
右边同理
因为i的范围是从1到len,所以最后的结果是要取这些所有可能的最大值
比如copynums = [1,3,1,5,8,1]的时候,选择i=3的时候,
dp[0][5] = dp[0][3]+dp[3][5]+1 * 1 * 5
可以按照dp[i][j]的定义,看看是不是已经包括了所有的可能情况
分治参考实现1
从下向上参考实现2
实现1
/**
* @param {number[]} nums
* @return {number}
*/
const burst = (memo, nums, left, right) => {
if (left + 1 === right) return 0;
if (memo[left][right] > 0) return memo[left][right];
let ans = 0;
for (let i = left + 1; i < right; i++) {
ans = Math.max(ans, nums[left] * nums[i] * nums[right] + burst(memo, nums, left, i) + burst(memo, nums, i, right));
}
memo[left][right] = ans;
return ans;
};
// Runtime: 448 ms, faster than 5.06% of JavaScript online submissions for Burst Balloons.
// Memory Usage: 40.1 MB, less than 89.40% of JavaScript online submissions for Burst Balloons.
export default (nums) => {
const len = nums.length;
const copyNums = new Array(len + 2);
copyNums[0] = 1;
copyNums[len + 1] = 1;
for (let i = 0; i < len; i++) {
copyNums[i + 1] = nums[i];
}
const memo = [];
for (let i = 0; i < len + 2; i++) {
memo[i] = new Array(len + 2).fill(0);
}
// console.log(memo);
return burst(memo, copyNums, 0, len + 1);
};
实现2
const n = nums.length;
nums = [1, ...nums, 1];
const len = nums.length;
const dp = [];
for (let i = 0; i < len; i++) {
dp[i] = new Array(len).fill(0);
}
// dp[i][j] 表示i到j-1的最大值
// i2jLen 表示dp[i][j]表示的数组长度
for (let i2jLen = 1; i2jLen <= n; i2jLen++) {
for (let i = 0, j = i2jLen + 1; j <= len - 1; i++, j++) {
for (let k = i + 1; k < j; k++) {
dp[i][j] = Math.max(dp[i][j], dp[i][k] + nums[i] * nums[k] * nums[j] + dp[k][j]);
}
}
}
return dp[0][len - 1];