回溯法解题框架
回溯问题就是一个决策树的遍历过程
考虑三个问题:
路径:也就是已经做出的选择选择列表:当前可以做的选择结束条件:到达决策树的底层,无法再做选择的条件
回溯法框架
result
[]
def
backtrack()
if满足结束条件
添加路径
return
for 选择in选择列表
选择
backtrack()
撤回选择
全排列问题(不包含重复数字)
路径就是已经做过的选择,选择列表就是当前可以做的选择,结束条件就是遍历到树的底层也就是选择列表为空的时候。 通过算法框架写出代码(用list时间效率比vector高)
class Solution {
public:
vector
<vector
<int>> permute(vector
<int>& nums
) {
vector
<int>track
;
if(nums
.size()<1)
return res
;
backtrack(nums
,track
);
return res
;
}
void backtrack(vector
<int>&nums
,vector
<int>track
)
{
if(track
.size()==nums
.size())
{
res
.push_back(track
);
return;
}
for(auto num
:nums
)
{
auto ishave
=find(track
.begin(),track
.end(),num
);
if(ishave
==track
.end())
{
track
.push_back(num
);
backtrack(nums
,track
);
track
.pop_back();
}
else
continue;
}
}
private:
vector
<vector
<int>>res
;
};
N皇后问题
这个问题很经典了,简单解释一下:给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。 PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。 这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。 直接套用框架:
class Solution {
public:
vector
<vector
<string
>> solveNQueens(int n
) {
vector
<string
>board(n
,string(n
,'.'));
if(n
<1)return res
;
backtrack(0,board
);
return res
;
}
void backtrack(int row
,vector
<string
>board
)
{
if(row
==board
.size())
{
res
.push_back(board
);
return;
}
for(int i
=0;i
<board
.size();i
++)
{
if(isvalid(board
,row
,i
))
{
board
[row
][i
]='Q';
backtrack(row
+1,board
);
board
[row
][i
]='.';
}
}
}
bool isvalid(vector
<string
>board
,int row
,int col
)
{
for(int currow
=0;currow
<board
.size();currow
++)
{
if(board
[currow
][col
]=='Q')
return false;
}
for(int currow
=row
-1,curcol
=col
-1;currow
>=0&&curcol
>=0;currow
--,curcol
--)
{
if(board
[currow
][curcol
]=='Q')
return false;
}
for(int currow
=row
-1,curcol
=col
+1;currow
>=0&&curcol
<board
.size();currow
--,curcol
++)
{
if(board
[currow
][curcol
]=='Q')
return false;
}
return true;
}
private:
vector
<vector
<string
>>res
;
};
动态规划:状态 dp定义 选择 base case 回溯:路径 选择 结束条件 某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。
例题
1.组合总和
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
套用框架
result
[]
def
backtrack()
if结束条件
result
.add(track
)
return;
for 选择in选择列表
选择
backtrack()
撤销选择
发现有重复组合(解集的各种排列组合) 解决:首先对原始数组进行排序;用一个索引数组标识路径中已有的索引;用prenum记录每一次选择循环当前元素的前一个元素,如果重复就剪去这个元素;用fathernum记录上一个路径节点,判断子节点如果小于父节点就剪去这个元素;
class Solution {
public:
vector
<vector
<int>> combinationSum2(vector
<int>& candidates
, int target
) {
vector
<int>nums
=candidates
;
sort(nums
.begin(),nums
.end());
vector
<int>track
;
vector
<bool>havevisit(candidates
.size(),false);
backtrack(nums
,havevisit
,track
,target
,0);
return res
;
}
void backtrack(vector
<int>& nums
,vector
<bool>&havevisit
,vector
<int>track
,int target
,int fathernum
)
{
int prenum
=0;
if(target
==0)
{
res
.push_back(track
);
return;
}
else if(target
<0)
return;
for(int i
=0;i
<nums
.size();i
++)
{
if(nums
[i
]==prenum
||havevisit
[i
]==true||nums
[i
]<fathernum
)
continue;
else
{
havevisit
[i
]=true;
prenum
=nums
[i
];
track
.push_back(nums
[i
]);
backtrack(nums
,havevisit
,track
,target
-nums
[i
],nums
[i
]);
track
.pop_back();
havevisit
[i
]=false;
}
}
}
private:
vector
<vector
<int>> res
;
};
优化:发现时间复杂度和空间复杂度都很高,想用哈希表替代排序、
class Solution {
public:
vector
<vector
<int>> combinationSum2(vector
<int>& candidates
, int target
) {
vector
<int>track
;
vector
<bool>havevisit(candidates
.size(),false);
backtrack(candidates
,havevisit
,track
,target
,0);
return res
;
}
void backtrack(vector
<int>& nums
,vector
<bool>&havevisit
,vector
<int>track
,int target
,int fathernum
)
{
if(target
==0)
{
res
.push_back(track
);
return;
}
else if(target
<0)
return;
unordered_set
<int> Myset
;
for(int i
=0;i
<nums
.size();i
++)
{
if(Myset
.find(nums
[i
])!=Myset
.end()||havevisit
[i
]==true||nums
[i
]<fathernum
)
continue;
else
{
havevisit
[i
]=true;
Myset
.insert(nums
[i
]);
track
.push_back(nums
[i
]);
backtrack(nums
,havevisit
,track
,target
-nums
[i
],nums
[i
]);
track
.pop_back();
havevisit
[i
]=false;
}
}
}
private:
vector
<vector
<int>> res
;
};
时间复杂度反而更高
2.有重复字符串的排列组合
有重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合。
示例1:
输入:S = "qqe"
输出:["eqq","qeq","qqe"]
示例2:
输入:S = "ab"
输出:["ab", "ba"]
提示:
字符都是英文字母。
字符串长度在[1, 9]之间。
回溯法框架
class Solution {
public:
vector
<string
> permutation(string S
) {
string track
;
vector
<bool>hasInTrack(S
.size(), false);
backtrack(S
, track
, hasInTrack
);
return result
;
}
void backtrack(string s
, string
& track
, vector
<bool>&hasInTrack
)
{
int n
= s
.size();
if (track
.size() >= s
.size())
{
result
.push_back(track
);
return;
}
unordered_set
<char>hasappear
;
for (int i
= 0; i
< n
; i
++)
{
if (auto iter
= hasappear
.find(s
[i
]) == hasappear
.end()&& hasInTrack
[i
] == false)
{
hasappear
.insert(s
[i
]);
hasInTrack
[i
] = true;
track
.push_back(s
[i
]);
backtrack(s
, track
, hasInTrack
);
track
.pop_back();
hasInTrack
[i
] = false;
}
else
continue;
}
}
private:
vector
<string
>result
;
};
减枝问题 1.已经存在于路径的节点:用一个数组记录哪一个位置的元素已被访问过,需要回溯 2.同一层的节点避免重复:用一个哈希表记录for循环时哪些元素是重复的