Leetcode每日一题(3)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode每日一题(3)相关的知识,希望对你有一定的参考价值。
参考技术A有 N 个网络节点,标记为 1 到 N 。
给定一个列表 times ,表示信号经过 有向 边的传递时间。 times[i] = (u, v, w) ,其中 u 是源节点, v 是目标节点, w 是一个信号从源节点传递到目标节点的时间。
现在,我们从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。
示例:
注意:
本题为一个图算法题,两点之间的时间可以抽象成路程,那么本题相当于求某一点到其他各点的最短路径,然后求出各点最短路径的最大值。
常见的最短路问题分为两类: 单源最短路 和 多源最短路 。前者只需要求一个 固定的起点 到各个顶点的最短路径,后者则要求得出 任意两个顶点 之间的最短路径。
Dijkstra(迪杰斯特拉)算法是典型的 单源最短路径算法 ,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。算法主要的思想是 贪心法 。
适用范围
使用步骤
Floyd算法是一个经典的 动态规划 算法。是解决 任意两点间的最短路径 (称为多源最短路径问题)的一种算法,可以正确处理有向图或负权的最短路径问题。
其核心思想就是从任意节点u到任意节点v的最短路径有2种:
所以,我们假设graph(u,v)为节点u到节点v的最短路径的距离(当然,不同的题目代表的不一样,比如有的可能是花费,需要灵活变通),对于每一个节点k(1~N个节点),我们检查graph(u,k) + graph(k,v) < graph(u,v)是否成立,如果成立,证明从u到k再到v的路径比u直接到v的路径短,我们便设置graph(u,v) = graph(u,k) + graph(k,v),当我们遍历完所有节点k,graph(u,v)中记录的便是u到v的最短路径的距离。
适用范围
使用步骤
2.1、根据k为中间跳节点更新(u, v)的最短距离
Dijkstra算法
Floyd算法 (十分暴力)
堆优化版的Dijkstra算法
参考链接:
LeetCode2022 7月 每日一题
【LeetCode】2022 7月 每日一题
前言
七月太忙了,又是项目又是练车又是各种比赛。大概有10天的每日一题没有当天写完(虽然后面补上了)。
将每日一题的所有思路记录在这里分享一下。
7.1 为运算表达式设计优先级
题目
给你一个由数字和运算符组成的字符串 expression
,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。
生成的测试用例满足其对应输出值符合 32 位整数范围,不同结果的数量不超过 104
。
示例 1:
输入:expression = "2-1-1"
输出:[0,2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2
示例 2:
输入:expression = "2*3-4*5"
输出:[-34,-14,-10,-10,10]
解释:
(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10
提示:
1 <= expression.length <= 20
expression
由数字和算符'+'
、'-'
和'*'
组成。- 输入表达式中的所有整数值在范围
[0, 99]
思路
本题是一道中等题,但是其思路万变不离其宗
复杂问题需要简单化,就需要分治。典型的分治问题
将一个表达式可以分成三个部分
- 左部分(当成一个计算出结果的数据)
- 中间部分(符号)
- 有部分(右边的计算出结果的数据)
这么分完之后,我们又可以对左部分
进行分解成三部分,右部分
分解成三部分
最后将所有的左部分和右部分得到的结果放入到数组中,返回这个数组即可
如果转化成代码,那么就可以使用dfs进行左中右的分治,dfs(left, right)表示在[left, right]
区间所有的计算结果的可能,而这个可能又可以分解成[left, i-1]
与[i, right]
两部分的结果相并
代码
ts版本
function diffWaysToCompute(expression: string): number[]
const s: string = expression
const calc: Function = (num1: number, num2: number, sign: string): number =>
let res: number = 0
switch(sign)
case '+':
res = num1 + num2
break
case '-':
res = num1 - num2
break
case '*':
res = num1 * num2
return res
const dfs: Function = (left: number, right: number): number[] =>
let res: number[] = []
for (let i = left; i <= right; i++)
let c = s[i]
if (c >= '0' && c <= '9') continue
let lRes = dfs(left, i - 1) // 左部分
let rRes = dfs(i + 1, right) // 右部分
for (let lr of lRes)
for (let rr of rRes)
res.push(calc(lr, rr, c))
// 计算数字
if (res.length == 0)
let tmp = 0
for (let i = left; i <= right; i++)
tmp = tmp * 10 + parseInt(s[i])
res.push(tmp)
return res
return dfs(0, s.length-1)
7.2 最低加油次数
题目
汽车从起点出发驶向目的地,该目的地位于出发位置东面 target
英里处。
沿途有加油站,每个 station[i]
代表一个加油站,它位于出发位置东面 station[i][0]
英里处,并且有 station[i][1]
升汽油。
假设汽车油箱的容量是无限的,其中最初有 startFuel
升燃料。它每行驶 1 英里就会用掉 1 升汽油。
当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。
为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1
。
注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0,仍然认为它已经到达目的地。
示例 1:
输入:target = 1, startFuel = 1, stations = []
输出:0
解释:我们可以在不加油的情况下到达目的地。
示例 2:
输入:target = 100, startFuel = 1, stations = [[10,100]]
输出:-1
解释:我们无法抵达目的地,甚至无法到达第一个加油站。
示例 3:
输入:target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]]
输出:2
解释:
我们出发时有 10 升燃料。
我们开车来到距起点 10 英里处的加油站,消耗 10 升燃料。将汽油从 0 升加到 60 升。
然后,我们从 10 英里处的加油站开到 60 英里处的加油站(消耗 50 升燃料),
并将汽油从 10 升加到 50 升。然后我们开车抵达目的地。
我们沿途在1两个加油站停靠,所以返回 2 。
提示:
1 <= target, startFuel, stations[i][1] <= 10^9
0 <= stations.length <= 500
0 < stations[0][0] < stations[1][0] < ... < stations[stations.length-1][0] < target
思路
困难题。属于是思路想不到,但是一旦想到,直接可以开始写。
我们可以把这个题看作,每次经过加油站,直接把油抬走。每次到车没油了,再在所有抬上车的油箱中,选择油最多的邮箱进行加油(贪心)
这样如果车上的油可以让我们到目的地,就成功返回加油次数,否则就返回-1
既然我们要每次加油加最多
油的油箱,有一个数据结构非常适合,那就是优先队列(堆),这里选择大根堆
,每次最多油的油箱放在最上面,需要用的时候就把他拿出来
代码
ts没有自带的优先队列实现,我这里直接硬写了一个实现的非常垃圾的有序数组来模拟大根堆(实际上仅仅是模拟堆头是最大的这一个功能罢了,其他的结构完全对不上哈哈哈哈…),原理就是每次插入,大的数据就差在前面,就这么简单
function minRefuelStops(target: number, startFuel: number, stations: number[][]): number
let loc = 0 // 当前行驶的路程
let res = 0 // 加油的次数
let nowFuel = startFuel // 当前拥有的油量
let heap = new Q() // 可以加油的加油站油量存储
let curIdx = 0 // 当前经过的加油站的idx
let ssl = stations.length
while (loc < target) // 只要没有到达target,就一直循环下去
if (nowFuel == 0) // 如果模拟到油没了,两种情况判断
if (!heap.isEmpty()) nowFuel += heap.poll(); res++ // 大根堆里存在加油站可以加油
else return -1 // 无法加油
loc += nowFuel
nowFuel = 0 // 继续行驶剩余油量
// 经过的加油站都存储到大根堆里
while (curIdx < ssl && stations[curIdx][0] <= loc)
heap.push(stations[curIdx++][1])
// console.log(heap.arr)
return res
// 憨憨方式模拟大根堆
class Q
arr: number[]
constructor()
this.arr = []
push(val: number)
if (this.arr.length == 0)
this.arr.push(val)
else
let l = this.arr.length
for (let i = 0; i < l; i++)
let curItem = this.arr[i]
if (val >= curItem)
if (i == 0)
this.arr = [val].concat(this.arr)
else
this.arr.splice(i, 0, val)
break
else if(val < curItem && i == l - 1)
this.arr = this.arr.concat([val])
poll()
return this.arr.shift() || 0
isEmpty()
return this.arr.length == 0
7.3 下一个更大元素 III
题目
给你一个正整数 n
,请你找出符合条件的最小整数,其由重新排列 n
中存在的每位数字组成,并且其值大于 n
。如果不存在这样的正整数,则返回 -1
。
注意 ,返回的整数应当是一个 32 位整数 ,如果存在满足题意的答案,但不是 32 位整数 ,同样返回 -1
。
示例 1:
输入:n = 12
输出:21
示例 2:
输入:n = 21
输出:-1
提示:
1 <= n <= 231 - 1
思路
这种题目属于是多解题了,我第一反应直接双指针。
讲讲我的思路吧,两个指针i、j
,i在前,j在后
i去寻找s[i] < s[i+1]的位置,也就是找到一个可以进行交换的位置
j在i固定好后(找到位置后),从后向前遍历,寻找到一个比s[i]小的数,与s[i]交换,这样就能保证前半部分(到i的部分)是最小的
后半部分(i+1到末尾),需要确保也是最小的,那么就得sort排个序(或者reverse一下)
前半部分加上后半部分,转为int,结果判断是否是32位有符号数
代码
function nextGreaterElement(n: number): number
let s = [...n + ""]
let l = s.length
for (let i = l - 2; i >= 0; i--)
if (s[i] >= s[i + 1]) continue // 左位比右位大,交换后的数会变小,直接跳过
for (let j = l - 1; j > i; j--)
// 左位比右位小,从后向前找比s[i]大的那一位
if (s[j] > s[i])
// 交换两个
let tmp = s[i]
s[i] = s[j]
s[j] = tmp
break
// 交换完后,要确保后面的部分是最小的
let left = s.slice(0, i + 1)
let right = s.slice(i + 1, l)
right.sort()
let res = parseInt(left.join('') + right.join(''))
return res <= 2 ** 31 - 1 ? res : -1
return -1
7.4 最小绝对差
题目
给你个整数数组 arr
,其中每个元素都 不相同。
请你找到所有具有最小绝对差的元素对,并且按升序的顺序返回。
每对元素对 [a,b
] 如下:
a , b
均为数组arr
中的元素a < b
b - a
等于arr
中任意两个元素的最小绝对差
示例 1:
输入:arr = [4,2,1,3]
输出:[[1,2],[2,3],[3,4]]
示例 2:
输入:arr = [1,3,6,10,15]
输出:[[1,3]]
示例 3:
输入:arr = [3,8,-10,23,19,-4,-14,27]
输出:[[-14,-10],[19,23],[23,27]]
提示:
2 <= arr.length <= 10^5
-10^6 <= arr[i] <= 10^6
思路
先按升序排序,第一次遍历,获取最小绝对差
第二次遍历得到绝对差为最小绝对差的元素对
代码
function minimumAbsDifference(arr: number[]): number[][]
arr.sort((a, b) => a - b) // 顺序排序
// 第一次遍历,寻找最小绝对差
let min = Number.MAX_VALUE
for (let i = 0; i < arr.length-1; i++)
let cha = arr[i+1] - arr[i]
if (cha < min)
min = cha
// 第二次遍历,获取绝对差为最小绝对差的元素对
let res = []
for (let i = 0; i < arr.length-1; i++)
if (arr[i+1] - arr[i] == min)
res.push([arr[i],arr[i+1]])
return res
7.5 我的日程安排表 I
题目
实现一个 MyCalendar
类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。
当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订 。
日程可以用一对整数 start
和 end
表示,这里的时间是半开区间,即 [start, end)
, 实数 x
的范围为, start <= x < end
。
实现 MyCalendar
类:
MyCalendar()
初始化日历对象。boolean book(int start, int end)
如果可以将日程安排成功添加到日历中而不会导致重复预订,返回true
。否则,返回false
并且不要将该日程安排添加到日历中。
示例:
输入:
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]]
输出:
[null, true, false, true]
解释:
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // return True
myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。
myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。
提示:
0 <= start < end <= 109
- 每个测试用例,调用
book
方法的次数最多不超过1000
次。
思路
使用公交车站的思路,理解为start时刻上车一人,end时刻下车一人
同时要保证公交车仅仅能够乘坐一个人,如果上车后多余一个人,那么就无法让其上车
要注意,输入的那个列表需要按时间排序,因为是按照时间来进行上下车的
代码
class MyCalendar
map: Map<number, number> = new Map()
constructor()
book(start: number, end: number): boolean
let map = this.map
map.set(start, map.get(start) === undefined ? 1 : map.get(start) + 1)
map.set(end, map.get(end) === undefined ? -1 : map.get(end) - 1)
let arr = Array.from(map)
arr.sort((a, b) => return a[0] - b[0] ) // 按时间排序
let tmp = 0
for (let val of arr)
tmp += val[1]
if (tmp > 1) // 交叉了,还原,返回false
map.set(start, map.get(start) === undefined ? -1 : map.get(start) - 1)
map.set(end, map.get(end) === undefined ? 1 : map.get(end) + 1)
return false
return true
7.6 Lisp 语法解析
题目
给你一个类似 Lisp 语句的字符串表达式 expression
,求出其计算结果。
表达式语法如下所示:
- 表达式可以为整数,let 表达式,add 表达式,mult 表达式,或赋值的变量。表达式的结果总是一个整数。
- (整数可以是正整数、负整数、0)
- let 表达式采用
"(let v1 e1 v2 e2 ... vn en expr)"
的形式,其中let
总是以字符串"let"
来表示,接下来会跟随一对或多对交替的变量和表达式,也就是说,第一个变量v1
被分配为表达式e1
的值,第二个变量v2
被分配为表达式e2
的值,依次类推;最终let
表达式的值为expr
表达式的值。 - add 表达式表示为
"(add e1 e2)"
,其中add
总是以字符串"add"
来表示,该表达式总是包含两个表达式e1
、e2
,最终结果是e1
表达式的值与e2
表达式的值之 和 。 - mult 表达式表示为
"(mult e1 e2)"
,其中mult
总是以字符串"mult"
表示,该表达式总是包含两个表达式e1
、e2
,最终结果是e1
表达式的值与e2
表达式的值之 积 。 - 在该题目中,变量名以小写字符开始,之后跟随 0 个或多个小写字符或数字。为了方便,
"add"
,"let"
,"mult"
会被定义为 “关键字” ,不会用作变量名。 - 最后,要说一下作用域的概念。计算变量名所对应的表达式时,在计算上下文中,首先检查最内层作用域(按括号计),然后按顺序依次检查外部作用域。测试用例中每一个表达式都是合法的。有关作用域的更多详细信息,请参阅示例。
示例 1:
输入:expression = "(let x 2 (mult x (let x 3 y 4 (add x y))))"
输出:14
解释:
计算表达式 (add x y), 在检查变量 x 值时,
在变量的上下文中由最内层作用域依次向外检查。
首先找到 x = 3, 所以此处的 x 值是 3 。
示例 2:
输入:expression = "(let x 3 x 2 x)"
输出:2
解释:let 语句中的赋值运算按顺序处理即可。
示例 3:
输入:expression = "(let x 1 y 2 x (add x y) (add x y))"
输出:5
解释:
第一个 (add x y) 计算结果是 3,并且将此值赋给了 x 。
第二个 (add x y) 计算结果是 3 + 2 = 5 。
提示:
1 <= expression.length <= 2000
exprssion
中不含前导和尾随空格expressoin
中的不同部分(token)之间用单个空格进行分隔- 答案和所有中间计算结果都符合 32-bit 整数范围
- 测试用例中的表达式均为合法的且最终结果为整数
思路
纯纯的困难题,思路其实很简单,要么dfs要么就分治(其实都算分治吧),写法上的区别存粹就是自己定义属性上的区别
我这里借鉴了高阅读量题解的思路,直接一整个复现了一下,debug搞了20多分钟,纯纯的细节题
自己还是理一遍思路吧:
对于一个expression表达式,我们首先进行一个分解
例如"(let a 114 b 514 (add a b))"
,我们应该分解成两个部分,let部分和add部分
- let部分我们叫做定义部分
- add部分叫做剩余expression部分
我们在定义部分,对其中的变量进行map的映射赋值操作
在剩余expression部分进行最终返回值的计算
例如上面的例子就可以分解成
- let a 114 b 514
- return (add a b)
对于剩余的expression部分,我们又可以对其进行分解,分解成定义部分和剩余部分,直到所有的值被计算完,就返回结果
代码
function evaluate(expression: string): number
const LET: string = 'let'
const ADD: string = 'add'
// const MULT: string = 'mult'
/**
* 对表达式进行分解
*
* @param expr 表达式
* @returns 分解后的表达式
*/
const parseExpress = (expr: string) =>
expr = expr.substring(1, expr.length - 1)
let res: string[] = []
let left: number = 0 // 左指针
let right: number = 0 // 右指针
let len = expr.length
while (right < len)
if (expr[right] === ' ')
// 是空格,那么可以分割成let v1 e1 和 一部分expr
res.push(expr.substring(left, right))
left = right + 1 // 左指针指向下一个内容
else if (expr[right] === '(')
// 是左括号,证明遍历到了expr的部分,进行括号统计,找到当前携带括号的expr的作用域
let cnt = 0
while (right < len)
if (expr[right] === '(') cnt++
else if (expr[right] === ')') cnt--
right++
if (cnt === 0) break // cnt再次为0表示括号统计完毕
res.push(expr.substring(left, right))
left = right + 1
right++ // 右指针继续向下遍历
if (left < len) // 如果没有出现左括号,补充剩余部分x
res.pushLeetCode每日一题:加一