[dp_1] 使用最小花费爬楼梯 | 解码方法 | 虚拟dp[0]=0
目录
1.使用最小花费爬楼梯
题解
动态规划解法一:
动态规划解法二:
2.解码方法
题解
1.使用最小花费爬楼梯
链接:746. 使用最小花费爬楼梯
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。
一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
题解
本题求达到楼梯顶部的最低花费。
- 向上爬楼梯需要支付本层的费用,然后可以爬一层或者两层。
- 可以从下标为 0 或下标为 1 的台阶开始爬楼梯。
注意要求的是爬到楼顶的最低花费,即使到达数组最后一个还需要在往上爬一步加上本层的费用。
动态规划解法一:
1.状态表示
经验+题目要求
向这种一维数组的dp一般经验分为两种:
- 以某个位置为结尾,巴拉巴拉(根据题目要求把它替换掉)
- 以某个位置为起点,巴拉巴拉
解法一用的是第一种以某个位置为结尾,巴拉巴拉,接下来看如何替换掉巴拉巴拉。
- 这道题让找达到楼梯顶部的最低花费。
- 那如果以 i 位置结尾,求的是最少花费。
那我就可以得到这样一个状态表示,dp[i]表示,到达 i 位置时,最少花费
2.状态转移方程
分析状态转移方程的一条总线:
- 用 i 位置之前或者之后的状态,推导 dp[i] 的值
- 如i之前状态 dp[i-2]、dp[i-1],i之后状态 dp[i+1],dp[i+2]
如何推导出dp[i]的值呢?
根据最近的一步,来划分问题
- 如这道题,先到达i-1的位置,从i-1位置花费i-1位置的费用走一步到i,或者可以先到达i-2的位置,花费i-2位置的费用走两步到i。
- 这是依据 i 位置最近的一步来划分出的两种情况。
- 因为要求花费最少,所有两种情况种选择最少的。
接下来看这两种情况能不能用之前的状态表示一下。
- cost[i-1]是定值无法改变,先到达i-1位置也是有一个花费,如果想求i位置最小花费,是不是要先找到i-1位置的最小花费
- 只要找到i-1位置的最小花费在加上cost[i-1]走一步,就是第一种情况的最小花费。
- 到达i-1位置最小花费不就是dp[i-1] 表示到达i-1位置,最小花费。
- 同理i-2也是这样分析的。
然后求的是两种情况的最小值。因此状态转移方程就有了。
3.初始化
- 确保填表时不能越界
- dp[0]=dp[1]=0
4.填表顺序
- 由前面两个位置填后面的位置。
- 从左往右
5.返回值
- 结合题目要求,返回到达楼梯最小花费。
- dp表数组下标为n的地方。所以返回 dp[n]
动态规划解法二:
其实第二种解决就是换了一种状态表示。
1.状态表示
经验+题目要求
以 i 位置为起点,巴拉巴拉
以i位置为起点,然后要去楼顶还要是最小花费
因此 dp[i]表示,从i位置出发,到达楼顶,此时最小花费。
2.状态转移方程
分析状态转移方程的一条总线:
- 用 i 位置之后的状态,推导 dp[i] 的值
- 如i之前状态 dp[i-2]、dp[i-1],i之后状态 dp[i+1],dp[i+2]
如何推导出dp[i]的值呢?
- 根据最近的一步,来划分问题
3.初始化
- 因为我们是从某一个位置到楼顶,所以dp数组不需要额外在开一个位置。
- 直接跟原始数组一样大就可以了。
- 其次我们需要先知道i+1的位置和i+2的位置才能知道dp[i]的值,因此先把最后两个位置初始化
- dp[n-1]=dp[n-2]=0
4.填表顺序
- 知道后面两个位置的值,就可以得到前面的值,因此从右往左
5.返回值
- 我们最开始要么是从0开始,要是是从1开始。所以返回的是dp[0],dp[1]中的最小值。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost)
{
//dp
//初始化
//方程
//返回
int n=cost.size();
vector<int> dp(n);//以i出发,到达n
dp[n-1]=cost[n-1];
dp[n-2]=cost[n-2];
for(int i=n-3;i>=0;i--)
{
//从右往左
dp[i]=cost[i]+min(dp[i+1],dp[i+2]);
}
return min(dp[0],dp[1]);
}
};
2.解码方法
链接:91. 解码方法
一条包含字母 A-Z
的消息通过以下映射进行了 编码 :
"1" -> 'A'
"2" -> 'B'
...
"25" -> 'Y'
"26" -> 'Z'
然而,在 解码 已编码的消息时,你意识到有许多不同的方式来解码,因为有些编码被包含在其它编码当中("2"
和 "5"
与 "25"
)。
例如,"11106"
可以映射为:
"AAJF"
,将消息分组为(1, 1, 10, 6)
"KJF"
,将消息分组为(11, 10, 6)
- 消息不能分组为
(1, 11, 06)
,因为"06"
不是一个合法编码(只有 "6" 是合法的)。
注意,可能存在无法解码的字符串。
给你一个只含数字的 非空 字符串 s
,请计算并返回 解码 方法的 总数 。如果没有合法的方式解码整个字符串,返回 0
。
题目数据保证答案肯定是一个 32 位 的整数。
示例 1:
输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
题解
1.状态表示
- 经验+题目要求
- 以 i 位置为结尾,巴拉巴拉。
- 接下来根据题目要求替换巴拉巴拉。
- 题目要求求s字符串有多少种解码方法。是不是就从从开始到结尾的解码总数。
那dp[i]就可以这样表示。dp[i]表示,以 i 位置为结尾时,解码方法的总数。
2.状态转移方程
- 根据i状态最近的一步,来划分问题(只考虑当下的事情,做好当下
最近一步就是解码到i位置的时候,解码到i位置有两种情况
- i位置单独解码
- i-1和i位置合在一起解码。
因为是以i位置为结尾的,所有i+1位置还没有到,暂时不考虑。
- 但是解码并不是想解码就解码,必须要符合条件,否则不能解码。
- 所有 单独解码 和 合在一起解码 都有成功或者失败的可能。
- 这时就要我们进行一个决策 if 之后,才能对 dp[i] 进行对上一状态的加等
3.初始化
- 因为会用到i-1和i-2所以要对0和1初始化。
4.填表顺序
- 填dp[i]要知道dp[i-1]和dp[i-2]的位置,所以从左到右(因为是以 i 结束)
5.返回值
- dp[i]表示以 i 位置为结尾时,解码方法的总数
- 题目要求求整个字符串所有解码方案,所以返回的是dp[n-1]。
class Solution {
public:
int numDecodings(string s)
{
if(s[0]=='0') return 0;//处理 特殊情况
int n = s.size();
vector<int> dp(n);
dp[0]=1;
//处理边界情况
if(n == 1) return 1;
if(s[0] != '0' && s[1] != '0')
dp[1] += 1;
int tmp = (s[0] - '0') * 10 + s[1] - '0'; //前两个位置表示的数
if(tmp >= 10 && tmp <= 26)
dp[1] += 1;
for(int i = 2; i < n; ++i)
{
if(s[i] != '0') dp[i] += dp[i - 1];//处理单独编码的情况
int tmp = (s[i - 1] - '0') * 10 + s[i] - '0';//处理合在一起编码的情况
if(tmp >= 10 && tmp <= 26)
dp[i] += dp[i - 2];
}
return dp[n-1];
}
};
之前写的dp代码都比较短,但是这里的dp初始化为什么这么长并且
- 部分初始化代码和填表中代码类似,有没有可能写在一起?
- 使代码编码简洁,是有的。
细节问题:
- 做dp问题的时候,会经常处理比较繁琐的边界情况以及初始化。
- 为了能更好的处理这些情况,对于一维数组我们可以把整个数组统一往后移动一位
- 也就是数组多开一个位置的技巧。
处理边界问题以及初始化问题的技巧
- 数组多开一个位置
之前旧dp表中的位置的值要在新的dp表中对应位置往后放一个。
- 多出来的位置我们称为虚拟位置。
- 多出来这个位置的作用,前面在旧的dp表要初始化0和1位置
在新dp表中虽然也初始化0和1的位置,但是确是方便了不少。
- 之前旧dp表初始化1的位置非常麻烦。
- 现在旧表中1的位置跑到新dp表中1填表的下标里面了。
- 我在新dp表中填表中就把旧dp表中1的位置干掉了。
这样就非常爽了~
但是却有两个注意事项:
- 虚拟节点里面的值,要保证后面填表是正确的
- 下标的映射关系
虚拟节点里面的值,要保证后面填表是正确的
比如新dp表中,填表时的 dp[2] = dp[1] + dp[0]
- dp[1]是不会错误的因为它的初始化是和旧dp[0]是一样的。
- 但是dp[0]是我们构建出来的,它里面值存放多少是不是就会影响到dp[2]的值。
- 一般情况下,这个虚拟节点的值存的是 0 ,但是这道题就不一样了,dp[0]里面存0是不正确的。
- 求dp[2]如果用到dp[0],是不是就是1和2的位置合在一起能解码成功,然后我才加上dp[0,
如果dp[0]是0那不就是少加了一种情况吗。因此这个dp[0]填1。
- 使用虚拟节点的时候,可以进行一个简单的分析不要想当然
- 总体来说就是具体问题具体分析!看虚拟节点的值到底填几。
下标的映射关系
- 在新dp表中,初始化dp[1]的时候,看的是s[0]这个位置能否解码成功,对应就是s[1-1] != ‘0’。
- 因为我们多加个一个位置,下标统一往后移动一位
那处理的原位置s[i] 就为 s[i-1] 了
class Solution {
public:
int numDecodings(string s)
{
if(s.empty() || s[0] == '0') return 0; //!!!!!!首位为0,就直接返回0
//dp
//虚拟 节点
int n=s.size();
vector<int> dp(n+1);
dp[0]=1; //初始1,因为 要实现 26位字母+
dp[1]=1;
//虚拟化节点 下标映射dp i //s i-1
if(n==1) return dp[1];
for(int i=2;i<=n;i++)
{
if(s[i-1]!='0') dp[i]+=dp[i-1];
int tmp=(s[i-2]-'0')*10+(s[i-1]-'0');
if(tmp>=10 && tmp<=26) dp[i]+=dp[i-2];
}
return dp[n];
}
};
- if(s.empty() || s[0] == '0') return 0; //!!!!!!首位为0,就直接返回0
- dp[0]=1; //初始1,因为 要实现 26位字母+
- 虚拟化节点 下标映射 原s[i]--->s[i-1]