LeetCode:Dynamic Programming

来源:互联网 发布:网络预警包括什么预警 编辑:程序博客网 时间:2024/04/28 02:52

3.Dynamic Programming

在刷这个专题之前,有必要温习一下DP的思想,知乎上有人总结的不错,可以参考之:

什么是动态规划?动态规划的意义是什么?中徐凯强 Andy和王勐同学的回答,他们的回答在于用浅显易懂的话来解释DP的思想,比复杂的数学推导更有意思更实际更让人不想睡觉,哈哈哈。

另外怎么列动态规划递推方程?比较实际,可以参考之。

以及它跟divide and conquer思想的不同:

What is the difference between dynamic programming and divide and conquer? 在其中指出它们的主要区别:

divide and conquer主要分为三个步骤:

1.divide-将一个大问题分成若干个小问题
2.conquer-对于每一个小问题,都用迭代的方法来分别算出他们独立的结果
3.combine-将每个小问题的独立结果合并起来,组成最终大问题的解

典型的应用算法有:Merge Sort, Binary Sort等。

DP算法在将大问题分解为小问题这一点上与divide and conquer类似。DP算法的关键在于remembering。这就是我们为什么要在一个table中存储小问题的结果,这样我们对于同样的小问题,就不用再计算了,从而节省了时间。

典型的应用算法有:Matrix Chain Multiplication, Tower of Hanoi puzzle等。

另一个区别是:在divide and conquer中,子问题的结果通常都是独立的,而DP的一个问题的求解可能需要另一个问题的结果。DP的小问题之间通常有重叠,因此对于重叠部分,可以利用之前算出的结果,而不用重新再算一次。可以简单地认为DP = recursion + re-use

stackoverflow上的回答,并附带例子,很形象

那么我们现在就开始刷题吧。

1.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 day i.

If you were only permitted to complete at most one transaction (ie, buy one and sell one share of the stock), design an algorithm to find the maximum profit.

Example 1:
Input: [7, 1, 5, 3, 6, 4]
Output: 5

max. difference = 6-1 = 5 (not 7-1 = 6, as selling price needs to be larger than buying price)
Example 2:
Input: [7, 6, 4, 3, 1]
Output: 0

In this case, no transaction is done, i.e. max profit = 0.

实际上,这个问题的抽象是找到数组中后元素与前元素的最大差。参考前面Maximum Subarray 用到的Kadane’s Algorithm。参考Discuss中Kadane’s Algorithm:

public class Solution {    public int maxProfit(int[] prices) {        int maxCur = 0, maxSoFar = 0;        for(int i = 1; i < prices.length; i++) {            maxCur = Math.max(0, maxCur + prices[i] - prices[i-1]);            maxSoFar = Math.max(maxCur, maxSoFar);        }        return maxSoFar;    }}

关于DP:

some elegant design principles—such as divide-andconquer, graph exploration, and greedy choice—that yield definitive algorithms for a variety
of important computational tasks. The drawback of these tools is that they can only be used on very specific types of problems. We now turn to the two sledgehammers of the algorithms craft, dynamic programming and linear programming, techniques of very broad applicability that can be invoked when more specialized methods fail.

Predictably, this generality often comes with a cost in efficiency.

2.Partition Equal Subset Sum

Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Note:
Each of the array element will not exceed 100.
The array size will not exceed 200.
Example 1:

Input: [1, 5, 11, 5]

Output: true

Explanation: The array can be partitioned as [1, 5, 5] and [11].
Example 2:

Input: [1, 2, 3, 5]

Output: false

Explanation: The array cannot be partitioned into equal sum subsets.

分析思路:

这道题给了我们一个数组,问我们这个数组能不能分成两个非空子集合,使得两个子集合的元素之和相同。那么我们想,原数组所有数字和一定是偶数,不然根本无法拆成两个和相同的子集合,那么我们只需要算出原数组的数字之和,然后除以2,就是我们的target,那么问题就转换为能不能找到一个非空子集合,使得其数字之和为target(重点!)。运用动态规划DP。先定义一个一维的dp数组,其中dp[i]表示数字i是否是原数组的任意个子集合之和,那么我们最后只需要返回dp[target]就行了。我们初始化dp[0]为true,由于题目中限制了所有数字为正数,那么我们就不用担心会出现和为0或者负数的情况。那么关键问题就是要找出递归公式了,我们需要遍历原数组中的数字,对于遍历到的每个数字nums[i],我们需要更新我们的dp数组,要更新[nums[i], target]之间的值,那么对于这个区间中的任意一个数字j,如果dp[j - nums[j]]为true的话,那么dp[j]就一定为true,于是地推公式如下:

dp[j] = dp[j] || dp[j - nums[i]] (nums[i] <= j <= target)

有了递推公式,那么我们就可以写出最终解答:

public class Solution {    public boolean canPartition(int[] nums) {        if(nums.length==0||nums==null){            return true;        }        int total = 0;        for (int i = 0; i < nums.length; i++) {            total+=nums[i];        }        if(total%2!=0){            return false;        }        int target = total/2;        boolean[] dp = new boolean[target+1];//0到target        dp[0] = true;        for (int i = 0; i < nums.length; i++) {            for (int j = target; j>=nums[i]; j--) {                dp[j] = dp[j]||dp[j-nums[i]];            }        }        return dp[target];    }}

3.Triangle

Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.

For example, given the following triangle
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).

思路1:从下到上,将相邻的数进行比较取最小,然后加到上一层对应的元素上,然后如此循环。这种思想相当于说,如果要去最小,那么我应该再加多少offset呢。解答如下:

public class Solution {    public int minimumTotal(List<List<Integer>> triangle) {        int m = triangle.size();        for (int i = m-1; i >0; i--) {            for (int j = 0; j < i; j++) {                int offset = Math.min(triangle.get(i).get(j),triangle.get(i).get(j+1));                int value = triangle.get(i-1).get(j)+offset;                triangle.get(i-1).set(j,value);            }        }        return triangle.get(0).get(0);    }}

我的程序跟DP Solution for Triangle中的bottom-to-up DP程序思想是一样的。

4.Maximum Product Subarray

Find the contiguous subarray within an array (containing at least one number) which has the largest product.

For example, given the array [2,3,-2,4],
the contiguous subarray [2,3] has the largest product = 6.

public class Solution {    //success 1    //需要维护三个重要变量,一个是maxsofar,代表【0,index】区间上最大子数组积;maxhere和maxmin表示包含当前index的结果。    public int maxProduct(int[] A) {        if (A.length == 0) {            return 0;        }        int maxherepre = A[0];        int minherepre = A[0];        int maxsofar = A[0];        int maxhere, minhere;        for (int i = 1; i < A.length; i++) {            maxhere = Math.max(Math.max(maxherepre * A[i], minherepre * A[i]), A[i]);            minhere = Math.min(Math.min(maxherepre * A[i], minherepre * A[i]), A[i]);            maxsofar = Math.max(maxhere, maxsofar);            maxherepre = maxhere;            minherepre = minhere;        }        return maxsofar;    }}

Note:
There’s no need to use O(n) space, as all that you need is a minhere and maxhere. (local max and local min), then you can get maxsofar (which is global max) from them.

该题Maximum Product Subarray与Maximum Subarray的思想基本上一样,因此我将上面代码的乘号改写成加号,然后提交到Maximum Subarray问题上,也是可以通过的。参见success 3。

但是Maximum Subarray的success 1方法却没法改写成Maximum Product Subarray版本,因为它们毕竟还是有些不同啊,负负得正,可能得到最大的积,而负负相加却只能更小。这就是success1没办法改写的原因。

5.Longest Increasing Subsequence(LIS)

LIS应该算是DP的经典应用了。

思路一:没有比这个视频讲的思路更简单的了,直接实现该思想。

public class Solution {    //success 1    //视频https://www.youtube.com/watch?v=CE2b_-XfVDk的思想的实现    public int lengthOfLIS(int[] nums) {        if(nums.length==0||nums==null){            return 0;        }        int length = nums.length;        int[] dp = new int[length];        Arrays.fill(dp,1);//至少存在自己一个LIS,所以LIS肯定大于等于1        for (int i = 1; i < nums.length; i++) {            for (int j = 0; j < i; j++) {                if(nums[j]<nums[i]){                    dp[i] = Math.max(dp[i],dp[j]+1);//状态转移方程                }            }        }        //find the largest        int largest=Integer.MIN_VALUE;        for (int i = 0; i < dp.length; i++) {            if(dp[i]>largest){                largest = dp[i];            }        }        return largest;    }}

状态dp[i]代表在区间0-i之间LIS的长度。此方法的时间复杂度为O(N×N)

能不能有所提升到O(logN×N)呢?

思路二:
参考Java/Python Binary search O(nlogn) time with explanation,它维护了一个数组,tails is an array storing the smallest tail of all increasing subsequences with length i+1 in tails[i].这个数组是递增数组,因此在情况2下要更新值时,就可以用binary search从而能够使复杂度降为O(logN×N)。代码如下:

public class Solution {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;}}
0 0
原创粉丝点击