递归 与 回溯
回溯其实是一种暴力解决问题的方式, 对于问题规模大于20以上的数据,个人计算机将不能处理。
下面基于几个问题分析体会一下
问题分析 :
对于 0 1 * # 不用考虑
对于 2~ 9 每个数字都有3中情况。 那么对于一个数字串 ,将面临多种组合现象 。
我们可以 画树形图。
树形图会很容易发现这是一个递归问题。
对于每个问题,又形成一个和原问题类似的问题。(不存在重叠)
那么 我们可以递归来处理
class Solution {
public:
vector<string> table = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
//原问题入口
vector<string> letterCombinations(string digits) {
vector<string> res;
func(res, "", digits, 0);
return res;
}
// 递归处理
void func(vector<string> &res, string str, string &digits, int i){
if(i == digits.size()){
if(str.size() > 0) res.push_back(str);
return;
}
else{//进进出出
// -2 是因为不考虑 0 和 1
string s = table[digits[i] - '2'];//拿到当前数字对应字母可能
for(int j = 0; j < s.size(); ++j){
str.push_back(s[j]);//放进去
func(res, str, digits, i+1);
str.pop_back();//取出来
}
}
}
};
上边这个看着有点凌乱,来来来,整理下代码
来看个精简版的
class Solution {
private:
vector<string> letter={"abc","def","ghi","jkl", "mno","pqrs", "tuv", "wxyz"};
private:
//index 代表当前处理到第几个字符了
void subQuestion(vector<string>& res, string& digits,int index, const string& dest)
{
if (index == digits.size())
{
res.push_back(dest);
return;
}
//获取当前字符串 , 下标由digits[index] -'2'决定
string tmp = letter[digits[index] - '2'];
for(int i=0; i<tmp.size(); ++i)
subQuestion(res, digits, index+1, dest+tmp[i]);
}
public:
vector<string> letterCombinations(string digits) {
vector<string> res;//保存返回
int n =digits.size();
if(n == 0) return res;
subQuestion(res, digits, 0, "");
return res;
}
};
再想想思路 ,其实 回溯法就是一种暴力,我们完全可以用多重循环来解决。
有的时候没思路,就只能用暴力 ,那么考虑下回溯。
回溯的时间复杂度是 O (2^N)
全排列问题:
力扣 46 题 :给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
思路 还是 画 树型图 的办法:
画图之后一目了然。
//下面这个题 的解法就很好的体现了回溯的想法。 虽然递归本来就有回溯的意思 ,但是, 有必要时,我们还是得注意变量的回溯。
因为排列中的元素是不能冲突的, 所以,我们还是得回溯标记状态。
class Solution {
private:
vector<bool> memo;
private:
void subQuestion(vector<vector<int>> & res, vector<int>nums, vector<int>& sub, int n)
{
if(n == nums.size())
{
res.push_back(sub);
return ;
}
//注意,这里每一层都会从这里开始,那么我们得避免重复排列
//换句话说: 每来到一个数字前,判断这个数字是在本排列出现过
//那么我们可以使用一趟遍历来查找。可是导致时间复杂变高
//我们采用辅助数组标记已经用过的数字。
for(int i=0; i < nums.size(); ++i)
if(!memo[i])
{
memo[i] = true;
sub.push_back(nums[i]);
subQuestion(res, nums, sub, n+1);
sub.pop_back();//回退(换个数字排列)
memo[i] = false;
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
if(n == 0) return res;
vector<int> sub;
memo = vector<bool>(n,false);
subQuestion(res, nums, sub, 0);
return res;
}
};
这道题还是和 之前的题不相同的。因为这里涉及到了吞下与吐出的考虑。
最后再体会一下辅助数组的妙处。
回溯法 --组合问题
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
为什么2以后的数字不考虑前边的数字呢?
因为: 前别的100%重复。
class Solution {
private:
vector<vector<int>>res;
private:
//start 只考虑之后
//tmp 存当前一种情况存的元素
void subQuestion(int n, int k,int start, vector<int>& tmp)
{
//一种组合完成
if(tmp.size() == k)
{
res.push_back(tmp);
return;
}
//这离别搞成 i <= n-k 因为虽然第一层不能选n-k以后的数
//但其他层可以选, 即使第一层选了 ,那么后边也会因为不够k个
//不会放入最终结果,但却一定程度上影响性能,
for(int i=start; i <= n; ++i)
{
tmp.push_back(i);
subQuestion(n, k, i+1, tmp);
tmp.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
res.clear();
if(n <= 0 || k<=0 || k > n) return res;
vector<int> tmp;
subQuestion(n, k, 1, tmp);
return res;
}
};
下面进行剪枝:
剪枝 :控制下循环条件
class Solution {
private:
vector<vector<int>>res;
private:
//start 只考虑之后
//tmp 存当前一种情况存的元素
void subQuestion(int n, int k,int start, vector<int>& tmp)
{
//一种组合完成
if(tmp.size() == k)
{
res.push_back(tmp);
return;
}
//这里其实可以剪枝
// k-tmp.size() 目前还需要的元素数量
// i取[start, n] 之间必须满足够 k-tmp.size()个元素
// 【start, n】前闭后闭 所以 + 1
for(int i=start; i <= (n-(k-tmp.size()))+1; ++i)
{
tmp.push_back(i);
subQuestion(n, k, i+1, tmp);
tmp.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
res.clear();
if(n <= 0 || k<=0 || k > n) return res;
vector<int> tmp;
subQuestion(n, k, 1, tmp);
return res;
}
};
力扣相关题号:
40
216
78
90
题目描述:Word Search
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
class Solution {
private:
int next[4][2] = {{-1,0}, {0,1}, {1,0}, {0,-1}};
vector<vector<bool>>memo;
int n, m;
private:
bool InArea(int x, int y)
{
return x>=0 && x < n && y>=0 && y<m;
}
bool Search(vector<vector<char>>&board, string& word, int index, int i, int j)
{
//长度够了
if(index == word.size()-1)
return word[index] == board[i][j];
//长度未够,判断是否是word上的字母
if(word[index] == board[i][j])
{
//是的话,上下左右找下一个word字母
memo[i][j] = true;
for(int k=0; k<4; ++k)
{
int newx = i + next[k][0];
int newy = j + next[k][1];
if( InArea(newx, newy) && !memo[newx][newy] &&Search(board,word,index+1,newx,newy) )
return true;
}
memo[i][j] = false;
}
return false;
}
public:
bool exist(vector<vector<char>>& board, string word) {
n= board.size();
if (n==0) return false;
m = board[0].size();
//保存走过的路径
memo =vector<vector<bool>>(n,vector<bool>(m,false));
for(int i=0; i<n; ++i)
for(int j=0; j<m; ++j)
if ( Search(board, word, 0, i, j) )
return true;
return false;
}
};
深搜: 完全被水包围的岛屿个数。
class Solution {
private:
int ret =0;
int l1,l2;
vector<vector<bool>>memo;
int next[4][2] ={{-1,0}, {0,1}, {1,0}, {0, -1}};
private:
bool inArea(int a, int b){
//完全被包围意味着 不能在边界, 那么暂时不判断边界是否是合理
return a>=0 && a <l1 && b>=0 && b<l2;
}
private:
bool touchEdge(int a, int b){
return a <=0 ||a >=l1-1 || b <=0 ||b >=l2-1;
}
private:
void dfs(vector<vector<int>>& grid, int x, int y, bool& flag){
//是否触碰边界
if(touchEdge(x,y)) {
flag = true;
return ;
}
memo[x][y] = true;//访问过了
for(int i=0; i<4; ++i){
int newx = x + next[i][0];
int newy = y + next[i][1];
//不再这里判断 flag第二次值 是为了保证生深度优先搜索走到底
if(inArea(newx, newy) && !grid[newx][newy] && !memo[newx][newy]){
dfs(grid, newx, newy, flag);
}
}
}
public:
int closedIsland(vector<vector<int>>& grid) {
l1 = grid.size();
if(l1==0) return 0;
l2 = grid[0].size();
memo=vector<vector<bool>>(l1,vector<bool>(l2,false));
for(int i=0; i<l1; ++i)
for(int j=0; j<l2; ++j)
if(!grid[i][j] && !memo[i][j]){
bool flag = false;
dfs(grid, i, j, flag);
if(!flag)
++ret;
}
return ret;
}
};