算法学习(16):暴力递归

暴力递归

什么是暴力递归

暴力递归就是尝试

  1. 把问题转化为规模缩小了的同类问题的子问题
  2. 有明确的不需要继续进行递归的条件(base case)
  3. 有当得到了子问题的结果之后的决策过程.
  4. 不记录每一个子问题的解

暴力递归尝试的时候不要去想全局到底是怎么保证的,只要保证局部的决策是对的,整体一定是对的
经典的试法:从左往右试

汉诺塔问题

假设一共有i层,从上到下依次是1~i,需要从左移到右。如果转换成一般情况,假设想让某层塔从from到to,另外一根柱子叫other移动步骤如下:

  1. 将1~i-1的塔从from移动到other
  2. 将最后一个i从from移动到to
  3. 将1~i-1的塔从other移动到to

base case是i=1移动最上面的塔的时候,直接从from移动到to
C++代码实现:

void process(int i, string from, string to, string other);

void hanoi(int i)
{
    if (i > 0)
    {
        process(i, "左", "右", "中");
    }

}

void process(int i, string from, string to, string other)
{
    if (i == 1)
    {   //base case 当只剩下一个圆盘的时候,它可以直接从from移动到to,所以直接打印
        cout << i << "从" << from << "移动到" << to << endl;
        return;
    }
    process(i - 1, from, other, to);      //1. 将1~i-1的塔从from移动到other
    cout << i << "从" << from << "移动到" << to << endl;    //2. 将最后一个i从from移动到to
    process(i - 1, other, to, from);      //3. 将1~i-1的塔从other移动到to
}

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

假设这个字符串有n个字符,每个字符都可以选择打印或者不打印,是两个分支,则n个字符就是2n种可能
从左到右试,i从0开始,在i时,先递归一次选择打印的分支,然后再递归一次选择不打印的分支,base case就是i等于字符串的长度时打印全部长度

void process(int i, string str);

void printAllSubsequence(string str)
{
    process(0, str);
}

void process(int i, string str)
{
    if (i == str.size())
    {
        cout << str << endl;
        return;
    }
    process(i + 1, str);
    int temp = str[i];   //利用系统栈会储存临时变量,把第i个字符换成0,就是不打印,递归完后再换回去,这种做法是在传入的字符串本身做改动,省空间
    str[i] = 0;
    process(i + 1, str);
    str[i] = temp;
}

打印一个字符串的全部排列,并且去除重复

从左往右试,从i开始,i~size()-1位置上所有的字符都可以到i位置上,for循环从i位置开始往后交换位置,交换完再递归到i+1位置继续后面的交换

void process(int i, string &str, vector<string> &res);

void swapChar(string &str, int i, int j);

vector<string> permutation(string str) 
{
    vector<string> res;
    if (str.size() == 0)
    {
        return res;
    }
    process(0, str, res);
    return res;
}

void process(int i, string &str, vector<string> &res)
{
    if (i == str.size())
    {
        res.push_back(str);
    }
    bool visit[26] = {0};
    for (int j = i; j < str.size(); j++)   //从i开始,i往后所有的字符都交换到i位置
    {
        if (!visit[str[j] - 'a'])
        {
            visit[str[j] - 'a'] = true;
            swapChar(str, i, j);            //交换到i位置
            process(i + 1, str, res);       //然后再从i+1位置开始再选择,交换完到base case储存后需要复原
            swapChar(str, i, j);           //复原
        }
    }
}

void swapChar(string &str, int i, int j)
{
    char temp = str[i];
    str[i] = str[j];
    str[j] = temp;
}

纸牌问题

给定一个整型数组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。


分先手和后手,先手先拿,要么拿左边,要么拿右边,然后是后手,也是要么拿左边,要么拿右边,先手的base case是剩最后一个,那就是是先手拿走这个分数,后手的base case是剩最后一个,因为是后手,所以会被先手拿走,所以就返回0分。

int first(vector<int> arr, int left, int right);

int second(vector<int> arr, int left, int right);

int maxScore(vector<int> arr)
{
    if (arr.size() == 0)
    {
        return 0;
    }
    return max(first(arr, 0, arr.size() - 1), second(arr, 0, arr.size() - 1));  //返回先手A选手和后手B选手分数较多的那个
}

int first(vector<int> arr, int left, int right)
{
    if (left == right)
    {
        return arr[left];
    }
    return max(arr[left] + second(arr, left + 1, right), arr[right] + second(arr, left, right - 1));  //先手的分数是选择左边或右边的牌的分数加上右边或左边后手的分数的最大值
}                                                                                                     //因为是绝顶聪明,所以会选择对自己最有利的,所以取最大值
int second(vector<int> arr, int left, int right)
{
    if (left == right)
    {
        return 0;
    }
    return min(first(arr, left + 1, right), first(arr, left, right - 1));              //A先手选完该后手的B先手选了,所以返回的是first()函数
}                                                                                     //因为是先手先选,所以一定会留给后手最差的结果,所以取最小值

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

很好地利用了系统栈暂时储存变量的机制

int popEnd(stack<int>& stack);

void reverse(stack<int>& stack)
{
    if (stack.empty())
    {
        return;
    }
    int i = popEnd(stack);   //弹出栈底,用i接住,然后调用递归,每次都弹出栈底,用一个变量接住,然后回溯压入栈,就实现逆序
    reverse(stack);
    stack.push(i);
}

int popEnd(stack<int> &stack)
{
    int result = stack.top();  //先弹出栈顶,然后用一个变量接住,暂时储存
    stack.pop();
    if (stack.empty())
    {
        return result;
    }
    int last = popEnd(stack);   //调用递归,当栈空的时候会返回原来在栈底的元素,一路返回到第一层的last里,
    stack.push(result);         //把栈底元素之外弹出的元素压栈,
    return last;               //把栈底元素返回
}

数字转换

规定1和A对应、2和B对应、3和C对应...那么一个数字字符串比如"111",就可以转化为"AAA"、"KA"和" AK"。
给定一个只有数字字符组成的字符串str,返回有多少种转化结果。


依旧是从左到右试,分情况讨论,从第i位开始:

  • 第i位是0,则整个过程返回0,因为没有与0开头的数对应的字母
  • 第i位是3~9,返回这一位数对应的字母,因为如果和后面一位数组合,则会超过26
  • 第i位是1,两种可选择的结果,一种是单独返回A,另一种是与后面一位数字组合
  • 第i位是2,如果后面一位数字小于等于6,有两种可选择的结果,一种是单独返回B,另一种是与后面的数字结合;如果后面一位数字大于6,则,第i位单独返回B

base case是当i来到了size()的位置时,返回1,这是返回之前所作的一系列决定所形成的一种有效情况

int process(int i, string str);

int number(string str)
{
    if (str.empty())
    {
        return 0;
    }
    return process(0, str);
}

int process(int i, string str)
{
    if (i == str.size())
    {
        return 1;
    }
    if (str[i] == '0')
    {
        return 0;
    }
    if (str[i] == '1')
    {
        int res = process(i + 1, str);
        if (i + 1 < str.size())
        {
            res += process(i + 2, str);
        }
        return res;
    }
    if (str[i] == '2')
    {
        int res = process(i + 1, str);
        if (i + 1 < str.size() && str[i + 1] <= '6')
        {
            res += process(i + 2, str);
        }
        return res;
    }
    return process(i + 1, str);
}

01背包

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


依旧是从左往右试,当前这个物品拿或者不拿,如果当前物品的重量加上已经选择的重量超重,就不拿当前的物品并返回,最后返回价值最多的

int process(vector<int> weights, vector<int> values, int i, int alreadyWeight, int bag);

int maxValue(vector<int> weights, vector<int> values, int bag)
{
    return process(weights, values, 0, 0, bag);
}

int process(vector<int> weights, vector<int> values, int i, int alreadyWeight, int bag)
{
    if (i != weights.size() && alreadyWeight + values[i] > bag)
    {
        return 0;
    }
    if (i == weights.size())
    {
        return 0;
    }
    return max(process(weights, values, i + 1, alreadyWeight, bag), values[i] + process(weights, values, i + 1, alreadyWeight + weights[i], bag));
}

还有另外一种写法,这种写法多了一个参数alreadyValue,用来储存已经选择的价值,这种写法如果前面的选择超重了,则这种选法就是错误的,整体返回0

int process(vector<int> weights, vector<int> values, int i, int alreadyWeight, int alreadyValue, int bag);

int a(vector<int> weights, vector<int> values, int bag)
{
    return process(weights, values, 0, 0, 0, bag);
}

int process(vector<int> weights, vector<int> values, int i, int alreadyWeight, int alreadyValue, int bag)
{
    if (alreadyWeight > bag)
    {
        return 0;
    }
    if (i == weights.size())
    {
        return alreadyValue;
    }
    return max(process(weights, values, i + 1, alreadyWeight, alreadyValue, bag), process(weights, values, i + 1, alreadyWeight + weights[i], alreadyValue + values[i], bag));
}

在试的过程中有一个原则,请找到可变参数形式最简单、数量最少的写法。可变参数形式最简单就是说尽量是int、string这种简单的参数而不是链表等复杂的参数,可变参数个数最少最好。对比第一种写法和第二种写法,两种写法都是int类型的参数,第一种可变参数少一个,所以第一种更好

N皇后问题,输入N,返回一共有多少种摆法

从矩阵的第一行开始往下试,当前行的每一列都判断一遍是否跟前面的皇后共列或共斜线,如果不是就记录下来这个位置,然后调用一次递归试下一行。base case是超过最后一行就返回1,这个1代表之前所作的一系列选择都是正确的,记录下来这一次结果。

优化前C++代码:

bool isValid(int record[], int i, int j);

int process(int *record, int i, int n);

int nQueen(int n)
{
    if (n < 1)
    {
        return 0;
    }
     //利用record数组储存之前选择摆放皇后的点,下标代表哪一行,下标对应的值代表哪一列
    int record[30];   //vector容器会越界,所以直接定义一个int类型的数组,而25皇后问题的解的数量已经到达千万亿级别,所以这里不妨把record数组的长度设置为30
    return process(record, 0, n);         
}

int process(int *record, int i, int n)
{
    if (i == n)
    {
        return 1;
    }
    int res = 0;
    for (int j = 0; j < n; j++)
    {
        if (isValid(record, i, j))  //判断第i行的第j位放置皇后会不会与前面的选择冲突
        {
            record[i] = j; 
            res += process(record, i + 1, n);   //调用递归试下一行
        }
    }
    return res;
}

bool isValid(int record[], int i, int j)   
{
    for (int k = 0; k < i; k++)
    {
        if (record[k] == j || abs(record[k] - j) == i - k)
        {
            return false;
        }
    }
    return true;
}

位运算优化

N皇后问题的思路已经确定,但是上面的代码可以通过位运算来优化,优化后的代码运行速度更快
大致思路是用二进制的位信息表示放皇后的限制,优化后代码的思路具体见https://www.bilibili.com/video/BV13g41157hK?p=11&vd_source=77d06bb648c4cce91c6939baa0595bcd P11 02:15:15
优化后C++代码:

int process(int limit, int colLim, int leftDiaLim, int rightDiaLim);

int nQueen(int n)
{ 
    if (n < 1 || n>32)   //请不要超过32皇后问题
    {
        return 0;
    }
    int limit = n == 32 ? -1 : (1 << n) - 1;    //是几皇后问题,二进制从右往左数就有几个1,其他高位全是0
    return process(limit, 0, 0, 0);
}

int process(int limit, int colLim, int leftDiaLim, int rightDiaLim)
{
    if (colLim == limit)  //当列限制与limit相等时代表所有的列都已经被放过皇后了,所以返回1
    {
        return 1;
    }
    int pos = limit & (~(colLim | leftDiaLim | rightDiaLim));   //可以放皇后的位置是1,其他都是0
    int mostRightOne;
    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;
}
posted @ 2022-07-29 11:06  小肉包i  阅读(140)  评论(0)    收藏  举报