单序列型动态规划

  • state: f[i] 表示前 i 个位置/数字/字符

  • function: f[i] = f[j] ...

  • initialize: f[0] = ...

  • answer: f[n] 一般 answer 是 f[n] 不是 f[n - 1]。


139. Word Break

  • state: f[i]表示“前i”个字符能否被完美切分
  • function: f[i] = OR{f[j]} 其中 j < i && j + 1 - i is a word
    • OR 运算的意思
    • 假如 j = 0, 1, 3, 5 时满足 j < i && j + 1 - i is a word
    • 那么 f[i] = f[0] || f[1] || f[3] || f[5]
    • 由于是nested for loop,每一个 i 对应很多个 j,也就是对应很多种不同的切分方式,对于每一个 i 来说,只要有一个切分成立,整个切分就是成立的。所以在这个地方我们要用 || 。
  • initialize: f[0] = true
  • answer: f[n]

这道题有至少三个优化可以做,等到复习的时候再提吧。

public class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        if (s == null || s.length() == 0) return true;

        int n = s.length();
        boolean[] f = new boolean[n + 1];
        f[0] = true;

        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j < i; ++j) {
                String str = s.substring(j, i);
                f[i] = f[i] || f[j] && wordDict.contains(str);
            }
        }
        return f[n];
    }
}

Word Break这题出现频率也不算低了,常见的follow-up:

  • 原题问的是True/False,改为问切几次可以break?
    • 把切完的词放到list里全部返回,就变成了LC140 Word Break II
    • 最少切几次可以切出完美切割?最多/少切几次可以切出字典里的词?
    • 很像下一道题LC132

132. Palindrome Partitioning II

名不虚传的高频Hard题目,这道题对动态规划的理解和时间空间的设计都提出了较高的要求,这道题的篇幅我会稍微写长一点,但可能分析和最终答案都不是最好的,凑合看吧。

  • state: f[i]表示前i个字符组成的子串能被分割为最少多少个回文串
  • function: f[i] = MIN{f[j] + 1}, j < i && j + 1 - i这一段是一个回文串
  • initialize: f[i] = i (f[0] = 0) 空串被切割为0个回文串。
  • answer: f[n] – 1
    • 为什么要减一,因为我们定义state的时候是最少分割为多少个回文串,题目问的是最少的切割次数,那么如果我们最少分割为 n 串的话,需要最少切 n - 1 刀。
public class Solution {
    public int minCut(String s) {
        if (s == null || s.length() == 0) return 0;

        int n = s.length();
        int[] f = new int[n + 1];
        f[0] = 0;

        for (int i = 1; i <= n; ++i) {
            f[i] = i;
            for (int j = 0; j < i; ++j) {
                if (valid(s.substring(j, i))) {
                    f[i] = Math.min(f[i], f[j] + 1);
                }
            }
        }
        return f[n] - 1;
    }
    private boolean valid(String s) {
        for (int i = 0; i < s.length(); ++i) {
            int j = s.length() - 1 - i;
            if (s.charAt(i) != s.charAt(j)) return false;
        }
        return true;
    }
}

然而,TLE了

挂在了喜闻乐见的"aaaaaaaaaaa..."case上。

我们原程序其实写的挺傻的,时间复杂度为 O(n^3)。nested for loop是没办法的,因为DP的强项就不是降低时间复杂度的数量级,我们必须想办法从判断valid回文这个地方入手。

冥冥中,耳边回响起了委座的教诲:以空间换时间。张学良表示附议。

所以在这里我们需要对字符串预处理,用一个变量存下valid palindrome的字段。

这就用上了区间型动态规划中valid palindrome的解法。

  • State: f[i][j]表示index从i到j这一段是不是一个回文串
  • Function: f[i][j] = f[i + 1][j - 1] && (s[i] == s[j])
  • Initialize: f[i][i] = true
  • Answer: f[x][y] // x, y是你想查询的那一段区间

注意不要越界,注意初始化。

i 依赖于i + 1,算的时候要倒过来,先算大的。本质上是先for区间长度,再for左端点。

这个valid解法和LC125 Valid Palindrome不一样,因为LC125要忽略空格和符号。这个解法其实实战并没有什么优势,只是区间型动态规划的一个例子。

public class Solution {
    public boolean isPalindrome(String s) {
        if (s == null || s.length() == 0) return true;
        int n = s.length();
        boolean[][] f = new boolean[n][n];
        for (int i = 0; i < n; ++i) {
            f[i][i] = true;
        }
        for (int i = 0; i < n - 1; ++i) {
            f[i][i + 1] = (s.charAt(i) == s.charAt(i + 1));
        }
        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 2; j < n; ++j) {
                f[i][j] = f[i + 1][j - 1] && s.charAt(i) == s.charAt(j);
            }
        }
        return f[0][n - 1];
    }
}

这里你可以直接把整个方法放到前面的代码中做一个预处理,然后拿过来用就行了,很简单的复制粘贴。

但是

写完观察一下,其实我们没必要写一个方法,合二为一就行了,边做valid验证,边进行切割字符串。

不过在这里面,为了适应合二为一的写法,为了迁就valid验证中f[i + 1][j - 1]不越界,让下标统一,我们要修改原来切割state的定义。

  • state: f[i]表示切到第i个字母,最少被切割几次可以切割为都是回文串。
  • function: f[i] = MIN{f[j - 1] + 1}, charAt(i) == charAt(j) && j - i这一段是一个回文串
  • initialize: 不用,因为我们从0开始,边做valid边做切割。
  • answer: f[n - 1]
public class Solution {
    public int minCut(String s) {
        if (s == null || s.length() == 0) return 0;

        int n = s.length();
        boolean[][] isPalindrome = new boolean[n][n];
        int[] f = new int[n];

        for(int i = 0; i < n; i++){
            f[i] = i;
            for(int j = 0; j <= i; j++){
                if(s.charAt(i) == s.charAt(j) && (i - j < 2 || isPalindrome[j + 1][i - 1])){
                    isPalindrome[i][j] = isPalindrome[j][i] = true;
                    if(j == 0){
                        f[i] = 0;
                    } else {
                        f[i] = Math.min(f[i], f[j - 1] + 1);
                    }
                }
            }
        }
        return f[n - 1];
    }
}

91. Decode Ways

FB御题。他们就是这么喜欢DP。

思路和climbing stairs没什么区别,就是注意一下以0开头的corner case。

public class Solution {
    public int numDecodings(String s) {
        if (s == null || s.length() == 0) {
            return 0;
        }
        int[] f = new int[s.length() + 1];

        f[0] = 1;
        f[1] = s.charAt(0) != '0' ? 1 : 0;

        for (int i = 2; i <= s.length(); ++i) {
            if (s.charAt(i - 1) != '0') {
                f[i] = f[i - 1];
            }
            int twoDigits = (s.charAt(i - 2) - '0') * 10 + s.charAt(i - 1) - '0';
            if (twoDigits <= 26 && twoDigits >= 10) {
                f[i] += f[i - 2];
            }
        }
        return f[s.length()];
    }
}

results matching ""

    No results matching ""