-
candidates 是
有无重复
,正整数数组 -
数组中的每一个值只能取一次;不可以重复取值,但是对于重复的值是可以取的,即 [1,1,2,3] -> 可以取 [1,1,2],[1,3] -> 4
-
为了不取到重复的值,就得跳过相同值,这个时候需要对数组
排序
-
在每一层进行枚举的时候,循环中出现重复值的时候,剪掉这部分的枚举,因为肯定有相同的一部分
-
由于不可以重复取,所以 dfs 第一个入参的下标是要 +1 的,表示不可以重复取上一次哪一个值
var combinationSum2 = function (candidates, target) {
candidates.sort((a,b)=>a-b)
const ret= []
const dfs = (start,arr,sum) => {
if(sum === target) {
ret.push(arr)
return
}
if(sum>target || start>= candidates.length) return
for(let i = start;i<candidates.length;i++){
// 将重复的剪掉
if(i > start && candidates[i] === candidates[i-1]) continue
// 这里的 start 是启动枚举的下标,但是插入到临时数组的值是当前下标的值
dfs(i+1,[…arr,candidates[i]],sum+candidates[i])
}
}
dfs(0,[],0)
return ret
}
复制代码
216. 组合总和 III
分析
-
给定的不是具体的数组,而是长度限制 k, 和目标值 target – 等同于 candidates 是
无重复
,1-9 的正整数数组 -
所以可以看做是 39. 组合总和 的特殊情况,只是判定条件有出入
var combinationSum3 = function (k, n) {
const ret = [];
const dfs = (start, arr, sum) => {
if (arr.length === k && sum === n) {
ret.push(arr);
return;
}
if (arr.length > k || sum > n) {
return;
}
for (let i = start + 1; i < 10; i++) {
dfs(i, […arr, i], sum + i);
}
};
dfs(0, [], 0);
return ret
};
复制代码
377. 组合总和 Ⅳ
分析 – 回溯
-
candidates 是
无重复
,正整数数组,可以重复取值且要取排列不同
的组合 -
这道题和组合总和很像,区别在于本题求的是排列的数量,而题1 求的是不重复的组合
-
所以这里不需要限制组合起始枚举的下标了,每一次都从 0 开始即可
-
然后超时了
*/
var combinationSum4 = function (nums, target) {
let ret = 0;
const dfs = (sum) => {
if (sum === target) {
ret++;
return;
}
if (sum > target) return;
for (let i = 0; i < nums.length; i++) {
dfs(sum + nums[i]);
}
};
dfs(0);
return ret;
};
复制代码
分析 – dp
-
dp[i] 表示值为 i 的时候存在的组合数量
-
状态转移方程 dp[i] = sum(dp[i-nums[k]])
-
base case dp[0] = 1
var combinationSum4 = function (nums, target) {
const dp = new Array(target+1)
dp[0]= 1 // 如果刚好得到的值是0,那么就有 1,因为不取也是一种取法
for(let i = 1;i<target+1;i++){
dp[i] = 0
for(let j =0;j<nums.length;j++){
if(i>=nums[j]){
dp[i]+=dp[i-nums[j]]
}
}
}
return dp[target]
}
复制代码
78. 子集
分析 – 找规律
-
数组元素不相同,返回值不包含重复的子集,也就是不考虑位置排列情况
-
由于跟排列无关,所以只需要遍历一遍 nums 即可,没遍历一次获取到的值,都可以和现有的 ret 组合成新的一批数组,然后和旧的item组合成新的枚举数组
-
时间复杂度 {O(n^2)}O(n2)
var subsets = function (nums) {
let ret = [[]]
for(let num of nums ){
ret = […ret,…ret.map(item => item.concat(num))]
}
return ret
}
复制代码
分析 – 迭代回溯
-
使用迭代的方法枚举所有的情况出来, 和多叉树遍历没啥区别
-
时间复杂度 {O(N^2)}O(N2)
var subsets = function (nums) {
const ret = []
const dfs = (start,arr) => {
ret.push(arr)
if(arr.length === nums.length || start=== arr.length) return
for(let i = start;i<nums.length;i++){
dfs(i+1,[…arr,nums[i]])
}
}
dfs(0,[])
return ret
}
复制代码
90. 子集 II
分析 – 有重复值
-
和78. 子集相比,就是多了重复值,且不允许重复值出现在返回数组中,所以明显要先排序了
-
然后在回溯过程中,如果下一次迭代的值和当前值一样,则跳过,达到去重的效果
var subsetsWithDup = function (nums) {
nums.sort((a,b)=> a-b)
const ret = []
const dfs = (start,arr) => {
ret.push(arr)
if(start === nums.length ) return // start 超出下标,就是取到了最大下标值的时候了
for(let i = start;i<nums.length;i++){
dfs(i+1,[…arr,nums[i]])
while(nums[i] === nums[i+1]){
i++ // 去重
}
}
}
dfs(0,[])
return ret
}
复制代码
131. 分割回文串
分析
-
这是一个变种的组合问题,因为排列顺序已经确定好了只要切割就好
-
所以在遍历过程中,只有当符合回文要求的子串,才能切割,然后往下走,否则剪掉较好
-
回文子串的判定可以简单的用左右双指针来实现
var partition = function(s) {
const ret = []
// 判断是否是回文子串
function isValid(s) {
if(s.length === 1) return true // 只有一个字符
let l = 0,r = s.length-1
while(l<r){
if(s[l] !== s[r]) return false
l++
r–
}
return true
}
const dfs = (start,arr) => {
if(start === s.length){
ret.push(arr)
return
}
let temp =‘’
for(let i =start;i<s.length;i++){
temp+=s[i]
if(isValid(temp)){
dfs(i+1,[…arr,temp])
}
}
}
dfs(0,[])
return ret
};
复制代码
93. 复原 IP 地址
分析
-
这道题和 131. 分割回文串 类似
-
这里也是切分字符串,只是判定条件变成了每一分段都要符合有效的 IP 地址,但是架子是一样的
-
这里的判定条件也多,只需要将合乎要求的条件算上,就能砍掉不少的分支
var restoreIpAddresses = function (s) {
const ret = [];
function isValid(s) {
if (s.length > 1 && s[0] == 0) return false; // 不能以 0 起头
if (s >= 1 << 8) return false; // 要在 [0,255] 之间
return true;
}
const dfs = (start, arr) => {
if (arr.length === 4 && start !== s.length) return; // 已经分成4分,但是还没分完
if (start === s.length) {
if (arr.length === 4) {
ret.push(arr.join(“.”));
}
// 无论是否分成四份,都离开了
return;
}
let str = “”;
for (let i = start; i < s.length && i < start + 3; i++) {
str += s[i];
if (isValid(str)) {
dfs(i + 1, […arr, str]);
}
}
};
dfs(0, []);
return ret;
};
复制代码
112. 路径总和
分析
-
路径是 root-leaf 完整路线上的和为 target
-
dfs 中序遍历走下去即可
-
时间复杂度 {O(n)}O(n)
var hasPathSum = function(root, targetSum) {
let ret = false
const dfs = (root,sum) => {
if(ret || !root) return // 只要一条路走通了,其他都不用走了
sum += root.val
if(!root.left && !root.right && sum === targetSum) {
ret = true
return
}
if(root.left) dfs(root.left,sum)
if(root.right) dfs(root.right,sum)
}
dfs(root,0)
return ret
};
复制代码
113. 路径总和 II
分析
-
找的还是 root - leaf 的路径,但是这一次要把找的所有符合要求的路径都保存起来
-
时间复杂度 {O(n)}O(n)
var pathSum = function(root, targetSum) {
const ret = []
const dfs = (root,arr,sum) => {
if(!root) return
sum+=root.val
arr = […arr,root.val]
if(!root.left && !root.right && sum == targetSum){
ret.push(arr)
}
if(root.left) dfs(root.left,[…arr],sum)
if(root.right) dfs(root.right,[…arr],sum)
}
dfs(root,[],0)
return ret
};
复制代码
437. 路径总和 III
分析
-
这次找的路径可以是树中任意
起始-结束
节点,; -
但是路径必须是向下的,也就是不能是 a.left - a - a.right 的样子,这其实是减轻难度的限制条件
-
所以还是一样的自顶向下遍历就好,但是遇到满足需求的路径,还是要继续遍历到叶子节点位置
-
和 112. 路径总和 与 113. 路径总和 II 最大不同是,这一次的路径是不限制起始点和终点的;
-
不限制终点,那么我可以在遍历过程中,只要满足 targetSum, 就记录一次,一直到叶子节点位置,不需要到了叶子节点再判断
-
而不限制起始点是根节点,那么就是可以以任意节点为起始点,也就是需要遍历整一棵树作为起始点时候,往下去找路径了;
-
时间复杂度{O(nlogn)}O(nlogn)
var pathSum = function (root, targetSum) {
let ret = 0;
// 这是以任意 root 节点找路径和的 dfs
const dfs = (root, sum) => {
if (!root) return;
sum += root.val;
if (sum === targetSum) ret++;
if (!root.left && !root.right) return; // 叶子节点了,结束
if (root.left) dfs(root.left, sum);
if (root.right) dfs(root.right, sum);
};
// 这是遍历整棵树,然后继续往下走
const outer = (root) => {
if (!root) return;
dfs(root, 0);
outer(root.left);
outer(root.right);
};
outer(root);
return ret;
};
复制代码
51. N 皇后
分析 – 直接求符合要求的 chessboard
-
行就是树递归的深度,列就是每一层的宽度,使用回溯的办法进行树的 dfs 遍历
-
整个过程需要 3 大部分,回溯的方式遍历树,找出符合要求的节点 chessboard[row][col], 将符合要求的二维数组转换成符合要求的字符串数组
-
时间复杂度 {O(n*logn)}O(n∗logn)
var solveNQueens = function (n) {
const ret = [];
// 1. N 皇后实际走的过程 – 回溯树
const dfs = (row, chessboard) => {
if (row === n) {
// 已经到了叶子结点下 null 了 –
// 但是 chessboard 是一个二维数组,不能随便就push 进去的,需要深拷贝一下
ret.push(getStrChessboad(chessboard));
return;
}
// 每一行都是从 0 - n-1 , 然后不符合要求的就回溯回去
for (let col = 0; col < n; col++) {
if (isValid(row, col, chessboard)) {
// 如果 chessboard[row][col] 符合要求,则算一条路
chessboard[row][col] = “Q”;
dfs(row + 1, chessboard);
chessboard[row][col] = “.”; // 回溯回来
}
}
};
// 判断当前节点是否符合 N 皇后的要求 – 需要注意,这里 [0,n-1] 是从左往右算
function isValid(row, col, chessboard) {
// 同一列
for (let i = 0; i < row; i++) {
if (chessboard[i][col] === “Q”) {
return false;
}
}
// 从左往右 45` 倾斜
for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i–, j–) {
if (chessboard[i][j] === “Q”) {
return false;
}
}
// 从右往左 135` 倾斜
for (let i = row - 1, j = col + 1; i >= 0 && j < n; i–, j++) {
if (chessboard[i][j] === “Q”) {
return false;
}
}
// 如果不是同一列或者左右斜线,则满足要求
return true;
}
// 将二维数组的 N 皇后转成一维数组字符串形式
function getStrChessboad(chessboard) {
const ret = [];
chessboard.forEach((row) => {
let str = “”;
row.forEach((item) => {
str += item;
});
ret.push(str);
});
return ret;
}
const chessboard = new Array(n).fill([]).map(() => new Array(n).fill(“.”));
dfs(0, chessboard);
return ret;
};
复制代码
52. N皇后 II
分析
-
问题和 51. N 皇后 基本一样,只是求的值从完整的 N 皇后方案,变成了只要知道有几个就可以了
-
所以第三部分转换可以直接删除,然后直接拷贝过来即可
var totalNQueens = function (n) {
let ret = 0;
const dfs = (row, chessboard) => {
if (row === n) {
ret++;
return;
}
for (let col = 0; col < n; col++) {
if (isValid(row, col, chessboard)) {
chessboard[row][col] = “Q”;
dfs(row + 1, chessboard);
chessboard[row][col] = “.”;
}
}
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
= function (n) {
let ret = 0;
const dfs = (row, chessboard) => {
if (row === n) {
ret++;
return;
}
for (let col = 0; col < n; col++) {
if (isValid(row, col, chessboard)) {
chessboard[row][col] = “Q”;
dfs(row + 1, chessboard);
chessboard[row][col] = “.”;
}
}
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-RmASW010-1715092505181)]
[外链图片转存中…(img-xj5AsZ0f-1715092505181)]
[外链图片转存中…(img-HdoNPbXB-1715092505181)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!