三、算法技巧
3.1 暴力搜索
3.1.1 回溯算法解题套路框架
框架
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
1、全排列 leetcode
题解:
时间复杂度:O(n x n!)
空间复杂度:O(n)
class Solution {
public:
vector<vector<int> > ans;
vector<int> vis;
void backtrace(vector<int>& nums, vector<int> path)
{
if(path.size()==nums.size())
{
ans.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(vis[i]==0)
{
vis[i]=1;
path.push_back(nums[i]);
backtrace(nums, path);
path.pop_back();
vis[i]=0;
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
vis = vector<int>(nums.size(), 0);
backtrace(nums, {
});
return ans;
}
};
2、N 皇后 leetcode
题解:
时间复杂度:O(n!)
空间复杂度:O(n)
class Solution {
public:
vector<vector<string> > ans;
bool judge(vector<string>& grid, int i, int j, int n)
{
for(int q=0;q<n;q++)
{
if(grid[i][q] == 'Q')
{
return false;
}
}
for(int p=0;p<n;p++)
{
if(grid[p][j] == 'Q')
{
return false;
}
}
int new_i = i;
int new_j = j;
while(new_i>=0 && new_j>=0)
{
if(grid[new_i][new_j]=='Q')
{
return false;
}
new_i--;
new_j--;
}
new_i = i;
new_j = j;
while(new_i<n && new_j<n)
{
if(grid[new_i][new_j]=='Q')
{
return false;
}
new_i++;
new_j++;
}
new_i = i;
new_j = j;
while(new_i>=0 && new_j<n)
{
if(grid[new_i][new_j]=='Q')
{
return false;
}
new_i--;
new_j++;
}
new_i = i;
new_j = j;
while(new_i<n && new_j>=0)
{
if(grid[new_i][new_j]=='Q')
{
return false;
}
new_i++;
new_j--;
}
return true;
}
void backtrace(vector<string>& grid, int index, int n)
{
if(index==n)
{
ans.push_back(grid);
return;
}
for(int i=0;i<n;i++)
{
if(judge(grid, index, i, n))
{
grid[index][i] = 'Q';
backtrace(grid, index+1, n);
grid[index][i] = '.';
}
}
}
vector<vector<string>> solveNQueens(int n) {
vector<string> grid(n);
string kong = "";
for(int i=0;i<n;i++)
{
kong+='.';
}
for(int i=0;i<n;i++)
{
grid[i]=kong;
}
backtrace(grid, 0, n);
return ans;
}
};
3.1.2 集合划分问题
1、划分为k个相等的子集 leetcode
题解:
时间复杂度:O(k^n)
空间复杂度:O(k)
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
if(k > nums.size()) return false;
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum % k != 0) return false;
// 记录每个子集中数字之和
bucket.resize(k);
// 每个桶中的数字之和应该为target
int target = sum/k;
// 从大到小放数字
sort(nums.rbegin(), nums.rend());
// index = 0, 表示从0号元素开始遍历
return backtrack(nums, 0, target);
}
private:
// 记录每个子集中数字之和
vector<int> bucket;
bool backtrack(vector<int> &nums, int index, int target){
// 如果所有数字遍历完了,是不需要检查bucket中的元素和是否都是target的。因为前面的 if(sum % k != 0) return false; 已经能保证只要所有元素都放入bucket中,那么bucket中的元素和都为target。
if(index == nums.size()){
return true;
}
// 注意:i 表示第i个子集,index 表示第index个数字
for(int i = 0; i < bucket.size(); i++){
// 如果这个数字放入子集i中使子集i中元素和超出target了
if(bucket[i] + nums[index] > target){
continue;
}
// 如果 当前子集的元素和 与 前一个子集的元素和 是一样的,那就跳过
if(i > 0 && bucket[i] == bucket[i-1]){
continue;
}
// 将数字放入子集i中
bucket[i] += nums[index];
// 递归穷举下一个数字的情况
if(backtrack(nums, index + 1, target)){
return true;
}
// 撤销选择
bucket[i] -= nums[index];
}
// 如果 nums[index] 放入哪个子集都不行
return false;
}
};
3.1.3 排列/组合/子集问题
1、子集 leetcode
(元素无重不可复选)
题解:
时间复杂度:O(n*2^n)
空间复杂度:O(n)
class Solution {
public:
vector<vector<int> > ans;
void backtrace(vector<int>& nums, int idx, vector<int> path)
{
ans.push_back(path);
for(int i=idx;i<nums.size();i++)
{
path.push_back(nums[i]);
backtrace(nums, i+1, path);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtrace(nums, 0, {
});
return ans;
}
};
2、组合 leetcode
(元素无重不可复选)
题解:转化为子集问题
时间复杂度:O( C n k C_{n}^k Cnk×k)
空间复杂度:O(n)
class Solution {
public:
vector<vector<int> > ans;
vector<int> nums;
void backtrace(int idx, int k, vector<int> path)
{
if(path.size()==k)
{
ans.push_back(path);
return;
}
for(int i=idx;i<nums.size();i++)
{
path.push_back(nums[i]);
backtrace(i+1, k, path);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
for(int i=1;i<=n;i++)
{
nums.push_back(i);
}
backtrace(0, k, {
});
return ans;
}
};
题解:提前剪枝
时间复杂度:O( C n k C_{n}^k Cnk×k)
空间复杂度:O(n)
class Solution {
public:
vector<vector<int> > ans;
vector<int> nums;
void backtrace(int cur, int k, vector<int> path, int n)
{
if(path.size() + (n - cur) < k)
{
return;
}
if(path.size()==k)
{
ans.push_back(path);
return;
}
path.push_back(nums[cur]);
backtrace(cur+1, k, path, n);
path.pop_back();
backtrace(cur+1, k, path, n);
}
vector<vector<int>> combine(int n, int k) {
for(int i=1;i<=n;i++)
{
nums.push_back(i);
}
backtrace(0, k, {
}, n);
return ans;
}
};
3、全排列 leetcode
(元素无重不可复选)
题解:
时间复杂度:O(n x n!)
空间复杂度:O(n)
class Solution {
public:
vector<vector<int> > ans;
vector<int> vis;
void backtrace(vector<int>& nums, vector<int> path)
{
if(path.size()==nums.size())
{
ans.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(vis[i]==0)
{
vis[i]=1;
path.push_back(nums[i]);
backtrace(nums, path);
path.pop_back();
vis[i]=0;
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
vis.resize(n, 0);
backtrace(nums, {
});
return ans;
}
};
4、子集 II leetcode
(元素可重不可复选)
题解:
时间复杂度:O(n x 2^n)
空间复杂度:O(n)
class Solution {
public:
vector<vector<int> > ans;
void backtrace(vector<int>& nums, int idx, vector<int> path)
{
ans.push_back(path);
for(int i=idx;i<nums.size();i++)
{
if(i>idx && nums[i]==nums[i-1])
continue;
path.push_back(nums[i]);
backtrace(nums, i+1, path);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
backtrace(nums, 0, {
});
return ans;
}
};
5、组合总和 II leetcode
题解:
时间复杂度:O(n x 2^n)
空间复杂度:O(n)
class Solution {
public:
vector<vector<int> > ans;
void backtrace(vector<int>& candidates, int idx, int sum, int target, vector<int> path)
{
if(sum > target)
{
return;
}
if(sum==target)
{
ans.push_back(path);
}
for(int i=idx;i<candidates.size();i++)
{
if(i>idx && candidates[i]==candidates[i-1])
continue;
path.push_back(candidates[i]);
backtrace(candidates, i+1, sum+candidates[i], target, path);
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrace(candidates, 0, 0, target, {
});
return ans;
}
};
6、全排列 II leetcode
题解:
时间复杂度:O(n x n!)
空间复杂度:O(n)
class Solution {
public:
vector<vector<int>> ans;
vector<int> vis;
void backtrace(vector<int>& nums, vector<int> path)
{
if(path.size()==nums.size())
{
ans.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(vis[i]==0)
{
// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
if(i>0 && nums[i]==nums[i-1] && vis[i-1]==0)
// 如果前面的相邻相等元素没有用过,那么当前元素也不能用,要跳过。只有当前面相等的元素用过了,当前相等元素才能用,这样保持了相对顺序。
continue;
vis[i]=1;
path.push_back(nums[i]);
backtrace(nums, path);
path.pop_back();
vis[i]=0;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
vis.resize(nums.size(), 0);
backtrace(nums, {
});
return ans;
}
};
7、组合总和 leetcode
(元素无重可复选)
题解:
class Solution {
public:
vector<vector<int> > ans;
void backtrace(vector<int>& candidates, int idx, int sum, int target, vector<int> path)
{
if(sum>target)
{
return;
}
if(sum==target)
{
ans.push_back(path);
return;
}
for(int i=idx;i<candidates.size();i++)
{
path.push_back(candidates[i]);
backtrace(candidates, i, sum+candidates[i], target, path);
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtrace(candidates, 0, 0, target, {
});
return ans;
}
};
8、总结
形式一、元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次
/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start)