灵神【基础算法精讲】视频的个人笔记。
- 单纯的循环嵌套,表达能力是局限的,有些问题直接循环嵌套不好做。
- 原问题和子问题相似,这种问题适合用递归。
递归不用思考怎么往下递,怎么往下归,只需要思考递归边界条件和非边界条件。
回溯三问
- 当前操作(每一步的操作)?
- 子问题?
- 下一个子问题?
子集型回溯:每个元素都可以选和不选。
视频例题
17.电话号码的字母组合
- 选择当前数字的字母映射
- 选完一个后回溯,删除前一个,继续选还没选过的字母映射
class Solution {
public:
string PHONE[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
vector<string> ans;
string path;
string digit;
int n;
void dfs(int i) {
if(i >= n) {
ans.emplace_back(path);
return;
}
for(char c: PHONE[digit[i] - '0']) {
path += c;
dfs(i + 1);
path.erase(path.end() - 1);
}
}
vector<string> letterCombinations(string digits) {
n = digits.length();
if(!n) return {};
digit = digits;
dfs(0);
return ans;
}
};
78.子集
选和不选
class Solution {
private:
vector<vector<int>> ans;
vector<int> path;
public:
void dfs(int i, vector<int>& nums) {
if(i >= nums.size()) {
ans.emplace_back(path);
return;
}
//选
path.emplace_back(nums[i]);
dfs(i + 1, nums);
path.pop_back(); //恢复
//不选
dfs(i + 1, nums);
}
vector<vector<int>> subsets(vector<int>& nums) {
dfs(0, nums);
return ans;
}
};
枚举选一个后面的数
class Solution {
private:
vector<vector<int>> ans;
vector<int> path;
public:
void dfs(int i, vector<int>& nums) {
ans.emplace_back(path);
for (int j = i; j < nums.size(); ++j) { // 选一个后面的数
path.emplace_back(nums[j]);
dfs(j + 1, nums);
path.pop_back(); //恢复
}
}
vector<vector<int>> subsets(vector<int>& nums) {
dfs(0, nums);
return ans;
}
};
131.分割回文串
选和不选
class Solution {
private:
vector<vector<string>> ans;
vector<string> path;
int n;
string str;
/**
* 判断是否回文串
* @param s
* @param left
* @param right
* @return
*/
bool isPalindrome(string &s, int left, int right) {
while(left < right)
if(s[left++] != s[right--])
return false;
return true;
}
void dfs(int i, int start) {
if(i >= n) {
ans.emplace_back(path);
return;
}
//分割
if(isPalindrome(str, start, i)) { //回文串才添加
path.emplace_back(str.substr(start, i - start + 1));
dfs(i + 1, i + 1);
path.pop_back(); //恢复
}
//不分割, start不变
if(i < n - 1)
dfs(i + 1, start);
}
public:
vector<vector<string>> partition(string s) {
n = s.length();
str = s;
dfs(0, 0);
return ans;
}
};
枚举选哪一个
- 上一题子集中,每一个path都是结果
- 这题结果是分割后的子串集合
[["a","a","b"],["aa","b"]] - 不能直接添加到ans,分割完了才添加到
class Solution {
private:
vector<vector<string>> ans;
vector<string> path;
int n;
string str;
/**
* 判断是否回文串
* @param s
* @param left
* @param right
* @return
*/
bool isPalindrome(string &s, int left, int right) {
while(left < right)
if(s[left++] != s[right--])
return false;
return true;
}
void dfs(int i, int start) {
if(i >= n) { //结果是分割后的子串集合, 最后才添加到ans
ans.emplace_back(path);
return;
}
for (int j = i; j < n; ++j) {
if(isPalindrome(str, start, j)) { //回文串才添加
path.emplace_back(str.substr(start, j - start + 1));
dfs(j + 1, j + 1);
path.pop_back(); //恢复
}
}
}
public:
vector<vector<string>> partition(string s) {
n = s.length();
str = s;
dfs(0, 0);
return ans;
}
};
课后作业
784.字母大小写全排列
思路
- 每个字母可以大写或者小写,相当于选和不选。不过字符串有数字和字母,要判断一下是否字母。
- 枚举每个字母,往后找一个字母,改成大写或小写。
枚举选哪一个数
class Solution {
private:
vector<string> ans;
string path;
void dfs(int i) {
ans.emplace_back(path);
for (int j = i; j < path.length(); ++j) {
if(path[j] >= 'a' && path[j] <= 'z') { //小写字母
path[j] -= 32;
dfs(j + 1);
path[j] += 32; //恢复
} else if(path[j] >= 'A' && path[j] <= 'Z') { //大写字母
path[j] += 32;
dfs(j + 1);
path[j] -= 32; //恢复
}
}
}
public:
vector<string> letterCasePermutation(string s) {
path = s;
dfs(0);
return ans;
}
};
1601.最多可达成的换楼请求数目
思路
- 枚举每个员工是否搬走,搬走为1,不搬为0。
- 最后判断是否能满足请求。
class Solution {
private:
vector<vector<int>> reqs;
vector<int> path;
int ans;
bool check() {
int n = path.size(), balance = 0, cnt[20] = { 0 };
for (int i = 0; i < n; ++i) {
if(path[i]) {
int from = reqs[i][0], to = reqs[i][1];
if(++cnt[from] == 1) balance++; //入度
if(--cnt[to] == 0) balance--; //出度
}
}
return balance == 0;
}
void dfs(int i) {
if(i >= reqs.size()) {
if(check())
ans = max(ans, accumulate(path.begin(), path.end(), 0));
return;
}
//选
path[i] = 1;
dfs(i + 1);
path[i] = 0;
//不选
dfs(i + 1);
}
public:
int maximumRequests(int n, vector<vector<int>>& requests) {
reqs = requests;
vector<int> p(reqs.size(), 0);
path = p;
dfs(0);
return ans;
}
};
官方题解中的二进制枚举力扣
- 用一个二进制数表示每个员工是否搬走。
- 数组
delta记录每一栋楼的员工变化量
class Solution {
public:
int maximumRequests(int n, vector<vector<int>> &requests) {
vector<int> delta(n);
int ans = 0, m = requests.size();
for (int mask = 0; mask < (1 << m); ++mask) {
int cnt = __builtin_popcount(mask);
if (cnt <= ans) {
continue;
}
fill(delta.begin(), delta.end(), 0);
for (int i = 0; i < m; ++i) {
if (mask & (1 << i)) {
++delta[requests[i][0]];
--delta[requests[i][1]];
}
}
if (all_of(delta.begin(), delta.end(), [](int x) { return x == 0; })) {
ans = cnt;
}
}
return ans;
}
};
2397.被列覆盖的最多行数
思路
- 每一列都是选和不选,选的列数到达限制后判断是否成立。
class Solution {
private:
int ans;
vector<vector<int>> mat;
vector<int> cols;
void dfs(int i, int numSelect) {
if(i >= mat[0].size() || numSelect <= 0) {
int cnt = 0; //覆盖最大行数
for (auto & j : mat) {
cnt++;
for (int k = 0; k < mat[0].size(); ++k) {
if(cols[k] == 1) continue;
if(j[k] == 1) {
cnt--; //还有1没被覆盖
break;
}
}
}
ans = max(ans, cnt);
return;
}
//选
cols[i] = 1;
dfs(i + 1, numSelect - 1);
cols[i] = 0; //恢复
//不选
dfs(i + 1, numSelect);
}
public:
int maximumRows(vector<vector<int>>& matrix, int numSelect) {
vector<int> c(matrix[0].size(), 0);
cols = c;
mat = matrix;
dfs(0, numSelect);
return ans;
}
};
题解 该题解使用二进制枚举,状态压缩
- 每一行只有0和1,可以看作一个二进制数。
- 二进制枚举,
&运算后如果没变,1都覆盖了。
class Solution {
public:
int maximumRows(vector<vector<int>>& matrix, int numSelect) {
int rows = matrix.size(); //行
int cols = matrix[0].size(); //列
int masks[rows];
memset(masks, 0, sizeof masks);
//每一行用二进制数表示
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
masks[i] += (matrix[i][j] << j);
}
}
int ans2 = 0;
int maxMask = 1 << cols; //最大列数
for (int mask = 0; mask < maxMask; ++mask) {
if(__builtin_popcount(mask) == numSelect) { //返回输入数据中,二进制中‘1’的个数。
int cnt = 0;
for (int i = 0; i < rows; ++i)
cnt += ((masks[i] & mask) == masks[i]); //&运算完,没变,1都覆盖了
ans2 = max(ans2, cnt);
}
}
return ans2;
}
};
306.累加数
力扣参考题解
class Solution {
public:
string num;
int n;
vector<vector<int>> list;
bool isAdditiveNumber(string s) {
num = s;
n = s.size();
return dfs(0);
}
bool dfs(int index) {
int size = list.size();
if (index >= n) return size >= 3; //必须 至少 包含 3 个数。
int m = num[index] == '0' ? index + 1 : n;
vector<int> cur;
for (int i = index; i < m; ++i) {
cur.insert(cur.begin(), num[i] - '0');
if(size < 2 || check(list[size - 2], list[size - 1], cur)) {
list.emplace_back(cur);
if(dfs(i + 1)) return true;
list.pop_back(); //恢复
}
}
return false;
}
bool check(vector<int>& a, vector<int>& b, vector<int>& c) {
vector<int> ans;
int t = 0;
for (int i = 0; i < a.size() || i < b.size(); ++i) {
if(i < a.size()) t += a[i];
if(i < b.size()) t += b[i];
ans.emplace_back(t % 10);
t /= 10;
}
if(t) ans.emplace_back(1); //处理最高位的进位
bool ok = (c.size() == ans.size()); //检查长度
for(int i = 0; i < c.size() && ok; i++){
if(c[i] != ans[i]) ok = false; //逐位检查
}
return ok;
}
};
2698.求一个整数的惩罚数
思路
- 预处理1到1000中符合要求的数,前缀和存放在
pre数组中。 - 用
dfs判断i是否符合要求。- 枚举所有子串。
class Solution {
public:
int pre[1010];
bool dfs(int i, int sum, string s, int num) {
if(i >= s.length()) return sum == num; //相等
int x = 0;
for (int j = i; j < s.length(); ++j) { //枚举子串
x = x * 10 + int(s[j] - '0'); //添加个位
if(dfs(j + 1, sum + x, s, num)) return true;
}
return false;
}
int punishmentNumber(int n) {
for (int i = 1; i <= 1000; ++i) { //预处理 1 <= n <= 1000
string s = to_string(i * i);
pre[i] = pre[i - 1] + (dfs(0, 0, s, i) ? i * i : 0);
}
return pre[n];
}
};