动态规划专题(III)

接上一篇

11. Minimum Path Sum

Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right whichminimizes the sum of all numbers along its path.

Note: You can only move either down or right at any point in time.

简单的动态规划题目,与上一篇文章中的Unique Path类似。dp[i][j]表示从左上角到第i行第j列的路径最短长度,最终答案就是dp[m][n]。因为一个格子只有可能从左边到达或者从上面到达,因此递推关系如下

                   dp[i][j-1]+grid[i][j]   i == 0

dp[i][j] = {    dp[i-1][j] + grid[i][j]   j == 0

                   min(dp[i-1][j], dp[i][j-1]} + grid[i][j]   others

与Unique Path那道题一样,这里的dp数组可以直接在grid中实现。

代码如下:

public class Solution {
    public int minPathSum(int[][] grid) {
        int i=0,j=0,m = grid.length,n = grid[0].length;
        for(i = 0;i < m;i++){
            for(j = 0;j < n;j++){
               if(i == 0){
                   if(j == 0) continue;
                   grid[i][j] += grid[i][j-1];
               }else if(j == 0){
                   if(i == 0) continue;
                   grid[i][j] += grid[i-1][j];
               }else{
                    grid[i][j] += Math.min(grid[i-1][j],grid[i][j-1]);     
               }
            }//
        }//for
        return grid[m-1][n-1];
    }
}

关于这道题多说几句,我们可以把这里的grid转换成一棵树,然后问题就变为最短路径问题

11.Climbing Stairs

You are climbing a stair case. It takes n steps to reach to the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

更加简单,直接上代码:

public class Solution {
    public int climbStairs(int n) {
       if(n == 0 || n == 1) return 1;
       int[] re = new int[n+1];
       re[0] = 1; re[1] = 1;
       for(int i = 2;i<=n;i++) re[i] = re[i-2] + re[i-1];
       return re[n];
    }
}

12. Edit Distance

Given two words word1 and word2, find the minimum number of steps required to convertword1 toword2. (each operation is counted as 1 step.)

You have the following 3 operations permitted on a word:

a) Insert a character
b) Delete a character
c) Replace a character

这道题目乍一看肯定会觉得很难,尤其跟前面几道题相比,但是如果你能发觉它与上面几道题的联系就不觉得它有那么难了。之所以觉得难,或许是因为它没有前面的题那么直观吧。把不同的领域(这里就是字符串与图形了)进行类比本身就是一种能力,也是一种解题的方法。那么它们的联系到底在哪里呢?前面比如爬楼梯问题,每一个当前台阶有可能是由它的前一个爬上来的,也有可能是它的前两个。那么这里也一样,每一个word1中的当前字符既有可能是word2中当前字符插入一个后匹配的,也有可能是删除一个后匹配的,还有可能是替换一个后匹配的,现在是不是就感觉简单了?

设dp[i][j]表示word1的前i个字符与word2中前j个字符的最短编辑距离,那么递推关系如下:

                 j if i == 0 or  if j == 0

dp[i][j] = {  

                min{dp[i-1][j-1], dp[i][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1]+1} others 括号中分别是 word1[i] == word2[j],以及word1[i] != word2[j]时插入,删除,替换操作对应的递推关系。

上面需要注意的一点就是即使word1[i] == word2[j],dp[i][j]也不一定就是dp[i-1][j-1],比如word1 = "hello"  word2 = "heello"

代码如下:

public class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length(),len2 = word2.length();
        if(word1.isEmpty()) return len2;
        if(word2.isEmpty()) return len1;
        int[][] d = new int[len1+1][len2+1];
        for(int i = 0;i <= len1;i++){
            for(int j = 0;j <= len2;j++){
                int min = Integer.MAX_VALUE;
                if(i == 0){ d[i][j] = j;  continue;}
                if(j == 0){ d[i][j] = i; continue;}
                char ch1 = word1.charAt(i-1);
                char ch2 = word2.charAt(j-1);
                if(ch1 == ch2){ min = Math.min(min,d[i-1][j-1]);}
                min = Math.min(d[i-1][j]+1,min);
                min = Math.min(d[i][j-1]+1,min);
                min = Math.min(d[i-1][j-1]+1,min);
                d[i][j] = min;
            }
        }
        return d[len1][len2];
    }
}


13.Scramble String

Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty substrings recursively.

Below is one possible representation of s1 ="great":

    great
   /    \
  gr    eat
 / \    /  \
g   r  e   at
           / \
          a   t

To scramble the string, we may choose any non-leaf node and swap its two children.

For example, if we choose the node "gr" and swap its two children, it produces a scrambled string"rgeat".

    rgeat
   /    \
  rg    eat
 / \    /  \
r   g  e   at
           / \
          a   t

We say that "rgeat" is a scrambled string of"great".

Similarly, if we continue to swap the children of nodes"eat" and"at", it produces a scrambled string"rgtae".

    rgtae
   /    \
  rg    tae
 / \    /  \
r   g  ta  e
       / \
      t   a

We say that "rgtae" is a scrambled string of"great".

Given two strings s1 and s2 of the same length, determine ifs2 is a scrambled string ofs1.

这道题目我之前是用回溯法做的,现在尝试用动态规划来做。

这里猜测是三维dp,设dp[i][j][k]表示s1的i到j与s2的k到k+j-i是否匹配,那么递推关系就是:

dp[i][j][k] = true  if i==j,同时s1[i] == s2[k]

dp[i][j][k] = (dp[i][m][k] & dp[m+1][j][k+m-i]) | (dp[i]][m][k+j-m] & dp[m+1][j][k])  i<=m<j others

Java代码

public class Solution {
    public boolean isScramble(String s1, String s2) {
        int l1 = s1.length(),l2 = s2.length();
		if(l1!=l2) return false;
		boolean[][][] dp = new boolean[l1+1][l1+1][l2+1];
		for(int j = 1; j <= l1;j++ ){
			for(int i = j;i >= 1;i--){
				for(int k = 1;k <= l2-j+i;k++){
					if(i == j && s1.charAt(i-1) == s2.charAt(k-1)){
						dp[i][j][k] = true;
						continue;
					}
					boolean res = false;
					for(int m = i;m<j;m++)
						res |= (dp[i][m][k]&dp[m+1][j][k+m-i+1])|(dp[i][m][k+j-m] & dp[m+1][j][k]);
					dp[i][j][k] = res;
				}
			}
		}
		return dp[1][l1][1];
    }
}


下面是回溯版本代码,写起来方便:

public class Solution {
    public boolean isScramble(String s1, String s2) {
        if(s1.equals(s2)) return true;
        int len1 = s1.length(), len2 = s2.length();
        if(len1 != len2) return false;
        
        int[] count = new int[26];
        for(int i = 0;i < len1;i++) {count[s1.charAt(i)-'a']++; count[s2.charAt(i)-'a']--;}
        for(int i = 0;i < 26;i++) if(count[i] != 0) return false;
        
        for(int i = 1;i < len1;i++){
            boolean res = (isScramble(s1.substring(0,i),s2.substring(0,i))&&isScramble(s1.substring(i,len1),s2.substring(i,len1)))
            ||(isScramble(s1.substring(0,i),s2.substring(len1-i,len1))&&isScramble(s1.substring(i,len1),s2.substring(0,len1-i)));
            if(res) return true;
        }
        return false;
    }
}

14.Decode Ways

A message containing letters from A-Z is being encoded to numbers using the following mapping:

'A' -> 1
'B' -> 2
...
'Z' -> 26

Given an encoded message containing digits, determine the total number of ways to decode it.

For example,
Given encoded message "12",it could be decoded as "AB" (1 2) or"L" (12).

The number of ways decoding "12" is 2.

这道题与之前的爬楼梯等题目思路也是类似的,每个当前字符位置处的编码方式的个数既有可能是在前一个的基础上过来的,也有可能是在上上个的基础上过来的。在爬楼梯题目中没有限制,但是这里限制就比较多了,比如如果当前位置的上一个位置数字是3,那么当前位置就不可能是从上上个位置过来的,因为它大于26(Z),同样即使上一个位置是2,如果当前位置数字大于6那么也不可能。还有一种比较容易遗漏的情况就是上一个位置是0的情况。这时它无法与当前位置的数字构成一个合法的字符。

代码如下:

public class Solution {
    public int numDecodings(String s) {
        if(s == null||s.length()==0||s.charAt(0) == '0') return 0;
        int len = s.length();
        int[] d = new int[len+1];
        d[0] = d[1] = 1;
        
        for(int i = 1; i < len;i++){
            int res = 10*(s.charAt(i-1)-'0')+(s.charAt(i)-'0');
            if(s.charAt(i) == '0'){
                if(s.charAt(i-1) == '0') return 0;
                if(res<=26) d[i+1] = d[i-1];
                else return 0;
            }else{
                if(s.charAt(i-1) == '0') {
                    d[i+1] = d[i-1];
                }else{
                    if(res <= 26){
                        d[i+1] = d[i] + d[i-1];
                    }else d[i+1] = d[i];
                }
            }
        }//for
        return d[len];
    }
}

15.Unique Binary Search Trees

Given n, how many structurally unique BST's (binary search trees) that store values 1...n?

For example,
Given n = 3, there are a total of 5 unique BST's.

   1         3     3      2      1
    \       /     /        / \      \
     3     2     1      1   3      2
    /     /       \                     \
   2     1         2                   3

首先需要搞清研究对象,包括它的性质。BST的定义如果不知道可以自行搜索。对于1...n,根据定义,我们知道它的左子树的值可以为1....n-1。当我们确定了左子树的元素后实际上根也就确定了,进一步右子树的元素也就确定了。那么如何用动态规划解决呢?我们发现左子树的个数乘以右子树的个数就是总共的个数。设dp[i]表示i个元素形成的BST的个数,递推关系就是

dp[i] = dp[j]*dp[n-i-1]     0<=j<n

下面分别是回溯版代码与动态规划版代码

public class Solution {
    private int[] dp;
    public int numTrees(int n) {
        dp = new int[n+1];
        return dfs(n);
    }
    public int dfs(int n){
        if(n<=1) return 1;
        if(dp[n] != 0) return dp[n];
        int res = 0;
        for(int i = 0;i < n;i++)
            res += dfs(i)*dfs(n-i-1);
        return dp[n] = res;
    }
}

public class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0] = 1; dp[1] = 1;
        for(int i = 2;i <= n;i++){
            int res = 0;
            for(int j = 0;j < i;j++){
                res += dp[j]*dp[i-j-1];
            }
            dp[i] = res;
        }
        return dp[n];
    }
}

16.Interleaving String

Given s1, s2, s3, find whethers3 is formed by the interleaving ofs1 ands2.

For example,
Given:
s1 = "aabcc",
s2 = "dbbca",

When s3 = "aadbbcbcac", return true.
When s3 = "aadbbbaccc", return false.

相当于两把梳子插在一起,性质:

1.每个字符串的元素在合并后的字符串中的相对顺序是不改变的。

2.如果s1的前i个字符与s2的前j个字符匹配,那么它们合并后的子串对应s3中的前(i+j)个。

3.s3中的第i个字符要么来自s1,要么来自s2.

还是考虑二维dp,设dp[i][j]表示s1的前i个字符与s2的前j个字符是否符合要求,那么结合前面的性质,如果s3[i+j] == s1[i] ,那么dp[i][j]就可能等于dp[i-1][j];如果s[i][j] == s2[j],那么dp[i][j]就可能等于dp[i][j-1],只要前面二者有一个为true,dp[i][j]就为true。代码如下:


public class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int len1 = s1.length();
    	int len2 = s2.length();
    	int len3 = s3.length();
    	if(len1 + len2 != len3) return false;
    	
    	boolean[][] f = new boolean[len1+1][len2+1];
    	f[0][0] = true;//第一个设置为true
    	
    	for(int i = 0; i < len1+1;i++){
    		for(int j = 0; j < len2+1; j++){
    			if(j > 0){
    				f[i][j] = f[i][j-1]&&(s3.charAt(i+j-1) == s2.charAt(j-1));
    			}
    			if(i > 0){
    				f[i][j] = f[i][j] || ( f[i-1][j]&&(s3.charAt(i+j-1) == s1.charAt(i-1)));
    			}
    		}
    	}
    	return f[len1][len2];
    }
}

17.Distinct Subsequences

Given a string S and a stringT, count the number of distinct subsequences ofT inS.

A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie,"ACE" is a subsequence of "ABCDE" while "AEC" is not).

Here is an example:
S = "rabbbit", T = "rabbit"

这个问题可以更本质的描述为这样:从一个集合S(元素可重复)中选取不同的元素构成集合T,一种有多少种方式。那么如何选取呢?可以这样,对于S中的每个元素,可以有两种操作,选或者不选,统计最后的结果中与T相同的个数即可。很明显,这里也可以用递归来做,不过效率极低。我们用动态规划来做,设dp[i][j]表示T[1.....j]在S[1...i]中的个数,那么递推关系就是如果S[i] == T[j],那么当前字符可选可不选,对应的方案数就是两种情况之和dp[i-1][j-1]+dp[i-1][j];如果S[i] != T[j],那么肯定不选,所以dp[i][j] = dp[i-1][j]。

再提一点,这里的T其实也是T与S的LCS,因此这引申出一个问题:如何求LCS的个数?我们前面的文章中讲过LCS,但是只是求解了其中一个。大家可以思考一下如何求解。

代码如下:

public class Solution {
    public int numDistinct(String s, String t) {
        int[][] dp = new int[t.length()+1][s.length()+1];
        dp[0][0] = 1;  
        for (int i = 1; i < s.length() + 1; ++i)   
            dp[0][i] = 1;  
        for (int i = 1; i < t.length() + 1; ++i)   
            dp[i][0] = 0;  
        for (int i = 1; i < t.length() + 1; ++i) {  
            for (int j = 1; j < s.length() + 1; ++j) {  
                dp[i][j] = dp[i][j - 1];  
                if (s.charAt(j - 1) == t.charAt(i - 1))  
                    dp[i][j] += dp[i - 1][j - 1];  
            }  
        }  
        return dp[t.length()][s.length()];  
    }
}

18 Best Time to Buy and Sell Stock

Say you have an array for which the ith element is the price of a given stock on dayi.

Design an algorithm to find the maximum profit. You may complete at mosttwo transactions.

Note:
You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

这里有两条重要性质:

1. 如果买卖两次,那么前一次的卖出必须在后一次的买入之前

2. 两次买卖的收益必须都是相应区间内最大的

第一条显而易见,第二条可以用反证法证明

思路正是通过上面的性质得到的,front[i]表示i之前最大收益(包括i),back[i]表示i之后的最大收益(包括i,因为可以一天内卖出后立刻买入)。遍历所有点找出front[i] + back[i]的最大值即可。代码如下:

public class Solution {
    public int maxProfit(int[] prices) {
        if(prices == null || prices.length == 0 || prices.length == 1) return 0;
        int len = prices.length;
        int[] front = new int[len];
        int[] back = new int[len];
        int low = prices[0],high = prices[len-1];
        for(int i = 1;i<len;i++){
            if(prices[i] < low) low = prices[i];
            front[i] = Math.max(prices[i] - low,front[i-1]);
        }
        high = prices[len-1];
        for(int i = len-2;i >= 0;i--){
            if(prices[i] > high) high = prices[i];
            back[i] = Math.max(high - prices[i],back[i+1]);
        }
        int max = Integer.MIN_VALUE;
        for(int i = 0;i < len;i++){
            max = Math.max(front[i]+back[i],max);
        }
        return max;
    }
}

19. Palindrome Partitioning

Given a string s, partition s such that every substring of the partition is a palindrome.

Return the minimum cuts needed for a palindrome partitioning ofs.

For example, given s = "aab",
Return 1 since the palindrome partitioning ["aa","b"] could be produced using 1 cut.

思路与切割钢条那道题是一样的,设dp[i]为s的前i个字符组成的子串所需要的最小切割次数,然后新增一个字符后最小切割次数需要遍历前面的位置j,看它到当前当前位置是否是回文串,如果是那么切割数就是dp[j-1]+1,从所有里面取最小的就是当前最小切割数。代码如下:下面代码并不是按照上面分析的顺序,而是从后向前,因为这样的话可以顺便计算某一子串是否是回文串

public class Solution {
    public int minCut(String s) {
        int len = s.length();
        boolean[][] isPalind = new boolean[len][len];
        int[] min = new int[len+1];
        min[len] = -1;
        for(int i = 0;i < len;i++)
            isPalind[i][i] = true;
        for(int i = len-1;i >= 0;i--){
            min[i] = min[i+1]+1;
            for(int j = i+1;j < len;j++){
                if(s.charAt(i) == s.charAt(j)){
                    if(i+1 == j || isPalind[i+1][j-1]){
                        isPalind[i][j] = true;
                        if(j == len-1) min[i] = 0;
                        else if(min[i] > min[j+1]+1){
                            min[i] = min[j+1] + 1;
                        }
                    }
                }
            }
        }
        return min[0];
    }
}

20.Word Break

Given a string s and a dictionary of wordsdict, add spaces ins to construct a sentence where each word is a valid dictionary word.

Return all such possible sentences.

For example, given
s = "catsanddog",
dict = ["cat", "cats", "and", "sand", "dog"].

A solution is ["cats and dog", "cat sand dog"].

不考虑动态规划的话,第一个想到的是递归,遍历从头开始不同长度的子串,如果在字典中就对剩余的子串做同样的操作。

动态规划:设dp[i]表示从开头到第i个位置的子串是否能分割成全部在字典中的单词。要求dp[i],我们需要遍历前面的不同位置j,如果dp[j]为true,并且从j+1到i的子串也在字典中,说明dp[i]也能满足。为了构造结果,需要记录分割点,最后dfs构造结果。

下面给出的程序,采用了不同的思路。遍历s中的每个位置,把从该位置开始的能够在字典中的单词加入到该位置对应的容器中,最后只需要从0开始采用回溯法构造解:

public class Solution {
    private ArrayList<String>[] scanned;  
    private List<String> results = new ArrayList<>();  
    private void dfs(String s, int from, List<String> words, Set<String> wordDict) {  
        if(from == s.length()) {  
            String result = "";  
            for(int i=0; i<words.size(); i++) {  
                if (i>0) result += " ";  
                result += words.get(i);  
            }  
            results.add(result);  
            return;  
        }  
        for(String word: scanned[from]) {  
            words.add(word);  
            dfs(s, from + word.length(), words, wordDict);  
            words.remove(words.size()-1);  
        }  
    }  
    public List<String> wordBreak(String s, Set<String> wordDict) {  
        scanned = new ArrayList[s.length()];  
        scanned[0] = new ArrayList<>();  
        boolean reachable = false;  
        for(int i=0; i<s.length(); i++) {  
            if (scanned[i] == null) continue;  
            for(String word: wordDict) {  
                if (i + word.length() <= s.length() && word.equals(s.substring(i, i+word.length()))) {  
                    scanned[i].add(word);  
                    if (i+word.length() == s.length()) reachable = true;  
                    if (i+word.length() < s.length() && scanned[i+word.length()] == null) {  
                        scanned[i+word.length()] = new ArrayList<>();  
                    }
                }
            }
        }
        if (!reachable) return results;  
        dfs(s, 0, new ArrayList<>(), wordDict);  
        return results;
    }
}










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值