回溯 回溯模板 void backtracking (参数) { if (终止条件) { 存放结果; return ; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking (路径,选择列表); 回溯,撤销处理结果 } }
1. 组合问题
组合问题 ==每次for都是从startIndex开始==
每个元素 用一次和用多次体现在 ==backtrack(i+1)==用一次还是==backtrack(i)用多次==上
组合问题==不需要used数组==,去重也不需要used数组那个判断
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
思路
按回溯模板 直接写
注意剪枝操作, 超过k个元素不再进入递归 24ms->8ms
代码 class Solution {public : int n; int k; vector<int > path; vector<vector<int >> all; vector<vector<int >> combine (int n, int k) { this ->n = n; this ->k = k; path.clear (); all.clear (); backtrack (1 ); return all; } void backtrack (int num) { if (path.size () == k){ all.push_back (path); return ; } for (int i = num; i<=n - (k-path.size ()) + 1 ; i++){ path.push_back (i); backtrack (i+1 ); path.pop_back (); } } };
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates = [2,3,6,7], target = 7 输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8 输出: [[2,2,2,2],[2,3,3],[3,5]]
思路
注意这个题 单个元素是可以重复使用的
表现在代码上 for循环内 backtrack是i 而不是i+1
sum 和 target 的判断逻辑注意下
代码 class Solution {public : vector<vector<int >> ans; vector<int > path; void backtracking (vector<int >& candidates, int index, int target, int sum) { if (sum>target) return ; if (sum == target) { ans.push_back (path); return ; } for (int i = index; i<candidates.size (); i++){ path.push_back (candidates[i]); backtracking (candidates, i, target, sum + candidates[i]); path.pop_back (); } } vector<vector<int >> combinationSum (vector<int >& candidates, int target) { backtracking (candidates,0 , target, 0 ); return ans; } };
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解释: 1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。
思路
结束终止满足两个条件之一 nowSum >= n || path.size() == k
即可
满足条件的结果 nowSum == n && path.size() == k
代码 class Solution {public : vector<vector<int >> all; vector<int > path; int k,n ; vector<vector<int >> combinationSum3 (int k, int n) { this ->k = k; this ->n = n; backtrack (1 , 0 ); return all; } void backtrack (int cur, int nowSum) { if (nowSum >= n || path.size () == k){ if (nowSum == n && path.size () == k) all.push_back (path); return ; } for (int i = cur; i<=9 ; i++){ path.push_back (i); backtrack (i+1 , nowSum + i); path.pop_back (); } } };
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意: 解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5, 输出: [ [1,2,2], [5] ]
代码 class Solution {public : vector<vector<int >> all; vector<int > path; vector<vector<int >> combinationSum2 (vector<int >& candidates, int target) { sort (candidates.begin (), candidates.end ()); backtrack (candidates, target, 0 , 0 ); return all; } void backtrack (vector<int >& candidates, int target, int curIndex, int nowSum) { if (nowSum>target) return ; if (nowSum == target){ all.push_back (path); return ; } for (int i = curIndex; i<candidates.size (); i++){ if (i>curIndex && candidates[i] == candidates[i-1 ]) continue ; path.push_back (candidates[i]); backtrack (candidates, target, i+1 , nowSum+candidates[i]); path.pop_back (); } } };
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4 输出:7 解释: 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3 输出:0
思路
名为组合 实为排列 解法dp
回溯超时 dp解答
记忆化dfs 其实就是dp了吧
暴力回溯代码 class Solution {public : int ans; int combinationSum4 (vector<int >& nums, int target) { ans = 0 ; backtrack (nums, 0 , target); return ans; } void backtrack (vector<int >& nums, int nowSum, int target) { if (nowSum>target) return ; if (nowSum == target){ ans++; return ; } for (int i = 0 ; i<nums.size (); i++){ backtrack (nums, nowSum+nums[i], target); } } };
记忆化dfs class Solution {public : int combinationSum4 (vector<int >& nums, int target) { return dfs (nums, target); } unordered_map<int , int > memo; int dfs (vector<int >& nums, int target) { if (target == 0 ) return 1 ; if (target < 0 ) return 0 ; if (memo.count (target) == 1 ) return memo[target]; int res = 0 ; for (int i = 0 ; i < nums.size (); i++){ res += dfs (nums, target - nums[i]); } memo[target] = res; return res; } };
dp代码 class Solution {public : int combinationSum4 (vector<int >& nums, int target) { vector<unsigned long long > dp (target+1 ) ; dp[0 ] = 1 ; for (int i = 1 ; i<=target; i++){ for (int num : nums){ if (i>=num) dp[i] += dp[i-num]; } } return dp[target]; } };
难度中等846收藏分享切换为英文接收动态反馈
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
例如:"0.1.2.201"
和 "192.168.1.1"
是 有效 IP 地址,但是 "0.011.255.245"
、"192.168.1.312"
和 "192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址 ,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"]
示例 2:
输入:s = "0000" 输出:["0.0.0.0"]
示例 3:
输入:s = "101023" 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
思路 看代码
代码 class Solution {private : vector<string> result; void backtracking (string& s, int startIndex, int pointNum) { if (pointNum == 3 ) { if (isValid (s, startIndex, s.size () - 1 )) { result.push_back (s); } return ; } for (int i = startIndex; i < s.size (); i++) { if (isValid (s, startIndex, i)) { s.insert (s.begin () + i + 1 , '.' ); pointNum++; backtracking (s, i + 2 , pointNum); pointNum--; s.erase (s.begin () + i + 1 ); } else break ; } } bool isValid (const string& s, int start, int end) { if (start > end) { return false ; } if (s[start] == '0' && start != end) { return false ; } int num = 0 ; for (int i = start; i <= end; i++) { if (s[i] > '9' || s[i] < '0' ) { return false ; } num = num * 10 + (s[i] - '0' ); if (num > 255 ) { return false ; } } return true ; } public : vector<string> restoreIpAddresses (string s) { result.clear (); if (s.size () > 12 ) return result; backtracking (s, 0 , 0 ); return result; } };
labuladong 题解 思路
难度中等1556
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
思路 简单回溯
代码 class Solution {public : vector<vector<int >> ans; vector<int > path; vector<vector<int >> subsets (vector<int >& nums) { path.clear (); ans.clear (); ans.push_back (path); backTrack (nums, 0 ); return ans; } void backTrack (vector<int >& nums, int startIndex) { if (startIndex>=nums.size ()) return ; for (int i = startIndex; i<nums.size (); i++){ path.push_back (nums[i]); ans.push_back (path); backTrack (nums, i+1 ); path.pop_back (); } } };
labuladong 题解
难度中等783收藏分享切换为英文接收动态反馈
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
思路 含重复元素的组合去重
代码 class Solution {public : vector<vector<int >> ans; vector<int > path; vector<vector<int >> subsetsWithDup (vector<int >& nums) { ans.clear (); path.clear (); ans.push_back (path); sort (nums.begin (), nums.end ()); backTrack (nums, 0 ); return ans; } void backTrack (vector<int >& nums, int startIndex) { if (startIndex>nums.size ()) return ; for (int i = startIndex; i<nums.size (); i++){ if (i>startIndex && nums[i] == nums[i-1 ]) continue ; path.push_back (nums[i]); ans.push_back (path); backTrack (nums, i+1 ); path.pop_back (); } } };
难度中等412收藏分享切换为英文接收动态反馈
给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1] 输出:[[4,4]]
思路
去重的产生 比 4 7 6 7 数组,不去重的话 4 7 会出现两次,但是去重不能用sort因为破坏原排列的顺序
应考虑用哈希 或者其他方式去重 最后去重的话 没有起到剪枝效果
代码
最终暴力去重 sort->unique->erase
class Solution {public : vector<int > path; vector<vector<int >> ans; vector<vector<int >> findSubsequences (vector<int >& nums) { path.clear (); ans.clear (); backTrack (nums, 0 ); sort (ans.begin (), ans.end ()); ans.erase (unique (ans.begin (), ans.end ()), ans.end ()); return ans; } void backTrack (vector<int >& nums, int startIndex) { if (startIndex>nums.size ()){ return ; } for (int i = startIndex; i<nums.size (); i++){ if (path.size ()>0 && nums[i]<path.back ()) continue ; path.push_back (nums[i]); if (path.size ()>1 ) ans.push_back (path); backTrack (nums, i+1 ); path.pop_back (); } } };
使用单层的set进行去重 注意 set定义在每一层 作用只在定义的这一层
class Solution {public : vector<int > path; vector<vector<int >> ans; vector<vector<int >> findSubsequences (vector<int >& nums) { path.clear (); ans.clear (); backTrack (nums, 0 ); return ans; } void backTrack (vector<int >& nums, int startIndex) { if (startIndex>nums.size ()) return ; unordered_set<int > uset; for (int i = startIndex; i<nums.size (); i++){ if (path.size ()>0 && nums[i]<path.back () || uset.count (nums[i])) continue ; path.push_back (nums[i]); if (path.size ()>1 ) ans.push_back (path); uset.insert (nums[i]); backTrack (nums, i+1 ); path.pop_back (); } } };
难度简单429
输入一个正整数 target
,输出所有和为 target
的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例 1:
输入:target = 9 输出:[[2,3,4],[4,5]]
示例 2:
输入:target = 15 输出:[[1,2,3,4,5],[4,5,6],[7,8]]
暴力回溯 时间换空间
class Solution {public : vector<vector<int >> ans; vector<int > path; vector<vector<int >> findContinuousSequence (int target) { for (int i = 1 ; i<target; i++){ backtrack (i, target, 0 ); } return ans; } void backtrack (int startIndex, int target, int nowSum) { if (nowSum == target){ ans.push_back (path); return ; } if (nowSum > target) return ; path.push_back (startIndex); backtrack (startIndex + 1 , target, nowSum + startIndex); path.pop_back (); } };
滑动窗口 vector 空间换时间
class Solution {public : vector<vector<int >> findContinuousSequence (int target) { int left = 1 , right = 1 ; vector<vector<int >> ans; vector<int > window; int winSum = 0 ; while (right<target){ winSum += right; window.push_back (right); while (winSum >= target){ if (winSum == target) ans.push_back (vector<int >(window.begin () + left - 1 , window.end ())); winSum-=left; left++; } right++; } return ans; } };
为啥链表效率特别低 时间长 内存占用大
class Solution {public : vector<vector<int >> findContinuousSequence (int target) { int left = 1 , right = 1 ; vector<vector<int >> ans; list<int > window; int winSum = 0 ; while (right<target){ winSum += right; window.push_back (right); while (winSum >= target){ if (winSum == target) ans.push_back (vector<int >(window.begin (), window.end ())); winSum-=left; window.pop_front (); left++; } right++; } return ans; } };
2. 排列问题
排列问题 ==每次for都是从0开始==
因为是排列 不能限制顺序 所以不传入index
==需要used数组==,去重判断重中之重
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1] 输出:[[0,1],[1,0]]
示例 3:
思路
数组不重复 最简简单单的全排列,基本回溯解法
不讲五的解法 next_permutation (略)
代码 class Solution {public : vector<vector<int >> ans; vector<int > path; void backtrack (vector<int >& nums, vector<bool >& used) { if (path.size () == nums.size ()){ ans.push_back (path); return ; } for (int i = 0 ; i<nums.size (); i++){ if (used[i]) continue ; used[i] = 1 ; path.push_back (nums[i]); backtrack (nums, used); path.pop_back (); used[i] = 0 ; } } vector<vector<int >> permute (vector<int >& nums) { ans.clear (); path.clear (); vector<bool > used (nums.size(), 0 ) ; backtrack (nums, used); return ans; } };
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,arr = [1,2,3]
,以下这些都可以视作 arr
的排列:[1,2,3]
、[1,3,2]
、[3,1,2]
、[2,3,1]
。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr = [1,2,3]
的下一个排列是 [1,3,2]
。
类似地,arr = [2,3,1]
的下一个排列是 [3,1,2]
。
而 arr = [3,2,1]
的下一个排列是 [1,2,3]
,因为 [3,2,1]
不存在一个字典序更大的排列。
给你一个整数数组 nums
,找出 nums
的下一个排列。
必须** 原地 **修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3] 输出:[1,3,2]
示例 2:
输入:nums = [3,2,1] 输出:[1,2,3]
示例 3:
输入:nums = [1,1,5] 输出:[1,5,1]
思路 题目要求实现 next_permutation
我们需要将一个左边的「较小数」与一个右边的「较大数」交换,以能够让当前排列变大,从而得到下一个排列。
同时我们要让这个「较小数」尽量靠右,而「较大数」尽可能小。当交换完成后,「较大数」右边的数需要按照升序重新排列。这样可以在保证新排列大于原来排列的情况下,使变大的幅度尽可能小。
以排列 [4,5,2,6,3,1]
为例:
我们能找到的符合条件的一对「较小数」与「较大数」的组合为 2与 3,满足「较小数」尽量靠右,而「较大数」尽可能小。
当我们完成交换后排列变为 [4,5,3,6,2,1],此时我们可以重排「较小数」右边的序列,序列变为 [4,5,3,1,2,6]。
不明白就调试调试
代码 class Solution {public : void nextPermutation (vector<int >& nums) { int i = nums.size () - 2 ; while (i >= 0 && nums[i] >= nums[i + 1 ]) { i--; } if (i >= 0 ) { int j = nums.size () - 1 ; while (j >= 0 && nums[i] >= nums[j]) { j--; } swap (nums[i], nums[j]); } reverse (nums.begin () + i + 1 , nums.end ()); } };
二刷 听说字节面试考了 class Solution {public : void nextPermutation (vector<int >& nums) { int n = nums.size (); int pos = -1 ; for (int i = n-1 ; i>=1 ; i--){ if (nums[i]>nums[i-1 ]){ pos = i-1 ; break ; } } if (pos == -1 ){ reverse (nums.begin (), nums.end ()); return ; } int pos2; for (int i = n-1 ; i>=0 ; i--){ if (nums[i] > nums[pos]){ pos2 = i; break ; } } swap (nums[pos], nums[pos2]); sort (nums.begin () + pos + 1 , nums.end ()); return ; } };
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
思路
重点!重复元素全排列主要是去重问题
(i>0 && !used[i-1] && nums[i] == nums[i-1])
代码 class Solution {public : vector<vector<int >> ans; vector<int > path; vector<vector<int >> permuteUnique (vector<int >& nums) { sort (nums.begin (), nums.end ()); vector<bool > used (nums.size(), 0 ) ; backtrack (nums, used); return ans; } void backtrack (vector<int >& nums, vector<bool >& used) { if (path.size () == nums.size ()){ ans.push_back (path); return ; } for (int i = 0 ; i<nums.size (); i++){ if (used[i] || (i>0 && !used[i-1 ] && nums[i] == nums[i-1 ])) continue ; used[i] = 1 ; path.push_back (nums[i]); backtrack (nums, used); path.pop_back (); used[i] = 0 ; } } };
难度中等368
给定一个字符串 s
,通过将字符串 s
中的每个字母转变大小写,我们可以获得一个新的字符串。
返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。
示例 1:
输入:s = "a1b2" 输出:["a1b2", "a1B2", "A1b2", "A1B2"]
示例 2:
输入: s = "3z4" 输出: ["3z4","3Z4"]
思路 代码 class Solution {public : vector<string> ans; string path; vector<string> letterCasePermutation (string s) { backtrack (s, 0 ); return ans; } void backtrack (string &s, int index) { if (index == s.size ()){ ans.push_back (path); return ; } if (!isalpha (s[index])){ path+=s[index]; backtrack (s, index+1 ); path.pop_back (); }else { path+=(tolower (s[index])); backtrack (s, index+1 ); path.pop_back (); path+=(toupper (s[index])); backtrack (s, index+1 ); path.pop_back (); } } };
难度困难641
给出集合 [1,2,3,...,n]
,其所有元素共有 n!
种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3
时, 所有排列如下:
"123"
"132"
"213"
"231"
"312"
"321"
给定 n
和 k
,返回第 k
个排列。
示例 1:
示例 2:
示例 3:
思路 完全回溯无法通过 要实现==精准剪枝==
既然所有的全排列是从小到大,那么可以对每一位的数字进行定位
。例如,假如给定题目为(5,46)。固定第一位数,后面4位的全排列数为24,math.ceil(46/24)=2,即处于第1位数的第二个循环中,即第一位数为2.同理,对于固定第二位数,math.ceil((46-24)/6)=4,即处于第2位数的第四个循环中(此时列表移除了已确定的数字2),即第2位数为5.同理,可依次推理出最后结果为“25341”.总复杂度为O(n).
代码 class Solution {public : void dsssfs (int n, int k, unordered_set<int > &used, string &tmp, vector<int > &factorial) { if (tmp.size () == n) { return ; } int ind = 0 ; for (int i = 1 ; i <= n; ++i) { if (used.find (i) != used.end ()) continue ; ++ind; int size = factorial[n - used.size () - 1 ]; if (k > (ind - 1 ) * size && k <= ind * size) { tmp.push_back (i + '0' ); used.insert (i); dsssfs (n, k - size * (ind - 1 ), used, tmp, factorial); } } } string getPermutation (int n, int k) { unordered_set<int > used; string tmp; vector<int > factorial = {1 , 1 , 2 , 6 , 24 , 120 , 720 , 5040 , 40320 , 362880 }; dsssfs (n, k, used, tmp, factorial); return tmp; } };
class Solution {public : string getPermutation (int n, int k) { k--; vector<int > dp (n) ; dp[0 ] = 1 ; for (int i = 1 ; i < n; ++i) dp[i] = dp[i - 1 ] * i; vector<int > nums (n) ; iota (nums.begin (), nums.end (), 1 ); string s; while (n--) { int ord = k / dp[n]; s.push_back (nums[ord] + '0' ); for (int j = ord; j < n; ++j) nums[j] = nums[j + 1 ]; k %= dp[n]; } return s; } };
数组A中给定可以使用的1~9的数,返回由A数组中的元素组成的小于n的最大数。例如A={1, 2, 4, 9},n=2533,返回2499
回溯思路 可以选择全部回溯,但是没有必要
先排序 再回溯,如果当前数>target 就直接return 后面就没有回溯的必要了
int ans_getlargestNum;int getlargestNum (vector<int > &nums, int target) { ans_getlargestNum = 0 ; sort (nums.begin (), nums.end ()); backtrack_getlargestNum (nums, target, 0 ); return ans_getlargestNum; } void backtrack_getlargestNum (vector<int > &nums, int target, int nowNum) { for (int i = 0 ; i < nums.size (); i++) { int backup = nowNum; nowNum *= 10 ; nowNum += nums[i]; if (nowNum >= target) { return ; } ans_getlargestNum = max (ans_getlargestNum, nowNum); backtrack_getlargestNum (nums, target, nowNum); nowNum = backup; } }
贪心思路 对于本题而言,证明的方法是从解的性质反推,归纳。
首先,小于n的数字有什么性质?显然可以分为两类:
位数和n相同,但是字典序小
位数比n小
对于情形1,我们要找字典序小的数,那么什么条件下字典序小?
两个字符串有一段相同的前缀(可以长度为0);
在这段前缀后的第一个字符更小。
我们只需枚举前述第一个更小的位置即可。对于这个更小的字符,我们应当让它尽可能大,但是不能等于或超过限值;对于其余字符,则没有限制,取可选的最大即可。这就得出了前面的构造方法。
对于情形2,位数小于 n 的数字中,最大的显然就是位数-1,但所有数位都最大的数字。
这样就推出了解法,同时从推理的过程中验证了解的正确性。
3. 分割问题 难度中等1062收藏分享切换为英文接收动态反馈
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
示例 2:
代码 class Solution {private : vector<vector<string>> result; vector<string> path; void backtracking (const string& s, int startIndex) { if (startIndex >= s.size ()) { result.push_back (path); return ; } for (int i = startIndex; i < s.size (); i++) { if (isPalindrome (s, startIndex, i)) { string str = s.substr (startIndex, i - startIndex + 1 ); path.push_back (str); } else { continue ; } backtracking (s, i + 1 ); path.pop_back (); } } bool isPalindrome (const string& s, int start, int end) { for (int i = start, j = end; i < j; i++, j--) { if (s[i] != s[j]) { return false ; } } return true ; } public : vector<vector<string>> partition (string s) { result.clear (); path.clear (); backtracking (s, 0 ); return result; } };
难度中等892英文版讨论区
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
例如:"0.1.2.201"
和 "192.168.1.1"
是 有效 IP 地址,但是 "0.011.255.245"
、"192.168.1.312"
和 "192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址 ,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"]
示例 2:
输入:s = "0000" 输出:["0.0.0.0"]
示例 3:
输入:s = "101023" 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
回溯 类似上述回文s串 进行有效判定 class Solution {public : vector<string> ans; vector<string> restoreIpAddresses (string s) { ans.clear (); backTrack (s, 0 , 0 ); return ans; } void backTrack (string& s, int startIndex, int pointNum) { if (pointNum == 3 ){ if (isValid (s, startIndex, s.size () - 1 )) ans.push_back (s); return ; } for (int i = startIndex; i<s.size (); i++){ if (isValid (s, startIndex, i)){ s.insert (s.begin () + i + 1 , '.' ); pointNum++; backTrack (s, i+2 , pointNum); pointNum--; s.erase (s.begin () + i + 1 ); } } } bool isValid (string& s, int start, int end) { if (start>end) return 0 ; if (s[start] == '0' && start != end) return 0 ; int num = 0 ; for (int i = start; i<=end; i++){ if (s[i]>'9' || s[i]<'0' ) return 0 ; num = num * 10 + (s[i] - '0' ); if (num>255 ) return 0 ; } return 1 ; } };
更符合我习惯的回溯方法 枚举所有可能的dot的位置 然后在最后进行valid判断
class Solution {public : vector<int > dotPos; vector<string> all; vector<string> restoreIpAddresses (string s) { backtrack (s, 0 ); return all; } void backtrack (string& s, int startIndex) { if (dotPos.size () == 3 ){ bool valid = 1 ; valid = isValid (s, 0 , dotPos[0 ] - 1 ) && isValid (s, dotPos[0 ], dotPos[1 ] - 1 ) && isValid (s, dotPos[1 ], dotPos[2 ] - 1 ) && isValid (s, dotPos[2 ], s.size () - 1 ); if (valid){ string temp = s; temp.insert (temp.begin () + dotPos[2 ], '.' ); temp.insert (temp.begin () + dotPos[1 ], '.' ); temp.insert (temp.begin () + dotPos[0 ], '.' ); all.push_back (temp); } return ; } for (int i = startIndex; i<s.size (); i++){ dotPos.push_back (i); backtrack (s, i+1 ); dotPos.pop_back (); } } bool isValid (const string& s, int left, int right) { if (left > right) return 0 ; if (left != right && s[left] == '0' ) return 0 ; int num = 0 ; for (int i = left; i<=right; i++){ if (s[i] > '9' || s[i] <'0' ) return 0 ; num*=10 ; num+=(s[i] - '0' ); if (num>255 ) return 0 ; } return 1 ; } };
集合划分问题 labuladong 题解 思路
难度中等564
给定一个整数数组 nums
和一个正整数 k
,找出是否有可能把这个数组分成 k
个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4 输出: True 说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
示例 2:
输入: nums = [1,2,3,4], k = 3 输出: false
遍历数字 然后按 数字遍历所有桶的情况 超时 遍历数字 每个数字遍历桶 容易超时
class Solution {public : vector<int > bucket; bool canPartitionKSubsets (vector<int >& nums, int k) { int n = nums.size (); int sum = accumulate (nums.begin (), nums.end (), 0 ); if (sum%k) return 0 ; int target = sum / k; bucket.resize (k, 0 ); sort (nums.begin (), nums.end (), [](const int & a, const int & b)->bool {return a>b;}); return backtrack (nums, 0 , target); } bool backtrack (vector<int >& nums, int index, int target) { if (index == nums.size ()){ for (int i = 0 ; i<bucket.size (); i++){ if (bucket[i] != target) return 0 ; } return 1 ; } for (int i = 0 ; i<bucket.size (); i++){ if (bucket[i] + nums[index] > target) continue ; bucket[i] += nums[index]; if (backtrack (nums, index+1 , target)) return 1 ; bucket[i] -= nums[index]; } return 0 ; } };
再优化 可以 通过集合划分问题:回溯 - 划分为k个相等的子集 - 力扣(LeetCode)
剪枝
将nums数组从大到小排序
,可以尽可能多的情况命中 if (bucket[i] + nums[index] > target) 这个剪枝
如果 当前子集的元素和 与 前一个子集的元素和 是一样的,那就跳过。因为当前这个数字放在前一个桶和后一个桶都是一样的,对后面的数字摆放没有影响。if(i > 0 && bucket[i] == bucket[i-1]) continue;
如果数字遍历完了,其实我们是不需要检查bucket中的元素和是否都是target的。因为前面的 if(sum % k != 0) return false; 已经能保证只要所有元素都放入bucket中,那么bucket中的元素和都为target。
class Solution {public : vector<int > bucket; bool canPartitionKSubsets (vector<int >& nums, int k) { int n = nums.size (); int sum = accumulate (nums.begin (), nums.end (), 0 ); if (sum%k) return 0 ; int target = sum / k; bucket.resize (k, 0 ); sort (nums.rbegin (), nums.rend ()); return backtrack (nums, 0 , target); } bool backtrack (vector<int >& nums, int index, int target) { if (index == nums.size ()){ return 1 ; } for (int i = 0 ; i<bucket.size (); i++){ if (bucket[i] + nums[index] > target) continue ; if (i>0 && bucket[i] == bucket[i-1 ]) continue ; bucket[i] += nums[index]; if (backtrack (nums, index+1 , target)) return 1 ; bucket[i] -= nums[index]; } return 0 ; } };
以桶的视角 以桶的视角进行穷举,每个桶需要遍历 nums
中的所有数字,决定是否把当前数字装进桶中;当装满一个桶之后,还要装下一个桶,直到所有桶都装满为止 。 但是不优化的话 仍然超时
class Solution {public : bool canPartitionKSubsets (vector<int >& nums, int k) { int n = nums.size (); int sum = accumulate (nums.begin (), nums.end (), 0 ); if (sum%k) return 0 ; int target = sum / k; vector<bool > used (n, 0 ) ; return backtrack (k, 0 , nums, 0 , target, used); } bool backtrack (int k, int nowBucketSum, vector<int >& nums, int startIndex, int target, vector<bool >& used) { if (k == 0 ) return 1 ; if (nowBucketSum == target) return backtrack (k-1 , 0 , nums, 0 , target, used); for (int i = startIndex; i<nums.size (); i++){ if (used[i] || nums[i] + nowBucketSum > target) continue ; used[i] = 1 ; nowBucketSum += nums[i]; if (backtrack (k, nowBucketSum, nums, i+1 , target, used)) return 1 ; used[i] = 0 ; nowBucketSum -= nums[i]; } return 0 ; } };
首先,在这个解法中每个桶都可以认为是没有差异的,但是我们的回溯算法却会对它们区别对待,这里就会出现重复计算的情况。
什么意思呢?我们的回溯算法,说到底就是穷举所有可能的组合,然后看是否能找出和为 target
的 k
个桶(子集)。
那么,比如下面这种情况,target = 5
,算法会在第一个桶里面装 1, 4
:
现在第一个桶装满了,就开始装第二个桶,算法会装入 2, 3
:
然后以此类推,对后面的元素进行穷举,凑出若干个和为 5 的桶(子集)。
但问题是,如果最后发现无法凑出和为 target
的 k
个子集,算法会怎么做?
回溯算法会回溯到第一个桶,重新开始穷举,现在它知道第一个桶里装 1, 4
是不可行的,它会尝试把 2, 3
装到第一个桶里:
现在第一个桶装满了,就开始装第二个桶,算法会装入 1, 4
:
好,到这里你应该看出来问题了,这种情况其实和之前的那种情况是一样的。也就是说,到这里你其实已经知道不需要再穷举了,必然凑不出来和为 target
的 k
个子集。
但我们的算法还是会傻乎乎地继续穷举,因为在她看来,第一个桶和第二个桶里面装的元素不一样,那这就是两种不一样的情况呀。
注意这两种情况的 used
数组肯定长得一样 ,所以 used
数组可以认为是回溯过程中的「状态」。
所以,我们可以用一个 memo
备忘录,在装满一个桶时记录当前 used
的状态,如果当前 used
的状态是曾经出现过的,那就不用再继续穷举,从而起到剪枝避免冗余计算的作用 。
对used数组进行记录和状态压缩 class Solution {public : unordered_map<int , bool > memo; bool canPartitionKSubsets (vector<int >& nums, int k) { int n = nums.size (); int sum = accumulate (nums.begin (), nums.end (), 0 ); if (sum%k) return 0 ; int target = sum / k; int used = 0 ; return backtrack (k, 0 , nums, 0 , target, used); } bool backtrack (int k, int nowBucketSum, vector<int >& nums, int startIndex, int target, int & used) { if (k == 0 ) return 1 ; if (nowBucketSum == target){ bool res = backtrack (k-1 , 0 , nums, 0 , target, used); memo[used] = res; return res; } if (memo.count (used)) return memo[used]; for (int i = startIndex; i<nums.size (); i++){ if (((used >> i) & 1 ) == 1 || nums[i]+nowBucketSum> target) continue ; used |= 1 <<i; nowBucketSum += nums[i]; if (backtrack (k, nowBucketSum, nums, i+1 , target, used)) return 1 ; used ^= 1 << i; nowBucketSum -= nums[i]; } return 0 ; } };
难度中等316
你将得到一个整数数组 matchsticks
,其中 matchsticks[i]
是第 i
个火柴棒的长度。你要用 所有的火柴棍 拼成一个正方形。你 不能折断 任何一根火柴棒,但你可以把它们连在一起,而且每根火柴棒必须 使用一次 。
如果你能使这个正方形,则返回 true
,否则返回 false
。
示例 1:
输入: matchsticks = [1,1,2,2,2] 输出: true 解释: 能拼成一个边长为2的正方形,每边两根火柴。
示例 2:
输入: matchsticks = [3,3,3,3,4] 输出: false 解释: 不能用所有火柴拼成一个正方形。
遍历数字遍历桶 勉强能过 class Solution {public : vector<int > bucket; bool makesquare (vector<int >& nums) { int sum = accumulate (nums.begin (), nums.end (), 0 ); if (sum%4 ) return 0 ; int target = sum / 4 ; bucket.resize (4 , 0 ); sort (nums.begin (), nums.end (), [](const int & a, const int & b)->bool {return a>b;}); return backtrack (nums, 0 , target); } bool backtrack (vector<int >& nums, int index, int target) { if (index == nums.size ()){ for (int i = 0 ; i<4 ; i++){ if (bucket[i]!=target) return 0 ; } return 1 ; } for (int i = 0 ; i<bucket.size (); i++){ if (nums[index] + bucket[i] > target) continue ; bucket[i] += nums[index]; if (backtrack (nums, index+1 , target)) return 1 ; bucket[i] -= nums[index]; } return 0 ; } };
特殊回溯 个人认为 特殊情况 应用的挺特殊的回溯
难度中等63
给定一个正整数数组 nums
和整数 k
,请找出该数组内乘积小于 k
的连续的子数组的个数。
示例 1:
输入: nums = [10,5,2,6], k = 100 输出: 8 解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。 需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
示例 2:
输入: nums = [1,2,3], k = 0 输出: 0
思路
这道题 正确解法 应该是滑动窗口
重点 right - left + 1
比如某次遍历符合题意的子数组为 ABCX,那么在该条件下符合条件的有X,CX,BCX,ABCX共四个(可以进行多个例子,发现个数符合right-left+1)
但是 做这道题的过程中 感觉这个 不跳步的回溯 挺有意思 外层循环backtrack
其实 好像相当于两层for循环了 卧槽 我真是垃圾
其实 也好像有点类似n叉树 有向图的遍历吧?
图论 | qianxunslimgのblog
代码 正确的滑动窗口解法
class Solution {public : int numSubarrayProductLessThanK (vector<int >& nums, int k) { int multi = 1 ; int n = nums.size (); if (k <= 1 ) return 0 ; int left = 0 , right = 0 ; int ans = 0 ; while (right < n){ multi*=nums[right]; while (multi>=k){ multi/=nums[left]; left++; } ans += (right - left + 1 ); right++; } return ans; } };
回溯(假)
class Solution {public : int ans; int numSubarrayProductLessThanK (vector<int >& nums, int k) { ans = 0 ; for (int i = 0 ; i<nums.size (); i++){ backtrack (nums, i, 1 , k); } return ans; } void backtrack (vector<int >& nums, int startIndex, int preK, int k) { if (startIndex>=nums.size () || preK>=k) return ; preK*=nums[startIndex]; if (preK<k){ ans++; } backtrack (nums, startIndex+1 , preK, k); preK /= nums[startIndex]; } };
难度中等28
正整数 n
代表生成括号的对数,请设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
解法 回溯,回溯终止条件为 左右括号次数用完 和 右括号用的次数大于左括号用的次数
class Solution {public : vector<string> ans; string path; vector<string> generateParenthesis (int n) { backtrack (n, n); return ans; } void backtrack (int left, int right) { if (left == 0 && right == 0 ){ ans.push_back (path); return ; } if (left < 0 || right < 0 || right < left) return ; path += "(" ; backtrack (left - 1 , right); path.pop_back (); path += ")" ; backtrack (left, right - 1 ); path.pop_back (); } };
给定一个长度为4的整数数组 cards 。你有 4 张卡片,每张卡片上都包含一个范围在 [1,9] 的数字。您应该使用运算符 [‘+’, ‘-‘, ‘*’, ‘/‘] 和括号 ‘(‘ 和 ‘)’ 将这些卡片上的数字排列成数学表达式,以获得值24。
你须遵守以下规则:
除法运算符 ‘/‘ 表示实数除法,而不是整数除法。 例如, 4 /(1 - 2 / 3)= 4 /(1 / 3)= 12 。 每个运算都在两个数字之间。特别是,不能使用 “-” 作为一元运算符。 例如,如果 cards =[1,1,1,1] ,则表达式 “-1 -1 -1 -1” 是 不允许 的。 你不能把数字串在一起 例如,如果 cards =[1,2,1,2] ,则表达式 “12 + 12” 无效。 如果可以得到这样的表达式,其计算结果为 24 ,则返回 true ,否则返回 false 。
暴力回溯 枚举所有情况 class Solution {public : bool judgePoint24 (vector<int > &cards) { vector<double > nums; for (int card : cards) { nums.push_back (double (card)); } return dfs (nums); } bool dfs (vector<double > &nums) { if (nums.size () == 1 ) return abs (nums[0 ] - 24 ) <= 1e-6 ; for (int i = 0 ; i < nums.size (); i++) { for (int j = 0 ; j < nums.size (); j++) { if (i == j) continue ; double a = nums[i]; double b = nums[j]; vector<double > shengyu; for (int k = 0 ; k < nums.size (); k++) { if (k == i || k == j) continue ; shengyu.push_back ( nums[k]); } double sum = a + b; double sub = a - b; double mul = a * b; double div = a / b; double yunsuan[4 ] = {sum, sub, mul, div}; for (int w = 0 ; w < 4 ; w++) { shengyu.push_back (yunsuan[w]); if (dfs (shengyu)) return true ; shengyu.pop_back (); } } } return false ; } };