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 为运算表达式设计优先级

题目

241. 为运算表达式设计优先级

给你一个由数字和运算符组成的字符串 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 最低加油次数

题目

871. 最低加油次数

汽车从起点出发驶向目的地,该目的地位于出发位置东面 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. 1 <= target, startFuel, stations[i][1] <= 10^9
  2. 0 <= stations.length <= 500
  3. 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

题目

556. 下一个更大元素 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 最小绝对差

题目

1200. 最小绝对差

给你个整数数组 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

题目

729. 我的日程安排表 I

实现一个 MyCalendar 类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。

当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订

日程可以用一对整数 startend 表示,这里的时间是半开区间,即 [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 语法解析

题目

736. 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" 来表示,该表达式总是包含两个表达式 e1e2 ,最终结果是 e1 表达式的值与 e2 表达式的值之
  • mult 表达式表示为 "(mult e1 e2)" ,其中 mult 总是以字符串 "mult" 表示,该表达式总是包含两个表达式 e1e2,最终结果是 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每日一题:加一

leetcode 每日一题 89. 格雷编码

leetcode每日一题

leetcode每日一题

leetcode每日一题

leetcode每日一题