Tag Archives: DP

LeetCode Ones and Zeroes

LeetCode Ones and Zeroes In the computer world, use restricted resource you have to generate maximum benefit is what we always want to pursue. For now, suppose you are a dominator of m 0s and n 1s respectively. On the other hand, there is an array with strings consisting of only 0s and 1s. Now your task is to find the maximum number of strings that you can form with given m 0s and n 1s. Each 0 and 1 can be used at most once. Note:

  1. The given numbers of 0s and 1s will both not exceed 100
  2. The size of given string array won’t exceed 600.
Example 1:
Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4
Explanation: This are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are “10,”0001”,”1”,”0”
Example 2:
Input: Array = {"10", "0", "1"}, m = 1, n = 1
Output: 2
Explanation: You could form "10", but then you'd have nothing left. Better form "0" and "1".

给定一个0,1字符串数组,问最多能从中选出多少个字符串,使得所有字符串的’0’和’1’的个数不超过m和n。 明显的背包问题,常规的0/1背包是若干个商品,费用不超过某个值。这里的背包是若干个商品,费用不超过m和n,也就是有两个限制。本题可以用二维背包解决,和一维背包很类似,都是假设某个商品要还是不要,然后更新dp数组。只不过这里有两个限制条件,所以更新数组时需要两个for循环。 一维背包为了优化空间,可以用一维数组,然后从后往前填。二维背包为了优化空间,可以用二维数组,也从后往前填。 假设dp[i][j]表示在i个’0’和j个’1’的限制下,最多能取出的字符串(商品)数,则二维背包代码如下: [cpp] class Solution { public: int findMaxForm(vector<string>& strs, int m, int n) { vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); for (int i = 0; i < strs.size(); ++i) { vector<int> cnts(2, 0); for (int j = 0; j < strs[i].size(); ++j)++cnts[strs[i][j] – ‘0’]; for (int j = m; j >= cnts[0]; –j) { for (int k = n; k >= cnts[1]; –k) { dp[j][k] = max(dp[j][k], dp[j – cnts[0]][k – cnts[1]] + 1); } } } return dp[m][n]; } }; [/cpp] 本代码提交AC,用时66MS。]]>

LeetCode Guess Number Higher or Lower II

LeetCode Guess Number Higher or Lower II We are playing the Guess Game. The game is as follows: I pick a number from 1 to n. You have to guess which number I picked. Every time you guess wrong, I’ll tell you whether the number I picked is higher or lower. However, when you guess a particular number x, and you guess wrong, you pay $x. You win the game when you guess the number I picked. Example:

n = 10, I pick 8.
First round:  You guess 5, I tell you that it's higher. You pay $5.
Second round: You guess 7, I tell you that it's higher. You pay $7.
Third round:  You guess 9, I tell you that it's lower. You pay $9.
Game over. 8 is the number I picked.
You end up paying $5 + $7 + $9 = $21.
Given a particular n ≥ 1, find out how much money you need to have to guarantee a win. Hint:
  1. The best strategy to play the game is to minimize the maximum loss you could possibly face. Another strategy is to minimize the expected loss. Here, we are interested in the first scenario.
  2. Take a small example (n = 3). What do you end up paying in the worst case?
  3. Check out this article if you’re still stuck.
  4. The purely recursive implementation of minimax would be worthless for even a small n. You MUST use dynamic programming.
  5. As a follow-up, how would you modify your code to solve the problem of minimizing the expected loss, instead of the worst-case loss?

猜数字升级版。假设正确数字在[1,n]之间,每猜一个x,如果没猜中,则需要花费x元。问最少需要花费多少钱才能保证一定能猜中数字。 这是一个minmax问题,即最小化最大花费。使用动态规划解决,假设dp[l][r]表示在区间[l,r]猜数字时,一定能猜中的最小花费钱数。 假设在区间[l,r]猜数字,当猜x错误时,可以根据大小关系继续在[l,x-1]或[x+1,r]区间继续猜。假设已经知道[l,x-1]和[x+1,r]区间猜中的最小花费是dp[l][x-1]和dp[x+1][r],则猜x猜中的最小花费就是f(x)=x+max(dp[l][x-1],dp[x+1][r])。x在[l,r]遍历,取最小的f(x)就是区间[l,r]猜中的最小花费。 代码如下: [cpp] class Solution { private: int helper(vector<vector<int>>& dp, int l, int r) { if (l >= r)return 0; if (dp[l][r] != 0)return dp[l][r]; dp[l][r] = INT_MAX; for (int k = l; k <= r; ++k) { dp[l][r] = min(dp[l][r], k + max(helper(dp, l, k – 1), helper(dp, k + 1, r))); } return dp[l][r]; } public: int getMoneyAmount(int n) { vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0)); return helper(dp, 1, n); } }; [/cpp] 本代码提交AC,用时59MS。 还可以用迭代的思路。DP的思路就是在一个大的区间[l,r],尝试猜任意一个数x,然后取x+max(dp[l][x-1],dp[x+1][r])。但前提是小区间dp[l][x-1]和dp[x+1][r]之前已经算过了。 所以可以令l=n-1→1,r=l+1→n,x在[l,r)任意取值,因为x是从x-1遍历到x的,所以dp[l][x-1]算过了;又因为l是从n-1,n-2,..,x+1,x,x-1,…遍历的,所以dp[x+1][r]也是算过了,这样就能求f(x)=x+max(dp[l][x-1],dp[x+1][r])。 为什么不能令l=1→n-1,r=l+1→n呢,假设此时选择x,则虽然算过dp[l][x-1],但没算过dp[x+1][r],因为左端点的遍历顺序是1,2,…,l,…,x-1,x,x+1,…,左端点只遍历到l,还没遍历到x+1,所以dp[x+1][r]的值还没算好。 迭代的代码如下: [cpp] class Solution { public: int getMoneyAmount(int n) { vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0)); for (int l = n – 1; l > 0; –l) { for (int r = l + 1; r <= n; ++r) { dp[l][r] = INT_MAX; for (int k = l; k < r; ++k) { dp[l][r] = min(dp[l][r], k + max(dp[l][k – 1], dp[k + 1][r])); } } } return dp[1][n]; } }; [/cpp] 本代码提交AC,用时9MS,快了好多。]]>

LeetCode Count Numbers with Unique Digits

LeetCode Count Numbers with Unique Digits Given a non-negative integer n, count all numbers with unique digits, x, where 0 ≤ x < 10n. Example: Given n = 2, return 91. (The answer should be the total numbers in the range of 0 ≤ x < 100, excluding [11,22,33,44,55,66,77,88,99]) Hint:

  1. A direct way is to use the backtracking approach.
  2. Backtracking should contains three states which are (the current number, number of steps to get that number and a bitmask which represent which number is marked as visited so far in the current number). Start with state (0,0,0) and count all valid number till we reach number of steps equals to 10n.
  3. This problem can also be solved using a dynamic programming approach and some knowledge of combinatorics.
  4. Let f(k) = count of numbers with unique digits with length equals k.
  5. f(1) = 10, …, f(k) = 9 * 9 * 8 * … (9 – k + 2) [The first factor is 9 because a number cannot start with 0].

给定n,问在[0,10n)内有多少个每一位都是不同数字的数。比如当n=2时,在[0,100)内,有91个这样的数,需要排除个位和十位数字相同的9个数。 其实这个题很简单,假设k表示数的位数,找一下规律:
  • k=1,一位数,可以填入0~9这10个数,结果为10
  • k=2,两位数,十位填入1~9这9个数,个位填入0~9中除了十位的那个数字,有9种情况,结果为9*9
  • k=3,三位数,百位填入1~9这9个数字,十位填入0~9中除了百位的那个数字,有9种情况,个位填入0~9中除了百位和十位的那两个数字,有8种情况,结果为9*9*8
  • k=n,结果为9*9*8*…*(9-n+2)
所以我们可以用DP解题,f(k)=f(k-1)*(9-k+2),为了节省空间,只需保存上一次的结果f(k-1)。在DP的过程中,累加f(k)的结果。 代码如下: [cpp] class Solution { public: int countNumbersWithUniqueDigits(int n) { if (n == 0)return 1; if (n == 1)return 10; int ans = 10, last = 9; for (int i = 2; i <= n; ++i) { last *= (9 – i + 2); ans += last; } return ans; } }; [/cpp] 本代码提交AC,用时3MS。]]>

LeetCode Integer Break

343. Integer Break

Given a positive integer n, break it into the sum of at least two positive integers and maximize the product of those integers. Return the maximum product you can get.

Example 1:

Input: 2
Output: 1
Explanation: 2 = 1 + 1, 1 × 1 = 1.

Example 2:

Input: 10
Output: 36
Explanation: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36.

Note: You may assume that n is not less than 2 and not larger than 58.


给定一个数n,要求把其拆分成至少两个数的和,使得他们的乘积最大。 数学题,没啥思路,参考网上题解。 把0~10的结果先列出来找找规律:

  • 0:0
  • 1:0
  • 2:1=1*1
  • 3:2=2*1
  • 4:4=2*2
  • 5:6=3*2
  • 6:9=3*3
  • 7:12=3*4
  • 8:18=3*3*2
  • 9:27=3*3*3
  • 10:36=3*3*4

可以发现,n从5开始,都至少分解出来一个3,可以推断的是,当n=11时,在10的基础上,把最后一个4改成5,然后5又可以分解成3*2,所以11=3+3+3+2使得乘积最大。 4只能分解出2+2,1+3的乘积是3小于2*2;5可以分解出3+2,所以也有3。也就是说,当n大于4时,不断分解出3来,直到剩余的数小于4为止。所以有如下代码:

class Solution {
public:
    int integerBreak(int n)
    {
        if (n <= 3)
            return n – 1;
        int ans = 1;
        while (n > 4) {
            ans *= 3;
            n -= 3;
        }
        return ans * n;
    }
};

本代码提交AC,用时0MS。
再次观察上述规律,n=10的结果是n=7=10-3的结果乘以3;n=9的结果也是n=6=9-3的结果乘以3。可以推断,n的结果是(n-3)的结果乘以3。所以先列出前几个结果,后面的结果通过其前3的结果乘以3推导得到。代码如下:

class Solution {
public:
    int integerBreak(int n)
    {
        vector<int> dp = { 0, 0, 1, 2, 4, 6, 9 };
        for (int i = 7; i <= n; ++i)
            dp.push_back(dp[i – 3] * 3);
        return dp[n];
    }
};

本代码提交AC,用时3MS。

LeetCode Perfect Squares

279. Perfect Squares

Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, ...) which sum to n.

Example 1:

Input: n = 12
Output: 3 
Explanation: 12 = 4 + 4 + 4.

Example 2:

Input: n = 13 Output: 2 Explanation: 13 = 4 + 9. 279. Perfect Squares 

本题问一个数n最少能由多少个完全平方数加和得到。 使用动态规划,设dp[i]表示数i最少能由多少个完全平方数加和得到,那么由i再加一个平方数j得到的数i+j*j可以由dp[i]+1个平方数组成。即递推公式为:
$$dp[i+j*j]=min(dp[i]+1)$$
具体实现时,我们是先求dp数组中前面的值dp[i],然后不断缩小并更新dp[i+j*j]的结果。代码如下:

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

本代码提交AC,用时99MS。
这题还可以用纯数学的方法来解,涉及到四平方和定理,数学方法暂时就不研究了。

二刷,对于数i,其最大的平方数就是(sqrt(i))^2,所以从sqrt(i)到1都试一试,从哪个降下去的数目最少。对于第一个样例,sqrt(12)=3,所以依次尝试3,2,1,发现2降下去的数目最少,因为3降下去就是9+1+1+1,要4个数,比2降下去4+4+4多。

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

本代码提交AC,用时204MS。

三刷。转换为背包问题,先把所有可能的平方数求出来放到数组squares中,把每个平方数当做一件商品,限制背包容量为n,问最少拿多少件商品能填满背包。则对于每件商品,选择拿或者不拿。完整代码如下:

class Solution {
public:
    int numSquares(int n) {
        vector<int> squares = {0};
        for(int i = 1; i * i <= n; ++i) {
            squares.push_back(i * i);
        }
        int m = squares.size();
        vector<vector<int>> dp(m, vector<int>(n + 1, 0));
        for(int i = 1; i < m; ++i) {
            for(int j = 1; j <= n; ++j) {
                if(i == 1) dp[i][j] = j;
                else {
                    dp[i][j] = dp[i - 1][j];
                    if(j >= squares[i]) dp[i][j] = min(dp[i][j], dp[i][j - squares[i]] + 1);
                }
            }
        }
        return dp[m - 1][n];
    }
};

本代码提交AC,用时688MS。还可以进一步优化,dp数组可以变成一维的。

LeetCode Range Sum Query – Immutable

LeetCode Range Sum Query – Immutable Given an integer array nums, find the sum of the elements between indices i and j (ij), inclusive. Example:

Given nums = [-2, 0, 3, -5, 2, -1]
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
Note:
  1. You may assume that the array does not change.
  2. There are many calls to sumRange function.

给定一个固定的数组,要查询某个区间[i,j]之间的和,且这样的查询很多。 明显需要先把结果存起来,这样每次查询的时候直接返回就好。用数组dp[i]表示区间[0,i]之间的和,这样区间[i,j]的和=dp[j]-dp[i-1]。注意当i=0的情况。完整代码如下: [cpp] class NumArray { private: vector<int> dp; public: NumArray(vector<int> nums) { int n = nums.size(); if (n == 0)return; dp.resize(n); dp[0] = nums[0]; for (int i = 1; i < n; ++i)dp[i] = dp[i – 1] + nums[i]; } int sumRange(int i, int j) { if (dp.empty())return 0; return dp[j] – (i == 0 ? 0 : dp[i – 1]); } }; [/cpp] 本代码提交AC,用时206MS。]]>

LeetCode Maximum Product Subarray

152. Maximum Product Subarray

Given an integer array nums, find the contiguous subarray within an array (containing at least one number) which has the largest product.

Example 1:

Input: [2,3,-2,4]
Output: 6
Explanation: [2,3] has the largest product 6.

Example 2:

Input: [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

本题要求一个数组中最大的连续子数组的积,和LeetCode Maximum Subarray很类似,后者是求最大连续子数组的和。
但是因为乘积有其独特性,即负负得正,所以即使前面算到有很大的负数,也不能舍弃,万一后面来一个负数,负负得正就会得到很大的正数。
本题也是使用动态规划来做。维护两个变量,令curMax和curMin分别表示当前最大连续乘积值和最小连续乘积值。则有
curMax = max(curMax*nums[i], curMin*nums[i], nums[i]);
curMin = min(curMax*nums[i], curMin*nums[i], nums[i]);
很好理解,不解释:

class Solution {
  public: int maxProduct(vector < int > & nums) {
    int ans = nums[0], curMax = nums[0], curMin = nums[0];
    for (size_t i = 1; i < nums.size(); ++i) {
      int tmp = curMax;
      curMax = max(max(curMax * nums[i], curMin * nums[i]), nums[i]);
      curMin = min(min(tmp * nums[i], curMin * nums[i]), nums[i]);
      ans = max(ans, curMax);
    }
    return ans;
  }
};

本代码提交AC,用时6MS。

再细究一下,假设curMax和curMin为当前最大最小连续乘积值,且规定curMax一定是正数。则当nums[i]为正数时,curMax更新为curMax*nums[i],无论curMin是否为正,curMin都更新为curMin*nums[i]。
当nums[i]为负数时,curMax更新为curMin*nums[i],curMin更新为curMax*nums[i]。因为curMax一定为正,所以curMax*nums[i]一定为负。即使curMin也为正,但curMin<curMax,所以curMin*nums[i]>curMax*nums[i]。如果curMin为负,则curMin*nums[i]为正,更大于curMax*nums[i]了。
这种思路就分析得比较细致,注意代码第6行需要记录前一次的最大值,因为运算过程中curMax有可能是0或者负数,为了保证curMax一定是正数的性质,需要第6行(比如输入为0,2,则i=0运算完之后curMax=curMin=0;当i=1时,如果第8行curMax*=nums[i],则curMax还是0)。且因为第12行会更新curMax,而第13行需要用到上一次的最大值。
完整代码如下:

class Solution {
  public: int maxProduct(vector < int > & nums) {
    int ans = nums[0], curMax = 1, curMin = 1;
    for (size_t i = 0; i < nums.size(); ++i) {
      int oldMax = max(curMax, 1);
      if (nums[i] > 0) {
        curMax = oldMax * nums[i];
        curMin *= nums[i];
      } else {
        curMax = curMin * nums[i];
        curMin = oldMax * nums[i];
      }
      ans = max(ans, curMax);
    }
    return ans;
  }
};

本代码提交AC,用时也是6MS。

LeetCode Jump Game II

45. Jump Game II

Given an array of non-negative integers, you are initially positioned at the first index of the array.

Each element in the array represents your maximum jump length at that position.

Your goal is to reach the last index in the minimum number of jumps.

Example:

Input: [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2.
    Jump 1 step from index 0 to 1, then 3 steps to the last index.

Note:

You can assume that you can always reach the last index.


上一题是问能否跳到数组右边,本题问最少需要多少跳能跳到数组右边,第一反应是DP。
设dp[i]表示从位置i跳到数组右边最少需要的跳数,则dp[n-1]=0,dp[0]就是最终结果。此时我们需要从数组右边往左边遍历,假设dp[i+1,…,n-1]都求出来了,现在要求dp[i]。站在位置i,我们最远能跳到i+A[i],所以我们就看看从i跳到哪个$j\in[i,i+A[i]]$能获得最少的跳数,从i跳到j需要增加一跳,所以递推式就是$dp[i]=min\{dp[j]+1\},\quad j\in[i+1,i+A[i]]$

这种思路的代码如下:

class Solution {
public:
    int jump(vector<int>& nums)
    {
        int n = nums.size();
        if (n <= 1)
            return 0;
        int maxJump = n – 1;
        vector<int> dp(n, 0);
        for (int i = n – 2; i >= 0; –i) {
            int curMinJump = maxJump;
            for (int j = i + 1; j <= nums[i] + i && j < n; ++j) {
                curMinJump = min(curMinJump, dp[j] + 1);
            }
            dp[i] = curMinJump;
        }
        return dp[0];
    }
};

可惜,本代码提交TLE,在最后一个大数据集上超时了。但是我还是觉得DP方法是一个基本法,而且如果要求出最小跳的路径,也很方便,如下代码,打印的时候,从pre[n-1]不断找前驱位置就好。

class Solution {
public:
    int jump(vector<int>& nums)
    {
        int n = nums.size();
        if (n <= 1)
            return 0;
        int maxJump = n – 1;
        vector<int> dp(n, 0), pre(n, 0);
        for (int i = n – 2; i >= 0; –i) {
            int curMinJump = maxJump, k = 0;
            for (int j = i + 1; j <= nums[i] + i && j < n; ++j) {
                if (dp[j] + 1 < curMinJump) {
                    curMinJump = dp[j] + 1;
                    k = j;
                }
            }
            dp[i] = curMinJump;
            pre[k] = i; // 从i跳到k能获得最小跳数
        }
        return dp[0];
    }
};

讨论区看到一个O(n)的解法,很厉害。思路和上一题LeetCode Jump Game很类似,依旧维护一个当前能跳到的最远位置curFarthest,同时维护一个当前跳能跳到的范围[curBegin, curEnd],一旦i超过了curEnd,说明应该开启下一跳了,同时更新curFarthest。

这种算法的思路很巧妙,他不管你当前跳到底跳到哪里,只管你能够跳到的范围,一旦走出这个范围,就必须开启下一跳了。代码如下:

class Solution {
public:
    int jump(vector<int>& nums)
    {
        int jumps = 0, curEnd = 0, curFarthest = 0;
        for (int i = 0; i < nums.size() – 1; ++i) {
            curFarthest = max(curFarthest, i + nums[i]);
            if (i == curEnd) {
                curEnd = curFarthest;
                ++jumps;
                if (curEnd >= nums.size() – 1)
                    break;
            }
        }
        return jumps;
    }
};

本代码提交AC,用时22MS。第10行是增加的提前结束的代码。
参考:

二刷。这题很有意思,时隔三年再刷此题,依然不会。首先想到的依然是DP的解法,不过这个DP比以前写的DP好理解一些,代码如下。思想就是steps[i]保存了要跳到i位置最小的步数,则初始时steps[0]=0,其他位置都是无穷大。然后,对于每个位置i,更新它能跳到的所有target位置的最小步数。最后返回steps[n-1]。

class Solution {
public:
	int jump(vector<int>& nums) {
		int n = nums.size();
		vector<int> steps(n, INT_MAX);
		steps[0] = 0;
		for (int i = 0; i < n; ++i) {
			for (int j = 1; j <= nums[i]; ++j) {
				int target = i + j;
				if (target < n) {
					steps[target] = min(steps[target], steps[i] + 1);
				}
				else {
					break;
				}
			}
		}
		return steps[n - 1];
	}
};

同样,DP解法在最后一个样例上TLE了。

评论区看到另一个O(n)的解法,比之前的更好理解: https://leetcode.com/problems/jump-game-ii/discuss/18028/O(n)-BFS-solution。这个解法把该问题想象为一个BFS层次遍历的问题。

以输入样例s=[2,3,1,1,4]为例,最开始我们在起点s[0],它最多能跳2步,所以能跳到s[1]和s[2]的位置,即3,1在BFS的同一层。s[1]=3能跳到除了该层的s[3]和s[4],即1,4在BFS的下一层;s[2]=1无法跳出第二层。因为4在最后一层,也就是第3层,所以最少可以2步到达。

2
3,1
1,4

代码如下:

class Solution {
public:
	int jump(vector<int>& nums) {
		int level = 0, current_fastest = 0, next_fastest = 0;
		int n = nums.size(), i = 0;
		if (n == 1)return 0;
		while (true) {
			++level;
			while (i <= current_fastest && i < n) {
				next_fastest = max(next_fastest, i + nums[i]);
				++i;
			}
			if (next_fastest >= n - 1)return level;
			current_fastest = next_fastest;
		}
		return 0;
	}
};

本代码提交AC,用时8MS。由于i指针一直在往前走,所以时间复杂度为O(n)。

三刷。 依然是BFS的思路,代码简洁易懂:

class Solution {
public:
    int jump(vector<int>& nums) {
        int n = nums.size();
        int step = 0, pre_max = 0, next_max = 0;
        for(int i = 0; i < n; ++i) {
            if(i > pre_max) {
                ++step;
                pre_max = next_max;
            } 
            next_max = max(next_max, i + nums[i]);
        }
        return step;
    }
};

本代码提交AC,用时28MS。

LeetCode Longest Increasing Subsequence

300. Longest Increasing Subsequence

Given an unsorted array of integers, find the length of longest increasing subsequence.

Example:

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

Note:

  • There may be more than one LIS combination, it is only necessary for you to return the length.
  • Your algorithm should run in O(n2) complexity.

Follow up: Could you improve it to O(n log n) time complexity?


给定一个数组,求最长上升子序列(Longest Increasing Subsequence, LIS)的长度。子序列可以是不连续的,和子串不一样。
此题有两种解法,一种$O(n^2)$,另一种$O(nlgn)$。
先来看$O(n^2)$。设length[i]表示到nums[i]时的LIS,那么此时已知了length[0,…,i-1],对于$j\in[0,…,i-1]$,如果nums[i]>nums[j],则在i处的LIS至少是length[j]+1,因为可以在nums[j]后面接nums[i],构成LIS,所以在i处的LIS加一。我们尝试所有的$j\in[0,…,i-1]$,找一个最大的length[j]+1赋给length[i]。
递推式如下:

length(i) =  max  { length(j) + 1 : if nums[j] < nums[i] }
           0≤j≤i-1

这种思路的代码如下,因为需要两层循环,所以时间复杂度为$O(n^2)$。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums)
    {
        int n = nums.size();
        if (n < 1)
            return 0;
        vector<int> length(n, 1);
        int ans = 1;
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) {
                    length[i] = max(length[i], length[j] + 1);
                }
            }
            ans = max(ans, length[i]);
        }
        return ans;
    }
};

本代码提交AC,用时33MS。
接下来看看$O(nlgn)$的解法。
对于数组d(代码中的数组nums的简称),我们定义一个辅助数组B,B[j]保存了到目前为止LIS=j时的最后一个数的最小的数,比如B[3]=8表示到目前为止长度为3的上升子序列的最后一个数的最小值为8。我们不断更新B的长度和数值,最后B的长度就是最长的上升子序列的长度,但是数组B并不是一个LIS。
举例说明。假设数组下标从1开始。数组d=[3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12],初始时B的长度为1,且B[1]=3表明此时最长上升子序列的长度为1,且该LIS的最后一个数的最小值是3。
d[2]=5,因为d[2]>B[1],所以5可以加入最长上升子序列,导致此时的最长上升子序列长度为2,且最后一个数的最小值就是5,所以B[2]=5。此时B[1]=3不变,因为长度为1的LIS的末尾数值还是3不变。B=[3,5]
d[3]=6,同上,d[3]>B[2],所以6也加入最长上升子序列,导致LIS=3,且B[3]=6B=[3,5,6]
d[4]=2,注意此时d[4]<B[3],所以2不能增加以B[3]=6结尾的最长上升子序列,所以数组B的长度不变。查找B,发现2比B[1]=3小,所以替换B[1]=3为B[1]=2,此含义为长度为1的LIS的末尾最小数由3变更为2。这很好理解,因为2自身可以作为一个LIS,且比3自身作为LIS要小。B=[2,5,6]
d[5]=5,同上,d[5]<B[3],查找B,更新B[2]=5,因为B[2]本身就是5,所以更新前后一样。B=[2,5,6]
d[6]=4,d[6]<B[3],无法增长最长上升子序列,查找B,发现4在B[1]=2和B[3]=6之间,更新B[2]=4,含义为长度为2的LIS的最后一个数的最小值由5更新为4。B=[2,4,6]。其实这样更新的目的就是为了把各个长度的LIS的末尾的最小数变小,使得后续的数更有可能接在之前LIS的末尾来增加LIS的长度。
d[7]=19,d[7]>B[3],贡献LIS长度,直接加入数组B,即B[4]=19B=[2,4,6,19]
d[8]=5,查找B,在4,19之间,所以更新B[3]=5B=[2,4,5,19]
d[9]=6,查找B,更新B[4]=6B=[2,4,5,6]
d[10]=7,d[10]>B[4],贡献LIS长度,直接加入数组B,即B[5]=7B=[2,4,5,6,7]
d[11]=12,d[11]>B[5],贡献LIS长度,直接加入数组B,即B[6]=12B=[2,4,5,6,7,12]
以上算法过程涉及到在数组B中查找当前元素d[i]的替换位置,因为数组B是有序的,所以可以使用二分查找,最终算法的时间复杂度降为了O(nlgn)。
STL中自带了在有序数组中二分查找的函数,lower_bound输入为一个数组B和目标target,返回数组B中不小于target的最小下标。我们也可以自己用二分查找实现。
这种思路的代码如下:

class Solution {
public:
    int lowerbound(vector<int>& B, int target)
    {
        int l = 0, r = B.size() – 1;
        while (l < r) {
            int m = (l + r) / 2;
            if (B[m] == target)
                return m;
            else if (B[m] > target)
                r = m – 1;
            else
                l = m + 1;
        }
        return B[l] < target ? (l + 1) : l;
    }
    int lengthOfLIS(vector<int>& nums)
    {
        int n = nums.size();
        if (n < 1)
            return 0;
        vector<int> B = { nums[0] };
        for (int i = 1; i < n; ++i) {
            if (nums[i] > B.back())
                B.push_back(nums[i]);
            else {
                //*lower_bound(B.begin(), B.end(), nums[i]) = nums[i];
                B[lowerbound(B, nums[i])] = nums[i];
            }
        }
        return B.size();
    }
};

本代码提交AC,用时3MS,速度一下提高了不少。
参考:

二刷。第二种解法,可以直接调stl的lower_bound,最好自己写,和 http://code.bitjoy.net/2020/03/21/leetcode-find-first-and-last-position-of-element-in-sorted-array/ 这题代码FindFirstIndex一致,记住二分搜索的标准写法。

class Solution {
public:
	int lengthOfLIS(vector<int>& nums) {
		int n = nums.size();
		if (n == 0)return 0;

		vector<int> curmin;
		for (int i = 0; i < n; ++i) {
			if (curmin.empty() || nums[i] > curmin.back()) {
				curmin.push_back(nums[i]);
			}
			else {

				//vector<int>::iterator it = lower_bound(curmin.begin(), curmin.end(), nums[i]);
				//*it = nums[i];

				int l = 0, r = curmin.size() - 1;
				while (l <= r) {
					int m = l + (r - l) / 2;
					if (nums[i] <= curmin[m]) {
						r = m - 1;
					}
					else {
						l = m + 1;
					}
				}
				curmin[r + 1] = nums[i];


			}
		}
		return curmin.size();
	}
};

本代码提交AC,用时8MS。

LeetCode Partition Equal Subset Sum

LeetCode 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:

  1. Each of the array element will not exceed 100.
  2. 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.

问一个数组能否划分成两个子数组,且这两个子数组的和相等。 如果成立,首先必须满足数组的和是偶数,然后必须能找到一个子数组的和是总和的一半。所以问题转换为从数组中取若干个元素,使得取出元素之和为target(原数组和的一半)。 用动态规划求解,令dp[j]表示能抽取一个子数组之和为j的子数组。递推式为: if dp[j] then dp[nums[i]+j]=true 含义为如果能得到子数组和为j的子数组,那么再取一个元素nums[i]扔到子数组中,就能得到子数组和为j+nums[i]的子数组。所以对于数组中的每个元素nums[i],遍历dp[0,…,target-nums[i]]应用上式。 有一点需要注意的是,遍历dp时,必须从大到小遍历,否则会出错。比如数组[1,2,5],dp[0]=1,对于元素1,如果我们从小到大遍历dp,则dp[0,…,3]都等于true;对于元素2,则会导致dp[2+2]=true。但是如果从大到小遍历dp,即dp[3,…,0],则对于元素1,只会设置dp[1]=true,最终不会导致dp[4]=true。 完整代码如下: [cpp] class Solution { public: bool canPartition(vector<int>& nums) { int sum = accumulate(nums.begin(), nums.end(), 0); if (sum & 1)return false; int target = sum / 2; vector<int> dp(target + 1); dp[0] = 1; for (int i = 0; i < nums.size(); ++i) { for (int j = target – nums[i]; j >= 0; –j) { if (dp[j])dp[j + nums[i]] = 1; } } return dp[target]; } }; [/cpp] 本代码提交AC,用时39MS。]]>