leetcode dp [LeetCode] Unique Binary Search Trees 矩阵中从左上角到右下角的路径条数

[编程题] 股票交易日

时间限制:3秒

空间限制:32768K

在股市的交易日中,假设最多可进行两次买卖(即买和卖的次数均小于等于2),规则是必须一笔成交后进行另一笔(即买-卖-买-卖的顺序进行)。给出一天中的股票变化序列,请写一个程序计算一天可以获得的最大收益。请采用实践复杂度低的方法实现。

给定价格序列prices及它的长度n,请返回最大收益。保证长度小于等于500。

测试样例:

[10,22,5,75,65,80],6
返回:87

一次交易的变版,只要从前后各计算一次,然后累加即可。(动态规划)

/** 
         * 求最大收益,交易次数小于等于两次 
         * @param prices 每一天的股票价格 
         * @param n 天数 
         * @return 返回小于等于两次交易所获取的最大利润 
         */  
        public static int maxProfix(int[] prices, int n) {  
            //第i天之前的最大利益  
            int[] preProfit = new int[n];  
            //第i天之后的最大  
            int[] postProfit = new int[n];  
            //总的最大利润  
            int max = Integer.MIN_VALUE;  
              
            //如果今天的价格减掉最小价格比截止到昨天的最大收益大,就用今天的价格减去最小价格,否则,用截止到昨天的最大收益  
            int minBuy = prices[0];  
            for(int i = 1;i<n;i++){  
                minBuy = Math.min(minBuy, prices[i]);  
                preProfit[i] = Math.max(preProfit[i-1], prices[i] - minBuy);  
            }  
            //如果最大价格减掉今天价格比明天以后买入的最大收益大,就用最大价格减掉今天价格,否则,用明天以后买入的最大收益  
            int maxSell = prices[n-1];  
            for(int i = n-2;i>=0;i--){  
                maxSell = Math.max(maxSell, prices[i]);  
                postProfit[i] = Math.max(postProfit[i+1], maxSell-prices[i]);  
            }  
            //求出两次交易的和,与总的最大利润进行比较  
            for(int i = 0;i<n;i++){  
                max = Math.max(preProfit[i] + postProfit[i], max);  
            }  
            return max;  
        }  

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

这道题实际上是 Catalan Number卡塔兰数的一个例子

我们先来看当 n = 1的情况,只能形成唯一的一棵二叉搜索树,n分别为1,2,3的情况如下所示:

                    1                        n = 1

                2        1                   n = 2
               /          
              1            2
  
   1         3     3      2      1           n = 3
           /     /      /       
     3     2     1      1   3      2
    /     /                        
   2     1         2                 3

就跟斐波那契数列一样,我们把n = 0 时赋为1,因为空树也算一种二叉搜索树,那么n = 1时的情况可以看做是其左子树个数乘以右子树的个数,左右字数都是空树,所以1乘1还是1。那么n = 2时,由于1和2都可以为跟,分别算出来,再把它们加起来即可。n = 2的情况可由下面式子算出:

dp[2] =  dp[0] * dp[1]   (1为根的情况)

    + dp[1] * dp[0]    (2为根的情况)

同理可写出 n = 3 的计算方法:

dp[3] =  dp[0] * dp[2]   (1为根的情况)

    + dp[1] * dp[1]    (2为根的情况)

      + dp[2] * dp[0]    (3为根的情况)

由此可以得出卡塔兰数列的递推式为:

leetcode dp
[LeetCode] Unique Binary Search Trees
矩阵中从左上角到右下角的路径条数

我们根据以上的分析,可以写出代码如下:

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

264. Ugly Number II

We have an array k of first n ugly number. We only know, at the beginning, the first one, which is 1. Then

k[1] = min( k[0]x2, k[0]x3, k[0]x5). The answer is k[0]x2. So we move 2's pointer to 1. Then we test:

k[2] = min( k[1]x2, k[0]x3, k[0]x5). And so on. Be careful about the cases such as 6, in which we need to forward both pointers of 2 and 3.

x here is multiplication.

class Solution {
public:
    int nthUglyNumber(int n) {
        if(n <= 0) return false; // get rid of corner cases 
        if(n == 1) return true; // base case
        int t2 = 0, t3 = 0, t5 = 0; //pointers for 2, 3, 5
        vector<int> k(n);
        k[0] = 1;
        for(int i  = 1; i < n ; i ++)
        {
            k[i] = min(k[t2]*2,min(k[t3]*3,k[t5]*5));
            if(k[i] == k[t2]*2) t2++; 
            if(k[i] == k[t3]*3) t3++;
            if(k[i] == k[t5]*5) t5++;
        }
        return k[n-1];
    }
};

198. House Robber

这道题的本质相当于在一列数组中取出一个或多个不相邻数,使其和最大。那么我们对于这类求极值的问题首先考虑动态规划Dynamic Programming来解,我们维护一个一位数组dp,其中dp[i]表示到i位置时不相邻数能形成的最大和,那么递推公式怎么写呢,我们先拿一个简单的例子来分析一下,比如说nums为{3, 2, 1, 5},那么我们来看我们的dp数组应该是什么样的,首先dp[0]=3没啥疑问,再看dp[1]是多少呢,由于3比2大,所以我们抢第一个房子的3,当前房子的2不抢,所以dp[1]=3,那么再来看dp[2],由于不能抢相邻的,所以我们可以用再前面的一个的dp值加上当前的房间值,和当前房间的前面一个dp值比较,取较大值当做当前dp值,所以我们可以得到递推公式dp[i] = max(num[i] + dp[i - 2], dp[i - 1]), 由此看出我们需要初始化dp[0]和dp[1],其中dp[0]即为num[0],dp[1]此时应该为max(num[0], num[1]),代码如下:

public int rob(int[] num) {
    int[][] dp = new int[num.length + 1][2];
    for (int i = 1; i <= num.length; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
        dp[i][1] = num[i - 1] + dp[i - 1][0];
    }
    return Math.max(dp[num.length][0], dp[num.length][1]);
}

还有一种解法,核心思想还是用DP,分别维护两个变量a和b,然后按奇偶分别来更新a和b,这样就可以保证组成最大和的数字不相邻,代码如下:

解法二:

 public int rob(int[] nums) {
        int n = nums.length;
        if (n == 0) return 0;
        if (n == 1) return nums[0];
        if (n > 2)
            nums[2] += nums[0];
        for (int i = 3; i < n; i++)
                nums[i] += Math.max(nums[i-2], nums[i-3]);
        return Math.max(nums[n-1], nums[n-2]);
    }

 213. House Robber II

这道题是之前那道House Robber 打家劫舍中的解题方法,然后调用两边取较大值

private int rob(int[] num, int lo, int hi) {
    int include = 0, exclude = 0;
    for (int j = lo; j <= hi; j++) {
        int i = include, e = exclude;
        include = e + num[j];
        exclude = Math.max(e, i);
    }
    return Math.max(include, exclude);
}

public int rob(int[] nums) {
    if (nums.length == 1) return nums[0];
    return Math.max(rob(nums, 0, nums.length - 2), rob(nums, 1, nums.length - 1));
}

279. Perfect Squares

给我们一个正整数,求它最少能由几个完全平方数组成。

1.Dynamic Programming: 440ms

class Solution 
{
public:
    int numSquares(int n) 
    {
        if (n <= 0)
        {
            return 0;
        }
        
        // cntPerfectSquares[i] = the least number of perfect square numbers 
        // which sum to i. Note that cntPerfectSquares[0] is 0.
        vector<int> cntPerfectSquares(n + 1, INT_MAX);
        cntPerfectSquares[0] = 0;
        for (int i = 1; i <= n; i++)
        {
            // For each i, it must be the sum of some number (i - j*j) and 
            // a perfect square number (j*j).
            for (int j = 1; j*j <= i; j++)
            {
                cntPerfectSquares[i] = 
                    min(cntPerfectSquares[i], cntPerfectSquares[i - j*j] + 1);
            }
        }
        
        return cntPerfectSquares.back();
    }
};

2.Static Dynamic Programming: 12ms

class Solution 
{
public:
    int numSquares(int n) 
    {
        if (n <= 0)
        {
            return 0;
        }
        
        // cntPerfectSquares[i] = the least number of perfect square numbers 
        // which sum to i. Since cntPerfectSquares is a static vector, if 
        // cntPerfectSquares.size() > n, we have already calculated the result 
        // during previous function calls and we can just return the result now.
        static vector<int> cntPerfectSquares({0});
        
        // While cntPerfectSquares.size() <= n, we need to incrementally 
        // calculate the next result until we get the result for n.
        while (cntPerfectSquares.size() <= n)
        {
            int m = cntPerfectSquares.size();
            int cntSquares = INT_MAX;
            for (int i = 1; i*i <= m; i++)
            {
                cntSquares = min(cntSquares, cntPerfectSquares[m - i*i] + 1);
            }
            
            cntPerfectSquares.push_back(cntSquares);
        }
        
        return cntPerfectSquares[n];
    }
};

why solution 1 and solution 2 have the same time complexity but run with such a difference in time?

Hi, the difference is that vector is defined as static in second one. So, when the Leetcode test cases are run, it would reuse previously computed results (available in static vector) from the earlier run for the new test cases. So, even though the logic is same, static vector<int> cntPerfectSquares is reused across all tests, speeding up the overall time to run the tests!

 300. Longest Increasing Subsequence

O(nlogn)解法:

tails is an array storing the smallest tail of all increasing subsequences with length i+1 in tails[i].
For example, say we have nums = [4,5,6,3], then all the available increasing subsequences are:

len = 1 : [4], [5], [6], [3] => tails[0] = 3
len = 2 : [4, 5], [5, 6] => tails[1] = 5
len = 3 : [4, 5, 6] => tails[2] = 6

We can easily prove that tails is a increasing array. Therefore it is possible to do a binary search in tails array to find the one needs update.

Each time we only do one of the two:

(1) if x is larger than all tails, append it, increase the size by 1
(2) if tails[i-1] < x <= tails[i], update tails[i]
Doing so will maintain the tails invariant. The the final answer is just the size.

public int lengthOfLIS(int[] nums) {
    int[] tails = new int[nums.length];
    int size = 0;
    for (int x : nums) {
        int i = 0, j = size;
        while (i != j) {
            int m = (i + j) / 2;
            if (tails[m] < x)
                i = m + 1;
            else
                j = m;
        }
        tails[i] = x;
        if (i == size) ++size;
    }
    return size;
}
// Runtime: 2 ms

求解两个字符串的最长公共子序列

一,问题描述

给定两个字符串,求解这两个字符串的最长公共子序列(Longest Common Sequence)。比如字符串1:BDCABA;字符串2:ABCBDAB

则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA

二,算法求解

这是一个动态规划的题目。对于可用动态规划求解的问题,一般有两个特征:①最优子结构;②重叠子问题

①最优子结构

设 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y)

找出LCS(X,Y)就是一个最优化问题。因为,我们需要找到X 和 Y中最长的那个公共子序列。而要找X 和 Y的LCS,首先考虑X的最后一个元素和Y的最后一个元素。

1)如果 xn=ym,即X的最后一个元素与Y的最后一个元素相同,这说明该元素一定位于公共子序列中。因此,现在只需要找:LCS(Xn-1,Ym-1)

LCS(Xn-1,Ym-1)就是原问题的一个子问题。为什么叫子问题?因为它的规模比原问题小。(小一个元素也是小嘛....)

为什么是最优的子问题?因为我们要找的是Xn-1 和 Ym-1 的最长公共子序列啊。。。最长的!!!换句话说,就是最优的那个。(这里的最优就是最长的意思)

2)如果xn != ym,这下要麻烦一点,因为它产生了两个子问题:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)

因为序列X 和 序列Y 的最后一个元素不相等嘛,那说明最后一个元素不可能是最长公共子序列中的元素嘛。(都不相等了,怎么公共嘛)。

LCS(Xn-1,Ym)表示:最长公共序列可以在(x1,x2,....x(n-1)) 和 (y1,y2,...yn)中找。

LCS(Xn,Ym-1)表示:最长公共序列可以在(x1,x2,....xn) 和 (y1,y2,...y(n-1))中找。

求解上面两个子问题,得到的公共子序列谁最长,那谁就是 LCS(X,Y)。用数学表示就是:

LCS=max{LCS(Xn-1,Ym),LCS(Xn,Ym-1)}

由于条件 1)  和  2)  考虑到了所有可能的情况。因此,我们成功地把原问题 转化 成了 三个规模更小的子问题。

这里可以使用递归或dp,dp比递归好在用了动态规划之后,有些子问题 是通过 “查表“ 直接得到的,而不是重新又计算一遍得到的。递归会重复计算

来看看,原问题是:LCS(X,Y)。子问题有 ❶LCS(Xn-1,Ym-1)    ❷LCS(Xn-1,Ym)    ❸LCS(Xn,Ym-1)

初一看,这三个子问题是不重叠的。可本质上它们是重叠的,因为它们只重叠了一大部分。举例:

第二个子问题:LCS(Xn-1,Ym) 就包含了:问题❶LCS(Xn-1,Ym-1),为什么?

因为,当Xn-1 和 Ym 的最后一个元素不相同时,我们又需要将LCS(Xn-1,Ym)进行分解:分解成:LCS(Xn-1,Ym-1) 和 LCS(Xn-2,Ym)

也就是说:在子问题的继续分解中,有些问题是重叠的。

说了这么多,还是要写下最长公共子序列的递归式才完整。借用网友的一张图吧:)

leetcode dp
[LeetCode] Unique Binary Search Trees
矩阵中从左上角到右下角的路径条数

c[i,j]表示:(x1,x2....xi) 和 (y1,y2...yj) 的最长公共子序列的长度。(是长度哦,就是一个整数嘛)。

public class LCSequence {
    
    //求解str1 和 str2 的最长公共子序列
    public static int LCS(String str1, String str2){
        int[][] c = new int[str1.length() + 1][str2.length() + 1];
        for(int row = 0; row <= str1.length(); row++)
            c[row][0] = 0;
        for(int column = 0; column <= str2.length(); column++)
            c[0][column] = 0;
        
        for(int i = 1; i <= str1.length(); i++)
            for(int j = 1; j <= str2.length(); j++)
            {
                if(str1.charAt(i-1) == str2.charAt(j-1))
                    c[i][j] = c[i-1][j-1] + 1;
                else if(c[i][j-1] > c[i-1][j])
                    c[i][j] = c[i][j-1];
                else
                    c[i][j] = c[i-1][j];
            }
        return c[str1.length()][str2.length()];
    }
    
    //test
    public static void main(String[] args) {
        String str1 = "BDCABA";
        String str2 = "ABCBDAB";
        int result = LCS(str1, str2);
        System.out.println(result);
    }
}

字符串编辑距离(Levenshtein距离)算法

给定两个字符串Q和T,对于T我们允许三种操作:

(1) 在任意位置添加任意字符
(2) 删除存在的任意字符
(3) 修改任意字符

问最少操作多少次可以把字符串T变成Q?

给定两个字符串 t 和 q,算法构造一个|t|+1 行, |q|+1 列的矩阵 D 来计算它们之间的编辑距离,用 D[i][j]表示 t[i]和 q[j]之间的编辑距离。

显然,对 1≤i≤|t|有 D[i][0]=i, 对1≤j≤|q|有 D[0][j]=j,比如说t[0]对q[5]的编辑距离,q[5]长度有5,t[0]需要增加5个字母才能相同。

公式是:

$D[i][j]=min(D[i-1][j]+1,D[i][j-1]+1,D[i-1][j-1]+δ) $    当$t[i]=q[j]$时, $δ=0$;否则 $δ=1$。

///和LCS不同,lcs是找最长公共子串,是找相同,所以 t[i]=q[j]时加1,编辑距离是找不同, t[i]=q[j]时不变。

int dp[1010][1010];
void work() {
    for(int i=1; i<=lena; i++) dp[i][0] = i;
    for(int j=1; j<=lenb; j++) dp[0][j] = j;
    for(int i=1; i<=lena; i++)
        for(int j=1; j<=lenb; j++)
            if(a[i-1]==b[j-1])
                dp[i][j] = dp[i-1][j-1];
            else
                dp[i][j] = min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j]))+1;
    printf("%d
", dp[lena][lenb]);
}

矩阵中从左上角到右下角的路径条数

题目:给定一个n*m矩阵,求从左上角到右下角总共存在多少条路径,每次只能向右走或者向下走。

可以用迭代法执行动态规划,这样做可以减少dp的维度,只需要一维数组即可,不过要改变dp的含义,假设当前所在位置为(m, n),那么dp[n]代表从左上角到达当前位置的不同路径书,原因为

移动方向只能向右和向下,所以只要向下移动一行,就不能再返回上一行。这说明,对于行的记录是可以忽略的。
只要遍历每一行的每一列,不断更新dp[n]直到到达右下角即可,假设当前在第m行的第n列,那么dp[n]表示从左上角开始移动可以有多少条不同的路径达到第m行第n列
对于每个位置,它可以从上一行的当前列向下移动到达当前位置,也可以从当前行的上一列向右移动到达当前位置
所以dp[n] = dp[n] + dp[n - 1],dp[n]代表从上一行的第n列到达右下角的不同路径数,dp[n - 1]代表从当前行的第n-1列到达右下角的不同路径数
直到遍历到右下角,即遍历了所有行所有列,那么dp[n]就代表从左上角到达右下角的不同路径数

class Solution {
public:
    int uniquePaths(int m, int n) {
        /* 到达第0行,第j列的路径数只有1条 */
        vector<int> dp(n, 1);
        /* 这里直接忽略了第0行和第0列,原因是到达第0行和第0列的某个位置的路径都只有1条 */
        for(int i = 1; i < m; ++i)
        {
            for(int j = 1; j < n; ++j)
            {
                /* dp[j]表示从左上角到达第i行第j列的不同路径数 */
                /* 当i为m-1,j为n-1时,就是从左上角到达右下角的不同路径数 */
                dp[j] = dp[j] + dp[j - 1];
            }
        }
        return dp[n - 1];
    }
};

 

256. Paint House 粉刷房子

There are a row of n houses, each house can be painted with one of the three colors: red, blue or green. The cost of painting each house with a certain color is different. You have to paint all the houses such that no two adjacent houses have the same color.

The cost of painting each house with a certain color is represented by a n x 3 cost matrix. For example, costs[0][0] is the cost of painting house 0 with color red; costs[1][2] is the cost of painting house 1 with color green, and so on... Find the minimum cost to paint all houses.

example:

Given costs = [[14,2,11],[11,14,5],[14,3,10]] return 10

house 0 is blue, house 1 is green, house 2 is blue, 2 + 5 + 3 = 10

Note:
All costs are positive integers.

解题思路:

房子i的最小涂色开销是房子i-1的最小涂色开销,加上房子i本身的涂色开销。但是房子i的涂色方式需要根据房子i-1的涂色方式来确定,所以我们对房子i-1要记录涂三种颜色分别不同的开销,这样房子i在涂色的时候,我们就知道三种颜色各自的最小开销是多少了。我们在原数组上修改,可以做到不用空间。

State: dp[i][j] // three colors: j = 0 or 1 or 2, 

Function:

    dp[i][0] = dp[i][0] + min(dp[i - 1][1], dp[i -1][2])

    dp[i][1] = dp[i][1] + min(dp[i - 1][0], dp[i - 1][2])

    dp[i][2] = dp[i][2] + min(dp[i - 1][0], dp[i - 1][1])    

Initialize: dp = costs

Return: min(dp[n][0], dp[n][1], dp[n][2])

java 1: Time: O(n), Space: O(n)

public class Solution {
    public int minCost(int[][] costs) {
        int len = costs.length;
        if(costs != null && len == 0) return 0;
        int[][] dp = costs;
        for(int i = 1; i < len; i++){
            dp[i][0] = costs[i][0] + Math.min(costs[i - 1][1], costs[i - 1][2]);
            dp[i][1] = costs[i][1] + Math.min(costs[i - 1][0], costs[i - 1][2]);
            dp[i][2] = costs[i][2] + Math.min(costs[i - 1][0], costs[i - 1][1]);
        }
        return Math.min(dp[len - 1][0], Math.min(dp[len - 1][1], dp[len - 1][2]));
    }
   
    public static void main(String args[]) {
        int[][] costs = new int[][]{{14,2,11},{11,14,5},{14,3,10}};
        Solution sol = new Solution();
        System.out.println(sol.minCost(costs));
    }
}

Java 2: Time: O(n), Space: O(1)

class Solution {
    public int minCost(int[][] costs) {
        if(costs != null && costs.length == 0) return 0;       
        // 直接用原数组
        for(int i = 1; i < costs.length; i++){
            // 涂第一种颜色的话,上一个房子就不能涂第一种颜色,这样我们要在上一个房子的第二和第三个颜色的最小开销中找最小的那个加上
            costs[i][0] = costs[i][0] + Math.min(costs[i - 1][1], costs[i - 1][2]);
            // 涂第二或者第三种颜色同理
            costs[i][1] = costs[i][1] + Math.min(costs[i - 1][0], costs[i - 1][2]);
            costs[i][2] = costs[i][2] + Math.min(costs[i - 1][0], costs[i - 1][1]);
        }
        // 返回涂三种颜色中开销最小的那个
        return Math.min(costs[costs.length - 1][0], Math.min(costs[costs.length - 1][1], costs[costs.length - 1][2]));
    }
}