目录
一、组合
1. 题目链接:77. 组合
2. 题目描述
给定两个整数
n
和k
,返回范围[1, n]
中所有可能的k
个数的组合。你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]示例 2:
输入:n = 1, k = 1 输出:[[1]]提示:
1 <= n <= 20
1 <= k <= n
3. 解法(回溯)
🌴算法思路:
题目要求我们从 1 到 n 中选择 k 个数的所有组合,其中不考虑顺序。也就是说,[1,2] 和 [2,1] 等价。我们需要找出所有的组合,但不能重复计算相同元素的不同顺序的组合。对于选择组合,我们需要进行如下流程:
- 所有元素分别作为首位元素进行处理;
- 在之后的位置上同理,选择所有元素分别作为当前位置元素进行处理;
- 为避免计算重复组合,规定选择之后位置的元素时必须比前⼀个元素大,这样就不会有重复的组合([1,2] 和 [2,1] 中 [2,1] 不会出现)。
递归函数设计:void dfs(vector<vector<int>>& ans, vector<int>& v, int step, int &n, int &k)
- 参数:step(当前需要进行处理的位置);
- 返回值:无;
- 函数作用:某个元素作为首位元素出现时,查找所有可能的组合。
具体实现方法如下:
1. 定义⼀个二维数组和⼀维数组。⼆维数组用来记录所有组合,⼀维数组用来记录当前状态下的组合。
2. 遍历 1 到 n-k+1,以当前数作为组合的首位元素进行递归(从 n-k+1 到 n 作为首位元素时,组合中一定不会存在 k 个元素)。
3. 递归函数的参数为两个数组、当前步骤以及 n 和 k。递归流程如下:
a. 结束条件:当前组合中已经有 k 个元素,将当前组合存进二维数组并返回。
- 剪枝:如果当前位置之后的所有元素放入组合也不能满足组合中存在 k 个元素,直接返回。
b. 从当前位置的下一个元素开始遍历到 n,将元素赋值到当前位置,递归下一个位置。
🌴算法代码:
class Solution
{
vector<int> path;
vector<vector<int>> ret;
int n, k;
public:
vector<vector<int>> combine(int _n, int _k)
{
n = _n, k = _k;
dfs(1);
return ret;
}
void dfs(int start)
{
if (path.size() == k)
{
ret.push_back(path);
return;
}
for (int i = start; i <= n; i++)
{
path.push_back(i);
dfs(i + 1);
path.pop_back();// 恢复现场
}
}
};
二、目标和
1. 题目链接:494. 目标和
2. 题目描述
给你一个非负整数数组
nums
和一个整数target
。向数组中的每个整数前添加
'+'
或'-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。返回可以通过上述方法构造的、运算结果等于
target
的不同 表达式 的数目。示例 1:
输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3示例 2:
输入:nums = [1], target = 1 输出:1提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
3. 解法(回溯)
🌴算法思路:
对于每个数,可以选择加上或减去它,依次枚举每⼀个数字,在每个数都被选择时检查得到的和是否等于目标值。如果等于,则记录结果。
需要注意的是,为了优化时间复杂度,可以提前计算出数组中所有数字的和 sum,以及数组的长度 len。这样可以快速判断当前的和减去剩余的所有数是否已经超过了目标值 target ,或者当前的和加上剩下的数的和是否小于目标值 target,如果满足条件,则可以直接回溯。
🌴递归流程:
1. 递归结束条件:index 与数组长度相等,判断当前状态的 sum 是否与目标值相等,若是计数加一;
2. 选择当前元素进行加操作,递归下⼀个位置,并更新参数 sum;
3. 选择当前元素进行减操作,递归下⼀个位置,并更新参数 sum;
特别地,此问题可以转化为另⼀个问题:若所有元素初始状态均为减,选择其中几个元素将他们的状态修改为加,计算修改后的元素和与目标值相等的方案个数。
1. 选择其中 x 个元素进行修改,并且这 x 个元素的和为 y;
2. 检查使得 -sum+2*y=target(移项:y=(sum+target)/2)成立的方案个数,即选择 x 个元素和为(sum+target)/2 的方案个数;
- 若 sum+target 为奇数,则不存在这种方案;
3. 递归流程:
- 传入参数:index(当前要处理的元素下标),sum(当前状态和),nums(元素数组),aim(目标值:(sum+target)/2);
- 递归结束条件:index 与数组长度相等,判断当前 sum 是否与目标值相等,若是返回 1,否则返回 0;
- 返回 递归选择当前元素 以及 递归不选择当前元素 函数值的和。
🌴算法代码:
class Solution
{
int ret, aim;
public:
int findTargetSumWays(vector<int>& nums, int target)
{
aim = target;
dfs(nums, 0, 0);
return ret;
}
void dfs(vector<int>& nums, int pos, int path)
{
if (pos == nums.size())
{
if (path == aim)
{
ret++;
}
return;
}
// 加法
dfs(nums, pos + 1, path + nums[pos]);
// 减法
dfs(nums, pos + 1, path - nums[pos]);
}
};
三、组合总和
1. 题目链接:39. 组合总和
2. 题目描述
给你一个 无重复元素 的整数数组
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]]示例 3:
输入: candidates = [2], target = 1 输出: []提示:
1 <= candidates.length <= 30
2 <= candidates[i] <= 40
candidates
的所有元素 互不相同1 <= target <= 40
3. 解法
🌴算法思路:
candidates 的所有元素 互不相同,因此我们在递归状态时只需要对每个元素进行如下判断:
1. 跳过,对下⼀个元素进行判断;
2. 将其添加至当前状态中,我们在选择添加当前元素时,之后仍可以继续选择当前元素(可以重复选择同⼀元素)。
- 因此,我们在选择当前元素并向下传递下标时,应该直接传递当前元素下标。
递归函数设计:void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx)
- 参数:target(当前状态和与目标值的差),idx(当前需要处理的元素下标);
- 返回值:无;
- 函数作用:向下传递两个状态(跳过或者选择当前元素),找出所有组合使得元素和为目标值。
递归函数流程如下:
1. 结束条件:
- 当前需要处理的元素下标越界;
- 当前状态的元素和已经与目标值相同;
2. 跳过当前元素,当前状态不变,对下⼀个元素进行处理;
3. 选择将当前元素添加至当前状态,并保留状态继续对当前元素进行处理,递归结束时撤销添加操作。
🌴算法代码:
class Solution
{
int aim;
vector<int> path;
vector<vector<int>> ret;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
aim = target;
dfs(candidates, 0, 0);
return ret;
}
void dfs(vector<int>& candidates, int pos, int sum)
{
if (sum == aim)
{
ret.push_back(path);
return;
}
if (sum > aim || pos > candidates.size())
{
return;
}
for(int i = pos; i < candidates.size(); i++)
{
path.push_back(candidates[i]);
dfs(candidates, i, sum + candidates[i]);
path.pop_back();
}
}
};
四、字母大小写全排列
1. 题目链接:784. 字母大小写全排列
2. 题目描述
给定一个字符串
s
,通过将字符串s
中的每个字母转变大小写,我们可以获得一个新的字符串。返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。
示例 1:
输入:s = "a1b2" 输出:["a1b2", "a1B2", "A1b2", "A1B2"]示例 2:
输入: s = "3z4" 输出: ["3z4","3Z4"]提示:
1 <= s.length <= 12
s
由小写英文字母、大写英文字母和数字组成
3. 解法
🌴算法思路:
只需要对英文字母进行处理,处理每个元素时存在三种情况:
- 不进行处理;
- 若当前字母是英文字母并且是大写,将其修改为小写;
- 若当前字母 是英文字母并且是小写,将其修改为大写。
递归函数设计:void dfs(int step)
- 参数:step(当前需要处理的位置);
- 返回值:无;
- 函数作用:查找所有可能的字符串集合,并将其记录在答案列表。
从前往后按序进行递归,递归流程如下:
- 递归结束条件:当前需要处理的元素下标越界,表示处理完毕,记录当前状态并返回;
- 对当前元素不进行任何处理,直接递归下⼀位元素;
- 判断当前元素是否为小写字母,若是,将其修改为大写字母并递归下⼀个元素,递归结束时撤销修改操作;
- 判断当前元素是否为大写字母,若是,将其修改为小写字母并递归下⼀个元素,递归结束时撤销修改操作;
🌴算法代码:
class Solution
{
string path;
vector<string> ret;
public:
vector<string> letterCasePermutation(string s)
{
dfs(s, 0);
return ret;
}
void dfs(string& s, int pos)
{
if (pos == s.size())
{
ret.push_back(path);
return;
}
char ch = s[pos];
// 不改变
path.push_back(ch);
dfs(s, pos + 1);
path.pop_back();
// 改变
if (ch < '0' || ch > '9')
{
char tmp = change(ch);
path.push_back(tmp);
dfs(s, pos + 1);
path.pop_back();
}
}
char change(char ch)
{
if (ch >= 'a' && ch <= 'z') ch -= 32;
else ch += 32;
return ch;
}
};