Leetcode练习题:递归与回溯

Leetcode练习题:递归与回溯

  • 39:组合总和
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 组合总和Ⅱ
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 79:单词搜索
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 93:复原IP地址
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 131:分割回文串
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 327:区间和的个数
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 698:划分为k个相等的子集
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 726:原子的数量
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 794:有效的井字游戏
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获
  • 996:正方形数组的数目
    • 问题描述
    • 解题思路
    • 代码实现
    • 反思与收获

递归和回溯这两种思想也是十分重要,但是如果没有接触过或者训练的比较少的话,难度就会比较大。因为很多解法可能想不到,只能学习积累。
这两个概念,有点点相似,但也有不同,为了让自己更好的区分,我直接把百度百科上的概念粘贴在最前面。

  • 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
  • 程序调用自身的编程技巧称为递归( recursion)。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。

39:组合总和

问题描述

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合,输出组合的数量。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。

解集不能包含重复的组合。

示例 1:

输入:candidates = [2,3,6,7], target = 7,

所求解集为:

[

[7],

[2,2,3]

]

输出:2

示例 2:

输入:candidates = [2,3,5], target = 8,

所求解集为:

[

[2,2,2,2],

[2,3,3],

[3,5]

]

输出:3

解题思路

使用回溯的方法,注意每个元素可以多次选择。
剪枝剪在当前和已经超过target时。
像这种求和的方法,通常有加和减两种方式来实现,加很容易想到,但其实减的代码会更加简洁一点。

代码实现

 void dfs(int start,int target)
    {
     
        //如果target为0了,则这是一种解答
        if(target==0)
        {
     
            ans.push_back(path);
            return;
        }
        //target-candidate[i]>=0剪枝操作
        for(int i=start;i<candidate.size()&&target-candidate[i]>=0;i++)
        {
     
            path.push_back(candidate[i]);
            //因为可以重复选,下一次dfs开始的下标还是i
            dfs(i,target-candidate[i]);
            //pop 回溯
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
     
            //先排序
            sort(candidates.begin(),candidates.end());
            this->candidate=candidates;
            //初始为从数组第一个元素开始,还需总和为target
            dfs(0,target);
            return ans;
    }

反思与收获

target使用减法会方便很多,少了一个参数也不需要进行sum==target的判断,牢记。

组合总和Ⅱ

问题描述

给定一个数组 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]

]

输出:4

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,

所求解集为:

[

[1,2,2],

[5]

]

输出:2

解题思路

跟前一题不一样的点在于,这次每个元素只能选取一次了。
一开始会很想当然的感觉 让下一次dfs从i+1开始即可。
但是会重复计算相同的情况,比如示例2:[1,2’,2’’]与[1,2’,2’’’]是同一种组合情况,所以去除重复的计算怎么来考虑呢。
在这一次循环中,如果当前元素跟前一个元素一样的话,其实它就不用考虑了,因为前一个元素已经将所有情况包含在里面了,并且考虑的范围更加广。
图解可以看leetcode题解

但是这个循环的第一个元素还是得放进去的,所以i>start,如果还不能理解可以看上述链接的评论一。

代码实现

 void dfs(int start,int target)
    {
     
        if(target==0)
        {
     
            ans.push_back(path);
            return;
        }
        for(int i=start;i<candidate.size()&&target-candidate[i]>=0;i++)
        {
     
            if(i>start && candidate[i]==candidate[i-1])
            {
     
                continue;
            }
            path.push_back(candidate[i]);
            dfs(i+1,target-candidate[i]);
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
     
            sort(candidates.begin(),candidates.end());
            this->candidate=candidates;
            dfs(0,target);
            return ans;
    }

反思与收获

这个剪枝思想十分重要和巧妙,无需想的太复杂,前面元素考虑的dfs情况肯定会包含了后面重复元素的情况,为了避免相同组合情况的出现,可以忽略。但注意是在同一个for循环当中。

if(i>start && candidate[i]==candidate[i-1])
            {
     
                continue;
            }

79:单词搜索

问题描述

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

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

示例:

board =

[

[‘A’,‘B’,‘C’,‘E’],

[‘S’,‘F’,‘C’,‘S’],

[‘A’,‘D’,‘E’,‘E’]

]

给定 word = “ABCCED”, 返回 true

给定 word = “SEE”, 返回 true

给定 word = “ABCB”, 返回 false

解题思路

非常典型的回溯算法题目,直接采用暴力的行为来解决,从每一个位置开始往四周走,如果走出了给定word就返回true。
由于每个字母只能使用一次,因此需要visited辅助数组。使用了二维方向数组来控制方向,直接学过一个一维的
由于只需返回能否找到,因此省去了path储存的要求,直接使用index来判断每个字符是否正确。

 bool dfs(int i,int j,int index,vector<vector<char>> &board,string &word,vector<vector<bool>> &visit)

i,j为在当前board的坐标,index为现在比较word的字符下标。

代码实现

    int dir[4][4]={
     {
     -1,0},{
     0,1},{
     0,-1},{
     1,0}};

    bool exist(vector<vector<char>>& board, string word) {
     
        int m=board.size();
        int n=board[0].size();
        //初始化visited
        vector<vector<bool>> visit(m,vector<bool>(n));

        for(int i=0;i<m;i++)
        {
     
            for(int j=0;j<n;j++)
            {
     
                //暴力解法,每个位置都进行一次探索
                if(dfs(i,j,0,board,word,visit))
                {
     
                    return true;
                }
            }
        }
        return false;
    }

    bool dfs(int i,int j,int index,vector<vector<char>> &board,string &word,vector<vector<bool>> &visit)
    {
     
        //最后一个字符时
        if(index==word.size()-1)
        {
     
            return word[index]==board[i][j];
        }

        if(word[index]==board[i][j])
        {
     
            visit[i][j]=true;
            for(int k=0;k<4;k++)
            {
     
                //控制方向
                int x=i+dir[k][0];
                int y=j+dir[k][1];

                if(x>=0&&x<board.size()&&y>=0&&y<board[0].size()&&!visit[x][y])
                {
     
                    if(dfs(x,y,index+1,board,word,visit))
                    {
     
                        return true;
                    }
                }
            }
            //回溯的操作,将该位改回未访问
            visit[i][j]=false;
        }
        return false;
    }

反思与收获

首先二维vector的初始化,总是忘记…

  vector<vector<bool>> visit(m,vector<bool>(n));

一定要记得回溯的操作,在所有操作结束后,要将该元素回到操作之前的状态。
像这题没有要求输出每一次的结果,只需要返回bool类型,就不需要path然后在比较字符串了,巧妙的使用index来简化。

93:复原IP地址

问题描述

给定一个只包含数字的字符串,复原它(在中间插入点号)并返回所有可能的 IP 地址格式,输出可能的格式的数量。

有效的 IP 地址正好由四个整数(每个整数位于 0 到 255 之间)组成,整数之间用 ‘.’ 分隔。

示例:

输入: “25525511135”

输出: 2

说明:因为可能的IP地址包括:[“255.255.11.135”, “255.255.111.35”]

解题思路

这题用几个循环也是能够实现的,但最主要的是dfs的思想。
这边最主要是使用一个参数n,来计算当前分割了几个数字,如果n=4了,且还需分割的字符串s已经为空了,则说明这是一种正确的分割方式,放入答案中。
否则就继续分割,数字位数有三种可能1,2,3,要判断是否符合(0,255]的要求,其实用例中还暗藏了一个要求,就是不能0开头,这个判断方式也很巧妙。

代码实现

    vector<string> ans;
    vector<string> restoreIpAddresses(string s) {
     
        string ip;
        dfs(s,0,ip);
        return ans;
    }
    //s为还需分割的字符串,n为当前数字个数,ip存储答案
    void dfs(string s,int n,string ip)
    {
     
        if(n==4)
        {
     
            if(s.empty())
            {
     
                ans.push_back(ip);
            }
        }
        else
        {
     
            //数字有1,2,3位的可能性
            for(int i=1;i<4;i++)
            {
     
                if(s.size()<i)
                {
     
                    break;
                }
                //判断整数范围是否符合要求
                int val=stoi(s.substr(0,i));
                //判断是否0开头
                if(val>255||i!=to_string(val).size())
                {
     
                    continue;
                }
                dfs(s.substr(i),n+1,ip+s.substr(0,i)+(n==3?"":"."));
            }
        }
        return ;
    }

反思与收获

首先是substr函数的使用方法,第一个参数为开始位置,第二个参数为切割长度默认一直切到字符串尾。
其次是使用i!=to_string(val).size()来判断字符串是否以0开头。
这里使用计数n,来实现大量剪枝的操作,可以学习到。

131:分割回文串

问题描述

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案的数量。

示例:

输入: “aab”

输出: 2

说明:可能的分割方案有:

[

[“aa”,“b”],

[“a”,“a”,“b”]

]

解题思路

需要一个vector来存储这一次的分割情况。
是否为回文字符串的判断函数,这个十分巧妙,无需传入切割下来的这个字符串,会有点复杂,直接传入原本字符串和起止下标即可,快速判断。
如果当前切割的字符串符合要求,则继续dfs。

代码实现

    vector<string> path;
    vector<vector<string>> ans;
    //是否回文判断函数
    bool isValid(string &s,int i,int j)
    {
     
        while(i<j)
        {
     
            if(s[i]!=s[j])
            {
     
                return false;
            }
                i++;
                j--;

        }
        return true;
    }

    //pre 为现在开始切割的下标
    void dfs(string &s,int pre)
    {
     
        //如果切到最后了,则这是一种切割方案
        if(pre==s.size())
        {
     
            ans.push_back(path);
            return;
        }

        for(int i=pre;i<s.size();i++)
        {
     

            if(isValid(s,pre,i))
            {
     
                path.push_back(s.substr(pre,i-pre+1));
                dfs(s,i+1);
                path.pop_back();
            }
        }

    }
    vector<vector<string>> partition(string s) {
     
        if(s.length()==0)
        {
     
            return ans;
        }
        dfs(s,0);

        return ans;
    }

反思与收获

其实做到这里,dfs的思想并不难领会,而且一些题目一看就是需要回溯来实现的,关键就是函数怎么来设计,参数选择哪几个才是最简洁,这里直接选择传入数组下标,回文字符串判断函数总也是下标参数,要学习。

327:区间和的个数

问题描述

给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper。

区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。

说明:

最直观的算法复杂度是 O(n2) ,请在此基础上优化你的算法。

示例:

输入: nums = [-2,5,-1], lower = -2, upper = 2,

输出: 3

解释: 3个区间分别是: [0,0], [2,2], [0,2],它们表示的和分别为: -2, -1, 2。

解题思路

暴力解法的话 肯定是O(n^2),一开始想到滑动窗口的解法,但是有负数所以无法实现,肯定是是需要用到前缀和的知识,并且需要将其存储在set中,还看到线段树的解法这学了后面知识再说。

对于任何子数组之和[i,j]都可以表示为prej-prei,
所以lower<=prej-prei<=upper转换为
prej-upper<=prei<=prej-lower
找符合该条件的前缀和有几个,则子数组有多少个,采用lower_bound(第一个大于等于)和upper_bound(第一个大于),通过计算两者之间的迭代器之间的距离表示个数,使用distance。
参考题解

代码实现

 int countRangeSum(vector<int>& nums, int lower, int upper) {
     
        int size=nums.size();
        multiset<long long> preSum;

        long long tempSum=0;
        int ans=0;

        for(int i=0;i<size;i++)
        {
     
            tempSum+=nums[i];

            if(tempSum>=lower&&tempSum<=upper)
            {
     
                ans++;
            }

            if(preSum.size()!=0)
            {
     
                long long temp1=tempSum-upper;
                long long temp2=tempSum-lower;

                auto it1=preSum.lower_bound(temp1);
                auto it2=preSum.upper_bound(temp2);
                ans+=std::distance(it1,it2);
            }
            preSum.insert(tempSum);
        }

    return ans;
   }

反思与收获

关于upper_bound和lower_bound的知识,返回的是地址,更详细内容,以及使用distance计算迭代器之间的距离。

698:划分为k个相等的子集

问题描述

给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。

示例 1:

输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4

输出: True

说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。

解题思路

比较典型的dfs算法的题目,可以先进行预先的判断sum/k是否为整,这里采用的是逐渐相加sum与target进行比较,也可以使用减法的,需要添加visited数组记录该数是否使用过,同时添加了start参数表示这一次从数组下标几开始考虑

代码实现

 bool dfs(vector<int> nums,int numsize,int k,int target, int temp,int start,int* visited)
    {
     
        //只剩一组肯定OK的
        if(k==1)
        {
     
            return true;
        }

        if(temp>target)
        {
     
            return false;
        }

        if(temp==target)
        {
     
        //这里两个参数都为0 的原因是 还是从头开始找,但是有visited的帮助,不会重复,否则不知道从哪开始找
            return dfs(nums,numsize,k-1,target,0,0,visited);
        }
        else{
     
            for(int i=start;i<numsize;i++)
            {
     
                //之前的元素都考虑过了
                if(!visited[i])
                {
     
                    visited[i]=1;
                    if(dfs(nums,numsize,k,target,temp+nums[i],i+1,visited))
                    {
     
                        return true;
                    }else
                    {
     
                        visited[i]=0;
                    }
                }
            }
        }
        return false;
    }

    bool canPartitionKSubsets(vector<int>& nums, int k) {
     
        int sum=0;
        for(int i=0;i<nums.size();i++)
        {
     
            sum+=nums[i];
        }
        int *visited = new int[nums.size()];
        memset(visited,0,sizeof(int)*nums.size());

        if(k==1)
        {
     
            return true;
        }
        if(sum%k!=0)
        {
     
            return false;
        }

        return dfs(nums,nums.size(),k,sum/k,0,0,visited);
    }

反思与收获

可以先预先进行整体判断节约时间,并且这里的start参数总是从头开始的,因为有visited的辅助,不需要考虑复杂的从何开始继续,而是从头开始简洁一点,不会重复。

726:原子的数量

问题描述

给定一个化学式formula(作为字符串),返回每种原子的数量。

原子总是以一个大写字母开始,接着跟随0个或任意个小写字母,表示原子的名字。

如果数量大于 1,原子后会跟着数字表示原子的数量。如果数量等于 1 则不会跟数字。例如,H2O 和 H2O2 是可行的,但 H1O2 这个表达是不可行的。

两个化学式连在一起是新的化学式。例如 H2O2He3Mg4 也是化学式。

一个括号中的化学式和数字(可选择性添加)也是化学式。例如 (H2O2) 和 (H2O2)3 是化学式。

给定一个化学式,输出所有原子的数量。格式为:第一个(按字典序)原子的名子,跟着它的数量(如果数量大于 1),然后是第二个原子的名字(按字典序),跟着它的数量(如果数量大于 1),以此类推。

示例 1:

输入:

formula = “H2O”

输出: “H2O”

解释:

原子的数量是 {‘H’: 2, ‘O’: 1}。

示例 2:

输入:

formula = “Mg(OH)2”

输出: “H2MgO2”

解释:

原子的数量是 {‘H’: 2, ‘Mg’: 1, ‘O’: 2}。

示例 3:

输入:

formula = “K4(ON(SO3)2)2”

输出: “K4N2O14S4”

解释:

原子的数量是 {‘K’: 4, ‘N’: 2, ‘O’: 14, ‘S’: 4}。

注意:

所有原子的第一个字母为大写,剩余字母都是小写。

formula的长度在[1, 1000]之间。

formula只包含字母、数字和圆括号,并且题目中给定的是合法的化学式。

解题思路

这题一看我就不会做…参考题解
对于s[i]而言有以下几种情况
1.大写字母,如果暂存的字符栈中有字符,则将栈清空,将新的大写字母加入,例如MgO,扫描到O时
2.小写字母,加入栈中,扫描到g时
3.数字,则需要对count进行操作,可能是Mg12O12的情况,需要count*=10的操作
4.如果是‘(’则需要进行递归操作,找到与其配对的‘)’,可能出现K4(ON(SO3)2)2多个配对括号的情况,所以需要bucket计数。将(里面这一串)进行递归操作,计算括号外面的count。
map作为返回结果,会自动按字典排序,因此能够实现题目的要求。

代码实现

map<string,int> parse(const string exp,int left, int right)
    {
     
        string atom;
        int count=0;
        map<string,int> m;
        int i=left;
        while(i<=right)
        {
     
            char c=exp[i];
            if(isupper(c))
            {
     
                if(!atom.empty())
                {
     
                    m[atom]+=max(count,1);
                    atom.clear();
                    count=0;
                }
                atom+=c;
            }else if(islower(c))
            {
     
                atom+=c;
            }else if(isdigit(c))
            {
     
                count*=10;
                count+=c-'0';
            }else if(c=='(')
            {
     
                if(!atom.empty())
                {
     
                    m[atom]+=max(count,1);
                    atom.clear();
                    count=0;
                }
                int temp= ++i;
                int bucket=1;
                while(i<=right&&bucket!=0)
                {
     
                    if(exp[i]=='(') ++bucket;
                    if(exp[i]==')') --bucket;
                    if(bucket==0) break;
                    ++i;
                }
                auto m1=parse(exp,temp,i-1);
                count=0;
                while(i+1<=right&&isdigit(exp[i+1]))
                {
     
                    count*=10;
                    count+=exp[++i]-'0';
                }
                count=max(count,1);

                for(auto& p:m1)
                {
     
                    m[p.first]+=p.second*count;
                }
                count=0;
            }
            ++i;
        }
        if(!atom.empty())
        {
     
            m[atom]+=max(count,1);
        }
        return m;
    }

     string countOfAtoms(string formula) {
     
         auto m=parse(formula,0,formula.size()-1);
         string s;
         for(auto&p:m)
         {
     
             s+=p.first;
             if(p.second>1)
             {
     
                 s+=to_string(p.second);
             }
         }
         return s;

    }

反思与收获

遇见复杂的题目还是要静下心来好好看,模拟一点点来,关于复杂递归就分情况开始套路,按解题思路来,这个函数返回类型为map学习到,以及使用count=0,max(count,1)来实现单个原子的数量计算,用能实现两位数以上的计算。

794:有效的井字游戏

问题描述

用字符串数组作为井字游戏的游戏板 board。当且仅当在井字游戏过程中,玩家有可能将字符放置成游戏板所显示的状态时,才返回 true。

该游戏板是一个 3 x 3 数组,由字符 " ",“X” 和 “O” 组成。字符 " " 代表一个空位。

以下是井字游戏的规则:

玩家轮流将字符放入空位(" ")中。

第一个玩家总是放字符 “X”,且第二个玩家总是放字符 “O”。

“X” 和 “O” 只允许放置在空位中,不允许对已放有字符的位置进行填充。

当有 3 个相同(且非空)的字符填充任何行、列或对角线时,游戏结束。

当所有位置非空时,也算为游戏结束。

如果游戏结束,玩家不允许再放置字符。

示例 1:

输入: board = ["O ", " ", " "]

输出: false

解释: 第一个玩家总是放置“X”。

示例 2:

输入: board = [“XOX”, " X ", " "]

输出: false

解释: 玩家应该是轮流放置的。

示例 3:

输入: board = [“XXX”, " ", “OOO”]

输出: false

示例 4:

输入: board = [“XOX”, “O O”, “XOX”]

输出: true

说明:

游戏板 board 是长度为 3 的字符串数组,其中每个字符串 board[i] 的长度为 3。

board[i][j] 是集合 {" ", “X”, “O”} 中的一个字符。

解题思路

该题主要是分类讨论,因为只有3*3,所以很好解决,首先统计X和O的个数,X一定是大于等于O的,X=O或X=O+1,否则直接false。
如果X玩家赢,则X=O+1;
如果O玩家赢,则X=O;
判断赢的话,直接实现就行,某一列或某一行或者对角线都是该字符则为赢

代码实现

bool validTicTacToe(vector<string>& board) {
     
        int oCount=0,xCount=0;
        for(int i=0;i<3;i++)
        {
     
            for(int j=0;j<3;j++)
            {
     
                if(board[i][j]=='O')
                {
     
                    oCount++;
                }else if(board[i][j]=='X')
                {
     
                    xCount++;
                }
            }
        }
        if(xCount!=oCount&&oCount!=xCount-1)
        {
     
            return false;
        }
        if(win(board,'X')&&oCount!=xCount-1)
        {
     
            return false;
        }
        if(win(board,'O')&&oCount!=xCount)
        {
     
            return false;
        }
        return true;
    }

    bool win(vector<string> A,char C)
    {
     
        for(int i=0;i<3;i++)
        {
     
            if(A[i][0]==C&&A[i][1]==C&&A[i][2]==C)
            {
     
                return true;
            }
             if(A[0][i]==C&&A[1][i]==C&&A[2][i]==C)
             {
     
                 return true;
             }
        }
        if(A[0][0]==C&&A[1][1]==C&&A[2][2]==C)
        {
     
            return true;
        }
        if(A[0][2]==C&&A[1][1]==C&&A[2][0]==C)
        {
     
            return true;
        }
        return false;
    }

反思与收获

虽然下棋总是需要递归的思想,但是该题因为情况比较简单所以直接分类就可以解决,而且速度也快。有时候要多注意题目的解释或者设定,一些总体的要求和判断可以先进行,省去很多不必要的情况。

996:正方形数组的数目

问题描述

给定一个非负整数数组 A,如果该数组每对相邻元素之和是一个完全平方数,则称这一数组为正方形数组。

返回 A 的正方形排列的数目。两个排列 A1 和 A2 不同的充要条件是存在某个索引 i,使得 A1[i] != A2[i]。

示例 1:

输入:[1,17,8]

输出:2

解释:

[1,8,17] 和 [17,8,1] 都是有效的排列。

示例 2:

输入:[2,2,2]

输出:1

说明:若一个数能表示成某个整数的平方的形式,则称这个数为完全平方数。完全平方数是非负数。

解题思路

首先要想到建构什么样子的 数据结构,考虑到相邻两个数字都是平方安全数,可以建立图模型,如果符合条件则两点之间有点线,几种排序方式就变成了有几种全图遍历不重复的情况。
不能计算重复的情况,因此建立了map和node分开来记录。

代码实现

class Solution {
     
public:
    //是否平方数
    bool isValid(int n)
    {
     
        int t=sqrt(n);
        return t*t==n;
    }

    //顶点度,邻接表,每个顶点的出现的次数,当前遍历的下标,目前遍历了几个数,一共有几个数,答案
    void dfs(const vector<int>& nodes,const vector<vector<int>>& ans,map<int,int>& count,int i,int m, int N,int &res)
    {
     
        //都遍历完成
        if(m==N)
        {
     
            res++;
            return;
        }

        for(auto j:ans[i])
        {
     
            if(count[nodes[j]]>0)
            {
     
                --count[nodes[j]];
                dfs(nodes,ans,count,j,m+1,N,res);
                ++count[nodes[j]];
            }
        }
    }

    int numSquarefulPerms(vector<int>& A) {
     
        //可能存在重复元素
        map<int,int> count;
        for(auto i:A)
        {
     
            count[i]++;
        }

        //建立节点
        vector<int> nodes;
        for (auto& p : count) {
     
            nodes.push_back(p.first);
        }

       int N=nodes.size();
       //邻接图
       vector<vector<int>> ans(N);

       for(int i=0;i<N;i++)
       {
     
           //如果自己多个元素,则有条自环
           if(count[nodes[i]]>1&&isValid(nodes[i]*2))
           {
     
               ans[i].push_back(i);
           }
           //满足条件,则i与j直接有一条线
           for(int j=i+1;j<N;j++)
           {
     
               if(isValid(nodes[i]+nodes[j]))
               {
     
                   ans[i].push_back(j);
                   ans[j].push_back(i);
               }
           }
       }

       int res=0;
       for(int i=0;i<N;i++)
       {
     
           --count[nodes[i]];
           dfs(nodes,ans,count,i,1,A.size(),res);
           ++count[nodes[i]];
       }
        return res;

    }
};

反思与收获

在去除重复情况的时候,考虑使用map来记录重复元素出现的个数。

————————————————————————————
即使知道了回溯和递归非常经典的解题套路和常用方法之后,有时题目一变化就不知道该如何设计函数和所需参数,怎么来更高效率的实现,尽量简化函数参数。有时需要结合不同的数据结构,建立怎样合适高效的数据结构也很重要。(;′⌒`)

你可能感兴趣的:(算法,递归法,leetcode)