蓬门今始为君开~
最近期末周,更新比较慢,放假后会更新大量优质题目的解析~
来源 力扣
在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days
的数组给出。每一项是一个从 1
到 365
的整数。
火车票有 三种不同的销售方式 :
- 一张 为期一天 的通行证售价为
costs[0]
美元; - 一张 为期七天 的通行证售价为
costs[1]
美元; - 一张 为期三十天 的通行证售价为
costs[2]
美元。
通行证允许数天无限制的旅行。 例如,如果我们在第 2
天获得一张 为期 7 天 的通行证,那么我们可以连着旅行 7 天:第 2
天、第 3
天、第 4
天、第 5
天、第 6
天、第 7
天和第 8
天。
返回 你想要完成在给定的列表 days
中列出的每一天的旅行所需要的最低消费 。
示例 1:
输入:days = [1,4,6,7,8,20], costs = [2,7,15] 输出:11 解释: 例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划: 在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。 在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, ..., 9 天生效。 在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。 你总共花了 $11,并完成了你计划的每一天旅行。
示例 2:
输入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15] 输出:17 解释: 例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划: 在第 1 天,你花了 costs[2] = $15 买了一张为期 30 天的通行证,它将在第 1, 2, ..., 30 天生效。 在第 31 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 31 天生效。 你总共花了 $17,并完成了你计划的每一天旅行。
提示:
1 <= days.length <= 365
1 <= days[i] <= 365
days
按顺序严格递增costs.length == 3
1 <= costs[i] <= 1000
思路分析:
我们每次拿到一个日期,可以有三种尝试方法:1天,7天和30天,我们记录每一种尝试方法,再找出最小花费即可,只用选择日期一个参数,很经典的一维动态规划。
为了方便大家入门,这里先不直接列出转移方程,而是先使用递归的方式呈现,比较直观
当然 直接暴力递归肯定会T,所以这里用了一个记忆数组,保证我们没有重复计算,极大地优化了时间
class Solution {
static int MAX=366;
static int []durations = {1,7,30};
static int dp[]=new int [MAX];
public int mincostTickets(int[] days, int[] costs) {
int n=days.length;
for(int i=0;i<n;i++)
{
dp[i]=-1;
}//初始化格子
return (dfs(0,days,costs));
}
//dfs 含义 返回完成从当前天数到旅行结束in所需要的最小价格
static int dfs(int in,int []days,int []costs)
{
if(in==days.length) return 0;//完成所有旅行
if(dp[in]!=-1) return dp[in];//记忆化搜索应该是在越界判断之下的
int ans=Integer.MAX_VALUE;
for(int i=0,j=i;i<3;i++)
{
while(j<days.length&&days[in]+durations[i]>days[j])
{
j++;//通过移动下标j 一直找到通行证过期的下一个旅行天数
}
//枚举
int sum=costs[i]+dfs(j, days, costs);
ans=Math.min(ans, sum);//找到花费最小的尝试
}
dp[in]=ans;
return ans;
}
}
这一版已经是最优解了
当然,也可以用dp的形式。
核心代码(转移方程都是从上一个版本扒过来的,这说明二者有相通之处)
稍微解释一下 首先定义dp表 长度为days的长度+1(因为从0开始遍历到n)
初始化,最后一个越界位置 ,代价是0,填上0
然后从n-2开始填表
dp【0】就是从第一个日期到结束所需要的最小花费拉~
class Solution {
public static int MAX=366;
public static int []durations = {1,7,30};
public static int dp[]=new int [MAX];
public int mincostTickets(int[] days, int[] costs) {
dp=new int [days.length+1];
int N=dp.length;
dp[N-1]=0;
for(int index=N-2;index>=0;index--)
{
int ans=Integer.MAX_VALUE;
for(int k=0,j=index;k<3;k++)
{
while(j<days.length && days[index]+durations[k]>days[j]) j++;//枚举
ans=Math.min(ans,costs[k]+dp[j]);//记录当前选择的 价格 然后从days【j】天再买
}
dp[index]=ans;
}
return dp[0];
}
//在第index对应的天 完成所有旅行的最低消费
}
来源 力扣
一条包含字母 A-Z
的消息通过以下映射进行了 编码 :
'A' -> "1" 'B' -> "2" ... 'Z' -> "26"
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106"
可以映射为:
"AAJF"
,将消息分组为(1 1 10 6)
"KJF"
,将消息分组为(11 10 6)
注意,消息不能分组为 (1 11 06)
,因为 "06"
不能映射为 "F"
,这是由于 "6"
和 "06"
在映射中并不等价。
给你一个只含数字的 非空 字符串 s
,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
示例 1:
输入:s = "12" 输出:2 解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226" 输出:3 解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
示例 3:
输入:s = "06" 输出:0 解释:"06" 无法映射到 "F" ,因为存在前导零("6" 和 "06" 并不等价)。
提示:
1 <= s.length <= 100
s
只包含数字,并且可能包含前导零。
解题思路:解码的方式有多种,最好想的办法就是dfs一路从左往右尝试,注意对应好1-26,排除前导0的情况。
class Solution {
public int numDecodings(String s) {
int dp[]=new int [s.length()];
char word[]=new char [s.length()];
for(int i=0;i<dp.length;i++)
{
dp[i]=-1;
}
word=s.toCharArray();
return( dfs(word,0,dp));
}
static int dfs(char word[],int in,int dp[])//dfs含义 得到从当前下标到结尾的所有解码方法个数
{
if(in==word.length) return 1;//代表这是一种可行的方法
if(dp[in]!=-1) return dp[in];
//拿到一个字母的处理方法
int ans=0;
if(word[in]=='0') return 0;//这是一条死路,直接断掉就行
ans=dfs(word, in+1,dp);//直接映射1-9
if(in+1<word.length&&(word[in]-'0')*10+word[in+1]-'0'<=26)//判断,不能越界,并且对应26以内
ans+=dfs(word, in+2,dp);
dp[in]=ans;
return ans;
}
}
仍然已经是最优算法~
当然 有dp版本
总体思路相同。
class Solution {
public int numDecodings(String s) {
char word[]=new char [s.length()];
word=s.toCharArray();
int dp[]=new int [s.length()+1];
int N=dp.length;
dp[N-1]=1;
for(int index=N-2;index>=0;index--)
{
int ans=0;
if(word[index]=='0') ans=0;
//单独出现前导0 说明这个方案不符
else {
ans+=dp[index+1];//处理一个
//if(word[index]<=2) //记得防止越界 字符要转换成数字
if(index+1<word.length&&((word[index]-'0')*10+word[index+1]-'0')<=26)
ans+=dp[index+2];
}
dp[index]=ans;
}
return dp[0];
}
}
有人会问,我dfs已经是最优,为啥还要搞成dp的形式呢?
因为对于有些题目,dp可以更好地优化,比如状压dp。
对于练习来说,建议都试着改一下dp的形式~
当然,对于一些路径依赖的题目,无法改成dp,后面会写文章讲讲~
怎么样,是不是有点感觉了??
再来一个升级版~
一条包含字母 A-Z
的消息通过以下的方式进行了 编码 :
'A' -> "1" 'B' -> "2" ... 'Z' -> "26"
要 解码 一条已编码的消息,所有的数字都必须分组,然后按原来的编码方案反向映射回字母(可能存在多种方式)。例如,"11106"
可以映射为:
"AAJF"
对应分组(1 1 10 6)
"KJF"
对应分组(11 10 6)
注意,像 (1 11 06)
这样的分组是无效的,因为 "06"
不可以映射为 'F'
,因为 "6"
与 "06"
不同。
除了 上面描述的数字字母映射方案,编码消息中可能包含 '*'
字符,可以表示从 '1'
到 '9'
的任一数字(不包括 '0'
)。例如,编码字符串 "1*"
可以表示 "11"
、"12"
、"13"
、"14"
、"15"
、"16"
、"17"
、"18"
或 "19"
中的任意一条消息。对 "1*"
进行解码,相当于解码该字符串可以表示的任何编码消息。
给你一个字符串 s
,由数字和 '*'
字符组成,返回 解码 该字符串的方法 数目 。
由于答案数目可能非常大,返回 109 + 7
的 模 。
示例 1:
输入:s = "*" 输出:9 解释:这一条编码消息可以表示 "1"、"2"、"3"、"4"、"5"、"6"、"7"、"8" 或 "9" 中的任意一条。 可以分别解码成字符串 "A"、"B"、"C"、"D"、"E"、"F"、"G"、"H" 和 "I" 。 因此,"*" 总共有 9 种解码方法。
示例 2:
输入:s = "1*" 输出:18 解释:这一条编码消息可以表示 "11"、"12"、"13"、"14"、"15"、"16"、"17"、"18" 或 "19" 中的任意一条。 每种消息都可以由 2 种方法解码(例如,"11" 可以解码成 "AA" 或 "K")。 因此,"1*" 共有 9 * 2 = 18 种解码方法。
示例 3:
输入:s = "2*" 输出:15 解释:这一条编码消息可以表示 "21"、"22"、"23"、"24"、"25"、"26"、"27"、"28" 或 "29" 中的任意一条。 "21"、"22"、"23"、"24"、"25" 和 "26" 由 2 种解码方法,但 "27"、"28" 和 "29" 仅有 1 种解码方法。 因此,"2*" 共有 (6 * 2) + (3 * 1) = 12 + 3 = 15 种解码方法。
提示:
1 <= s.length <= 105
s[i]
是0 - 9
中的一位数字或字符'*'
解决方法;
本题多了一个“ *
” 字符,可以代表任意非0数字(很关键) 我们依然分类讨论即可
拿到一个字符 判断
1.不是*
1.1 是0 拱!
1.2 不是0 1.11 直接合成一个字符 1-9
1.12 跟后面一个一起合成
1.111 后面是数字 判断 26
1.112 后面是* 1* 和2*符合题意
1*有9种 2*有6种
2.是*
1.1 直接合成 那就是9种
1.2 跟后面的一起合成
1.21 后面是数字 *(1-6) 有2*1=2种
*(7-9 ) 有1*1=1种
1.22 后面是* ** 有15种
到了这里,恭喜,你已经讨论结束了 然后代码实现就可以拉
别忘了取模 你有没有忘了?O-O
class Solution {
public static long mod=1000000007L;
public int numDecodings(String s) {
char word[]=new char [s.length()];
long dp[]=new long [s.length()];
for(int i=0;i<dp.length;i++)
{
dp[i]=-1;
}
word=s.toCharArray();
return (int)(dfs(word,0,dp));
}
//表示从第index位置开始看,最多有几种编码方法
static long dfs(char word[],int index,long dp[])
{
if(index==word.length) return 1;//走到头了 表示这种解法符合题意
if(word[index]=='0') return 0;
if(dp[index]!=-1) return dp[index];
//讨论index位置是*还是数字,是*有九种可能
long ans=dfs(word,index+1,dp)*(word[index]=='*'? 9:1);
if(index+1<word.length)
{
if(word[index]!='*')
{
if(word[index+1]!='*')
{
if((word[index]-'0')*10+word[index+1]-'0'<=26)
ans+=dfs(word,index+2,dp);//数字 +数字
}else
{//数字+*
if(word[index]=='1')
{
ans+=9*dfs(word, index+2, dp);
}
if(word[index]=='2')
{
ans+=6*dfs(word, index+2, dp);
}
}
}else //*加数字
{
if(word[index+1]!='*') {
if((word[index+1]-'0')<=6)
ans+=2*dfs(word,index+2,dp);
else
ans+=dfs(word,index+2,dp);
}else //* + *
{
ans+=dfs(word, index+2, dp)*15;
}
}
}
ans%=mod;
dp[index]=ans;
return ans;
}
}
一维dp版本
class Solution {
public static long mod=1000000007L;
public int numDecodings(String s) {
char word[]=new char [s.length()];
long dp[]=new long [s.length()+1];
int N=dp.length;
dp[N-1]=1;
word=s.toCharArray();
for(int index=N-2;index>=0;index--)
{
long ans=0;
if(word[index]!='0')
{//很关键 如果是0 直接跳过 不参与一切运算
//讨论index位置是*还是数字,是*有九种可能
ans=ans+dp[index+1]*(word[index]=='*'? 9:1);
if(index+1<word.length)
{
if(word[index]!='*')
{
if(word[index+1]!='*')
{
if((word[index]-'0')*10+word[index+1]-'0'<=26)
ans+=dp[index+2];//数字 +数字
}else
{//数字+*
if(word[index]=='1')
{
ans+=9*dp[index+2];
}
if(word[index]=='2')
{
ans+=6*dp[index+2];
}
}
}else //*加数字
{
if(word[index+1]!='*') {
if((word[index+1]-'0')<=6)
ans+=2*dp[index+2];
else
ans+=dp[index+2];
}else //* + *
{
ans+=dp[index+2]*15;
}
}
}
}
ans%=mod;
dp[index]=ans;
}
return (int)(dp[0]);
}
//表示从第index位置开始看,最多有几种编码方法
}
题解大佬的版本,直接用两个变量优化掉整个数组 值得学习!!
public static int numDecodings4(String str) {
char[] s = str.toCharArray();
int n = s.length;
long cur = 0, next = 1, nextNext = 0;
for (int i = n - 1; i >= 0; i--) {
if (s[i] != '0') {
cur = (s[i] == '*' ? 9 : 1) * next;
if (i + 1 < n) {
if (s[i] != '*') {
if (s[i + 1] != '*') {
if ((s[i] - '0') * 10 + s[i + 1] - '0' <= 26) {
cur += nextNext;
}
} else {
if (s[i] == '1') {
cur += nextNext * 9;
}
if (s[i] == '2') {
cur += nextNext * 6;
}
}
} else {
if (s[i + 1] != '*') {
if (s[i + 1] <= '6') {
cur += nextNext * 2;
} else {
cur += nextNext;
}
} else {
cur += nextNext * 15;
}
}
}
cur %= mod;
}
nextNext = next;
next = cur;
cur = 0;
}
return (int) next;
}
后面开始做一些直接dp的题目,就不一步步递归分析了
来源 你猜 ?当然是力扣
给你一个整数 n
,请你找出并返回第 n
个 丑数 。
丑数 就是质因子只包含 2
、3
和 5
的正整数。
示例 1:
输入:n = 10 输出:12 解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
示例 2:
输入:n = 1 输出:1 解释:1 通常被视为丑数。
提示:
1 <= n <= 1690
暴力思维 我直接枚举每一个数,看看是不是丑数?
弊端 仔细想想会发现 当n慢慢增大时,我们枚举的数增长速度快到离谱!
那么循环次数直接爆炸了,肯定T
那么,可不可以优化一下? 我用现成的数,去直接得到下一个丑数??
当然 丑数 就是质因子只包含 2
、3
和 5
的正整数。 那么 丑数的2 3 5倍一定也是丑数
哦哦,那我就依次找到我已经有的丑数的2 3 5倍中最小的数做下一个丑数就可以了
为了方便定位,我们使用三个指针来分别记录当下最小的2 3 5倍
为啥想到用多指针法? 因为可能性出现了单调性,数一定是越乘越大
不会双指针的朋友可以看这里 --->力扣双指针刷题(算法日记)-CSDN博客
class Solution {
public int nthUglyNumber(int n) {
int dp[]=new int [n+1];
dp[1]=1;
//思路 某一个丑数一定是前面丑数*2或*3或*5的倍数
for(int i=2,p2=1,p3=1,p5=1;i<=n;i++)
{//三个指针先同时指向第一个丑数
int a=dp[p2]*2;
int b=dp[p3]*3;//依次枚举
int c=dp[p5]*5;
int now=Math.min(Math.min(a,b),c);//定位到最小 的丑数 就是下一个丑数
if(a==now) p2++;//如果枚举到了 就让对应的指针往后走一格
if(b==now) p3++;
if(c==now) p5++;
dp[i]=now;
}
//System.out.println(dp[n]);
return dp[n];
}
}
码字好累呜呜呜,再打一个题吧 TWT
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()" 输出:2 解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())" 输出:4 解释:最长有效括号子串是 "()()"
示例 3:
输入:s = "" 输出:0
提示:
0 <= s.length <= 3 * 104
s[i]
为'('
或')'
这题的数据量就比较大了,递归可能太好搞。。。
dp一波 还是从左向右分析
定义dp【i】含义 , 在当前index位置之前 一个最长有效串的长度,
那么答案就是所有的dp【】表里最大的那一个
然后开始分类讨论了
拿到左括号 : 未匹配 ,返回0
拿到右括号: 往前找,定位到左括号
如果找到了 那么长度至少是2 然后再加上左括号前面的格子的计算长度
比如( ) ( ) 计算4的时候要加上前面的2
dp 2 4
找不到 那么这个右括号废了 对应格子填0
稍微注意一下边界处理就行
class Solution {
public int longestValidParentheses(String s) {
//思路
//定义递归函数 在当前index位置之前 最长有效串的长度
//)(()())
char []a=new char [30005];
int dp[]=new int [s.length()];
a=s.toCharArray();
int ans=0;
for(int i=1,p;i<s.length();i++)
{
if(a[i]=='(') dp[i]=0;
if(a[i]==')')
{
p=i-dp[i-1]-1;//定位找到需要判定的位置
if(p>=0&&a[p]=='(')
{
dp[i]=2+dp[i-1]+(p>0? dp[p-1]:0);
}else
dp[i]=0;
}
ans=Math.max(ans, dp[i]);
}
return ans;
}
}
dp刷题,未完待续,新人博主,欢迎关注,如果不行的话,点赞个赞再走叭~