动态规划算法-思维练习

197 阅读2分钟

前提概要

前一篇文章,主要介绍了动态规划解题的整体思路。 动态规划算法
但是要达到熟练运用,我们都少不了要多加练习,只有刻意练习后才有可能产生更好的联想力,从而帮助你在工作中用来解决难题。

题目描述

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

思路分析

1、是否符合多阶段决策最优解

  • 字符串t是s字符串的子集,前提约束s字符串的长度必须小于等于t字符串的长度。
  • 定义dp[i][j]: s的子序列s[i:]中字符串t[j:]出现的个数,其中i<=len(s)、j<=len(t)
    • j = len(t), t[j:]为空字符串,此时t始终为s的子序列,dp[i][len(t)] = 1, 0<=i<=len(s)-1.
    • i = len(s), s[i:]为空字符串,此时t始终不为s的子序列, dp[len(s)][j] = 0, 0<=j<=len(t)-1.
    • 对于s和t从最后一个字符走到最前面要走len(s) + len(t)步,每一步对应的决策
      • s[i]==t[j]: s和t都往前移动一步(i-1, j-1); 或者s往前移动一步t保持不变(i-1,j)
      • s[i]!=t[j]: s往前移动一步、t保持不变即:(i-1,j)
    • 每次决策后会产生对应阶段的状态集合dp[i][j], 表示从(len(s),len(t))走到(0,0)的最大子序列长度,所以这个问题符合多阶段决策最优解的思路。

2、是否存在重复子问题

主要针对使用回溯算法解题时,是否存在重复走到(i, j)点位的情况。

  • 在s[i] == t[j]时,s和t可以都往后一步,或者只有s往后移动一步t保持不变;
  • 在s[i] != t[j]时,s往前移动一步,t保持不变.

上面两种情况,对字符串进行回溯时,肯定存在重复走到(i,j)点的情况。

3、是否无后效性

从上面dp[i][j]的推理,走到(i,j)点位只存在从后往前的路线,可以从(i+1,j+1)位置或者(i+1,j)位置变更而来的情况,不能从前再往后走,所以无后效性。

4、最优子结构

基于前面的推导,可以得到如下方程

dp[i][j]={dp[i+1][j+1]+dp[i+1][j],s[i]=t[j]dp[i+1][j],s[i]!=t[j]dp[i][j]=\begin{cases} dp[i+1][j+1]+dp[i+1][j], s[i]=t[j]\\ dp[i+1][j], s[i]!=t[j] \end{cases}

代码求解

1、使用回溯+备忘录的代码实现

private int[][] memo;
public int numDistinct(String s, String t) {
    if (t.length() > s.length()) {
        return 0;
    }
    this.memo = new int[s.length()+1][t.length()+1];
    for (int i = 0; i <= s.length(); i++) {
        for (int j = 0; j <= t.length(); j++) {
            this.memo[i][j] = -1;
        }
    }
    return getNumDistinct(0,0,s,t);
}

private int getNumDistinct(int i, int j, String s, String t) {
    if (j == t.length()) {
        this.memo[i][j] = 1;
        return 1;
    }
    if (i == s.length()) {
        this.memo[i][j] = 0;
        return 0;
    }

    if (this.memo[i][j] > -1) {
        return this.memo[i][j];
    }

    if (s.charAt(i) == t.charAt(j)) {
        int f = getNumDistinct(i + 1, j + 1, s, t)
                + getNumDistinct(i + 1, j, s, t);
        this.memo[i][j] = f;
        return f;
    } else {
        int f = getNumDistinct(i + 1, j, s, t);
        this.memo[i][j] = f;
        return f;
    }
}

2、使用动态规划实现代码

public int numDistinct(String s, String t) {
    if (t.length() > s.length()) {
        return 0;
    }
    int m = s.length();
    int n = t.length();
    int[][] dp = new int[m + 1][n + 1]; // dp[i][j] 表示在s[i:] 的子序列中 t[j:] 出现的个数。
    for (int i = 0; i <= m; i++) {
        dp[i][n] = 1;
    }
    for (int i = m - 1; i >= 0; i--) {
        char sChar = s.charAt(i);
        for (int j = n - 1; j >= 0; j--) {
            char tChar = t.charAt(j);
            if (sChar == tChar) {
                dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
            } else {
                dp[i][j] = dp[i + 1][j];
            }
        }
    }
    return dp[0][0];
}

总结

不管是动态规划还是回溯算法,对思路和原理理解很重要。但是如果你不理解思路和原理,经过大量的练习后,你其实也可以自己总结规律和方法论,只是这个练习和思考的过程中会比较耗时。站在前人总结的方法论基础上,自己再多加练习,才会更有所收获。