抱佛脚-刷题系列之排列组合

抱佛脚一时爽,一直抱佛脚一直爽!这篇文章总结常见的排列组合问题~
参考链接:leetcode 剑指offer

要求返回结果集的题目


主要思路

  • 把当前结果cur引用传递给递归函数,在它后面添加字符/元素,符合条件再push进result
  • 若cur是string,则在调递归时用右值(比如cur + '.'),不改变原cur的值
  • 若cur是vector,则先push入cur,调用完递归之后在cur.pop_back

括号生成(lc22)

class Solution {
public:
    vector generateParenthesis(int n) {
        vector result;
        if (!n) return result;
        helper(n, n, "", result);
        return result;
    }

    void helper(int left, int right, string cur, vector& result) { 
        //left表示还需要多少个(,right表示还需要多少个)
        if (left > right) return;
        if (left == 0 && right == 0) result.push_back(cur);
        if (left > 0) helper(left - 1, right, cur + "(", result); // cur是string,所以传cur + "("这个右值
        if (right > 0) helper(left, right - 1, cur + ")", result);
    }
};

复原ip地址(lc93)

class Solution {
public:
  vector restoreIpAddresses(string s) {
    vector res;
    if (s.size() < 4 || s.size() > 12) return res;
    restoreIpAddresses(s, 4, "", res);
    return res;
  }

  void restoreIpAddresses(string s, int k, string cur, vector& res) {
    if (k == 1 && isValid(s)) {
      res.push_back(cur.substr(1) + "." + s);
      return;
    }
    for (int i = 1; i <= 3; ++i) { // 把前i位分出来
      if (i <= s.size() && isValid(s.substr(0, i))) {
        restoreIpAddresses(s.substr(i), k - 1, cur + "." + s.substr(0, i), res); // cur是string,所以传cur + "."+...这个右值
      }
    }
  }

  bool isValid(string s) {
    if (s.empty() || s.size() > 3 || (s.size() > 1 && s[0] == '0')) return false;
    int res = atoi(s.c_str());
    return res <= 255 && res >= 0;
  }
};

电话号码字母的组合(lc17)

  • 思路:每次把一个字母attach到cur后面,符合条件就把cur push到result中
vector letterCombinations(string digits) {
  vector result;
  int n = digits.size();
  if (!n) return result;
  vector dict{"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
  helper(digits, result, 0, "", dict);
  return result;
}

void helper(string& digits, vector& result, int pos, string cur, vector& dict) {
  if (pos >= digits.size()) { // pos表示到digits的第几位了
    result.push_back(cur);
    return;
  }
  int num = digits[pos] - '0';
  for (int i = 0; i < dict[num].size(); ++i) { // 每次取出一个attach到cur后面
    helper(digits, result, pos + 1, cur + dict[num][i], dict); // cur是string,所以传cur + dict[num][i]这个右值
  }
}

路径总和(lc113)

  void findPath(TreeNode* root, int sum, vector>& result, vector& cur) {
    if (!root) return;
    cur.push_back(root->val);
    if (!root->left && !root->right) {
      if (root->val == sum) result.push_back(cur);
    }
    if (root->left) findPath(root->left, sum - root->val, result, cur);
    if (root->right) findPath(root->right, sum - root->val, result, cur);
    cur.pop_back(); // cur是vector,所以先push入cur,传cur,再cur.pop
  }

  vector> pathSum(TreeNode* root, int sum) {
     vector> result;
     vector cur;
     findPath(root, sum, result, cur);
     return result;
  }

组合(lc77)

class Solution {
public:
    vector> combine(int n, int k) {
        vector> result;
        if (k > n) return result;
        vector cur;
        helper(result, n, k, 1, cur);
        return result;
    }

    void helper(vector>& result, int n, int k, int start, vector cur) {
        if (cur.size() == k) {
            result.push_back(cur);
            return;
        }
        for (int i = start; i <= n; ++i) {
            cur.push_back(i);
            helper(result, n, k, i + 1, cur);
            cur.pop_back();
        }
    }
};

组合总和(元素可取任意次;完全背包)(lc39)

  • 法一:原始递归
  vector> combinationSum(vector& candidates, int target) {
    vector> result;
    vector cur;
    helper(candidates, result, target, cur, 0);
    return result;
  }

  void helper(vector& candidates, vector>& result, int target, vector& cur, int pos) {
    if (target < 0) return;
    if (target == 0) {
      result.push_back(cur);
      return;
    }
    for (int i = pos; i < candidates.size(); ++i) {
      cur.push_back(candidates[i]);
      helper(candidates, result, target - candidates[i], cur, i);
      cur.pop_back(); // cur是vector,所以先push入cur,传cur,再cur.pop
    }
  }
  • 法二:先排序;遍历每个元素,与第一个元素交换,求从第二个元素开始的结果
vector> combinationSum(vector& candidates, int target) {
    vector> res;
    sort(candidates.begin(), candidates.end());
    for (int i = 0; i < candidates.size(); ++i) {
        if (candidates[i] > target) break;
        if (candidates[i] == target) {res.push_back({candidates[i]}); break;}
        vector vec = vector(candidates.begin() + i, candidates.end());
        vector> tmp = combinationSum(vec, target - candidates[i]);
        for (auto a : tmp) {
            a.insert(a.begin(), candidates[i]);
            res.push_back(a);
        }
    }
    return res;
}

组合总和Ⅱ(元素只能取1次)(lc40)

class Solution {
public:
    vector> combinationSum2(vector& num, int target) {
        vector> res;
        vector cur;
        sort(num.begin(), num.end()); // 避免重复
        helper(num, target, 0, cur, res);
        return res;
    }
    void helper(vector& num, int target, int pos, vector& cur, vector>& res) {
        if (target < 0) return;
        if (target == 0) { res.push_back(cur); return; }
        for (int i = pos; i < num.size(); ++i) {
            if (i > pos && num[i] == num[i - 1]) continue; // 避免重复
            cur.push_back(num[i]);
            helper(num, target - num[i], i + 1, cur, res); // 传i + 1
            cur.pop_back();
        }
    }
};

单词拆分(单词可以取任意次;完全背包)(lc139)

  • 法一:原始递归
class Solution {
public:
    bool wordBreak(string s, vector& wordDict, int pos) {
      if (pos == s.size()) return true;
      bool res = false;
      for (string word : wordDict) {
        int len = word.size();
        if (s.size() >= pos + len && s.substr(pos, len) == word) res = res || wordBreak(s, wordDict, pos + len);
      }
      return res;
    }

    bool wordBreak(string s, vector& wordDict) {
      if (!s.size()) return true;
      if (!wordDict.size()) return false;
      return wordBreak(s, wordDict, 0);
    }
};
  • 法二:递归+记忆数组
    【思路:memo[i] 为 [i, n] 的子字符串是否可以拆分;-1表示没有计算过;如果可以拆分,则赋值为1,反之为0】
class Solution {
public:
    bool wordBreak(string s, vector& wordDict) {
        unordered_set wordSet(wordDict.begin(), wordDict.end());
        vector memo(s.size(), -1);
        return check(s, wordSet, 0, memo);
    }
    bool check(string s, unordered_set& wordSet, int start, vector& memo) {
        if (start >= s.size()) return true;
        if (memo[start] != -1) return memo[start];
        for (int i = start + 1; i <= s.size(); ++i) {
            if (wordSet.count(s.substr(start, i - start)) && check(s, wordSet, i, memo)) {
                return memo[start] = 1;
            }
        }
        return memo[start] = 0;
    }
};
  • 法三:dp;dp[i]:开头到第i位是否可拆分
bool wordBreak(string s, vector& wordDict) {
  if (!s.size()) return true;
  int n = wordDict.size(), m = s.size();
  if (!n) return false;
  vector dp(m + 1, false);
  dp[0] = true;
  for (int i = 1; i <= m; ++i) {
    for (string word : wordDict) {
      int len = word.size();
      if (len <= i && dp[i - len] && word == s.substr(i - len, len)) {
        dp[i] = true;
        break;
      }
    }
  }
  return dp[m];
}

N皇后(lc51)


class Solution {
public:
    vector> solveNQueens(int n) {
        vector> res;
        vector queens(n, string(n, '.'));
        helper(0, queens, res);
        return res;
    }
    void helper(int curRow, vector& queens, vector>& res) {
        int n = queens.size();
        if (curRow == n) {
            res.push_back(queens);
            return;
        }
        for (int i = 0; i < n; ++i) {
            if (isValid(queens, curRow, i)) {
                queens[curRow][i] = 'Q';
                helper(curRow + 1, queens, res);
                queens[curRow][i] = '.';
            }
        }
    }
    bool isValid(vector& queens, int row, int col) {
        for (int i = 0; i < row; ++i) {
            if (queens[i][col] == 'Q') return false;
        }
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {
            if (queens[i][j] == 'Q') return false;
        }
        for (int i = row - 1, j = col + 1; i >= 0 && j < queens.size(); --i, ++j) {
            if (queens[i][j] == 'Q') return false;
        }
        return true;
    }
};

全排列的题目


主要思路

每次取出一个数a,把a插入到当前res中的每个result中

全排列(lc46)

  • 法一:
vector> permute(vector& num) {
    vector> res{{}};
    for (int a : num) { // 每次取出一个数a
        for (int k = res.size(); k > 0; --k) { // 把a插入到当前res中的每个result中
            vector t = res.front();
            res.erase(res.begin());
            for (int i = 0; i <= t.size(); ++i) {
                vector one = t;
                one.insert(one.begin() + i, a);
                res.push_back(one);
            }
        }
    }
    return res;
}
  • 法二:遍历每个元素,与第一个元素交换,求从第二个元素开始的全排列;记得要换回来!
  vector> permute(vector& nums) {
    vector> result;
    if (!nums.size()) return result;
    if (nums.size() == 1) {
      result.push_back({nums[0]});
      return result;
    }
    for (int i = 0; i < nums.size(); ++i) {
      if (nums[i] != nums[0]) swap(nums[i], nums[0]);
      vector recurVec;
      recurVec.assign(nums.begin() + 1, nums.end());
      vector> recurRes = permute(recurVec);
      for (auto v : recurRes) {
        v.insert(v.begin(), nums[0]);
        result.push_back(v);
      }
      if (nums[i] != nums[0]) swap(nums[i], nums[0]);
    }
    return result;
  }

求所有子集(lc78)

vector> subsets(vector& nums) {
    vector> res(1);
    int n = nums.size();
    for (int i = 0; i < n; ++i) { // 依次取出nums中的数a
        int size = res.size();
        for (int j = 0; j < size; ++j) { // 在当前所有子集中都插入a
            res.push_back(res[j]);
            res.back().push_back(nums[i]);
        }
    }     
    return res;
}

其他题目


把数组排成最小的数(jz32)

  • 思路:遍历、交换
string PrintMinNumber(vector numbers) {
  int n = numbers.size();
  if (!n) return "";
  for (int i = 0; i < n - 1; ++i) {
    for (int j = i + 1; j < n; ++j) {
      string str1 = to_string(numbers[i]) + to_string(numbers[j]);
      string str2 = to_string(numbers[j]) + to_string(numbers[i]);
      if (str1 > str2) swap(numbers[i], numbers[j]);
    }
  }
  string res = "";
      bool flag = false; // 处理"00"的情况
  for (int i = 0; i < n; ++i) {
    if (!flag && numbers[i] == 0) continue;
    res += to_string(numbers[i]);
    flag = true;
  }
  return flag ? res : "0";
}
  • 更简洁的写法
string largestNumber(vector& nums) {
    string res;
    sort(nums.begin(), nums.end(), [](int a, int b) {
       return to_string(a) + to_string(b) > to_string(b) + to_string(a); 
    });
    for (int i = 0; i < nums.size(); ++i) {
        res += to_string(nums[i]);
    }
    return res[0] == '0' ? "0" : res;
}

下一个排列(lc31)

  • 思路:从右往左扫描,找到第一个满足num[i] > num[i - 1]的位置,把num[i - 1]和i及之后的数字中最后一个大于num[i-1]的数字交换,再sort num[i]~最后;若找不到满足num[i] > num[i - 1]的位置,reverse整个数组

1-n可以构成多少棵二叉搜索树(lc96)

  • 思路:f(n) = f(n - 1) * f(0) // 以n为根,左子树大小为n - 1,右子树大小为0;设f(0) = 1
    + f(n - 2) * f(1) // 以n-1为根,左子树大小为n - 2,右子树大小为1
    + ...
    + f(0) * f(n - 1) // 以1为根,左子树大小为0,右子树大小为n - 1;设f(0) = 1
int numTrees(int n) {
  if (!n) return 0;
  vector dp(n + 1);
  dp[0] = 1;
  dp[1] = 1;
  for (int i = 2; i <= n; ++i) {
    int cur = 0;
    for (int j = 0; j < i; ++j) {
      cur += dp[i - j - 1] * dp[j];
    }
    dp[i] = cur;
  }
  return dp[n];
}

最大交换(lc670)

  • 法一:递归,从左到右遍历pos;每次求pos及其右边的最大值,若最大值不是pos处的数则交换它俩,否则pos++
class Solution {
public:
    void helper(vector& nums, int pos) {
        if (pos == 0) return;
        int max_num = nums[pos], index = pos;
        for (int i = 0; i < pos; ++i) {
            if (nums[i] > max_num) {
                max_num = nums[i];
                index = i;
            }
        }
        if (index != pos) swap(nums[index], nums[pos]);
        else helper(nums, pos - 1);
    }

    int maximumSwap(int num) {
        vector numArray;
        while (num) {
            numArray.push_back(num % 10);
            num /= 10;
        }
        helper(numArray, numArray.size() - 1);
        int res = 0;
        reverse(numArray.begin(), numArray.end());
        for (int i : numArray) {
            res = res * 10 + i;
        }
        return res;
    }
};
  • 法二:从右到左找到每个数字右边的最大数字(包括其自身);再从左到右遍历,如果某一位上的数字小于其右边的最大数字,说明需要调换;由于最大数字可能不止出现一次,这里希望能跟较低位的数字置换,这样置换后的数字最大,所以就从低位向高位遍历来找那个最大的数字,找到后进行调换即可
int maximumSwap(int num) {
    string res = to_string(num);
    string back = res;
    for (int i = back.size() - 2; i >= 0; --i) {
        back[i] = max(back[i + 1], back[i]);
    }
    for (int i = 0; i < res.size(); ++i) {
        for (int j = res.size() - 1; j > i; --j) {
            if (res[j] == back[i] && res[i] != res[j]) { // 若res[i]=res[j],则无需swap
                swap(res[i], res[j]);
                return stoi(res);
            }
        }
    }
    return stoi(res);
}

水壶问题(lc365)

  • 思路:z = m * x + n * y,根据裴蜀定理,只需z是x、y最大公约数的倍数即可
class Solution {
public:
    bool canMeasureWater(int x, int y, int z) {
        return z == 0 || (x + y >= z && z % gcd(x, y) == 0);
    }
    int gcd(int x, int y) {
        return y == 0 ? x : gcd(y, x % y);
    }
};

字典序排数(lc386)

class Solution {
public:
    vector lexicalOrder(int n) {
        vector res;
        for (int i = 1; i <= 9; ++i) {
            helper(i, n, res);
        }
        return res;
    }
    void helper(int cur, int n, vector& res) {
        if (cur > n) return;
        res.push_back(cur);
        for (int i = 0; i <= 9; ++i) {
            if (cur * 10 + i <= n) {
                helper(cur * 10 + i, n, res);
            } else break;
        }
    }
};

1-n中按字典序第k大的数字(lc440)

你可能感兴趣的:(抱佛脚-刷题系列之排列组合)