动态规划基本概念
以凑零钱来讲述概念
1、确定 base case,这个很简单,显然目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。
2、确定「状态」,也就是原问题和子问题中会变化的变量。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 amount。
3、确定「选择」,也就是导致「状态」产生变化的行为。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。
4、明确 dp 函数/数组的定义。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。所以我们可以这样定义 dp 函数
dp(n) 的定义:输入一个目标金额 n,返回凑出目标金额 n 的最少硬币数量。
264. 丑数 II
编写一个程序,找出第 n 个丑数。丑数就是质因数只包含 2, 3, 5 的正整数。
思路
1个丑数也只能是从2,3,5中衍生出来动态规划: 一个数字可以拆分成若干因子之积,那么我们也可以使用不同因子去构造这个数
定义三个指针 p2、p3、p5 分别乘以 2、3、5 的数字
dp[i] 表示第 i 个丑数。那么 dp[p2] * 2、dp[p3] * 3 和 dp[p5] * 5 中的最小值就是下一个丑数。小顶堆:
将计算后的全部加入到小顶堆中,注意排重,堆顶就是ans
//动态规划
//一个数的因数也可以构造这个数
int nthUglyNumber(int n) {
vector<int> dp(n, 0);
dp[0] = 1;
//p2表示乘2的指针, p3表示乘3
int p2 = 0, p3 = 0, p5 = 0;
for(int i = 1; i < n; i ++){
//计算当前指针乘相应的数后 取出最小那个,即为构造出的数
dp[i] = min(min(dp[p2]*2, dp[p3]*3), dp[p5]*5);
if(dp[i] == dp[p2]*2) p2++;
if(dp[i] == dp[p3]*3) p3++;
if(dp[i] == dp[p5]*5) p5++;
}
return dp[n - 1];
}
//小顶堆
int nthUglyNumber(int n) {
priority_queue<long long, vector<long long>, greater<long>> dq;
set<long long> set;
dq.push(1);
set.insert(1);
vector<int> vt = {2,3,5};
int ans = 1;
for (int i = 1; i < n; i++) {
auto top = dq.top(); dq.pop();
for (auto t : vt) {
if (set.find(top * t) == set.end()) {
dq.push(top * t);
set.insert(top * t);
}
}
ans = dq.top();
}
return ans;
}
313.超级丑数
编写一段程序来查找第 n 个超级丑数。超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数。
思路
丑数问题都可以用小顶堆解决,当然是效率最慢的解决方法小顶堆
将计算后的值放入队列中,第n次弹出的数就是答案动态规划
实际就是把之前的3数换成了数组而已, 原理一样,每个primes数都对应indexs数组中记录着指向dp的index
//小顶堆
int nthSuperUglyNumber(int n, vector<int> &primes) {
priority_queue<long long , vector<long long>, greater<long long>> min_buf;
unordered_set<long long> set;
min_buf.push(1);
set.insert(1);
long long ans = 1;
for (int i = 0; i < n; i++) {
// 当i是1的时候弹出的就是第一个丑数
ans = min_buf.top(); min_buf.pop();
for (auto p : primes) {
long long num = (long long)ans * p;
//题目不可大于32位
if (num <=INT32_MAX && set.find(num) == set.end()) {
min_buf.push(num);
set.insert(num);
}
}
}
return ans;
}
//动态规划
int nthSuperUglyNumber(int n, vector<int>& primes) {
int len = primes.size();
vector<int> dp(n,1);
//记录每个指针指向dp的index
vector<int> index(len,0);
for(int i = 1; i < n; i++){
int minNum = INT_MAX;
//找出最小
for(int j = 0; j < len; j++) {
minNum = min(minNum, dp[index[j]]*primes[j]);
}
//找到能够相乘为minNum的指针都+1
for(int j = 0; j < len; j++) {
if(dp[index[j]]*primes[j] == minNum) {
index[j]++;
}
}
dp[i] = minNum;
}
return dp[n-1];
}
746.使用最小花费爬楼梯
思路
要踏上第i级台阶的话, 那么这个台阶最小花费可能来自
- 第i - 1 最小花费
- 第i - 2 最小花费
比如你准备踏上第3级台阶
那么最小花费可能出自 1 , 3 或者 2, 3
public int minCostClimbingStairs(int[] cost) {
// dp[i] 表示第i个台阶花费最小数
int[] dp = new int[cost.length];
int len = dp.length;
dp[0] = cost[0];
dp[1] = cost[1];
for (int i = 2; i < dp.length; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
// dp[len - 1] 表示踏到了最后一个台阶
// dp[len - 2] 表示跳过了最后一个台阶
return Math.min(dp[len - 1], dp[len - 2]);
}
354. 俄罗斯套娃信封问题
思路: 先按w从小到大, 然后按h从小到大
求h的最长上升子序列
public int maxEnvelopes(int[][] envelopes) {
if (envelopes.length == 0) {
return 0;
}
int n = envelopes.length;
Arrays.sort(envelopes,(e1, e2) ->{
return e1[0] != e2[0] ? e1[0] - e2[0] : e2[1] - e1[1];
});
int[] f = new int[n];
Arrays.fill(f, 1);
int ans = 1;
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
// 如果满足可装入条件
if (envelopes[j][1] < envelopes[i][1]) {
// f[i] 表示第i个最大信封数
f[i] = Math.max(f[i], f[j] + 1);
}
}
ans = Math.max(ans, f[i]);
}
return ans;
}