动态规划的实质是对暴力递归的加速,可以减少暴力递归时重复性的计算。所以在解题时可以先不考虑时间复杂度写出暴力递归,然后将其进一步修改成动态规划。
机器人达到指定位置方法数
【题目】 假设有排成一行的 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 为例)
1 | 2 | 3 | 4 | 5 | |
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。表的最终结果如下:
1 | 2 | 3 | 4 | 5 | |
0 | 0 | 0 | 1 | 0 | 0 |
1 | 0 | 1 | 0 | 1 | 0 |
2 | 1 | 0 | 2 | 0 | 1 |
3 | 0 | 3 | 0 | 3 | 0 |
开始位置为(3,2),f(3,2) = 3,所以当 N=5,M=2,K=3,P=3时有三种走法。
通过计算得出这张表的过程就是动态规划。
通过动态规划表,可以直到实现动态规划的难度完全取决于可变参数的选取。这道题目中只有两个可变参数,且这两个可变参数都是单独的变量而不是数组,如果是数组的话改动态规划的过程会非常复杂;如果可变参数的数量更多,表也不再是二维的了,如果有三个可变参数,表就是3维的。所以再将暴力算法改成动态规划时一定要尽量选择参数是0维的(单独的变量,不是数组)和可变参数尽量少的尝试。
通过这道题目可以总结出由暴力递归到动态规划的套路:
- 找出可变参(代表一个递归状态即哪些参数可以确定);
- 将可变参数映射成一张表,1个可变参数代表1维表,2个可变参数代表2维表;
- 标出题目答案的位置;
- 填出base case部分
- 分析其他部分要怎么计算,根据暴力算法中的非base case部分,确定填写顺序;
- 填好表,返回题目所需答案再表中的值。
动态规划完成上道题目的代码如下:
//动态规划
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;
}