动态规划(dynamic programming),以下简称dp,本质是带了记忆+枚举,分析时从上至下(深度搜索),dp的思考方式是从底至上,少数从上至下。
1.整数拆分
给定一个正整数?n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 ×3 ×4 = 36。
思路:
n拆分成至少两个以上的数
f(n) = i*f(n-i) //对n-i继续拆分
f(n) = f(i)(n-i) //对i继续拆分
f(n) = f(i)f(n-i) //对i和n-i继续拆分
i与n-i对称,考虑其中一种,i*(n-i)和if(n-i)包括所有情况,也包括f(i)f(n-i) ,所以f(n) = max(i(n-i),if(n-i))
递归树解法
int dfs(n){
if(n<2){
return 0;
}
if(n==2){
return 1;
}
//枚举i
int res = 0;
for(int i=1;i<n;i++){
res = max(res,max(i*(n-i),i*dfs(n-i)));
}
return res;
}
带记忆的递归
int dfs(n){
if(n<2){
return 0;
}
if(n==2){
return 1;
}
if(memo[n]!=-1){
return meno[n];
}
//枚举i
int res = 0;
for(int i=1;i<n;i++){
res = max(res,max(i*(n-i),i*dfs(n-i)));
}
return meno[n] = res;
}
//动态规划
1、确定状态(子问题) ,拆与不拆,拆,n拆成i,n-i,
2、确定转移方程(原问题与子问题的关系),n-i是否继续拆? 是dp[n-i],否n-i,那么
dp[i] = max(dp[i],max(j*(i-j),j*dp[i-j]))
3、确定base(边界条件)
0,1不能拆(原子数),dp[0] = dp[1] = 1;dp[2] = 2;2=1+1, 1*1
初始化dp[i] = 0; i>=2; 未拆,数小于2时,乘积设置为0
dp[2] = max(dp[2],1*(2-1),1*dp[2-1]) = max(0,1,1) = 1;
dp[3] = max(dp[3],1*(3-1),1*dp[3-1]) = max(0,2,1*dp[2]) = 2; 即:3=1+2,1*2=2
dp代码
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n+1,0);
dp[0] = 1;
dp[1] = 1;
for(int i=2;i<=n;i++){
int cur = 0;
for(int j=1;j<i;j++){
cur = max(cur,max(j*(i-j),j*dp[i-j]));
}
dp[i] = cur;
}
return dp[n];
}
};
类似的:剪绳子
2.丑数
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
分析:由自底之上的方法,维护三条有序链表,p2,p3,p5分别是2,3,5的倍数,合并三个有序数组,每次取最小的数
nums2 = {1*2, 2*2, 3*2, 4*2, 5*2, 6*2, 8*2...}
nums3 = {1*3, 2*3, 3*3, 4*3, 5*3, 6*3, 8*3...}
nums5 = {1*5, 2*5, 3*5, 4*5, 5*5, 6*5, 8*5...}
那么 dp[i] = min(dp[p2]*2,dp[p3]*3,dp[p5]*5),每次将最小的数的指针++,对应的表格:
index | 0 | 2 | 1 | 3 | 4 |
---|---|---|---|---|---|
p2 | 0 | 1,min(1×2,1×3,1×5),p2++ | 1 | 2,min(2×2,2×3,1×5),p2++ | … |
p3 | 0 | 1 | 1,min(2×2,1×3,1×5),p3++ | 1 | … |
p5 | 0 | 0 | 0 | 0 | … |
nums | 1 | 2 | 3 | 4 | … |
class Solution {
private:
int min(int a,int b){
return a<b?a:b;
}
public:
int nthUglyNumber(int n) {
vector<int> dp(n,0);
int p2=0,p3=0,p5=0;
dp[0] = 1;
for(int i=1;i<n;i++){
int minvalue = min(dp[p2]*2,min(dp[p3]*3,dp[p5]*5));
dp[i] = minvalue;
if(minvalue%2==0){
p2++;
}
if(minvalue%3==0){//同时被2和3整除的数会被执行,比如6
p3++;
}
if(minvalue%5==0){
p5++;
}
}
return dp[n-1];
}
};
3.完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 10^4
分析:
定义:dp![i]![j] 表示前i个完全平方数组成数字j的最小数量,问题转化为完全背包,并且是恰好装满,状态是第i个完全平方数放或者不放
状态转移方程:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
i
∗
i
]
+
1
)
,
i
f
(
i
∗
i
<
j
)
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
i
f
(
i
∗
i
>
=
j
)
dp[i][j] = min(dp[i-1][j],dp[i][j-i*i]+1),if(i*i<j) dp[i][j] = dp[i-1][j] if(i*i>=j)
dp[i][j]=min(dp[i−1][j],dp[i][j−i∗i]+1),if(i∗i<j)dp[i][j]=dp[i−1][j]if(i∗i>=j)
base:
dp![0]![0] = 1;其余dp![i]![j]=INF,因为是求最小
状态压缩:
d
p
[
j
]
=
m
i
n
(
d
p
[
j
]
,
d
p
[
j
−
i
∗
i
]
+
1
)
dp[j] = min(dp[j],dp[j-i*i]+1)
dp[j]=min(dp[j],dp[j−i∗i]+1)
dp代码:
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1,65535);
dp[0] = 0;
for(int i=0;i<=n;i++){//背包
for(int j=1;j*j<=i;j++){//物品
dp[i] = min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
};
思路2:
BFS+贪心
构建N叉树
(1) 节点的值(即余数)也是一个完全平方数。
(2) 在满足条件(1)的所有节点中,节点和根之间的距离应该最小。
使用队列将数字n入队列,减去n以内完全平方数,得到余数13-1,13-4,13-9,再将余数入队列,如果遇到剩余队列的数字都是完全平方数,则找到一组解
需要用set对每层的余数进行去重
class Solution {
public:
int numSquares(int n) {
set<int> s;
s.insert(n);
vector<int> squreNum;
for(int i=1;i*i<=n;i++){
squreNum.push_back(i*i);
}
int level=0;
while(!s.empty()){
set<int> s1;
level += 1;
for(int reminder:s){
for(int snum : squreNum){
if(snum==reminder){
return level;
}else if(snum>reminder){
break; //不能在减做余数
}else{
s1.insert(reminder-snum);
}
}
}
s = s1;
}
return level;
}
};
4.零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
链接:https://leetcode-cn.com/problems/coin-change
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//思考,硬币是无限的,是一个完全背包问题
//amount是背包的总容量
//凑成是一个,恰好装满的完全背包问题,初始化dp[0]=0,其他为无穷大,因为是求最小
//定义,dp[i],容量为i时所需要的最少钱币数
//dp[i] = min(dp[j],dp[j-nums[i]]+1)//放或不放
if(amount == 0){
return 0;
}
vector<int> dp(amount+1,0);
for(int i=0;i<=amount;i++){
dp[i] = 65535;
}
dp[0] = 0;
for(int i=0;i<coins.size();i++){//枚举物品
for(int j=coins[i];j<=amount;j++){//完全背包正向枚举背包容量
dp[j] = min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount]!=65535?dp[amount]:-1;
}
};