九章算法题解记录【五】动态规划II

继续上一篇的的单序列

1 Palindrome Partitioning II http://www.lintcode.com/problem/palindrome-partitioning-ii/

public class Solution {
    /**
     * @param s: A string
     * @return: An integer
     */
    public int minCut(String s) {
        // 错误输入条件
        if (s == null) {
            return 0;
        }
        // 状态定义 dp[i]表示从0到i位置的最小分割次数
        int[] dp = new int[s.length() + 1];

        // 初始化
        for (int i = 0; i <= s.length() ; i++) {
            dp[i] = i - 1;
        }

        // 递归求解
        for (int i = 2; i <= s.length(); i++) {
            for (int j = i; j >= 1; j--) {
                if (isPalindrome(s.substring(j - 1, i))){
                    dp[i] = Math.min(dp[i], dp[j - 1] + 1);
                }
            }
        }
        // 答案
        return dp[s.length()];
    }

    private boolean isPalindrome(String str){
        if (str == null){
            return true;
        }
        int start = 0, end = str.length() - 1;
        while (start <= end){
            if (str.charAt(start++) != str.charAt(end--)){
                return false;
            }
        }
        return true;
    }
}

debug了很久,原因1 是substring的时候也不能忽略了一个字母的情况,这样就是前面的+1,假如没考虑到 就会出错。。。还有一个trick是要new int(length + 1)的数组,并dp = -1 从而全部满足 dp[start] + 1。第三个注意点是假如序列+1 那么第一层循环是 <=
目前这种情况勉强过了leetcode100%,LintCode OOM了只过了96%。。可能要再做优化,但是先看大局。。
优化方案:再加一个二维矩阵保存i到j是否回文。假如没有才计算一遍。(Redis、MySQL缓存思想么???似乎一切都是通的。。)

2 Word Break http://www.lintcode.com/problem/word-break/

public class Solution {
    /*
     * @param s: A string
     * @param dict: A dictionary of words dict
     * @return: A boolean
     */
    public boolean wordBreak(String s, Set<String> dict) {
        // 错误输入条件
        if (s == null || dict == null) {
            return false;
        }
        // 状态定义 dp[i]从0到i全部在dict中
        boolean[] dp = new boolean[s.length() + 1];

        // 初始化
        dp[0] = true;

        // 递归求解
        for (int i = 1; i <= s.length(); i++) {
            for (int j = i; j >= 1; j--) {
                if (dict.contains(s.substring(j - 1, i))){
                    dp[i] |= dp[j - 1];
                }
            }
        }
        // 答案
//        for (int i = 0; i < s.length(); i++) {
//            System.out.print(dp[i] + " ");
//        }
        return dp[s.length()];
    }
}

又过了91%。。。本题开始没A的原因是 dp[i]找到一个包含的就break, 应该是其中只要有就行,最后用了|=才能过91%。。

后来优化成:

public class Solution {
    /*
     * @param s: A string
     * @param dict: A dictionary of words dict
     * @return: A boolean
     */
    public boolean wordBreak(String s, Set<String> dict) {
        // 错误输入条件
        if (s == null || dict == null) {
            return false;
        }
        int minLength = getMinLength(dict);

        // 状态定义 dp[i]从0到i全部在dict中
        boolean[] dp = new boolean[s.length() + 1];

        // 初始化
        dp[0] = true;

        // 递归求解
        for (int i = 1; i <= s.length(); i++) {
            for (int j = Math.max(i - minLength + 1, 1); j >= 1; j--) {
                if (dict.contains(s.substring(j - 1, i))){
                    dp[i] |= dp[j - 1];
                }
            }
        }
         //答案
        for (int i = 0; i < s.length(); i++) {
            System.out.print(dp[i] + " ");
        }
        return dp[s.length()];
    }

    private int getMinLength(Set<String> dict) {
        int minLength = Integer.MAX_VALUE;
        for (String s : dict) {
            if (s.length() < minLength) {
                minLength = s.length();
            }
        }
        return minLength;
    }
}

使用了一个MinLength来缩减所需要计算的范围,但是测试用例中dict很小,String很长,这样实际上不能减少多少,依然91%,于是改成了要MaxLength来缩减。

public class Solution {
    /*
     * @param s: A string
     * @param dict: A dictionary of words dict
     * @return: A boolean
     */
    public boolean wordBreak(String s, Set<String> dict) {
        // 错误输入条件
        if (s == null || dict == null) {
            return false;
        }
        int maxLength = getLongestLength(dict);

        // 状态定义 dp[i]从0到i全部在dict中
        boolean[] dp = new boolean[s.length() + 1];

        // 初始化
        dp[0] = true;

        // 递归求解
        for (int i = 1; i <= s.length(); i++) {
            for (int j = Math.max(i - maxLength, 0); j < i; j++) {
                if (dp[j] && dict.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break;
                }
            }
        }
         //答案
//        for (int i = 0; i < s.length(); i++) {
//            System.out.print(dp[i] + " ");
//        }
        return dp[s.length()];
    }

    private int getLongestLength(Set<String> dict) {
        int maxLength = 0;
        for (String s : dict) {
            if (s.length() > maxLength) {
                maxLength = s.length();
            }
        }
        return maxLength;
    }
}

最后勉强过了UT,还是得结合测试用例做文章。
同时这里把|=的判断 改成了dp[i] && contains 则true并break,这样也进行了一些剪枝。
PS. Leetcode中dict是一个List,假如把他变成一个dict则与本题相同,同时beat95%在leetcode上。LintCode真的很严格!

三 Two Sequences Dp
state: f[i][j]代表了第一个sequence的前i个数字
/字符 配上第二个sequence的前j个…
function: f[i][j] = 研究第i个和第j个的匹配关系
intialize: f[i][0] 和 f[0][i]
answer: f[s1.length()][s2.length()]

3 Longest Common Subsequence http://www.lintcode.com/problem/longest-common-subsequence/

public class Solution {
    /**
     * @param A: A string
     * @param B: A string
     * @return: The length of longest common subsequence of A and B
     */
    public int longestCommonSubsequence(String A, String B) {
        // 错误输入条件
        if (A == null || B == null) return 0;
        // 状态定义 dp[i][j] 从i到j的LCS
        int[][] dp = new int[A.length() + 1][B.length() + 1];

        // 初始化

        // 递归求解
        for (int i = 0; i < A.length(); i++) {
            for (int j = 0; j < B.length(); j++) {
                if (A.charAt(i) == B.charAt(j)){
                    dp[i + 1][j + 1] = Math.max(dp[i][j + 1], dp[i + 1][j]);
                    dp[i + 1][j + 1] = Math.max(dp[i][j] + 1, dp[i + 1][j + 1]);
                } else {
                    dp[i + 1][j + 1] = Math.max(dp[i + 1][j], dp[i][j + 1]);
                }
            }
        }
        // 答案
        return dp[A.length()][B.length()];
    }
}

本题开始没AC原因是状态转移方程没写出来,应该是:

f[i][j] = MAX(f[i-1][j], f[i][j-1], f[i-1][j-1] + 1) // a[i] == b[j]
        = MAX(f[i-1][j], f[i][j-1]) // a[i] != b[j]

改了之后一遍AC,还是得画个矩阵并结合一些case来确定转移条件,双序列最重要的是三个方向:左上!左!右! 与2Dmatrix区别在于双序列并无主次之分,所以有三个方向。

得出一个小trick,序列型问题一般都要比长度大1

4 Longest Common SubString http://www.lintcode.com/problem/longest-common-substring/

public int longestCommonSubstring(String A, String B) {
        if (A == null || A.length() == 0 || B == null || B.length() == 0){
            return 0;
        }

        // 状态定义 dp[i][j] 表示从i到j的LCSubString
        int[][] dp = new int[A.length() + 1][B.length() + 1];

        // 初始化 第一行第一列为0

        // 递归求解
        for (int i = 0; i < A.length(); i++) {
            for (int j = 0; j < B.length(); j++) {
                if (A.charAt(i) == B.charAt(j)){
                    dp[i + 1][j + 1] = dp[i][j] + 1;
                } else {
                    dp[i + 1][j + 1] = 0;
                }
            }
        }
        // 答案  MAX(f[0..a.length()][0..b.length()])
        int max = Integer.MIN_VALUE;
        for (int i = 0; i <= A.length(); i++) {
            for (int j = 0; j <= B.length(); j++)
            max = Math.max(max, dp[i][j]);
        }
        return max;
    }

转移条件是:

function: f[i][j] = f[i-1][j-1] + 1 // a[i] == b[j]
                  = 0 // a[i] != b[j]

同时,答案是 dp矩阵中所有元素的最大值。

解题思路: 一刷未AC,把答案想的太简单,总是想要在终止位置得到结果,但实际上结果可以保存在整个dp矩阵中,并不会提升时空复杂度!
SubString问题的启发:在dp矩阵全部值中得到最大值!

5 Edit Distance http://www.lintcode.com/problem/edit-distance/

public class EditDistance {
    public int minDistance(String word1, String word2) {
        int size1 = word1.length();
        int size2 = word2.length();

        //子串都应包含空串,所以长度都+1
        int[][] dp = new int[size1 + 1][size2 + 1];

        for (int i = 0; i <= size1; i++) {
            dp[i][0] = i;
        }
        for (int j = 0; j <= size2; j++) {
            dp[0][j] = j;
        }

        //都从不为空串的第一个子串开始
        for (int i = 1; i <= size1; i++) {
            for (int j = 1; j <=size2; j++) {
                if (word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1];
                } else {
                    dp[i][j] = Math.min(Math.min(dp[i-1][j-1], dp[i][j-1]), dp[i-1][j]) + 1;
                }
            }
        }
        return dp[size1][size2];

    }
}

四步走,转移方程三个方向!

state: f[i][j]a的前i个字符最少要用几次编辑可 以变成b的前j个字符
function:   f[i][j] = MIN(f[i-1][j]+1, f[i][j-1]+1, f[i-1][j-1]) // a[i] == b[j]
                    = MIN(f[i-1][j]+1, f[i][j-1]+1, f[i-1][j-1]+1) // a[i] != b[j]
intialize: f[i][0] = i, f[0][j] = j
answer: f[a.length()][b.length()]

6 Distinct Subsequences http://www.lintcode.com/problem/distinct-subsequences/

class Solution {
    /**
     * @param S: A string
     * @param T: A string
     * @return: Count the number of distinct subsequences
     */
    public int numDistinct(String S, String T) {
        // 错误输入条件
        if (S == null || T == null){
            return 0;
        }

        // 状态定义 dp[i][j]为长度为i的子串T在长度为j的母串S中出现次数
        int[][] dp = new int[T.length() + 1][S.length() + 1];

        // 初始化
        for (int i = 0; i <= S.length(); i++) {
            dp[0][i] = 1;
        }
        // 递归求解
        for (int i = 1; i <= T.length(); i++) {
            for (int j = 1; j <= S.length(); j++) {
                if (T.charAt(i - 1) == S.charAt(j - 1)){
                    dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1];
                } else {
                    dp[i][j] = dp[i][j - 1];
                }
            }
        }
        // 答案
        return dp[T.length()][S.length()];
    }
}

本题首先状态定义,应该i是子串j是母串,每次增加母串,相等时则分为用此结尾和不用此结尾
不等时则说明加不加这个结尾都一样。
注意初始化时 子串为空 则母串0 - slength 都为1.
理解了很久,说明双序列需要确定长度,有主次。

可以优化空间复杂度,只用一维矩阵,但是目前i与i i-1都有关,因此改写成i是母串 j是子串形式,依然AC

class Solution {
    /**
     * @param S: A string
     * @param T: A string
     * @return: Count the number of distinct subsequences
     */
    public int numDistinct(String S, String T) {
        // 错误输入条件
        if (S == null || T == null){
            return 0;
        }

        // 状态定义 dp[i][j]为长度为i的子串T在长度为j的母串S中出现次数
        int[][] dp = new int[S.length() + 1][T.length() + 1];

        // 初始化
        for (int i = 0; i <= S.length(); i++) {
            dp[i][0] = 1;
        }
        // 递归求解
        for (int i = 1; i <= S.length(); i++) {
            for (int j = 1; j <= T.length(); j++) {
                if (S.charAt(i - 1) == T.charAt(j - 1)){
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        // 答案
        return dp[S.length()][T.length()];
    }
}

此时,i只与i-1有关,进行矩阵压缩。

class Solution {
    /**
     * @param S: A string
     * @param T: A string
     * @return: Count the number of distinct subsequences
     */
    public int numDistinct(String S, String T) {
        // 错误输入条件
        if (S == null || T == null){
            return 0;
        }

        // 状态定义 dp[i][j]为长度为i的子串T在长度为j的母串S中出现次数
        int[] dp = new int[T.length() + 1];

        // 初始化
        dp[0] = 1;

        // 递归求解
        for (int i = 1; i <= S.length(); i++) {
            for (int j = T.length(); j >= 1; j--) {
                if (S.charAt(i - 1) == T.charAt(j - 1)){
                    dp[j] = dp[j] + dp[j - 1];
                } else {
                    dp[j] = dp[j];
                }
            }
        }
        // 答案
        return dp[T.length()];
    }
}

轻松改成一维dp  注意此时j循环是逆序的,背包思想无脑改。

7 Interleaving String http://www.lintcode.com/problem/interleaving-string/
开始的错误代码

public class InterleavingString {
    /**
     * @param s1: A string
     * @param s2: A string
     * @param s3: A string
     * @return: Determine whether s3 is formed by interleaving of s1 and s2
     */
    public boolean isInterleave(String s1, String s2, String s3) {
        // 错误输入条件,排除了所有为null的情况
        if (s3 == null || s3.length() == 0){
            if ((s1 == null || s1.length() == 0) && (s2 == null || s2.length() == 0)){
                return true;
            } else {
                return false;
            }
        } else {
            if (s1 == null || s1.length() == 0){
                if (s2.equals(s3)){
                    return true;
                } else {
                    return false;
                }
            } else if (s2 == null || s2.length() == 0){
                if (s1.equals(s3)){
                    return true;
                } else {
                    return false;
                }
            }
        }


        if ((s1.length() + s2.length()) != s3.length()){
            return false;
        }

        // 状态定义  dp[i][j]表示 str1的0 - i 与 str2的 0 - j 可以构成长度为i + j的交叉字符串
        boolean[][] dp = new boolean[s1.length() + 1][s2.length() + 1];


        // 初始化
        dp[0][0] = true;

        // 递归求解
        for (int i = 0; i < s1.length(); i++) {
            for (int j = 0; j < s2.length(); j++) {
                if (dp[i][j]){
                    if (s1.charAt(i) == s3.charAt(i + j)){
                        dp[i + 1][j] = true;
                    }
                    if (s2.charAt(j) == s3.charAt(i + j)){
                        dp[i][j + 1] = true;
                    }
                }
            }
        }
//         答案
        for (int i = 0; i <= s1.length() ; i++) {
            for (int j = 0; j <= s2.length(); j++) {
                System.out.print(dp[i][j] + " ");
            }
            System.out.println("");
        }

        return (dp[s1.length()][s2.length() - 1] && s2.charAt(s2.length() - 1) == s3.charAt(s1.length() + s2.length() - 1)) ||
                (dp[s1.length() - 1][s2.length()] && s1.charAt(s1.length() - 1) == s3.charAt(s1.length() + s2.length() - 1));
    }
}

解题思路:
本题的dp方程定义是对的,但是有几点错误:

  1. 初始化条件时,只初始化了原点,应该尽力匹配,完全初始化第一列的结果
  2. 转换方程想错了思路,用该点的位置来推后面的点。。。违背了DP的基本思路。。

修改的状态转化方程如下:

dp[i][j] = dp[i - 1][j] | dp[i][j - 1]     s1.charAt(i - 1) == s3.charAt(i + j - 1) == s2.charAt(j - 1) (第一次做木有考虑到这种情况)
dp[i][j] = dp[i - 1][j]               s1.charAt(i - 1) == s3.charAt(i + j - 1);
dp[i][j] = dp[i][j - 1]               s2.charAt(j - 1) == s3.charAt(i + j - 1);

改正后的代码如下:

public class Solution {
    /**
     * @param s1: A string
     * @param s2: A string
     * @param s3: A string
     * @return: Determine whether s3 is formed by interleaving of s1 and s2
     */
    public boolean isInterleave(String s1, String s2, String s3) {
        // 错误输入条件,排除了所有为null的情况
        if (s3 == null || s3.length() == 0){
            if ((s1 == null || s1.length() == 0) && (s2 == null || s2.length() == 0)){
                return true;
            } else {
                return false;
            }
        } else {
            if (s1 == null || s1.length() == 0){
                if (s2.equals(s3)){
                    return true;
                } else {
                    return false;
                }
            } else if (s2 == null || s2.length() == 0){
                if (s1.equals(s3)){
                    return true;
                } else {
                    return false;
                }
            }
        }


        if ((s1.length() + s2.length()) != s3.length()){
            return false;
        }

        // 状态定义  dp[i][j]表示 str1的0 - i 与 str2的 0 - j 可以构成长度为i + j的交叉字符串
        boolean[][] dp = new boolean[s1.length() + 1][s2.length() + 1];


        // 初始化
        dp[0][0] = true;
        for (int i = 0; i < s1.length(); i++) {
            if (s1.charAt(i) == s3.charAt(i)){
                dp[i + 1][0] = true;
            } else {
                break;
            }
        }
        for (int i = 0; i < s2.length(); i++) {
            if (s2.charAt(i) == s3.charAt(i)){
                dp[0][i + 1] = true;
            } else {
                break;
            }
        }

        // 递归求解
        for (int i = 1; i <= s1.length(); i++) {
            for (int j = 1; j <= s2.length(); j++) {
                if (s1.charAt(i - 1) == s3.charAt(i + j - 1)){
                    dp[i][j] |= dp[i - 1][j];
                }
                if (s2.charAt(j - 1) == s3.charAt(i + j - 1)){
                    dp[i][j] |= dp[i][j - 1];
                }
            }
        }

        return dp[s1.length()][s2.length()];
    }
}

注意用的是 |= 只要上边右边有一个满足即可。
双序列在使用了dp[+1][+1]之后,还是尽量从1开始到<= charAt(i - 1) dp[i][j] = dp[i - 1][j - 1]此类。
从此统一!不然在charAt(i + j的时候可能会出现不知名bug)…

四 其他类别问题之背包类
8 Backpack https://www.lintcode.cn/problem/backpack/discuss (从此变成cn域名,一秒进入)

public class BackPack {
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @return: The maximum size
     */
    public int backPack(int m, int[] A) {
        if (m == 0 || A == null || A.length == 0){
            return 0;
        }
        // 状态定义 dp[i + 1][j]为前 i 个物品中选出重量不超过j时总价值的最大值
        int[][] dp = new int[A.length + 1][m + 1];

        // 初始化  均为0

        // 递归求解
        for (int i = 0; i < A.length; i++) {
            for (int j = 1; j <= m ; j++) {
                if (A[i] > j){
                    dp[i + 1][j] = dp[i][j];
                } else {
                    dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - A[i]] + A[i]);
                }
            }
        }

        // 答案
        return dp[A.length][m];

    }
}

01背包问题, 价值等于重量。其实贪心也能做。一遍DP方法AC,关键点在于一个物品不断放大他的重量,其实可以优化成一维空间的。。。

9 Backpack II https://www.lintcode.cn/problem/backpack-ii/description

public class Solution {
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @param V: Given n items with value V[i]
     * @return: The maximum value
     */
    public int backPackII(int m, int[] A, int[] V) {
        if (m == 0 || A == null || A.length == 0){
            return 0;
        }
        // 状态定义 dp[i + 1][j]为前 i 个物品中选出重量不超过j时总价值的最大值
        int[][] dp = new int[A.length + 1][m + 1];

        // 初始化  均为0

        // 递归求解
        for (int i = 0; i < A.length; i++) {
            for (int j = 1; j <= m ; j++) {
                if (A[i] > j){
                    dp[i + 1][j] = dp[i][j];
                } else {
                    dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - A[i]] + V[i]);
                }
            }
        }

        // 答案
        return dp[A.length][m];
    }
}

01背包。 稍微改了一点转移方程。一遍AC

10 最小调整代价 https://www.lintcode.cn/problem/minimum-adjustment-cost/description
这道题比较难想。。dp[i][j]表示第i的数变成j的最小调整代价。
对于第i个数,其取值为j的话,那么i-1的时候,其取值范围为[j-target][j+target]。则转移方程可知为:

for k = j-target to j+target:
	dp[i][j]=min(dp[i][j], dp[i-1][k]+abs(j-A[i]))

代码如下:也是很多校招题的母题,考虑每个数的所有可能性,最后遍历求出最小代价

public class Solution {
    /*
     * @param A: An integer array
     * @param target: An integer
     * @return: An integer
     */
    public int MinAdjustmentCost(List<Integer> A, int target) {
        if (A == null || A.size() == 0) return 0;
        int max = getMax(new ArrayList<Integer>(A));
        // 状态定义  dp[i][j] 表示走到第i个数,并且把其变成j的最小步数 (j = 0 ~ 100)
        int[][] dp = new int[A.size()][max + 1];
        // 初始化
        for (int i = 0; i <= max ; i++) {
            dp[0][i] = Math.abs(i - A.get(0));
        }

        // 递归求解  dp
        for (int i = 1; i < A.size() ; i++) {
            for (int j = 1; j <= max; j++) {
                dp[i][j] = Integer.MAX_VALUE;
                for(int k = Math.max(1, j - target); k <= Math.min(max, j + target); k++){
                    dp[i][j] = Math.min(dp[i][j], dp[i - 1][k] + Math.abs(j - A.get(i)));
                }
            }
        }

        // 答案
        int min = Integer.MAX_VALUE;
        for (int i = 1; i <= max; i++) {
            min = Math.min(min, dp[A.size() - 1][i]);
        }
        return min;
    }

    private int getMax(ArrayList<Integer> A) {
        int max = A.get(0);
        for (int i = 1; i < A.size(); i++) {
            max = Math.max(max, A.get(i));
        }
        return max;
    }
}

debug了很久。。。
1.初始化条件时,就要把dp[0][i]给赋值成 k - A[i];
2. k的上界下界要加条件限制
3. 要把所有的代价要包含一个绝对值。
4. target == 0时不能直接返回0.。

最后的答案是 j取遍 0 - max的最小代价。

11 ksum https://www.lintcode.cn/problem/k-sum/description
经典题,三维dp并优化成二维
先写三维:

public class Solution {
    public int kSum(int[] A, int k, int target) {
        if (A == null || A.length == 0 || k <= 0 || target <= 0) {
            return 0;
        }
        int sum = 0;
        for (int i = 0; i < A.length; i++) {
            sum += A[i];
        }
        if (sum < target) {
            return 0;
        }
        if (sum == target) {
            return 1;
        }

        // 方程定义 dp[i][j][k]表示 前0 - i个数中选j个 和为k的方案数总和
        int[][][] dp = new int[A.length + 1][k + 1][target + 1];

        // 初始化 选k个和为0有一种
        for (int i = 0; i <= A.length; i++) {
            dp[i][0][0] = 1;
        }

        // 递推
        for (int i = 1; i <= A.length; i++) {
            for (int j = 1; j <= k && j <= i; j++) {
                for (int t = 0; t <= target; t++) {
                    if (A[i - 1] <= t) {
                        dp[i][j][t] = dp[i - 1][j - 1][t - A[i - 1]] + dp[i - 1][j][t]; //放的进去,分为两种情况 放不放,方案数量相加
                    } else {
                        dp[i][j][t] = dp[i - 1][j][t]; // 给一个初始值,放不进去就等于它的兄弟
                    }
                }
            }
        }
        return dp[A.length][k][target];
    }
}

开始没做出来是没想好状态转换方程,同时写了A[i - 1] < t 导致没有加,后来多加了一个等号就行。。

不能优化成2D空间,因为每次递推与i-1有关,j t并没有形成只与上一个有关的性质。

五 其他类别问题之区间类

博弈DP
1 硬币排成线 https://www.lintcode.cn/problem/coins-in-a-line/note/177781

public class Solution {
    /**
     * @param n: An integer
     * @return: A boolean which equals to true if the first player will win
     */
    public boolean firstWillWin(int n) {
        if (n <= 0) {
            return false;
        }
        if (n == 1 || n == 2) {
            return true;
        }
        
        boolean[] dp = new boolean[n + 1];
        dp[1] = true;
        dp[2] = true;
        for (int i = 3; i <= n; i++) {
            dp[i] = !dp[i - 1] || !dp[i - 2];
        }
        return dp[n];
    }
}

解题思路 上两步先手假如能赢,则这步先手必败 也可直接 return !(n % 3 == 0)

2 硬币排成线II https://www.lintcode.cn/problem/coins-in-a-line-ii/description

public class Solution {
    public boolean firstWillWin(int[] values) {
        int n = values.length;
        if(values==null || n==0){
            return false;
        }
        if(n <= 2){
            return true;
        }

        int[] DP = new int[n+1];
        DP[n]=0;
        DP[n-1]=values[n-1];
        DP[n-2]=values[n-1]+values[n-2];
        DP[n-3]=values[n-2]+values[n-3];

        for(int i=n-4;i>=0;i--){
            //我们取一个数或取两个数
            DP[i]=values[i]+Math.min(DP[i+2],DP[i+3]);
            DP[i]=Math.max(DP[i],values[i]+values[i+1]+Math.min(DP[i+3],DP[i+4]));
        }

        int sum=0;
        for(int i:values){
            sum+=i;
        }

        return DP[0]*2>sum;
    }

}

解题思路:
定义dp[i]表示从i到end能取到的最大值
当我们在i处,有两种选择:
1.若取values[i],对方可以取values[i+1] 或者values[i+1] + values[i+2]
当对方取values[i+1] 后 ,我们只能从 i+2 到end内取,我们所取得最大值是dp[i+2], 注意:对方所选取的结果一定是使得我们以后选取的值最小
当对方取values[i+1] + values[i+2]后,我们只能从i+3到end内取,我们所取得最大值是dp[i+3]。
此时:dp[i] = values[i] + min(dp[i+2],dp[i+3]) , 注意:对方所选取的结果一定是使得我们以后选取的值最小
2.若取values[i] + values[i+1],对方可取values[i+2] 或者values[i+2] + values[i+3]
当对方取values[i+2]后,我们只能从i+3到end内取,我们取得最大值是dp[i+3]
当对方取values[i+2]+values[i+3]后,我们只能从i+4到end内去,我们取得最大值是dp[i+4]
此时:dp[i] = values[i] + values[i+1]+min(dp[i+3],dp[i+4])
这里的取最小值和上面一样的意思,对方选取过之后的值一定是使得我们选取的值最小,对方不傻并且还很聪明
最后我们可以取上面两个dp[i]的最大值,就是答案,这里意思是:对方留得差的方案中,我们选取的最大值。

3 硬币排成线III https://www.lintcode.cn/problem/coins-in-a-line-iii/description

class Solution {
    /**
     * @param values: a vector of integers
     * @return: a boolean which equals to true if the first player will win
     */
    public boolean firstWillWin(int[] values) {
        int n = values.length;
        if (n <= 2) return true;
        int[][] dp = new int[n][n];

        for (int i = 0; i < n; i++)
            dp[i][i] = values[i];

        for (int i = n-2; i >= 0; i--)
            for (int j = i+1; j < n; j++)
                dp[i][j] = Math.max(values[i] - dp[i+1][j], values[j] - dp[i][j-1]);

        return dp[0][n-1] > 0;
    }
};

解题思路:
定义dp[i][j]表示从i到j这个区间,先手能比后手多拿的分数
对于某一时刻的状态i,j,只有两个拿硬币的方法,一个是从左,一个是从右,选取其中的最大值,也就是
dp[i][j] = max(values[i] - dp[i+1][j], values[j] - dp[i][j-1])
对于初始状态,dp[i][i] = values[i],因为只有一个硬币。
trick:n为偶数时,先手是必胜的。

4 Scramble String https://www.lintcode.cn/problem/scramble-string/description

public class Solution {
    /**
     * @param s1: A string
     * @param s2: Another string
     * @return: whether s2 is a scrambled string of s1
     */
    public boolean isScramble(String s1, String s2) {
        if (s1.length() != s2.length()) {
            return false;
        }
        int len = s1.length();
        boolean[][][] dp = new boolean[len][len][len];
        // dp[i][j][k-1] == true: s1[i:i+k] is scramble of s2[j:j+k]
        for (int k = 0; k < len; k++) {
            for (int i = 0; i + k < len; i++) {
                for (int j = 0; j + k < len; j++) {
                    if (k == 0) {
                        dp[i][j][k] = s1.charAt(i) == s2.charAt(j);
                    } else {
                        for (int l = 0; l < k && !dp[i][j][k]; l++) {
                            dp[i][j][k] = dp[i][j][l] && dp[i + l + 1][j + l + 1][k - l - 1] ||
                                    dp[i][j + k - l][l] && dp[i + l + 1][j][k - l - 1];
                        }
                    }
                }
            }
        }
        return dp[0][0][len - 1];
    }
}

解题思路:
太困了,随便找的别人的答案,区间型DP,有空再看。
动态规划。三维动态规划数组dp[i][j][k]表示存在s1[i:i+k]的二叉树表示t1和s2[j:j+k]的二叉树表示t2,使得t1和t2只相差一次左右子树置换
计算dp[i][j][k]时,l从1遍历到k-1,用l对s1[i:i+k]和s2[j:j+k]进行分割,判断分割后的字符串是否相等或互为置换
注:实际代码中为了减少dp数组大小,将上述所有k映射为k-1,所有l映射为l-1

或者

这是一道三维动态规划的题目,我们提出维护量res[i][j][n],其中i是s1的起始字符编号,j是s2的起始字符编号,
而n是当前的字符串长度;res[i][j][len]表示的是:以i和j分别为s1和s2起点的长度为len的字符串是不是互为scramble。
有了维护量我们接下来看看递推式,也就是怎么根据历史信息来得到res[i][j][len]。
判断这个是不是满足,其实我们首先是把当前s1[i,……,i+len-1]字符串劈一刀分成两部分,然后分两种情况:
第一种情况是:左边和s2[j,……,j+len-1]左边部分是不是scramble,以及右边和s2[j…j+len-1]右边部分是不是scramble;
第二种情况是:左边和s2[j…j+len-1]右边部分是不是scramble,以及右边和s2[j…j+len-1]左边部分是不是scramble。
以上两种情况只要有一种成立,就说明s1[i,……,i+len-1]和s2[j,……,j+len-1]是scramble的。
对于判断这些左右部分是不是scramble,我们是有历史信息的,
因为长度小于n的所有情况我们都在前面求解过了(也就是长度是最外层循环)。
上面说的是劈一刀的情况,对于s1[i,……,i+len-1]我们有len-1种劈法,
在这些劈法中只要有一种成立,那么两个串就是scramble的。
总结起来递推式如下:
res[i][j][len] ||=((res[i][j][k]&&res[i+k][j+k][len-k])||(res[i][j+len-k][k]&&res[i+k][j][len-k]))
对于所有1<=k<len,也就是对于所有len-1种劈法的结果求 或运算。
因为信息都是计算过的,对于每种劈法只需要常量操作即可完成,因此求解递推式是需要O(len)(因为len-1种劈法)。
总时间复杂度:因为是三维动态规划,需要三层循环,加上每一步需要线行时间求解递推式,所以是O(n^4)。
虽然已经比较高了,但是至少不是指数量级的,动态规划还是有很大优势的,空间复杂度是O(n^3)。

明天复习整理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值