力扣_中级算法_回溯算法_2~4题

一位C++小白的力扣刷题_成长记录_welcome to visit  ^_^  

( 从这一篇开始,调整一下排版顺序。将 “学习心得” 放到 “实现(C++)” 后面 )

 

回溯算法_第2题:括号生成

题目描述:

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

举例 

示例:

输入:n = 3
输出:[
       "((()))",
       "(()())",
       "(())()",
       "()(())",
       "()()()"
     ]

解题思路:利用 容器+递归+指针(其实是下标)。观察其特征:在储存 “(” 和 “)” 的容器内, “(” 的最大数量为 n ,而 “)” 的数量总是 小于等于 “(” 的数量。

要充分利用这一点来建立 回溯递归函数。

学习心得:今天做得每道题,我都先自己尝试敲一遍,但还是“回溯功夫”不到家,有些算法思维

没有掌握,所以后面都看了题解。但我没彻底理解前辈的谋篇代码,是不会轻易去学习的。这道题

归我最大的感触就是, 那一句“ left >= right ”。

实现:(C++)

class Solution {
  public:
    string temp="";                     //临时字符串( 可以把它看成“伪”全局变量 ):用来储存含有“(”和“)”的字符串
    void helper( vector<string>& vec,int n, int left, int right )    //left:表示 “(” 的数量。 right:表示“)” 的数量
      {
          if( temp.size()==2*n )
       {
        vec.push_back( temp );       //满足要求时,将 临时字符串 压入 二维容器
        return ;
       }
       if( left<n )         //只要 “(” 的数量不超过 n,继续加“(”
      {
        temp.push_back( '(' );
        helper( vec,n,left+1,right );
        temp.pop_back();
         }
      if( left > right )        //建议画一画图,画几下就清晰明了了!
      {
         temp.push_back( ')' );
         helper( vec,n,left,right+1 );
         temp.pop_back();
        }
      }

    vector<string> generateParenthesis(int n)
    {
      vector<string> vec;
      if( n==0 ) return vec;      //特殊情况,特殊处理
           helper( vec,n,0,0 );
          return vec;
    }
};

运行结果: 

代码执行结果:
我的输入
3
我的答案
["((()))","(()())","(())()","()(())","()()()"]
预期答案
["((()))","(()())","(())()","()(())","()()()"]

学习心得:今天做得每道题,我都先自己尝试敲一遍,但还是“回溯功夫”不到家,有些算法思维

没有掌握,所以后面都看了题解。但我没彻底理解前辈的谋篇代码,是不会轻易去学习的。这道题

归我最大的感触就是, 那一句“ if( left > right) ”。太6了!

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

回溯算法_第3题:全排列

题目描述:

 给定一个 没有重复 数字的序列,返回其所有可能的全排列。

举例:

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

解题思路:先参考一下 力扣里的一位前辈的 解题思路,太棒啦!。。。参考完了,听我说,主要就是那个 “做选择”,之后 “撤销选择” 这个 算法思维很赞!

》》》》代码方面,回溯算法的框架:

result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return

for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。

作者:labuladong
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。《《《《《《《

实现:(C++) 

class Solution {
  public:
    void helper( vector<vector<int>>&vec,vector<int>& nums,int start,int n)
     {
        if( start==n )          //终止条件。
       {
        vec.push_back( nums );
        return ;
       }
     int i;
       for( i=start;i<n;i++ )          //这三句话,真得费神去理解呢....也是最好作一作图。而且友情提醒:坚持循环下去。到了后面的循环你就会发现奥秘!
     {
      swap( nums[i],nums[start] );    //有很多次都是原地交换,我还没搞清楚这是什么原因...
      helper( vec,nums,start+1,n);     //当 start值为2 作为递归返回时,真正的故事就开始了。 那个时候 i 的值也为2,但要进行循环 i++ 后为3。
                      // 之后又递归到 这一句。 i为1,start值为1。i再次进行循环 i++ ,而start没变, 全排列 拉开帷幕。
      swap( nums[i],nums[start] );
       }
      return ;
     }
    vector<vector<int>> permute(vector<int>& nums)
     {
     vector<vector<int>> vec;
     if( nums.size()==0 ) return {};
     if( nums.size()==1 ) return { { nums[0]} };      //特殊情况,特殊处理
      helper( vec,nums,0,nums.size() );
    return vec;
  }
};

运行结果:

代码执行结果:
我的输入
[1,2,3]
我的答案
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,2,1],[3,1,2]]
预期答案
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

学习心得:这道回溯题,让我感受到,一种“简单到复杂思维”,也就是我们想就 比较简单的例子 而言

来敲代码,然后可以把这个代码延伸到更复杂的例子上。而且那个 “回溯算法的框架”,太赞了!(注:这个

全排列是从末尾开始逐渐 交换的。)

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

回溯算法_第4题:子集

题目描述:

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

举例:

示例:

输入: nums = [1,2,3]
输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

解题思路:和上一题 “全排列” 有很大的相似之处。连 “回溯算法的模板” 都一样。但是 终止条件和递归实参 不同。

 

实现:(C++)

class Solution {
  public:
    vector<int> temp;
    void backtrack( vector<vector<int>>&res, vector<int>& nums, int start)
      {
      res.push_back( temp );
      int i;
      for( i=start;i<nums.size();i++ )        //其实 i<nums.size() 也就是终止条件。
        {
           temp.push_back( nums[i] );        //“做出选择”
           backtrack( res,nums,i+1 );       //这里的 i+1 非同一般,为什么呢?在全排列那里, 是“start+1”, 而这里是 “i+1” 。作作图就好理解了。
           temp.pop_back();           //“撤销选择”
           }
         }
    vector<vector<int>> subsets(vector<int>& nums)
      {
      vector<vector<int>>res;
      if( nums.size()==0 ) return {{}};         //特殊情况,特殊处理
           backtrack( res,nums,0 );
      return res;
        }
};

运行结果:

代码执行结果:
我的输入
[1,2,3]
我的答案
[[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]
预期答案
[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

学习心得:上一题的 全排列 和这道 子集 很相似,但想自己独立做出来,还是不容易。我发现它们都有个

for循环,然后在这个for循环里面进行 回溯 。现在我还没太关注 时间复杂度 和 空间复杂度 ,我觉得题解里

能把 空间时间复杂度 用 美丽的数学公式表示出来的,都厉害!回溯回溯,多练多记,然后 融会贯通。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

回溯算法_第5题: 单词搜索

题目描述:

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

举例:

示例:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false

提示:

  • board 和 word 中只包含大写和小写英文字母。
  • 1 <= board.length <= 200
  • 1 <= board[i].length <= 200
  • 1 <= word.length <= 10^3

解题思路:两层for循环遍历二维数组,把二维数组里面的 每个元素作为 “火车头”,都来试一试,如果有满足情况的 “火车” ,就直接返回 true 了。另外在回溯函数中,其形参

要有一个 下标 ,专门用来记录 目标单词里面字母的 下标位置。

实现:(C++)

class Solution {
  public:
    bool backtrack( vector<vector<char>>& board,string& word,int wordindex,int i,int j )      //wordindex:目标单词里面的字母下标
      {
         if( word[wordindex]!=board[i][j] )          //只要 下一节 “火车厢” 不对口,返回 false
          return false;
      if( wordindex==word.size()-1 )          //这是一个终止条件,不可缺少
          return true;
     char temp=board[i][j];
        board[i][j]='*';         // 前辈解题代码的 精髓,换一个符号,相当于做个标记,防止该节 车厢(或车头)被重复使用
        wordindex++; //下标向右移一位
        if( i>0 && backtrack(board,word,wordindex, i-1,j)           //向左找合适的“车厢”
      ||i<board.size()-1 && backtrack(board,word,wordindex,i+1,j)     //向右找合适的“车厢”
      ||j>0 && backtrack(board,word,wordindex,i,j-1)            //向上找合适的“车厢”
      ||j<board[0].size()-1 && backtrack(board,word,wordindex,i,j+1) )   //向下找合适的“车厢”
          return true;
        board[i][j]=temp;       //如果没找到合适的“车厢”,把标记过的前一节(或多节,因为递归时可能多次执行)“车厢”恢复原样。
     return false;
         }
    bool exist(vector<vector<char>>& board, string word) {
    int i,j;
    for( i=0;i<board.size();i++ )
       for( j=0;j<board[0].size();j++ )
        if( backtrack(board,word,0,i,j) )
          return true;
    return false;        //如果都不行,才返回false
  }
};

运行结果:

代码执行结果:
我的输入
[["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]]
"ABCCED"
我的答案
true
预期答案
true


学习心得:这道题很有趣的哟。我一看到,我就想用昨天学到的那个“感染函数”,但还是差那么一段距离

写出正确的代码。没想到的是,那个 if 语句可以写这么 长,连我都被惊呆了!_  !。做了这道题后,再次

加深了对 标记法 的认识与理解。

原文地址:https://www.cnblogs.com/Wang-dou-dou/p/13338264.html