LeetCode 动态规划
题目
1. 数字相关
263 丑数
判断一个数 num 是否是丑数。
通用的方法是自底向上求出大于等于 num 的第一个数来判断 num 是否是丑数。但这道题已经给出了数 num,直接通过模运算就能得到结果了。
class Solution {
public:
bool isUgly(int num) {
if (num < 1)
return false;
while (num % 2 == 0)
num /= 2;
while (num % 3 == 0)
num /= 3;
while (num % 5 == 0)
num /= 5;
return num == 1;
}
};
264 丑数 II
求第 n 个丑数。
用一个数组 ugly 来保存前 m 个丑数,用三个质因数 2,3,5 乘以其当前系数对应的丑数,得到新的丑数,最小的一个就是第 m + 1 个丑数。时间复杂度是 O(m * n),其中 m 是质因数的个数,n 是要找的第 n 个丑数。
class Solution {
public:
int nthUglyNumber(int n) {
vector<int> ugly(n, 1);
int base_2 = 0, base_3 = 0, base_5 = 0;
int m = INT_MAX;
for (int i = 1; i < n; ++i) {
ugly[i] = min({2 * ugly[base_2], 3 * ugly[base_3], 5 * ugly[base_5]});
if (2 * ugly[base_2] == ugly[i])
++base_2;
if (3 * ugly[base_3] == ugly[i])
++base_3;
if (5 * ugly[base_5] == ugly[i])
++base_5;
cout << ugly[i] << endl;
}
return ugly[n - 1];
}
};
313 超级丑数
给定质因数数组 primes,求第 n 个丑数。
跟上一题完全相同,只是把原有的三个质因数 2,3,5 换成了一个数组。时间复杂度是 O(n * m),其中 n 是第 n 个丑数,m 是数组的长度。
class Solution {
public:
int nthSuperUglyNumber(int N, vector<int>& primes) {
int n = primes.size(), m = INT_MAX;;
vector<int> ugly(N, 1), base(n, 0);
for (int i = 1; i < N; ++i) {
m = INT_MAX;
for (int j = 0; j < n; ++j)
m = min(m, primes[j] * ugly[base[j]]);
ugly[i] = m;
for (int j = 0; j < n; ++j)
if (primes[j] * ugly[base[j]] == m)
++base[j];
}
return ugly[N - 1];
}
};
279 完全平方数
给一个数 n,其可以被表示为 m 个完全平方数的,找到最小的 m。
n 只能由比 n 小 1, 4, 9 等等的数的最优值加一得到,因此用一个数组 dp[n] 保存小于等于 n 的数被表示为 k 个完全平方数的和的最小的 k 的数量,根据状态转移方程 dp[n] = min({dp[n], dp[n - 1], dp[n - 4], dp[n - 9], …}) + 1 计算得到结果。时间复杂度是 O(n * w),w 是比 n 小的完全平方数的个数,空间复杂度是 O(n)。
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0, dp[1] = 1;
for (int i = 2; i <= n; ++i)
for (int j = 1; i - j * j >= 0; ++j)
dp[i] = min(dp[i], dp[i - j * j] + 1);
return dp[n];
}
};
343 整数拆分
给一个数 n,将其拆分为至少两个数的和,求这些整数的最大乘积。
n 可以被拆分为 2 个数的和,这两个数又可以被拆分为若干个数的和,因此只需要知道其被拆分为两个数时这两个数的最大乘积,自下往上地计算小于等于 n 的数被拆分时的最大乘积即可,状态转移方程是 dp[i] = max(dp[i], dp[j] * dp[i - j])。时间复杂度是 O(n ^ 2),空间复杂度是 O(n)。
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1, 0);
if (n < 3) return 1;
if (n == 3) return 2;
dp[2] = 2, dp[3] = 3;
for (int i = 4; i <= n; ++i)
for (int j = 1, k = i - j; j <= k; ++j, --k)
dp[i] = max(dp[i], dp[j] * dp[k]);
return dp[n];
}
};
1155 掷骰子的 N 种方法
对于每一个骰子来说,它可以在之前的基础上有 f 种投掷的方法,它之后的状态是 dp[i + 1][j + k],i 是投掷过的骰子的个数,k 是它投掷不同的 f 种方法,j 是到它为止投掷出和为 j 的种数,自下而上动态规划即可。
class Solution {
public:
int numRollsToTarget(int d, int f, int target) {
int dp[d + 1][target + f + 1];
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
for (int i = 0; i < d; ++i)
for (int j = 0; j < target; ++j)
if (dp[i][j])
for (int k = 1; k <= f; ++k)
if (j + k <= target)
dp[i + 1][j + k] = (dp[i + 1][j + k] + dp[i][j]) % 1000000007;
return dp[d][target];
}
};
2. 买卖股票
121 买卖股票的最佳时机
给一个股票数组,只能进行一次交易,求最大利润。
最大化当前值与之前的最小值之差。时间复杂度是 O(n)。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int min_val = INT_MAX, res = 0;
for (auto &p:prices) {
min_val = min(min_val, p);
res = max(res, p - min_val);
}
return res;
}
};
122 买卖股票的最佳时机 II
给一个股票数组,不限制交易次数,求最大利润。
每一个严格递增的区间都是交易的时机,所以将严格递增区间内的差值全部加上即可。时间复杂度是 O(n)。
class Solution {
public:
int maxProfit(vector<int> &prices) {
int res = 0;
for (int i = 1; i < prices.size(); ++i)
res += max(prices[i] - prices[i - 1], 0);
return res;
}
};
123 买卖股票的最佳时机 III
给一个股票数组,只能进行两笔交易,求最大利润。
因为要进行两笔交易,所以需要最大化两个值:一个是到第 i 天为止的最大收益,一个是第 i 天之后的最大收益。第一种方法是两次遍历,第一次计算到第 i 天为止的最大收益,第二次反向遍历计算第 i 天之后的最大收益,方法跟第一题相同,注意第二次买入操作必须在第一次卖出操作之后,不能发生在同一天。时间复杂度是 O(n)。
class Solution {
public:
int maxProfit(vector<int> &prices) {
int n = prices.size(), pre_min = INT_MAX, post_max = 0, res = 0;
vector<int> pre(n, 0), post(n, 0);
for (int i = 0; i < n - 1; ++i) {
pre_min = min(pre_min, prices[i]);
pre[i] = max(pre[max(0, i - 1)], prices[i] - pre_min);
}
for (int i = n - 1; i >= 0; --i) {
post_max = max(post_max, prices[i]);
post[i] = max(post[min(n - 1, i + 1)], post_max - prices[i]);
}
for (int i = 0; i < n; ++i)
res = max(res, pre[i] + post[i]);
return res;
}
};
第二种方法则是基于每天只有四种可能的操作:第一次买入,第一次卖出 res1,第二次买入,和第二次卖出 res2。第一次买入需要最大化之前买入股票的最小花费,第一次卖出需要最大化到第 i 天为止的股票价格与第一次买入的差值,第二次买入需要最大化在第 i 天买入股票并减去第一次卖出的收益,最后第二次卖出需要最大化到第 i 天为止的股票价格与第二次买入的差值。最后得到第二次卖出的最优值。时间复杂度是 O(n)。
class Solution {
public:
int maxProfit(vector<int> &prices) {
int buy1 = INT_MIN, sell1 = 0, buy2 = INT_MIN, sell2 = 0;
for (auto &p:prices) {
buy1 = max(buy1, -p);
sell1 = max(sell1, p + buy1);
buy2 = max(buy2, sell1 - p);
sell2 = max(sell2, p + buy2);
cout << buy1 << ' ' << sell1 << ' ' << buy2 << ' ' << sell2 << ' ' << endl;
}
return sell2;
}
};
309 最佳买卖股票时机含冷冻期
给一个股票数组,不限制交易次数,卖出和买入之间需要隔一天,求最大利润。
和上一题的第二种方法类似,我们可以用两个数组 buy 和 sell 分别表示买入和卖出操作,对于买入操作,需要最大化两天前卖出的最优值于今天买入的差值,对于卖出操作,需要最大化当天价格与前一天买入的最优值的差值。时间复杂度是 O(n)。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n < 2)
return 0;
vector<int> buy(n, 0), sell(n, 0);
buy[0] = -prices[0], sell[0] = 0;
for (int i = 1; i < n; ++i) {
buy[i] = max(buy[i - 1], sell[max(0, i - 2)] - prices[i]);
sell[i] = max(sell[i - 1], buy[i - 1] + prices[i]);
}
return sell[n - 1];
}
};
714 买卖股票的最佳时机含手续费
给一个股票数组,不限制交易次数,每次卖出有一定手续费,求最大利润。
和上一题类似,区别在于没有了交易间隔,以及每次进行 sell 操作的时候需要减去手续费。时间复杂度是 O(n)。
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
if (n < 2)
return 0;
vector<int> buy(n, 0), sell(n, 0);
buy[0] = -prices[0];
for (int i = 1; i < n; ++i) {
buy[i] = max(buy[i - 1], sell[i - 1] - prices[i]);
sell[i] = max(sell[i - 1], buy[i - 1] + prices[i] - fee);
}
return sell[n - 1];
}
};
188 买卖股票的最佳时机 IV
给一个股票数组,最多能进行 k 笔交易,求最大利润。
把上面五道题都做完之后这道题就很简单了。相比于第二题,因为这道题的 k 是未知的,所以要用一个循环将所有可能的 k 笔交易的最优值都计算出来,因此用一个三维数组 dp[n][k][2],或是分开成两个二维数组 buy[n][k] 和 sell[n][k],来表示前 n 天进行 k 笔交易的最优的买入和卖出值。同样的,买入和卖出操作都是在之前一次的卖出和买入的基础上进行的,使用 buy[i][j] = max({buy[i][j - 1], buy[i - 1][j], sell[i - 1][j - 1] - prices[i]}) 来表示前 i 天进行了 j 次交易的最优的买入值,第一项在 j <= i / 2 时会填充为 buy[i][j - 1]的值,防止后面操作时不会取到空值或默认值造成错误,第二项是当前买入操作不能取得最优值的结果,第三项则是当前买入操作能取得最优值的结果;对应的卖出操作则是 sell[i][j] = max({sell[i][j - 1], sell[i - 1][j], buy[i - 1][j] + prices[i]})。
值得注意的是,当 k 远大于数组长度的两倍,或 k 非常大时,构造二维数组会造成MLE,此时可以直接用第二题的思路解决。时间复杂度是 O(n * k),空间复杂度是 O(n * k)。
class Solution {
public:
int maxProfit(int k, vector<int> &prices) {
int n = prices.size(), res = 0;
if (n < 2 || k == 0)
return 0;
if (k >= n * 2) {
for (int i = 1; i < n; ++i)
res += max(0, prices[i] - prices[i - 1]);
return res;
}
vector<vector<int>> buy(n, vector<int>(k, INT_MIN)), sell(n, vector<int>(k, 0));
buy[0][0] = -prices[0], sell[0][0] = 0;
for (int i = 1; i < n; ++i) {
buy[i][0] = max(buy[i - 1][0], -prices[i]);
sell[i][0] = max(sell[i - 1][0], buy[i - 1][0] + prices[i]);
for (int j = 1; j < k; ++j) {
buy[i][j] = max({buy[i][j - 1], buy[i - 1][j], sell[i - 1][j - 1] - prices[i]});
sell[i][j] = max({sell[i][j - 1], sell[i - 1][j], buy[i - 1][j] + prices[i]});
}
}
return sell[n - 1][k - 1];
}
};