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

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

要求返回结果集的题目


主要思路

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

括号生成(lc22)

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

    void helper(int left, int right, string cur, vector<string>& 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<string> restoreIpAddresses(string s) {
    vector<string> 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<string>& 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<string> letterCombinations(string digits) {
  vector<string> result;
  int n = digits.size();
  if (!n) return result;
  vector<string> dict{"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
  helper(digits, result, 0, "", dict);
  return result;
}

void helper(string& digits, vector<string>& result, int pos, string cur, vector<string>& 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<vector<int>>& result, vector<int>& 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<vector<int>> pathSum(TreeNode* root, int sum) {
     vector<vector<int>> result;
     vector<int> cur;
     findPath(root, sum, result, cur);
     return result;
  }

组合(lc77)

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

    void helper(vector<vector<int>>& result, int n, int k, int start, vector<int> 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<vector<int>> combinationSum(vector<int>& candidates, int target) {
    vector<vector<int>> result;
    vector<int> cur;
    helper(candidates, result, target, cur, 0);
    return result;
  }

  void helper(vector<int>& candidates, vector<vector<int>>& result, int target, vector<int>& 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<vector<int>> combinationSum(vector<int>& candidates, int target) {
    vector<vector<int>> 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<int> vec = vector<int>(candidates.begin() + i, candidates.end());
        vector<vector<int>> 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<vector<int>> combinationSum2(vector<int>& num, int target) {
        vector<vector<int>> res;
        vector<int> cur;
        sort(num.begin(), num.end()); // 避免重复
        helper(num, target, 0, cur, res);
        return res;
    }
    void helper(vector<int>& num, int target, int pos, vector<int>& cur, vector<vector<int>>& 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<string>& 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<string>& 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<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<int> memo(s.size(), -1);
        return check(s, wordSet, 0, memo);
    }
    bool check(string s, unordered_set<string>& wordSet, int start, vector<int>& 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<string>& wordDict) {
  if (!s.size()) return true;
  int n = wordDict.size(), m = s.size();
  if (!n) return false;
  vector<bool> 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<vector<string>> solveNQueens(int n) {
        vector<vector<string>> res;
        vector<string> queens(n, string(n, '.'));
        helper(0, queens, res);
        return res;
    }
    void helper(int curRow, vector<string>& queens, vector<vector<string>>& 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<string>& 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<vector<int>> permute(vector<int>& num) {
    vector<vector<int>> res{{}};
    for (int a : num) { // 每次取出一个数a
        for (int k = res.size(); k > 0; --k) { // 把a插入到当前res中的每个result中
            vector<int> t = res.front();
            res.erase(res.begin());
            for (int i = 0; i <= t.size(); ++i) {
                vector<int> one = t;
                one.insert(one.begin() + i, a);
                res.push_back(one);
            }
        }
    }
    return res;
}
  • 法二:遍历每个元素,与第一个元素交换,求从第二个元素开始的全排列;记得要换回来!
  vector<vector<int>> permute(vector<int>& nums) {
    vector<vector<int>> 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<int> recurVec;
      recurVec.assign(nums.begin() + 1, nums.end());
      vector<vector<int>> 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<vector<int>> subsets(vector<int>& nums) {
    vector<vector<int>> 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<int> 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<int>& 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<int> 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<int>& 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<int> 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<int> lexicalOrder(int n) {
        vector<int> res;
        for (int i = 1; i <= 9; ++i) {
            helper(i, n, res);
        }
        return res;
    }
    void helper(int cur, int n, vector<int>& 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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342