排列组合与子集

        Leetcode上有几道题比较类似,Permutation、Combination和Subsets等。说它们相似,是因为思路类似。要求出某个序列的排列组合和子集,初看起来,要对序列中的每个元素排列、取舍,初看起来,似乎要写一个循环层数正比于元素个数的大循环,而语言并没有提供这种功能。这时就要转变思路,先求部分解或子问题的解。

例如Permutation这题:

vector<vector<int>> permute(vector<int>& nums) {
        
        vector<vector<int> > result;
        vector<int> temp;
        
        result.push_back(nums);
        
        /*
          每次确定一个字符,第i个字符之前的序列确定后,第i个字符有nums.size()-i种选择。通过将第i个字符与第i+1到第nums.size()-1个字符交换得到不同的排列。
          类似于数学归纳法?动态规划?迭代计算出所有排列
        */
        for(int i = 0; i < nums.size()-1; ++i) 
        {
            /*
              必须倒序遍历,从算法逻辑和实现正确性上考虑都必须如此。初始式j = result.size()-1只会执行一次,若采用顺序遍历,
              j < result.size()每次都会执行,使result.size()重新计算,而循环每执行一次,result.size()都会增大,导致无穷循环
            */
            for(int j = result.size()-1; j >= 0; --j) 
            {
                for(int k = i+1; k < nums.size(); ++k)
                {
                    temp = result[j];

                    swap(temp[i], temp[k]);
                    
                    result.push_back(temp);
                }
            }
        }
        
        return result;
    }
大体思路就是每次只考虑(一个)子问题,构造出部分解或子问题的解,然后再通过操作部分解或子问题的解来构造更完整的解,迭代直到构造出所有解。也就是说每个解的构造过程不是独立的,较大问题的解需要依赖较小问题的解。
Subsets这题的代码可能能更好地展示这种方法:

/*
    求所有子集,就是从原集合中取出若干元素组成新的集合,每个元素要么取要么不取,共有2^n种情况。本题和求排列的问题十分相似,因此思路几乎一致。求n个元素的集合其子集,那就先求前n-1个元素之集合的子集。对于前n-1个元素来说,根本就不知道第n个元素的存在,因此无须考虑,相当于先求子问题的解,类似于动态规划?对于原问题的集合来说,相当于第n个元素不取,先求前n-1个元素的子集,待求得后,再将第n个元素添加进去,即得原问题的解。
    思路总结如下:每个元素要么取要么不取,先求第0~i元素的子集,第i+1~n-1个元素不考虑,相当于不取;待求得前i个元素的子集后,再将第i+1个元素添加进去,即得前i+1个元素的子集;而当求前i个元素的子集时,前i-1个元素的子集已经构造完毕,我们只需遍历这些集合,将第i个元素添加进去就行。
    与求排列问题不同的是,求排列是先得出部分解,再通过部分解迭代构造出所有解;求子集,是先求子问题的解(虽然子问题的解也是原问题的部分解),再对子问题的解扩展得到更大子问题的解,迭代直到得出原问题的所有解。
*/
    vector<vector<int>> subsets(vector<int>& nums) {
        
        sort(nums.begin(), nums.end());
        
        vector<vector<int> > result;
        vector<int> temp;
        
        result.push_back(temp);
        
        for(int i = 0; i < nums.size(); ++i)
        {
            int n = result.size();
            
            for(int j = 0; j < n; ++j)
            {
                temp = result[j];
                temp.push_back(nums[i]);
                result.push_back(temp);
            }
        }
        
        return result;
    }

求子集时,我们不是独立地构造每个解,而是在求得的子问题的解中再添加元素得到新的(更大子问题)解。例如,空集{}是集合{1,2,3}的子集,那么在空集中添加元素1,得到集合{1}是原集合的子集,再向集合{1}中添加元素2得到{1,2}就是原问题的更大的子集。

        问题本身都不难,都是常见的问题,但是其中的思路具有通用性和普适性。有点难度的东西不会也就算了,(其实我也学过一些算法,动态规划什么的,但始终不得要领,学的时候似乎没什么问题,用的时候就不会了,再回过头看,当时可能回了,以后遇到又不会了,反反复复如此,说到底,还是手生,练得少。这个其实不光是学算法、编程如此,学一切知识都可能出现这种情况,是一个普遍性问题),简单的东西就要保证会且熟。根据我的经验,简单的知识就可以解决大部分问题了,很多问题并不需要多高深的知识,人们常常在实践中用到了一些复杂的技术却反而忘记了基本的原则,这就本末倒置了。并且,如果熟练掌握了简单的知识,在需要更高深知识的情况下,也可以较容易地进一步学习,这就是打好基础的作用。

        在以后的学习和工作中,希望自己一方面思维能够更开阔、活跃,另一方面,也要更踏实一些。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值