Skip to content

Latest commit

 

History

History
372 lines (301 loc) · 10.1 KB

基础08.md

File metadata and controls

372 lines (301 loc) · 10.1 KB

8. 暴力递归

MyRecursion.cpp

暴力递归就是尝试

1,把问题转化为规模缩小了的同类问题的子问题

2,有明确的不需要继续进行递归的条件(base case)

3,有当得到了子问题的结果之后的决策过程

4,不记录每一个子问题的解

一定要学会怎么去尝试,因为这是动态规划的基础,这一内容我们将在提升班讲述

8.1 汉诺塔问题

打印 n 层汉诺塔从最左边移动到最右边的全部过程

3层汉诺塔:

1 左 -> 右

2 左 -> 中

1 右 -> 中

3 左 -> 右

1 中 -> 左

2 中 -> 右

1 左 -> 右

[提示] 从局部出发

(1) 1 ~ i-1: start -> other

(2) i: start -> end

(3) 1 ~ i-1: other -> end

✅tested

void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
    int n = A.size();
    process(n, A, C, B);
}
void processHanota(int n, vector<int>& from, vector<int>& to, vector<int>& other){
    if(n == 1){
        to.push_back(from.back());
        from.pop_back();
        return;
    }
    processHanota(n - 1, from, other, to);
    to.push_back(from.back());
    from.pop_back();
    processHanota(n - 1, other, to, from);
}

8.2 字符串的全部子序列 (从左往右尝试)

打印一个字符串的全部子序列,包括空字符串

void process(string& s, int i, string& res, vector<string>& str){
    if(i == s.size()){
        str.push_back(res);
        return;
    }
    string resKeep = res;
    resKeep += s[i];
    process(s, i+1, resKeep);
    string resNoInclude = res;
    process(s, i+1, resNoInclude);
}
  • 节省空间做法
void process(string str, int i) {
    if (i == str.size()) {
        cout << str << endl;
        return;
    }
    process(str, i + 1);    //要当前字符
    char tmp = str[i];
    str[i] = 0;
    process(str, i + 1);    // 不要当前字符
    str[i] = tmp;
}

8.3 字符串的全部排列 (从左往右尝试)

打印一个字符串的全部排列,要求不要出现重复的排列

[提示] 分支限界

✅tested

void swap(string& s, int i, int j) {
    char tmp = s[i];
    s[i] = s[j];
    s[j] = tmp;
}
vector<string> permutation(string s) {
    vector<string> res;
    process(s, 0, res);
    return res;
}
// str[i...]范围上,所有的字符,都可以在i位置上,后续都去尝试
// str[0...i-1]范围上,是之前做的选择
// res: 所有的结果
void process(string s, int i, vector<string>& res) {
    if (i == s.size()) {
        res.push_back(s);
        return;
    }
    vector<bool> visit;
    for(int k = 0; k < 26; k++){
        visit.push_back(false);
    }
    for (int j = i; j < s.size(); j++) {
        if(!visit[s[j] - 'a']){ // 分支限界,指标上没有优化,常数项有优化
            visit[s[j] - 'a'] = true;
            swap(s, i, j);
            process(s, i + 1, res);
            swap(s, i, j);
        }  
    }
}

8.4 纸牌游戏 (范围上尝试)

给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。

【举例】 arr=[1,2,100,4]。

开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来玩家 B可以拿走2或4,然后继续轮到玩家A.

如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A...

玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家 A拿走。玩家A会获胜,分数为101。所以返回101。

arr=[1,100.2]。

开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜,分数为100。所以返回100。

  • 改为动态规划

参考:基础提升08

// 先手
int f(vector<int>& arr, int i, int j) {
    if (i == j) {
        return arr[i];
    }
    return max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
}
// 后手
int s(vector<int>& arr, int i, int j) {
    if (i == j) {
        return 0;
    }
    return min(f(arr, i + 1, j), f(arr, i, j - 1));
}
int win(vector<int>& arr) {
    if (arr.size() < 1) {
        return 0;
    }
    return max(f(arr, 0, arr.size() - 1), s(arr, 0, arr.size() - 1));
}

8.5 逆序栈

给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现?

void reverse(stack<int> s) {
    if (s.empty()) {
        return;
    }
    int i = f(s);
    reverse(s);
    s.push(i);
}
int f(stack<int> s) {
    int result = s.top();
    s.pop();
    if (s.empty()) {
        return result;
    }
    else {
        int last = f(s);
        s.push(result);
        return last;
    }
}

8.6 字符串转化 (从左往右尝试)

规定1和A对应、2和B对应,3和C对应..

那么一个数字字符串比如“111”,就可以转化为“AAA”、“KA”和“AK”.

给定一个只有数字字符组成的字符串str,返回有多少种转化结果。

✅tested

// i 之前的位置如何转化已经做过决定
// i... 有多少种转化的结果
int translateNum(int num) {
    string str = to_string(num);
    return process(str, 0);
}
int process(string str, int i){
    if(i == str.size() ){
        return 1;
    }
    if(str[i] == '1'){
        int res = process(str, i+1);    // i 自己作为单独的部分,后续有多少种方法
        if(i+1 < str.size()){
            res += process(str, i+2);   // i 和 i+1 作为单独的部分
        }
        return res;
    }
    if(str[i] == '2'){
        int res = process(str, i+1);
        // i 和 i+1 作为单独的部分且不超过25
        if(i+1 < str.size() && (str[i+1] >= '0' && str[i+1] < '6')){
            res += process(str, i+2);
        }
        return res;
    }
    //str[i] == '0' || '3' ~ '9'
    return process(str, i+1);
}

8.7 装物品 (从左往右尝试)

给定两个长度都为N的数组 weights 和 values,weights[i] 和 values[i] 分别代表i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?

  • 尝试要求可变参数形式简单,数量少

  • 背包问题

// i... 的货物自由选择,形成的最大价值返回
// alreadyweight: 之前的决定所达到的重量
int process(vector<int>& weights, vector<int>& values, int i, int alreadyweight, int bag) {
    if (alreadyweight > bag) {
        return 0;
    }
    if (i == weights.size()) {
        return 0;
    }
    return max(process(weights, values, i + 1, alreadyweight, bag),
        values[i] + process3(weights, values, i + 1, alreadyweight + weights[i], bag));
}

8.8 N皇后问题

N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。

给定一个整数n,返回n皇后的摆法有多少种。

n=1,返回1。

n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。

n=8,返回92。

[时间复杂度]: O(n!)

✅tested

int totalNQueens(int n) {
    vector<int> record; // record[i] -> 第i行的皇后,放在了第几列
    for(int i = 0; i < n; i++){
        record.push_back(-1);
    }
    return process(record, 0, n);
}
// record[0,...,i-1] 的皇后,任何两个皇后一定不共行、不共列、不共斜线
// 目前来到了第 i 行
// record[0,...,i-1] 表示之前的行,放过了哪些皇后
// n 代表整体一共有多少行
// res 表示摆完所有的皇后,合理的摆法有多少种
int process(vector<int>& record, int i, int n){
    if(i == n){ // 终止行:最后一行再下一行的位置
        return 1;
    }
    int res = 0;
    for(int j = 0; j < n; j++){
        if(isVaild(record, i, j)){
            record[i] = j;
            res += process(record, i+1, n);
        }
    }
    return res;
}
// 当前i行的皇后,放在j列,会不会和之前(0...i-1)的皇后共行、共列或共斜线
bool isVaild(vector<int>& record, int i, int j){
    for(int k = 0; k < i; k++){
        if(j == record[k] || abs(record[k] - j) == abs(i - k)){
            return false;
        }
    }
    return true;
}
  • 常数时间的优化,使用位运算
// colLim 列的限制,1的位置不能放皇后,0的位置可以
// leftDiaLim 左斜线的限制
// rightDiaLim 右斜线的限制
int process(int limit, int colLim, int leftDiaLim, int rightDiaLim) {
    if (colLim == limit) {  // base case
        return 1;
    }
    int pos = 0;
    int mostRightOne = 0;
    // 所有候选皇后的位置都在pos上
    pos = limit & (~(colLim | rightDiaLim | leftDiaLim));
    int res = 0;
    while (pos != 0) {
        mostRightOne = pos & (~pos + 1);
        pos = pos - mostRightOne;
        res += process(limit,
                        colLim | mostRightOne, 
                        (leftDiaLim | mostRightOne) << 1, 
                        (rightDiaLim | mostRightOne) >> 1); // 此处要使用无符号右移
    }
    return res;
}
// 请不要超过32皇后问题
int totalNQueens(int n) {
    if (n < 1 || n > 32) {
        return 0;
    }
    // 生成二进制的数,8皇后问题,后8为是1
    int limit = n == 32 ? -1 : (1 << n) - 1;
    return process2(limit, 0, 0, 0);
}