由暴力递归到动态规划

本文深入讲解动态规划原理,从暴力递归到记忆化搜索,再到动态规划表的构建,通过实例解析动态规划在解决机器人行走、纸牌博弈、马跳棋盘等问题的应用,最后介绍动态规划中的矩阵压缩技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态规划的实质是对暴力递归的加速,可以减少暴力递归时重复性的计算。所以在解题时可以先不考虑时间复杂度写出暴力递归,然后将其进一步修改成动态规划。

机器人达到指定位置方法数

【题目】 假设有排成一行的 N 个位置,记为 1~N,N 一定大于或等于 2。开始时机器人在其中的 M 位 置上(M 一定是 1~N 中的一个),机器人可以往左走或者往右走,如果机器人来到 1 位置, 那 么下一步只能往右来到 2 位置;如果机器人来到 N 位置,那么下一步只能往左来到 N-1 位置。 规定机器人必须走 K 步,最终能来到 P 位置(P 也一定是 1~N 中的一个)的方法有多少种。给 定四个参数 N、M、K、P,返回方法数。

【举例】 N=5,M=2,K=3,P=3 上面的参数代表所有位置为 1 2 3 4 5。机器人最开始在 2 位置上,必须经过 3 步,最后到 达 3 位置。走的方法只有如下 3 种: 1)从2到1,从1到2,从2到3 2)从2到3,从3到2,从2到3 3)从2到3,从3到4,从4到3 所以返回方法数 3。 N=3,M=1,K=3,P=3 上面的参数代表所有位置为 1 2 3。机器人最开始在 1 位置上,必须经过 3 步,最后到达 3 位置。怎么走也不可能,所以返回方法数 0。

对于这道题目,使用暴力递归的解法如下

#include <iostream>

using namespace std;

//暴力递归
//n:一共有n个位置
//p:目标位置
//m:当前位置
//rest:剩余步数
int f(int n,int p,int m,int rest){
    if(rest == 0){
        return m == p ? 1 : 0;
    }

    if(m == 1){
        return f(n,p,m + 1,rest - 1);
    }else if(m == n){
        return f(n,p,m - 1,rest - 1);
    }else{
        return f(n,p,m - 1,rest - 1) + f(n,p,m + 1,rest - 1);
    }
}

int robot(int n,int p,int m,int k){
    return f(n,p,m,k);
}

int main()
{
    //cout << robot(5,3,2,3) << endl;
    cout << robot(3,3,1,3) << endl;
    return 0;
}

继续分析,暴力方法中需要4个参数,其中n和p是不变的,可变参数只有m和rest,也就是说是m和rest决定了暴力方法的返回值。递归函数可以表述为f(m,rest),递归过程可以表现成以下形式(以 N=5,M=2,K=3,P=3 为例):

可以看出,在使用暴力方法时,有很多操作是重复性的,如上图的红色部分,动态规划就是为了减少这部分的操作而提出的优化。

记忆化搜索

记忆化搜索并不是动态规划,但它同样可以达到减少暴力方法的重复操作的目的。记忆化搜索即创建一个变量(如map),该变量记录每一次递归过程的返回值,当遇到条件相同的递归时,直接从变量里读取结果从而减少不必要的操作。

使用记忆化搜索完成上道题目的代码如下

//记忆化搜索
int f(int n,int p,int m,int rest,map<string,int>resMap){
    if(rest == 0){
        return m == p ? 1 : 0;
    }
    if(resMap.find(m + "_" + rest) != resMap.end())
        return resMap.find(m + "_" + rest)->second;
    if(m == 1){
        int res = f(n,p,m + 1,rest - 1,resMap);
        resMap[m + "_" + rest] = res;
        return res;
    }else if(m == n){
        int res = f(n,p,m - 1,rest - 1,resMap);
        resMap[m + "_" + rest] = res;
        return res;
    }else{
        int res = f(n,p,m - 1,rest - 1,resMap) + f(n,p,m + 1,rest - 1,resMap);
        resMap[m + "_" + rest] = res;
        return res;
    }
}

int robot(int n,int p,int m,int k){
    //调用暴力递归方法
    //return f(n,p,m,k);
    //记忆化搜索
    map<string,int> resMap;
    return f(n,p,m,k,resMap);
}

从暴力递归到动态规划

继续分析暴力方法的参数,n和p是固定参数,用来充当条件,作为递归的base case;而m和rest是可变参数,决定递归方法的返回值。我们根据这两个可变参数可以列出一张动态规划表,m的范围是[1,n],rest的范围是[0,k]。以 N=5,M=2,K=3,P=3 为例)

 12345
0     
1     
2     
3     


表中横行代表m的取值范围,列代表rest的取值范围,所以f(m,rest)的所有返回值都可以表示在这张表上。(3,2)是开始位置,(0,3)是终止位置。现在填充这张动态规划表:当在终止位置时,返回值为1,即f(0,3) = 1;根据题目描述或暴力算法,f(1,2) = f(1,4) = 1;f(2,1) = 1,f(2,3) = 2, f(2,5) = 1;f(3,2) = 3,f(3,4) = 3。表的最终结果如下:

 12345
000100
101010
210201
303030

开始位置为(3,2),f(3,2) = 3,所以当 N=5,M=2,K=3,P=3时有三种走法。

 

通过计算得出这张表的过程就是动态规划。

通过动态规划表,可以直到实现动态规划的难度完全取决于可变参数的选取。这道题目中只有两个可变参数,且这两个可变参数都是单独的变量而不是数组,如果是数组的话改动态规划的过程会非常复杂;如果可变参数的数量更多,表也不再是二维的了,如果有三个可变参数,表就是3维的。所以再将暴力算法改成动态规划时一定要尽量选择参数是0维的(单独的变量,不是数组)和可变参数尽量少的尝试。

通过这道题目可以总结出由暴力递归到动态规划的套路:

  1. 找出可变参(代表一个递归状态即哪些参数可以确定);
  2. 将可变参数映射成一张表,1个可变参数代表1维表,2个可变参数代表2维表;
  3. 标出题目答案的位置;
  4. 填出base case部分
  5. 分析其他部分要怎么计算,根据暴力算法中的非base case部分,确定填写顺序;
  6. 填好表,返回题目所需答案再表中的值。

动态规划完成上道题目的代码如下:


//动态规划
int dp(int n,int p,int m,int k){
    int arr[k + 1][n + 1];
    arr[0][p] = 1;
    for(int i = 1; i < k + 1; i++){
        for(int j = 1; j < n + 1; j++){
            if(j == 1){
                arr[i][j] = arr[i - 1][2];
            }else if(j == n){
                arr[i][j] = arr[i - 1][n - 1];
            }else{
                arr[i][j] = arr[i - 1][j - 1] + arr[i - 1][j+ 1];
            }
        }
    }
    return arr[k][m];
}

int robot(int n,int p,int m,int k){
    //调用暴力递归方法
    //return f(n,p,m,k);
    //记忆化搜索
//    map<string,int> resMap;
//    return f(n,p,m,k,resMap);
    //动态规划
    return dp(n,p,m,k);
}

 

排成一条线的纸牌博弈问题

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

#include <iostream>

using namespace std;

//暴力尝试
int pre(int arr[],int n,int i,int j);

int next(int arr[],int n,int i,int j){
    if(i == j){
        return 0;
    }
    return min(pre(arr,n,i + 1,j),pre(arr,n,i, j - 1));
}

int pre(int arr[],int n,int i,int j){
    if(i == j){
        return arr[i];
    }
    return max(arr[i] + next(arr,n,i + 1, j),arr[j] + next(arr,n,i, j - 1));
}


//动态规划
int win(int arr[],int n){
    if(n <= 0){
        return 0;
    }
    int dp_pre[n][n];
    int dp_next[n][n];
    for(int i = 0; i < n; i++){
        for(int j = 0; j < n; j++){
            dp_pre[i][j] = 0;
            dp_next[i][j] = 0;
            if(i == j){
                dp_pre[i][i] = arr[i];
                dp_next[i][i] = 0;
            }
        }
    }
    for(int i = 0; i < n; i++){
        for(int j = i + 1; j < n; j++){
            dp_pre[i][j] = max(arr[i] + dp_next[i + 1][j],arr[j] + dp_next[i][j - 1]);
            dp_next[i][j] = min(dp_pre[i + 1][j],dp_pre[i][j - 1]);
        }
    }
    return max(dp_pre[n - 2][n - 2],dp_next[n - 2][n - 2]);
}


int cardGame(int arr[],int n){
    if(n <= 0){
        return 0;
    }
    return win(arr,n);
}


int main()
{
    int arr[4] = {1,2,100,4};
    cout << cardGame(arr,4);

    return 0;
}

象棋中马的跳法

【题目】 请同学们自行搜索或者想象一个象棋的棋盘,然后把整个棋盘放入第一象限,棋盘的最左下 角是(0,0)位置。那么整个棋盘就是横坐标上9条线、纵坐标上10条线的一个区域。给你三个 参数,x,y,k,返回如果“马”从(0,0)位置出发,必须走k步,最后落在(x,y)上的方法数 有多少种?

#include <iostream>

using namespace std;

//暴力递归
int process(int x,int y,int k){
    if(x > 8 || x < 0 || y > 9 || y < 0){
        return 0;
    }
    if(k == 0){
        return (x == 0 && y == 0) ? 1 : 0;
    }

    return process(x + 1, y + 2,k - 1) + process(x + 2, y + 1, k - 1) + process(x + 2, y - 1, k - 1) + process(x + 1, y - 2, k - 1) +
           process(x - 1, y - 2,k - 1) + process(x - 2, y - 1, k - 1) + process(x - 2, y + 1, k - 1) + process(x - 1, y + 2, k - 1);
}

//动态规划
int getValue(int x,int y){
    if(x > 8 || x < 0 || y > 9 || y < 0){
        return 0;
    }
    return 1;
}

int func(int x,int y, int k){
     if(x > 8 || x < 0 || y > 9 || y < 0){
        return 0;
    }
    int dp[9][10][k + 1];
    for(int i = 0; i < 9; i++){
        for(int j = 0; j < 10; j++){
            for(int n = k; n >= 0; n--){
                dp[i][j][n] = 0;
            }
        }
    }
    dp[0][0][0] = 1;
    for(int n = 1; n <= k; n++){
        for(int i = 0; i < 9; i++){
            for(int j = 0; j < 10; j++){
                dp[i][j][n] += getValue(i - 1,j - 2) == 0 ? 0 : dp[i - 1][j - 2][n - 1];
                dp[i][j][n] += getValue(i - 2,j - 1) == 0 ? 0 : dp[i - 2][j - 1][n - 1];
                dp[i][j][n] += getValue(i - 2,j + 1) == 0 ? 0 : dp[i - 2][j + 1][n - 1];
                dp[i][j][n] += getValue(i - 1,j + 2) == 0 ? 0 : dp[i - 1][j + 2][n - 1];
                dp[i][j][n] += getValue(i + 1,j + 2) == 0 ? 0 : dp[i + 1][j + 2][n - 1];
                dp[i][j][n] += getValue(i + 2,j + 1) == 0 ? 0 : dp[i + 2][j + 1][n - 1];
                dp[i][j][n] += getValue(i + 2,j - 1) == 0 ? 0 : dp[i + 2][j - 1][n - 1];
                dp[i][j][n] += getValue(i + 1,j - 2) == 0 ? 0 : dp[i + 1][j - 2][n - 1];
                //cout << i << j << k << " " << dp[i][j][n] << endl;
            }
        }
    }
    return dp[x][y][k];
}


int chess(int x,int y,int k){
    //return process(x,y,k);
    return func(x,y,k);
}

int main()
{
    cout << chess(7,7,10) <<endl;
    return 0;
}

 

 

Bob的生存概率

【题目】 给定五个参数n,m,i,j,k。表示在一个N*M的区域,Bob处在(i,j)点,每次Bob等概率的向上、 下、左、右四个方向移动一步,Bob必须走K步。如果走完之后,Bob还停留在这个区域上, 就算Bob存活,否则就算Bob死亡。请求解Bob的生存概率,返回字符串表示分数的方式。

#include <iostream>
#include <math.h>
#include<sstream>

using namespace std;

//求最大公约数
int gcd(int m, int n) {
		return n == 0 ? m : gcd(n, m % n);
	}
//暴力递归
int process(int n,int m,int i,int j,int k){
    if(i < 0 || j < 0){
        return 0;
    }
    if(k == 0){
        return (i <= n && j <= m) ? 1 : 0;
    }
    return process(n,m,i + 1, j ,k - 1) + process(n,m,i - 1,j, k - 1) + process(n,m,i,j + 1, k - 1) + process(n,m,i,j - 1, k - 1);

}

//动态规划
int func(int n,int m,int i,int j,int k){
    int dp[n + 2][m + 2][k + 1];//加2防止越界情况
    for(int p = 0; p < n + 2; p++){
        for(int q = 0; q < m + 2; q++){
            dp[p][q][0] = (p <= n && q <= m && p > 0 && q > 0) ? 1 : 0;
        }
    }
    for(int r = 1; r < k + 1; r++){
        for(int p = 0; p < n + 2; p++){
            for(int q = 0; q < m + 2; q ++){
                dp[p][q][r] = (p + 1 > n || q > m || p + 1 < 1 || q < 1) ? 0 : dp[p + 1][q][r - 1];
                dp[p][q][r] += (p - 1 > n || q > m || p - 1 < 1 || q < 1) ? 0 : dp[p - 1][q][r - 1];
                dp[p][q][r] += (p > n || q + 1 > m || p < 1 || q + 1 < 1) ? 0 : dp[p][q + 1][r - 1];
                dp[p][q][r] += (p > n || q - 1 > m || p < 1 || q - 1 < 1) ? 0 : dp[p][q - 1][r - 1];
            }
        }
    }
    return dp[i + 1][j + 1][k];
}


string Bob(int n,int m,int i,int j,int k){
     if(n <= 0 || m <= 0){
        return 0;
    }
    int survival = func(n,m,i,j,k);
    int all = pow(4,k);
    int g = gcd(all,survival);
    stringstream ss;
    ss << (survival / g) << " / " << (all / g);
    string res = ss.str();
    return res;
}


int main()
{
    cout << Bob(10,10,3,2,5) << endl;
    return 0;
}

动态规划技巧--矩阵压缩

当只要求返回结果而不需要求解过程时,可以用一些技巧来压缩动态规划产生的矩阵。但这个技巧并不能减少计算操作,而且不能返回求解过程。

矩阵的最小路径和

【题目】 给定一个矩阵 m,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径 上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。

【举例】 如果给定的 m 如下: 1359 8134 5061 8840 路径 1,3,1,0,6,1,0 是所有路径中路径和最小的,所以返回12

#include <iostream>

using namespace std;

int process(int arr[][4],int m,int n){
    int matrix[m][n];
    matrix[0][0] = arr[0][0];
    for(int i = 1; i < n; i++){
        matrix[0][i] = arr[0][i] + matrix[0][i - 1];
    }
    for(int i = 1; i < m; i++){
        matrix[i][0] = matrix[i - 1][0] + arr[i][0];
        for(int j = 1; j < n; j++){
            matrix[i][j] = min(matrix[i - 1][j] + arr[i][j],matrix[i][j - 1] + arr[i][j]);
        }
    }
    return matrix[m - 1][n - 1];
}

int func(int arr[][4],int m,int n){
    int a[n];
    a[0] = arr[0][0];
    for(int i = 1; i < n; i++){
        a[i] = arr[0][i] + a[i - 1];
    }
    for(int i = 1; i < m;i++){
        a[0] = a[0] + arr[i][0];
        for(int j = 1; j < n; j++){
            a[j] = min (a[j],a[j - 1]) + arr[i][j];
        }
    }
    return a[n - 1];
}

int matrix(int arr[][4],int m,int n){
    if(m <= 0 || n <= 0){
        return 0;
    }
    return func(arr,m,n);
}

int main()
{
    int arr[4][4] = {{1,3,5,9},{8,1,3,4},{5,0,6,1},{8,8,4,0}};
    cout << matrix(arr,4,4) << endl;
    return 0;
}

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值