No.1 动态规划解题套路

一、引言

遇到一到动态规划题后,必须先自己思考找到一个暴力递归求解的方法,然后判断尝试过程是否有后效性。若无后效性,才可以套用动态规划题的模板。这里要说明的是,想出问题的暴力尝试方法这一步是最难的同时也是最重要的,而且没有固定的方法,只能通过不断做题才能又快又好的写出暴力解!

无后效性:是指一个递归状态的返回值与怎么到达这个状态的路径无关

二、使用动态规划优化暴力方法

暴力尝试方法一旦写好后,后面的优化过程全是固定的套路。

步骤如下:

当然尝试方法必须是无后效性的。

  1. 找到递归函数中所有可变参数,它们代表着每次递归时的递归状态,一旦可变参数确定,返回值也就确定了
  2. 把所有可变参数的组合映射成一张表,一个可变参数就是数组,两个可变参数就是二维矩阵,…
  3. 在表中找到最终答案,在表中标出。
  4. 初始化表中一些不需要依赖其他位置的位置的值填好,这就是base case
  5. 根据暴力尝试方法中的递归方法,把表中非base case的位置填好
  6. 返回最终答案在表中位置的值。

三、实战演示

【题目】

一行中有N个位置,即1~N。开始时,机器人在其中的M位置上(1 \leq M \leq N),机器人可以左走( \gets )或者右走( \to ),但是若机器在1位置时,那么它只能( \to );若机器人在N位置,那么它只能( \gets )。现在规定了机器人必须走K步,最终来到P位置的方法有多少种?

【举例】

N=5,M=2,K=3,P=3

扫描二维码关注公众号,回复: 9926141 查看本文章

上面的参数代表位置:

1 2 3 4 5

机器人起始位置为2,必须经过3步最后到达3位置。走的方法有三种。

a.2 \to 1,1 \to 2,2 \to 3
b.2 \to 3,3 \to 2,2 \to 3
c.2 \to 3,3 \to 4,4 \to 3

所以返回方法数3

【解答】

最难且最重要的方法:暴力递归

假设机器人现在在cur位置,还剩rest步没走,那么下一步该怎么走?

  • cur==1,下一步只能走到2位置,还剩rest-1步
  • cur==N,下一步只能走到N-1位置,还剩rest-1步
  • 其余位置的话,下一步可以是cur-1或者cur+1位置,还剩rest-1步

每次尝试怎么算结束?所有步数K用完了就算结束

结束后返回啥呢?如果步数用完了后,停在K位置说明该尝试有效,即找到1种;

否则返回0,即没有找到任何一种方法。

尝试的递归过程如walk方法:

//N:位置为1~N,固定参数

//cur:当前在cur位置,可变参数

//rest:还剩rest步没有走,可变参数

//P:最终目标位置是P,固定参数

//当前在cur位置,走完rest步之后,最后停在P位置的方法数作为返回值

public int walk(int N,int cur,int rest,int P){
  if(rest==0)
    return cur==P?1:0;
  if(cur==1)
    return walk(N,2,rest-1,P);
  if(cur==N)
    return walk(N,N-1,rest-1,P);
  return walk(N,cur-1,rest-1,P)+walk(N,cur+1,rest-1,P);
}

至此,我们已经完全找到并写出暴力递归方法。后面的动态规划就可以仿真第二节的套路来写了。

【优化过程】

首先结合walk函数的含义,分析整个递归过程是不是无后效性。这里说明的是99%的问题是无后效性的。但是分析问题的后效性是很重要的。walk函数中有两个固定参数N和P,他们任何时候不会改变,直接忽略他们。只需要关注两个可变参数cur和rest即可。walk(cur,rest)代表当前来到cur位置,还剩rest步的有效方法数量。

比如cur=5,rest=7,walk(5,7)代表当前位置在5,还剩7步,有效方法有多少种。之后可以自己画出树状图,必然会有很多重复的状态。比如walk(4,6)下一步可以是walk(5,5),或者是walk(6,6)的下一步也可以是walk(5,5)。不管是通过什么路径来到walk(5,5),这个值都是不变的,不会应为上一步不同而发生改变,即递归过程是无后效性的。

分析完后效性后,后面的优化过程就与原问题无关了。

【动态规划模板】

  • 前提:问题的无后效性得到了证明
  1. walk函数中可变参数cur和rest一旦确定,返回值也就确定了
  2. 将cur和rest组合成一张二维表,这张表一定可以装下所有的返回值。表的大小根据cur和rest来考虑,cur含义是当前位置,它只可能在1N之间。rest含义是剩余步数,它只可能在0K之间。谁做行谁做列都行,这张表就记为dp[][],那么walk(cur,rest)的返回值为dp[rest][cur]
  3. N=7,M=4,K=9,P=5的最终答案,就是dp[9][4]的值
  4. 递归过程的base case是指问题的规模小到什么程度,就不需要再划分子问题,答案就可以直接得到了。结合暴力递归的方法:
if(rest==0)
  return cur==P?1:0;

dp表的第一行就是basecase,根据上面代码可以把dp[0][1~N]手动填好。本题中P=5,所以第一行只有dp[0][5]=1;

/*
base case
**/
dp[0][P]=1;

在这里插入图片描述
5. base case之外的情况都是普遍位置了,结合暴力递归的方法:

if(cur==1)
    return walk(N,2,rest-1,P);
  if(cur==N)
    return walk(N,N-1,rest-1,P);
  return walk(N,cur-1,rest-1,P)+walk(N,cur+1,rest-1,P);
  • 如果cur=1,dp[rest][cur]=dp[rest-1][2](图中A依赖B)
  • 如果cur=N,dp[rest][cur]=dp[rest-1][N-1](图中F依赖D)
  • 其余位置,dp[rest][cur]=dp[rest-1][cur-1]+dp[rest-1][cur+1](图中E依赖C和D)

1 cur N

在这里插入图片描述

  1. 返回dp[9][4]的值

所以本题动态规划的解法就是把规模N*K的表填好,时间复杂度为O(N*K)。下面就是动态规划算法:

public int ways2(int N,int M,int K,int P){
  if(N<2||K<1||M<1||M>N||P<1||P>N)
    return 0;
  int[][] dp=new int[K+1][N+1];
  dp[0][p]=1;
  for(int i=1;i<=K;i++){
    for(int j=1;j<=N;j++){
      if(j==1)
        dp[i][j]=dp[i-1][2];
      else if(j==N)
        dp[i][j]=dp[i-1][N-1];
      else
        dp[i][j]=dp[i-1][j+1]+dp[i-1][j-1]}
  }
  return dp[K][M];
}

下面在介绍动态规划的空间压缩法

它纯粹是为了降低空间复杂度的,它改变不了时间复杂度。

若一个算法的空间复杂度为O(M*N),那么使用空间压缩法后空间复杂度为O(min[M,N])

结合上题来说,空间压缩优化空间复杂度的步骤:

  1. 生成一个一位数组dp[],它的大小由两个可变参数中范围小的那个参数决定。N=7,K=9,所以dp大小由N决定,dp[i]代表1~N中i位置由多少种方法的值。
int[] dp=new int[N+1];//必须为N+1,因为机器人只能在1~N中活动。
  1. 初始化dp。由未优化空间时的base case决定,在rest=0时,dp[0][P]=1。这里优化后少了第一维信息,所以优化后的dp是一个复用的数组空间。
dp[P]=1;
  1. 按照未优化的第5步,写出计算逻辑,由于缺少一维信息,所以势必需要少量的辅助变量。
for(int i=1;i<=K;i++){
  int leftup=dp[1];//暂存左上角的信息
  for(int j=1;j<=N;j++){
    int temp=dp[j];
    if(j==1)
      dp[j]=dp[j+1];//对应A--B
    else if(j==N)
      dp[j]=left;//对应F--D
    else 
      dp[j]=left+dp[j+1];//对应E--C、D
    leftup=temp;//更新左上角的值
  }
}
发布了11 篇原创文章 · 获赞 5 · 访问量 8097

猜你喜欢

转载自blog.csdn.net/baidu_36161424/article/details/103539899