Showing posts with label recursion. Show all posts
Showing posts with label recursion. Show all posts

Monday, February 10, 2020

Leetcode @ Subsets, Permutations, Combination Sum, Coin Change 题目总结

Subsets, Permutations, Combinations 此类题目都类似,都可以用recursion, backtracking来解,时间复杂度都很高,因为需要in depth search

leetcode上的总结


Let us first review the problems of Permutations / Combinations / Subsets, since they are quite similar to each other and there are some common strategies to solve them.
First, their solution space is often quite large:
  • PermutationsN!.
  • CombinationsC_N^k = \frac{N!}{(N - k)! k!}
  • Subsets: 2^N, since each element could be absent or present.
Given their exponential solution space, it is tricky to ensure that the generated solutions are complete and non-redundant. It is essential to have a clear and easy-to-reason strategy.
There are generally three strategies to do it:
  • Recursion
  • Backtracking
  • Lexicographic generation based on the mapping between binary bitmasks and the corresponding
    permutations / combinations / subsets.
As one would see later, the third method could be a good candidate for the interview because it simplifies the problem to the generation of binary numbers, therefore it is easy to implement and verify that no solution is missing.
Besides, this method has the best time complexity, and as a bonus, it generates lexicographically sorted output for the sorted inputs.
下面是具体的题目:

Subsets

Given a set of distinct integers, nums, return all possible subsets (the power set).
Note: The solution set must not contain duplicate subsets.
Example:
Input: nums = [1,2,3]
Output:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]
Accepted
485,557
Submissions
838,363



BackTracking


class Solution {

public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> res;
        vector<int> cur;
        res.push_back(cur);
        
        for (int &n : nums)
        {
            vector<vector<int>> cur;
            for (auto r : res)
            {
                r.push_back(n);
                cur.push_back(r);
            }
            for (auto c : cur)
            {
                res.push_back(c);
            }
        }
        return res;
    }
};


1. Combination Sum :没有重复的数组,找出所有unique combinations,数字可以重复利用。
注意这里不需要sort,因为数字无重复。因为数字可以给重复利用,所以 i = index, 也就是自己可以继续add to sum,进入recursion,直到等于或者超过target,再退回来,去加下一个number,所以recursion的退出条件,sum == target, sum > target都很必要。

class Solution {
    void helper(vector<int>& candidates, int target, vector<vector<int>> &res, vector<int> &cur, int index, int sum)
    {
        if (sum == target)
        {
            res.push_back(cur);
            return;
        }
        
        if (sum > target) return;
        
        for (int i = index; i < candidates.size(); i++)
        {
            cur.push_back(candidates[i]);
            helper(candidates, target, res, cur, i, sum + candidates[i]);
            cur.pop_back();
        }
        return;
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        vector<int> cur;
        helper(candidates, target, res, cur, 0, 0);
        return res;
    }
};


2. Combination Sum II: 和combination sum有很大的不同,首先数组里面有重复数字,但数字不可重复利用,同时返回的结果要是unique的。因为有重复数字,那么就可能有重复结果,只是顺序变了。如何处理?

这个地方破题的关键点在于,如果有多个重复的数字,产生单个结果时我可以用上它们,但是呢,在用它们来产生全局结果时,很可能产生duplicate的结果。

此时,我们可以先sort数组,然后用一个判断条件 if (i > index && candiates[i] == candidates[i-1]) continue; 这句是关键。i > index,  只可能发生在i == index 的recursion已经执行完了,for循环第二层开始了,如果此时当前元素和前一个元素相等, 需要跳过,因为,前面那个同样值的candidate已经产生了所有的含有candidate的结果,不需要再来一遍了。

由于值不能重复利用,在下一个recursion时,要i + 1. 如果可以重复利用,就直接 i 就行了。

class Solution {
    void helper(vector<int>& candidates, int target, vector<vector<int>> &res, vector<int> &cur, int index, int sum)
    {
        if (sum == target)
        {
            res.push_back(cur);   
            return;
        }
        
        if (sum > target) return;
        
        for (int i = index; i < candidates.size(); i++)
        {
            if (i > index && candidates[i] == candidates[i-1]) continue;
            cur.push_back(candidates[i]);
            helper(candidates, target, res, cur, i+1, sum + candidates[i]);
            cur.pop_back();
        }
        return;
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        vector<int> cur;
        sort(candidates.begin(), candidates.end());
        helper(candidates, target, res, cur, 0, 0);
        return res;
    }
};


3. Combination Sum III: given k numbers that add up to number n, numbers from 1 to 9 can be used. Each combination should be an unique set. 

熟练掌握了I,II后,III就很简单了。相当于{1...9} 的数组,数字不能重复这样的case,只是加了一个限定条件就是结果的size。

class Solution {
    void helper(vector<int> cand, vector<vector<int>> &res, vector<int> &cur, int k, int n, int index, int sum)
    {
        if (cur.size() == k && sum == n)
        {
            res.push_back(cur);
            return;
        }
        
        if (cur.size() > k || sum > n) return;
        
        for (int i = index; i<cand.size(); i++)
        {
            cur.push_back(cand[i]);
            helper(cand, res, cur, k, n, i+1, sum + cand[i]);
            cur.pop_back();    
        }
    }
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        vector<int> cand;
        for (int i = 1; i<=9; i++)
        {
            cand.push_back(i);
        }
        vector<vector<int>> res;
        vector<int> cur;
        helper(cand, res, cur, k, n, 0, 0);
        return res;
    }
};




4. Combination IV: 给一个正数没有重复数的数组,找出所有可能的combinations的和是一个给定正数,数字可以重复使用, 和I不同的是,这里的order matters, 也就说两个结果,数字相同,但是顺序不同,也算不同的结果,更像permutation sum instead of combination sum。以及这里只需要返回possible number of result就行。follow up: 如果数组里面有负数呢?

这里用的是DP而非recursion。得想想这里怎么做的。

class Solution {
public:
    int combinationSum4(vector<int>& candidates, int target) {
        vector<int> dp(target + 1);
        dp[0] = 1;
        sort (candidates.begin(), candidates.end());
        for (int i = 1; i <= target; i++) {
            for (auto num : candidates) {
                if (i < num) break;
                dp[i] += dp[i - num];
            }
        }
        return dp.back();
    }
};


有人的总结 https://leetcode.com/problems/combination-sum-iv/discuss/85120/C%2B%2B-template-for-ALL-Combination-Problem-Set

5. Coin Change

给一堆硬币,和一个值,找到最少硬币 == 值的硬币个数。看上去和combination sum有点类似,brute force 可以把所有 combination sum都找出来,然后个数最少的返回,不过会超时。既然有最少,那么一看就是DP的感觉,所以看哪些是可重复的计算值,并且思考推导公式。

举例, f(1) = 1, f(2) = 1, f(5) = 1. f(amount) = min(f(amount - coins[i])) + 1
再想想code咋写。得到了推导式,但是怎么一步一步得到f(amount)呢?这里我卡住了。

这里的思路是,针对每一个value, 按照value - coin 的思路循环一次,计算所有已经有最小值的dp[i - coin]。要满足条件就是, i - coin >= 0, dp[i - coin] != -1, dp[i - coin ] < min.

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if (amount == 0) return 0;
        int dp [amount+1] = {-1};
        dp[0] = 0;
        for (int i = 0; i<coins.size() && coins[i] <= amount; i++)
        {
            dp[coins[i]] = 1;
        }
        for (int i = 1; i<=amount; i++)
        {
            int min = INT_MAX;
            for (int &coin : coins)
            {
                if (i - coin >= 0 && dp[i-coin] != -1 && min > dp[i-coin])
                {
                    min = dp[i-coin] + 1;
                }
            }
            dp[i] = min != INT_MAX? min : -1;
        }
        return dp[amount];
    }
};

6. Letter Combinations of a phone number
电话号码排列组合,不难,先把相应数字对应的string给弄出来。再搞两个for loop搞定,反正顺序是一定的。
class Solution {
    void helper(vector<string> cand, vector<string> &res, string &cur, int index)
    {
        if (cand.empty() || cur.size() > cand.size()) return;
        
        if (cur.size() == cand.size())
        {
            res.push_back(cur);
            return;
        }
        for (int i = index; i<cand.size(); i++)
        {
            for (int j = 0; j<cand[i].size(); j++)
            {
                cur.push_back(cand[i][j]);
                helper(cand, res, cur, i+1);
                cur.pop_back();
            }
        }
    }
public:
    vector<string> letterCombinations(string digits) {
        string rec [] = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        vector<string> cand;
        for (auto &c : digits)
        {
            cand.push_back(rec[c-'0'-2]);
        }
        vector<string> res;
        string cur;
        helper(cand, res, cur, 0);
        return res;
    }
};

Monday, July 22, 2013

Flatten binary tree to linked list@leetcode

刷题必备书籍Cracking the Coding Interview: 150 Programming Questions and Solutions 

简历:The Google Resume: How to Prepare for a Career and Land a Job at Apple, Microsoft, Google, or any Top Tech Company
算法学习书籍:Introduction to Algorithms
编程珠玑:Programming Pearls (2nd Edition)
C++ 学习:The C++ Programming Language, 4th Edition
经典操作系统书籍,龙书:Operating System Concepts
创业:The Start-up of You: Adapt to the Future, Invest in Yourself, and Transform Your Career
Given a binary tree, flatten it to a linked list in-place.
For example,
Given
         1
        / \
       2   5
      / \   \
     3   4   6
The flattened tree should look like:
   1
    \
     2
      \
       3
        \
         4
          \
           5
            \
             6
» Solve this problem

这道题思路很清晰,就是递归,然后分别flat右和左,刚开始以为flat哪个先都无所谓,但实际是要先flatten右边的,不然flatten完左边,把左边的连上右边去,右边的已经不是bst了。这是第一点。

第二点,难点,也是我卡住的地方,就是如何把点连接起来。我的思路会,但就是连点连错了,导致出不来。之前我写的是

build(root->right);
build(root->left);

root->right->left=root->right;
root->right=root->left;

这样看上去是对的,但是对于完成了一颗子树,换到另一颗子树的连接就出了问题。这个时候我们要用一个treenode来保存之前跑过的node的root,以便下一次连接,于是就有了下面的代码,多了一个tmp.

Saturday, July 20, 2013

Maximum depth of binary tree@leetcode

微博:http://www.weibo.com/cathyhwzn

刷题必备书籍:Cracking the Coding Interview: 150 Programming Questions and Solutions
Given a binary tree, find its maximum depth.
The maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.
简单题,就是递归然后计算长度,但是要用queue来存储暂时的node。和上几题思路都是一样的,还是 BFS. 

Sunday, July 14, 2013

Unique Binary Search Tree@leetcode

刷题必备书籍Cracking the Coding Interview: 150 Programming Questions and Solutions 

简历:The Google Resume: How to Prepare for a Career and Land a Job at Apple, Microsoft, Google, or any Top Tech Company
算法学习书籍:Introduction to Algorithms
编程珠玑:Programming Pearls (2nd Edition)
C++ 学习:The C++ Programming Language, 4th Edition
经典操作系统书籍,龙书:Operating System Concepts
创业:The Start-up of You: Adapt to the Future, Invest in Yourself, and Transform Your Career
Given n, how many structurally unique BST's (binary search trees) that store values 1...n?
For example,
Given n = 3, there are a total of 5 unique BST's.
   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3
» Solve this problem

此题刚开始看上去无从下手,但是稍作分析就会发现,比较简单。简单在于它不用我们输出所有的 BST,而仅仅是给出有多少个就可以了。这样我们可以一个一个当作root看子树的个数。这时候就是recursion and dp了。这个时候又注意到,当结点i当root时,所有小于i的,会在左子树,所有大于i的,会在右子树。就是个简单的递归了。




更简单的逻辑.

Sunday, July 7, 2013

Generate Parenthesis@leetcode

刷题必备书籍Cracking the Coding Interview: 150 Programming Questions and Solutions 

简历:The Google Resume: How to Prepare for a Career and Land a Job at Apple, Microsoft, Google, or any Top Tech Company
算法学习书籍:Introduction to Algorithms
编程珠玑:Programming Pearls (2nd Edition)
C++ 学习:The C++ Programming Language, 4th Edition
经典操作系统书籍,龙书:Operating System Concepts
创业:The Start-up of You: Adapt to the Future, Invest in Yourself, and Transform Your Career
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
For example, given n = 3, a solution set is:
"((()))", "(()())", "(())()", "()(())", "()()()"
一个小破题,dp还有recursion,关键是思路要清楚还有recursion要很熟练。
先排左边括号,再看中间所有的可能,就是top down,再botton up。先编织最外面一层,再进入到最里面,从每个基本情况排列起来。可以和permutation, subsets对比,都是recursion和dp的题目。


Thursday, July 4, 2013

Subsets II @leetcode

刷题必备书籍Cracking the Coding Interview: 150 Programming Questions and Solutions 

简历:The Google Resume: How to Prepare for a Career and Land a Job at Apple, Microsoft, Google, or any Top Tech Company
算法学习书籍:Introduction to Algorithms
编程珠玑:Programming Pearls (2nd Edition)
C++ 学习:The C++ Programming Language, 4th Edition
经典操作系统书籍,龙书:Operating System Concepts
创业:The Start-up of You: Adapt to the Future, Invest in Yourself, and Transform Your Career
Given a collection of integers that might contain duplicates, S, return all possible subsets.
Note:


  • Elements in a subset must be in non-descending order.
  • The solution set must not contain duplicate subsets.
For example,
If S = [1,2,2], a solution is:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

本题和permutation和subsets思路还是一样的,套路一样,注意两点:1. 避免重复 2. 避免顺序的问题

解决这两个问题用两个判断条件来解决。

Analysis: For example S = [1, 2, 2], we let S[1,2,2] as the all unduplicated subsets of [1,2,2]. Now
we have S[1,2,2] = {} + S[1] + S[2,2] + ( [1] + S[2,2] ). Then S[2, 2] = S[2] + ( [2] + S[2] ). We can observe that, S[2] is already included in S[2,2]. So we can get the rule that when S[i] == S[i-1], we skip.
 

Thursday, June 27, 2013

combinations@leetcode

刷题必备书籍Cracking the Coding Interview: 150 Programming Questions and Solutions 

简历:The Google Resume: How to Prepare for a Career and Land a Job at Apple, Microsoft, Google, or any Top Tech Company
算法学习书籍:Introduction to Algorithms
编程珠玑:Programming Pearls (2nd Edition)
C++ 学习:The C++ Programming Language, 4th Edition
经典操作系统书籍,龙书:Operating System Concepts
创业:The Start-up of You: Adapt to the Future, Invest in Yourself, and Transform Your Career
Given two integers n and k, return all possible combinations of k numbers out of 1 ... n.
For example,
If n = 4 and k = 2, a solution is:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
» Solve this problem

这些题都和permutations一个思路,把permutations给弄明白了,这些枚举的方法就搞定了,其实这些题的本质就是搜索,或者search,又或者是pointers题,就是用一种正确而且全面的方式去搜索这个数据结构。很多算法的本质也是做同一件事情,比如树的遍历,比如图的bfs, dfs, 全都是一种如何去遍历or iterate through一个数据结构的方法。

写熟一点儿,发现pattern。

Leetcode 316. Remove Duplicate Letters

 这道题表面问的是如何删除重复,实际在问如何从多个字符选取一个保留,从而让整个字符串按升序排列。那么策略就是对于高顺位的字符比如‘a',就要选靠前位置的保留,而低顺位字符如’z'则应该尽量选取靠后位置保留。 算法大概思路:每看到一个字符,我们要决定是否保留 1. ...