畅游面试中的动态规划套路

写在前面的话

本文主题是讲动态规划,涉及题目近40题,每题超过3种方法解决,共100多种写法,思路和目录结构来源于收藏夹中的一个课程Grokking Dynamic Programming Patterns for Coding Interviews,其中grok是深刻领会,深入掌握的意思,我翻译成畅游,加上自己的理解,消化(截取的课程的目录),得到本文,文章很长,其实人生的路也很长,是吧?

  • 这是一篇摘要文章,具体内容见文末的跳转链接

0.什么是动态规划

动态规划(DP)是一种求解优化问题的算法,它将问题分解为更简单的子问题,并利用整体问题的最优解取决于子问题的最优解这一事实

看个例子斐波那契数列,众所周知,斐波那契数列是一系列的数字,其中每个数字都是前面两个数字的和,前几个斐波那契数列是0、1、1、2、3、5和8,以此类推。
如果我们要求计算第n个斐波那契数列,我们可以用下面的方程来做:

Fib(n) = Fib(n-1) + Fib(n-2), for n > 1

我们可以清楚地看到,为了解决整个问题(即Fib(n)),我们将其分解为两个更小的子问题(Fib(n-1)Fib(n-2))。这说明我们可以用DP来解决这个问题

动态规划的特点

在理解DP问题的不同方法之前,让我们先看看一个问题的哪些特征告诉我们可以应用DP来解决它。

1.重叠子问题

子问题是原始问题的更小粒度。如果找到它的解决方案涉及到多次解决相同的子问题,那么任何问题都有重叠的子问题。以斐波那契数列为例,为了找到fib(4),我们需要将其分解为以下子问题:

请添加图片描述

我们可以清楚地看到重叠的子问题模式,因为fib(2)已经计算了两次,fib(1)已经计算了三次。

2.最优子结构性质

如果它的整体最优解可以由其子问题的最优解构造出来,那么任何问题都具有最优子结构性质。对于斐波那契数列,我们知道

Fib(n) = Fib(n-1) + Fib(n-2)

这清楚地表明大小为n的问题已经被简化为大小为n-1n-2的子问题。因此,斐波那契数具有最优子结构性质。

动态规划的方法

DP问题一般有两种方式去解决

2.1.自顶向下记忆化(Top-down)

在这种方法中,我们试图通过递归地找到较小子问题的解决方案来解决较大的问题。每当我们解决一个子问题时,对子问题的结果缓存起来,这样当它被多次调用时,我们就不会重复地解决它。相反,我们可以只返回保存的结果。这种存储已经解决的子问题的结果的技术叫做记忆化

我们将在斐波那契数列的例子中看到这种技术。首先,让我们看看寻找第n个斐波那契数列的非DP递归解:

class Fibonacci {

  public int CalculateFibonacci(int n) {
    if(n < 2) return n;
    return CalculateFibonacci(n-1) + CalculateFibonacci(n-2);
  }

  public static void main(String[] args) {
    Fibonacci fib = new Fibonacci();
    System.out.println("5th Fibonacci is ---> " + fib.CalculateFibonacci(5));
    System.out.println("6th Fibonacci is ---> " + fib.CalculateFibonacci(6));
    System.out.println("7th Fibonacci is ---> " + fib.CalculateFibonacci(7));
  }
}

output

5th Fibonacci is ---> 5
6th Fibonacci is ---> 8
7th Fibonacci is ---> 13

正如我们上面所看到的,这个问题显示了重叠子问题模式,所以让我们在这里利用记忆。我们可以使用一个数组来存储已经解决的子问题(请参阅高亮显示的行中的更改)。

class Fibonacci {

  public int CalculateFibonacci(int n) {
    int memoize[] = new int[n+1];
    return CalculateFibonacciRecursive(memoize, n);
  }

  public int CalculateFibonacciRecursive(int[] memoize, int n) {
    if(n < 2)
      return n;
    // if we have already solved this subproblem, simply return the result from the cache
    if(memoize[n] != 0)
      return memoize[n];

    memoize[n] = CalculateFibonacciRecursive(memoize, n-1) + CalculateFibonacciRecursive(memoize, n-2);
    return memoize[n];
  }

  public static void main(String[] args) {
    Fibonacci fib = new Fibonacci();
    System.out.println("5th Fibonacci is ---> " + fib.CalculateFibonacci(5));
    System.out.println("6th Fibonacci is ---> " + fib.CalculateFibonacci(6));
    System.out.println("7th Fibonacci is ---> " + fib.CalculateFibonacci(7));
  }
}
2.2.自底向上填表(Bottom-up)

填表与自顶向下方法相反,避免了递归。在这种方法中,我们自底向上地解决问题(即首先解决所有相关的子问题)。这通常通过填充一个n维表来完成。根据表中的结果,然后计算顶部/原始问题的解决方案。

填表记忆化是相反的,因为在记忆化中,我们解决问题,并维护已经解决的子问题的映射。换句话说,在记忆化中,我们从上到下,也就是说我们先解决最上面的问题(通常递归下来解决子问题)。

让我们将填表应用到我们的斐波那契数列的例子中。因为我们知道每个斐波那契数都是前面两个数的和,所以我们可以使用这个事实来填充我们的表。

下面是自底向上动态规划的代码:

class Fibonacci {

  public int CalculateFibonacci(int n) {
    if (n==0) return 0;
    int dp[] = new int[n+1];
    //base cases
    dp[0] = 0;
    dp[1] = 1;
    for(int i=2; i<=n; i++)
      dp[i] = dp[i-1] + dp[i-2];
    return dp[n];
  }

  public static void main(String[] args) {
    Fibonacci fib = new Fibonacci();
    System.out.println("5th Fibonacci is ---> " + fib.CalculateFibonacci(5));
    System.out.println("6th Fibonacci is ---> " + fib.CalculateFibonacci(6));
    System.out.println("7th Fibonacci is ---> " + fib.CalculateFibonacci(7));
  }
}

我们将始终从一个暴力递归解决方案开始,这是开始解决任何DP问题的最佳方式。一旦我们有了递归的解决方案,我们就会应用记忆化填表

让我们应用这些知识来解决一些常见的DP问题。

1.畅游面试中的动态规划套路-01背包系列

2.畅游面试中的动态规划套路-完全背包系列

3.畅游面试中的动态规划套路-斐波那契数列系列

4.畅游面试中的动态规划套路-回文子序列系列

5.最长子串

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值