14 子集型回溯

260 阅读4分钟

灵神【基础算法精讲】视频的个人笔记。

  • 单纯的循环嵌套,表达能力是局限的,有些问题直接循环嵌套不好做。
  • 原问题子问题相似,这种问题适合用递归。

递归不用思考怎么往下递,怎么往下归,只需要思考递归边界条件非边界条件

回溯三问

  1. 当前操作(每一步的操作)?
  2. 子问题?
  3. 下一个子问题?

子集型回溯:每个元素都可以选和不选。

视频例题

17.电话号码的字母组合

image.png

  • 选择当前数字的字母映射
  • 选完一个后回溯,删除前一个,继续选还没选过的字母映射
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.子集

image.png

选和不选

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.分割回文串

image.png

选和不选

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.字母大小写全排列

image.png

思路

  1. 每个字母可以大写或者小写,相当于选和不选。不过字符串有数字和字母,要判断一下是否字母。
  2. 枚举每个字母,往后找一个字母,改成大写或小写。

枚举选哪一个数

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.最多可达成的换楼请求数目

image.png image.png image.png

思路

  • 枚举每个员工是否搬走,搬走为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.被列覆盖的最多行数

image.png

思路

  1. 每一列都是选和不选,选的列数到达限制后判断是否成立。
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.累加数

image.png 力扣参考题解

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.求一个整数的惩罚数

image.png

思路

  • 预处理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];
    }
};