动态规划中 用O(n)时间遍历数组

来源:互联网 发布:小眼睛美女 知乎 编辑:程序博客网 时间:2024/06/06 02:54

  上上节已经初步介绍了动态规划算法,并结合LeetCode 279题进行了详细讲解。然而在针对一些需要遍历一个数组,同时使用广义动态规划(即至少有一个狭义动态规划条件不满足)的方法中,如何不使用递归,并且只用O(n)时间遍历数组呢?
  面对一个数组,在解决问题时,大脑通常都是进行的跳跃性思维,你可能一眼就看出了数组中的解。然而计算机只会一步一步去遍历数组,才能保证每一个值它都看过。这是很多初学者在解决问题时的一个常态。然而为了用计算机程序获得你想要的答案,就需要考虑计算机的思维方式。
  不使用递归,只用O(n)时间遍历数组,其中一个关键点在于你要明确:遍历的每一步,程序都会停留在一个状态,这个状态结合上一个状态的信息可以得到一些信息,并且这个状态的信息在下一步,即下一个状态需要用到。不要受大脑的跳跃性思维干扰,想当然地认为是跳跃几步到下一个状态。一定记住是每一步一个状态。
  接下来将用三道例题进行讲解。

LeetCode 121题

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.

  对于 Example 1,许多人刚开始得到答案的思路可能是这样的:在数组中找一个最小值,接下来看位置在其后的最大值是多少,记录一下差值;再看第二小的,接下来看位置在其后的最大值与其的差值;对两个差值进行比较,留下较大者;以此类推。
  这种思维当然对各种测试用例都是理论可行的,然而实际达不到效率要求。找最小值的过程就会至少花费O(NlogN)时间,更不要说在此基础上再去找该最小值位置后的最大值,以及各个差值的比较了。
  思考一下O(n)的做法,既然是O(n),则必然是只遍历数组一次,一般来说,每一步都是顺序获得数组中的一个新的值。那么对于每一步获得的新的值你可以获得哪些信息呢?
  第一,可以知道遍历至今的最小值是多少;
  第二,可以知道遍历至上一步为止最大的差是多少;
  第三,可以知道遍历至这一步的最小值与当前这一步的值的差是多少。
  因此,将两步的差进行比较,较大者记录下来,即为到这一步为止最大的差。是不是瞬间就明朗了呢?

Java代码如下:
public int maxProfit(int[] prices) {    int l = prices.length;    if(l < 2)        return 0;    int min = prices[0], diff = 0;    for(int i = 1; i<l; i++){        if(prices[i] < min){            min = prices[i]; //遍历至今的最小值        }else{            diff = Math.max(prices[i]-min,diff); //上一步与这一步的差进行比较,记录较大者        }    }    return diff;}

LeetCode 53题

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

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

  与上题类似,跳跃性的思维可能是从数组两端先分别找到一个正数,计算他们及其中间数的和,记录下来,在从左边或右边往中间缩到下一个正数,以此类推。同样是理论上可行,实际效率不达标。
  我们来思考一下,以O(n)时间遍历数组,每一步可以获得的信息:
  第一,上一步获得的子数组的和;
  第二,上一步获得的子数组的和加上这一步的值所得的结果;
  第三,截止上一步所得最大子数组的和;
  因此,将第一、第二进行比较,较大者与第三再进行比较,就可以得出截止这一步所得最大子数组的和。
  

Java代码如下:
public static int maxSubArray(int[] A) {    int maxSoFar=A[0];//截止上一步所得最大子数组的和    int maxEndingHere=A[0];//上一步获得的子数组的和    for (int i=1;i<A.length;++i){        maxEndingHere= Math.max(maxEndingHere+A[i],A[i]);        maxSoFar=Math.max(maxSoFar, maxEndingHere); //比较    }    return maxSoFar;}

LeetCode 198题

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.  

  以O(n)时间遍历数组,每一步可以获得的信息:
  第一,上一步执行抢劫,所获得的总金钱;
  第二,上一步不执行抢劫,所获得的总金钱;
  第三,这一步执行抢劫,所获得的总金钱;
  第四,这一步不执行抢劫,所获得的总金钱;
  因此,第二项加上当前抢劫金额的和即为第三项,将第一、第二进行比较,较大者即为第四项的答案。遍历结束,比较第三和第四项,即为所要的答案。

Java代码如下:
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]);}
阅读全文
0 0