动态规划解题模版:双序列型

动态规划:双序列型

双序列型,就是有两个子序列/字符串,每个序列本身是一维的,可以转换为二维dp,序列型开数组开n+1,双序列型也是开n+1。

突破口:看串A和串B的最后一个字符是否匹配,是否需要串A/串B的最后一个字符,来缩减规模。

两种类型:计数型:情况1+情况2+…以及最值型min/max{情况1,情况2…}

初始条件:要特别当心空串的处理。

1、LintCode 77 Longest Common Subsequence

【问题】最长公共子序列。给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。

【分析】字符串A的长度为m,字符串B的长度为n,要组成最长公共子串一定是一个个对子,不能交叉,要按照顺序来,假设现在得到了最长公共子序列,有这么几种情况:

  • 字符串A的最后一个字符不在这个LCS中,那最长公共子串就是A中下标为0~m-2与B中下标为0~n-1的字符串的最长公共子序列
  • 字符串B的最后一个字符不在这个LCS中,那最长公共字串就是B中下标为0~n-2与A中下标为0~m-1的字符串的最长公共子序列
  • 字符串A中的最后一个字符与B中的一个字符正好是一对,那最长公共字串就是A中下标为0~m-2与B中下标为0~n-2的字符串的最长公共子序列+A[m-1]

【转移方程】dp[i] [j]代表A中前i个字符和B中前j个字符

  • dp[i][j] = max{dp[i-1][j], dp[i][j-1], dp[i-1][j-1] + 1|A[i-1]=B[j-1]}

时间复杂度O(MN),空间复杂度O(MN)

public int longestCommonSubsequence(String A, String B) {
        int n = A.length();
        int m = B.length();

        if (n == 0 || m == 0) {
            return 0;
        }

        int[][] dp = new int[n + 1][m + 1]; //双序列型的本质还是序列型
        //初始化第0行和第0列
        for (int i = 0; i <= m; i++) {
            dp[0][i] = 0;
        }
        for (int i = 0; i <= n; i++) {
            dp[n][0] = 0;
        }

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                if (A.charAt(i - 1) == B.charAt(j - 1)) {       
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
                }
            }
        }
        return dp[n][m];
    }

可以用滚动数组优化空间复杂度至O(N)

Plus:要求打印所有路径

private static void LCS(String A, String B) {
        int m = A.length();
        int n = B.length();
        int[][] dp = new int[m + 1][n + 1];
        //初始化
        int i, j;
        for (j = 0; j <= n; j++) {
            dp[0][j] = 0;
        }
        for (i = 0; i <= m; i++) {
            dp[i][0] = 0;
        }
        for (i = 1; i <= m; i++) {
            for (j = 1; j <= n; j++) {
                //如果A的最后一个不在其中,或者是B的最后一个不在其中的情况
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                //如果最后一个都在其中
                if (A.charAt(i - 1) == B.charAt(j - 1)) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);    //+1 !!!
                }
            }
        }

        //获得了dp数组,dfs获取结果
        Set<String> set = new TreeSet<>();
        dfs("", m, n, A, B, dp, set);

        //打印结果
        for (String s : set) {
            System.out.println(s);
        }

    }

    private static void dfs(String temp, int i, int j, String A, String B, int[][] dp, Set<String> set) {
        if (temp.length() == dp[A.length()][B.length()]) {
            set.add(new StringBuilder(temp).reverse().toString());
            return;
        }
        if (A.charAt(i - 1) == B.charAt(j - 1)) {   //只有相等时才添加
            temp += A.charAt(i - 1);
            dfs(temp, i - 1, j - 1, A, B, dp, set);
        } else {
            //上边更大
            if (dp[i - 1][j] >= dp[i][j - 1]) {
                dfs(temp, i - 1, j, A, B, dp, set);
            }
            //左边更大
            if (dp[i][j - 1] >= dp[i - 1][j]) {
                dfs(temp, i, j - 1, A, B, dp, set);
            }
        }
    }

2、LintCode 29 Interleaving String

【问题】交错字符串。给出三个字符串:s1、s2、s3,判断s3是否由s1和s2交叉构成。

输入:s1=“aabcc” s2=“dbbac”, s3=“aadbbcbcac” 

输出:True( s3=“aadbbcbcac” )

【分析】首先如果s3的长度不等于s1+s2的长度,直接输出false,设s1的长度为n,s2的长度为m,s3的长度为n+m,从最后一步出发,假设s3是由s1和s2交错构成的,那么s3的最后一个字符,要么是s1的最后一个字符,要么是s2的最后一个字符。这就是两种情况:

  • 如果是s1的最后一个字符,那么s3[0...n+m-2]是由s1[0..n-2]与s2[0..m-1]交错形成的
  • 如果是s2的最后一个字符,那么s3[0...n+m-2]是由s1[0..n-1]与s2[0..m-2]交错形成的

这两种情况只要一种成立即可。

【状态】dp[s][i][j]为s3前s个字符是否由A前i个字符A[0..i-1]和B前j个字符B[0..j-1]交错形成,这是最直观的,由于s = i + j,便可以开成两维,设dp[i][j]为s3前i+j个字符是否由A前i个字符 A[0..i-1]和B前j个字符B[0..j-1]交错形成。

【转移方程】dp[i][j] = (dp[i-1] [j] && s1[i] == s3[i+j-1]) || (dp[i][j-1] && s2[j] == s3[]i+j-1)

【初始条件】空串本身可以由s1的空串和s2的空串交错形成,dp[0][0] = true

【边界情况】如果i=0,不考虑情况一,因为没有s1[i-1];如果j=0,不考虑情况二,因为没有s2[j-1]

【计算顺序】

  • f[0] [0], f[0] [1], …, f[0] [m]
  • f[1] [0], f[1] [1], …, f[1] [m]
  • f[n] [0], f[n] [1], …, f[n] [m]

时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化

public boolean isInterleave(String s1, String s2, String s3) {
        int n = s1.length();
        int m = s2.length();
        int l = s3.length();
        if (l != n + m) {
            return false;
        }
        boolean[][] dp = new boolean[n + 1][m + 1];
        //初始化
        dp[0][0] = true;

        //需要把空串也纳入考虑
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                //如果是s1中最后一个字符
                if (i > 0 && dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)) {
                    dp[i][j] = true;
                }
                //如果是s2中最后一个字符
                if (j > 0 && dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1)) {
                    dp[i][j] = true;
                }
            }
        }
        return dp[n][m];
    }

3、LintCode 119 Edit Distance

【问题】编辑距离。给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。你总共三种操作方法:插入一个字符、删除一个字符、替换一个字符。

输入: "horse", "ros",输出: 3

解释: horse -> rorse (替换 'h' 为 'r')、rorse -> rose (删除 'r')、rose -> ros (删除 'e')

【分析】要变成一模一样,一定要有个顺序的概念,不然会做起来很麻烦,比如从左往右的顺序。A长度为m,B长度为n,编辑过后A长度为n且与B的字符顺序一样。从最后一步出发,最后一步就是让A的最后一个字符变为B的最后一个字符,一共有三种操作,每种操作考虑一番,得到以下四种情况。

  • 情况一:A最后插入B[n-1],才能转换为B,剩下要做的就是要先将A[0…m-1](前面不动)变成B[0…n-2]
  • 情况二:A最后一个字符替换为B[n-1],才能转换为B,剩下要做的就是要先将A[0…m-2]变成B[0…n-2]
  • 情况三:A删去最后一个字符,才能转换为B,剩下要做的就是要先将A[0…m-2]变成B[0…n-2]
  • 情况四:A和B最后一个字符相等,就是要先将A[0…m-2]变成B[0…n-2]

【状态】dp[i][j]代表A中前i个字符和B中前j个字符的最小编辑距离

【转移方程】dp[i][j] = min{dp[i][j-1]+1,dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1] && A[i-1] = B[j-1]}

  • 增加dp[i][j-1]+1
  • 替换dp[i-1][j-1]+1
  • 删除dp[i-1][j]+1

【初始条件】一个空串和一个长度为L的串的最小编辑距离是L

【计算顺序】

  • f[0] [0], f[0] [1], …, f[0] [m]
  • f[1] [0], f[1] [1], …, f[1] [m]
  • f[n] [0], f[n] [1], …, f[n] [m]

时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化

public int minDistance(String A, String B) {
        int n = A.length();
        int m = B.length();

        int[][] dp = new int[n + 1][m + 1];
        int i, j;
        //初始化,空串到任意非空串的编辑距离
        for (j = 0; j <= m; j++) {
            dp[0][j] = j;
        }
        for (i = 0; i <= n; i++) {
            dp[i][0] = i;
        }

        for (i = 1; i <= n; i++) {
            for (j = 1; j <= m; j++) {
                dp[i][j] = Math.min(dp[i][j - 1] + 1, Math.min(dp[i - 1][j - 1] + 1, dp[i - 1][j] + 1));
                if (A.charAt(i - 1) == B.charAt(j - 1)) {
                    dp[i][j] = Math.min(dp[i][j], dp[i - 1][j - 1]);
                }
            }
        }
        return dp[n][m];
    }

4、LintCode 118 Distinct Subsequences

【问题】给定字符串 ST,计算 S 的所有子序列中有多少个 T。子序列字符串是原始字符串删除一些(或零个)字符之后得到的字符串,并且要求剩下的字符的相对位置不能改变。(比如 "ACE"ABCDE 的一个子序列, 而 "AEC" 不是)

输入: S = "rabbbit", T = "rabbit"
输出: 3
解释: 你可以删除 S 中的任意一个 'b', 所以一共有 3 种方式得到 T.

输入: S = "abcd", T = ""
输出: 1
解释: 只有删除 S 中的所有字符这一种方式得到 T

【分析】给定序列A和B,问B在A中出现多少次,可以不连续。相当于A和B的LCS是B,但这的侧重点是B。 从最后一步出发,就是B的最后一个字符,设A的长度为n,B的长度为m,有两种情况:

  • B[m-1] != A[n-1],需要考虑A[0…n-2]与B[0…m-1]
  • B[m-1] = A[n-1],只需考虑A[0…n-2]与B[0…m-2]
  • 问次数,就是考虑加法,无重复无遗漏。

【转移方程】dp[i][j] = dp[i-1][j] + dp[i-1][j-1] && A[i-1]=B[i-1]

【初始条件】考虑空串

  • 若A是空串,B不是空串,B在A中出现次数为0,dp[0][j] = 0
  • 若B是空串,B在A中出现次数是1(A可以是空串),就是把A中的字符都删掉dp[i][0] = 1

【计算顺序】

  • f[0] [0], f[0] [1], …, f[0] [m]
  • f[1] [0], f[1] [1], …, f[1] [m]
  • f[n] [0], f[n] [1], …, f[n] [m]

时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化成O(N)

public int numDistinct(String A, String B) {
        int n = A.length();
        int m = B.length();

        int[][] dp = new int[n + 1][m + 1];
        int i, j;
        //初始化:若A是空串而B不是空串,则出现次数为0
        for (j = 1; j <= m; j++) {
            dp[0][j] = 0;
        }
        //初始化:若B是空串,则出现次数为1
        for (i = 0; i <= n; i++) {
            dp[i][0] = 1;
        }
        for (i = 1; i <= n; i++) {
            for (j = 1; j <= m; j++) {
                dp[i][j] = dp[i - 1][j];
                if (A.charAt(i - 1) == B.charAt(j - 1)) {
                    dp[i][j] += dp[i - 1][j - 1];
                }
            }
        }
        return dp[n][m];
    }

5、LintCode 154 Regular Expression Matching

【问题】正则表达式匹配。实现支持.*的正则表达式匹配。.匹配任意一个字母。*匹配零个或者多个前面的元素。匹配应该覆盖整个输入字符串,而不仅仅是一部分。

需要实现的函数是:bool isMatch(string s, string p)

isMatch("aa","a") → false

isMatch("aa","aa") → true

isMatch("aaa","aa") → false

isMatch("aa", "a*") → true

isMatch("aa", ".*") → true

isMatch("ab", ".*") → true

isMatch("aab", "c*a*b") → true

【分析】从最后一步出发,关注最后进来的字符。假设A的长度为n,B的长度为m,关注正则表达式B的最后一个字符是谁,它有三种可能,正常字符*.

  1. 如果B的最后一个字符是正常字符,那就是看A[n-1]是否等于B[m-1],相等则看A[0..n-2]B[0..m-2],不等则是不能匹配,break

  2. 如果B的最后一个字符是.,它能匹配任意字符,直接看A[0..n-2]B[0..m-2]

  3. 如果B的最后一个字符是*它代表B[m-2]=c可以重复0次或多次,它们是一个整体c*

    • 情况一:A[n-1]是0个c,B最后两个字符废了,能否匹配取决于A[0…m-1]和B[0…n-3]是否匹配
    • 情况二:A[n-1]是多个c中的最后一个(这种情况必须A[n-1]=c或者c='.'),所以A匹配完往前挪一个,B继续匹配,因为可以匹配多个,继续看A[0…n-2]和B[0…m-1]是否匹配。

【转移方程】dp[i] [j]代表A的前i个和B的前j个能否匹配

  • 对于1和2,可以合并成一种情况dp[i][j] = dp[i-1][j-1] (if A[i-1]=B[j-1] || B[j-1]='.')

  • 对于3,分为不看c*和看c*两种情况

    • 不看:直接砍掉dp[i][j] = dp[i][j-2]
    • 看:dp[i][j] = dp[i-1][j](if A[i-1]=B[j-2] || B[j-2]='.')

【初始条件】考虑空串空正则

  • 空串和空正则是匹配的,dp[0][0] = true
  • 非空串和空正则必不匹配,dp[1][0]=...=dp[n][0]=false
  • 空串和非空正则,不能直接定义true和false,必须要计算出来。(在1、2中不能计算,在3中dp[i][j] = dp[i][j-2]可能出现,比如A="",B=a*b*c*
  • 大体上可以分为空正则和非空正则两种

【计算顺序】

  • f[0] [0], f[0] [1], …, f[0] [m]
  • f[1] [0], f[1] [1], …, f[1] [m]
  • f[n] [0], f[n] [1], …, f[n] [m]

时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化成O(N)

public boolean isMatch(String A, String B) {
        int n = A.length();
        int m = B.length();

        boolean[][] dp = new boolean[n + 1][m + 1];
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                //分为空正则与非空正则两种讨论
                if (j == 0) {
                    if (i == 0) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = false;
                    }
                } else {
                    //非空正则,大致分为最后一个是不是 *
                    if (B.charAt(j - 1) != '*') {
                        if (i > 0 && (A.charAt(i - 1) == B.charAt(j - 1) || B.charAt(j - 1) == '.')) {
                            dp[i][j] = dp[i - 1][j - 1];
                        }
                    } else {
                        //最后一个是 * ,分为不看和看两种情况
                        //不看
                        if (j >= 2) {
                            dp[i][j] |= dp[i][j - 2];
                        }
                        //看
                        if (i >= 1 && j >= 2 && (A.charAt(i - 1) == B.charAt(j - 2) || B.charAt(j - 2) == '.')) {
                            dp[i][j] |= dp[i - 1][j];
                        }
                    }
                }
            }
        }
        return dp[n][m];
    }

6、LintCode 192 Wildcard Matching

【问题】通配符匹配,上一题是正则表达式匹配。判断两个可能包含通配符*的字符串是否匹配。匹配规则如下:?可以匹配任何单个字符,* 可以匹配任意字符串(包括空字符串)。两个串完全匹配才算匹配成功。

【分析】通配符匹配和正则表达式匹配很像,正则表达式中的.与通配中的?作用是一样的,不同的是*,正则表达式中的*能匹配零个或者多个前面的元素,通配中的*能匹配0个或多个任意字符,实际上通配的情况要比正则表达式中的情况简单得多。仍然从B的最后一个字符出发,有三种可能:正常字符?*,讨论如下:(前两条情况和正则表达式一样)

  1. 如果B的最后一个字符是正常字符,那就是看A[m-1]是否等于B[n-1],相等则看A[0…m-2]与B[0…n-2],不等则是不能匹配,break

  2. 如果B的最后一个字符是,它能匹配任意字符,直接看A[0…m-2]与B[0…n-2]

  3. 如果B的最后一个字符是*,他能匹配0个或多个任意字符,那就分为两种情况

    • 匹配0个:就是这个*直接废了,需要看A[0..n-1]B[0..m-2]
    • 匹配多个:则需要看A[0..n-2]B[0..m-1]

【转移方程】dp[i] [j]代表A的前i个和B的前j个能否匹配

  • 对于1和2,可以合并成一种情况dp[i][j] = dp[i-1][j-1] (if A[i-1]=B[j-1] || B[j-1]='?')
  • 对于3,分为不看c*和看c*两种情况
    • 匹配0个,就是不看,直接砍掉:dp[i][j] = dp[i][j-1]
    • 匹配多个:dp[i][j] = dp[i-1][j](if B[j-1]='*')

【初始条件】大体上依旧是分为空正则和非空正则两种

  • 空正则和空串匹配
  • 空正则和非空串必不匹配
  • 非空正则和空串需要看情况

【计算顺序】

  • f[0] [0], f[0] [1], …, f[0] [m]
  • f[1] [0], f[1] [1], …, f[1] [m]
  • f[n] [0], f[n] [1], …, f[n] [m]

时间复杂度O(NM),空间复杂度O(NM),可以用滚动数组优化成O(N)

public boolean isMatch(String A, String B) {
        int n = A.length();
        int m = B.length();

        boolean[][] dp = new boolean[n + 1][m + 1];

        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                //空正则与非空正则两种情况讨论
                if (j == 0) {
                    if (i == 0) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = false;
                    }
                } else {
                    //分为最后一个字符是不是 * 的两种情况
                    if (B.charAt(j - 1) != '*') {
                        if (i > 0 && (A.charAt(i - 1) == B.charAt(j - 1) || B.charAt(j - 1) == '?')) {
                            dp[i][j] = dp[i - 1][j - 1];
                        }
                    } else {
                        //分为匹配0个和匹配多个两种情况,这里j必定>1
                        //匹配0个
                        dp[i][j] |= dp[i][j - 1];

                        if (i > 0) {
                            dp[i][j] |= dp[i - 1][j];
                        }
                    }
                }
            }
        }
        return dp[n][m];
    }

7、LintCode 668 Ones And Zeroes

【问题】假设你分别是 m个 0 和 n个 1 的统治者。 另一方面, 有一个只包含 01 的字符串构成的数组。现在你的任务是找到可以由 m个 0 和 n个 1 构成的字符串的最大个数。每一个 01 均只能使用一次

输入:["10", "0001", "111001", "1", "0"] 5 3
输出:4
解释:这里总共有 4 个字符串可以用 5个 0s 和 3个 1s来构成, 它们是 "10", "0001", "1", "0"。

输入:["10", "0001", "111001", "1", "0"] 7 7
输出:5
解释:所有字符串都可以由7个 0s 和 7个 1s来构成.

【分析】如果没有0,只有1,这就相当于背包问题。这边只是多了个0,用背包思路考虑,看最后一个物品有没有进去,就是分为放和不放两种情况:

  • 情况一:不放,最后一个字符串(物品)没有进去,一共给定了T个串,那就是去看前T-1个串中,用给的0和1最多能组成多少个01串
  • 情况二:放,最后一个字符串(物品)进去了,最后一个串中有多少个0和1,那么就在m和n中减去,比如最后一个串中有j个0,k个1,那么剩下0就是m-j,剩下1就是n-k,看这些剩下的在前T-1个串中最多能组成多少个。

【转移方程】用dp[i][j][k]代表前i个串最多能有多少个被j个0和k个1组成

  • dp[i][j][k] = max{dp[i-1][j][k],dp[i-1][j-a][k-b]},a代表放的这个01串中0的个数,b代表放的这个01串中1的个数。

【转移方程】前0个串,最多组成0个

  • f[0][0~m][0~n] = 0

【答案】dp[T][m][n],len为字符串的个数

时间复杂度:O(Tmn),空间复杂度:O(Tmn),可以用滚动数组优化至 O(mn)

public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;

        if (len == 0) {
            return 0;
        }
        int[][][] dp = new int[len + 1][m + 1][n + 1];
        int i, j, k;
        //初始化
        for (j = 0; j <= m; j++) {
            for (k = 0; k <= n; k++) {
                dp[0][j][k] = 0;
            }
        }
        for (i = 1; i <= len; i++) {
            for (j = 0; j <= m; j++) {
                for (k = 0; k <= n; k++) {
                    //不放
                    dp[i][j][k] = dp[i - 1][j][k];
                    //放
                    String s = strs[i - 1];
                    char[] chs = s.toCharArray();
                    int count0 = 0, count1 = 0;
                    for (int l = 0; l < chs.length; l++) {
                        if (chs[l] == '0') {
                            count0++;
                        } else {
                            count1++;
                        }
                    }
                    if (j >= count0 && k >= count1) {
                        dp[i][j][k] = Math.max(dp[i][j][k], dp[i - 1][j - count0][k - count1] + 1);
                    }
                }
            }
        }
        return dp[len][m][n];
    }

滚动数组优化,当前的i之和前一个i-1有关联,空间复杂度O(mn)

public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;

        if (len == 0) {
            return 0;
        }
        int[][][] dp = new int[2][m + 1][n + 1];
        int i, j, k;
        //初始化
        for (j = 0; j <= m; j++) {
            for (k = 0; k <= n; k++) {
                dp[0][j][k] = 0;
            }
        }
        int old = 0, now = 0;
        for (i = 1; i <= len; i++) {
            old = now;
            now = 1 - now;
            for (j = 0; j <= m; j++) {
                for (k = 0; k <= n; k++) {
                    //不放
                    dp[now][j][k] = dp[old][j][k];
                    //放
                    String s = strs[i - 1];
                    char[] chs = s.toCharArray();
                    int count0 = 0, count1 = 0;
                    for (int l = 0; l < chs.length; l++) {
                        if (chs[l] == '0') {
                            count0++;
                        } else {
                            count1++;
                        }
                    }
                    if (j >= count0 && k >= count1) {
                        dp[now][j][k] = Math.max(dp[now][j][k], dp[old][j - count0][k - count1] + 1);
                    }
                }
            }
        }
        return dp[now][m][n];
    }
发布了43 篇原创文章 · 获赞 6 · 访问量 3907

猜你喜欢

转载自blog.csdn.net/weixin_44424668/article/details/104017428