首页 > 其他分享 >LeetCode 周赛 346(2023/05/21)仅 68 人 AK 的最短路问题

LeetCode 周赛 346(2023/05/21)仅 68 人 AK 的最短路问题

时间:2023-05-22 13:33:05浏览次数:50  
标签:周赛 21 val 05 Int 短路 target 复杂度 dis

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。

单周赛 345 概览

T1. 删除子串后的字符串最小长度(Easy)

标签:栈

T2. 字典序最小回文串(Medium)

标签:贪心、双指针

T3. 求一个整数的惩罚数(Medium)

标签:回溯、状态压缩、前缀和

T4. 修改图中的边权(Hard)

标签:贪心、最短路


T1. 删除子串后的字符串最小长度(Easy)

https://leetcode.cn/problems/minimum-string-length-after-removing-substrings/

题解(栈)

使用栈模拟扫描过程,当扫描到 DB 时检查栈顶元素,最后栈内剩余的元素个数就是无法消除的最小长度:

class Solution {
    fun minLength(s: String): Int {
        val stack = ArrayDeque<Char>()
        for (c in s) {
            if (c == 'D' && stack.isNotEmpty() && stack.peek() == 'C') stack.pop()
            else if (c == 'B' && stack.isNotEmpty() && stack.peek() == 'A') stack.pop()
            else stack.push(c)
        }
        return stack.size
    }
}

复杂度分析:

  • 时间复杂度:$O(n)$ 其中 n 为 s 字符串的长度;
  • 空间复杂度:$O(n)$ 栈空间。

T2. 字典序最小回文串(Medium)

https://leetcode.cn/problems/lexicographically-smallest-palindrome/

题解(贪心)

贪心思路:当对称位置不相等时,只需要将其中一个位置修改到与另一个位置相同时,得到的操作次数是最少的:

class Solution {
    fun makeSmallestPalindrome(s: String): String {
        val arr = s.toCharArray()
        val n = s.length
        // 判断回文串写法
        for (i in 0 until n / 2) {
            val j = n - 1 - i
            if(arr[i] != arr[j]) {
                val temp = if(arr[i] < arr[j]) arr[i] else arr[j]
                arr[i] = temp
                arr[j] = temp
            }
        }
        return String(arr)
    }
}

复杂度分析:

  • 时间复杂度:$O(n)$ 其中 n 为 s 字符串的长度;
  • 空间复杂度:$O(n)$ 字符数组空间。

T3. 求一个整数的惩罚数(Medium)

https://leetcode.cn/problems/find-the-punishment-number-of-an-integer/

题解一(子集型回溯)

枚举每个数,使用子集型回溯检查是否存在满足条件的切分方案:

class Solution {
    fun punishmentNumber(n: Int): Int {
        if (n <= 3) return 1
        var ret = 0
        for (x in 4 .. n) {
            val target = x * x
            if (backTrack("$target", 0, x)) ret += target
        }
        return ret + 1 /* 1 满足条件 */
    }

    // 子集型回溯
    private fun backTrack(str : String, i : Int, target : Int) : Boolean {
        if (i == str.length) return target == 0
        var cur = 0
        for (to in i until str.length) {
            cur = cur * 10 + (str[to] - '0')
            if (backTrack(str, to + 1, target - cur)) return true
        }
        return false
    }
}

复杂度分析:

  • 时间复杂度:$O(n^2)$ 每个数字 i 转字符串后的长度为 $log_i$,而枚举长度为 $log_i$ 的字符串的切分方案后 $2^{log_i}$ = i 种方案,因此整体的时间复杂度是 $O(n^2)$;
  • 空间复杂度:$O(lgn)$ 递归栈空间。

题解二(状态压缩)

由于数字的长度小于 32,我们可以用 int 表示所有切分方案,再检查是否存在满足条件的切分方案:

class Solution {
    fun punishmentNumber(n: Int): Int {
        if (n <= 3) return 1
        var ret = 0
        for (x in 4 .. n) {
            val target = x * x
            if (check("$target", x)) ret += target
        }
        return ret + 1 /* 1 满足条件 */
    }

    // 状态压缩
    private fun check(str : String, target : Int) : Boolean {
        val m = str.length
        val upper = (1 shl m) - 1
        for (k in 1 .. upper) {
            var last = 0
            var sum = 0
            for (i in 0 until m) {
                val cur = str[i] - '0'
                if (k and (1 shl i) != 0) {
                    // 拆
                    sum += last
                    last = cur
                } else{
                    // 不拆
                    last = last * 10 + cur
                }
            }
            if (sum + last == target) return true
        }
        return false
    }
}

复杂度分析:

  • 时间复杂度:同上;
  • 空间复杂度:$O(1)$ 仅使用常量级别空间。

题解三(预处理 + 前缀和)

题解一和题解二在多个测试用例间会重复计算相同数字的切分方案,我们可以预处理 1 - 1000 中所有满足条件的数平方,并维护前缀和数组:

class Solution {

    companion object {
        private val U = 1000
        private val preSum = IntArray(U + 1)
        init {
            for (x in 4 .. U) {
                val target = x * x
                if (check("$target", x)) preSum[x] += target
                preSum[x] += preSum[x - 1]
            }
        }

        // 状态压缩
        private fun check(str : String, target : Int) : Boolean {
        }
    }

    fun punishmentNumber(n: Int): Int {
        return preSum[n] + 1
    }
}

复杂度分析:

  • 时间复杂度:$O(U^2)$ 其中 U 是数据大小上界;
  • 空间复杂度:$O(U)$ 前缀和数组空间。

T4. 修改图中的边权(Hard)

https://leetcode.cn/problems/modify-graph-edge-weights/submissions/434224996/

LeetCode 少有的难题,排进历史 Top 10 没问题吧?

问题无解的情况:

  • 1、假设将所有负权边设置为 INF(2*10^9)时的最短路长度 dis < target(不论是否经过负权边),由于无法继续增大边权来增大最短路长度,因此问题无解;
  • 2、假设将所有负权边设置为 1 时的最短路长度 dis > target(不论是否经过负权边),由于继续增大边权最短路不可能变小,因此问题无解。

错误的思路:

先把所有负权边设置为 1,再跑 Dijkstra 最短路,如果最短路长度 dis < target,那么将其中一条负权边继续增大 “target - dis”,就能是该路径的长度恰好为 target。然而,由于增加权重后最短路长度有可能变化,所以这个思路不能保证正确性。

正确的思路:

  • 1、先把所有负权边改为 1 跑 Dijkstra 最短路,计算出起点到终点的最短路长度。同时,如果该长度 dis > target,则问题无解;如果该长度 dis == target,则直接返回;如果该长度 dis < target,则需要补全。
  • 2、问题的关键在于,按什么顺序修改,以及修改到什么值。
    • 顺序:利用 Dijkstra 最短路算法每次使用「确定集」中最短路长度最短的节点去松弛其他点的时机,由于修改该点不会影响已确定路径,因此这是一个不错的时机;
    • 修改到什么值:需要满足 dis[0][x] + w + dis[y][e] = target,那么有 w = target - dis[0][x] - (dis[0][e] - dis[0][y]) = delta - dis[0][x] + dis[0][y]
  • 3、虽然修改后最短路不一定经过 w,但由于不断的使用最短路长度最短的节点,因此最终总能修改成功,除非修改后最短路依然小于 target(例如存在直接从 s 到 e 的边)
  • 4、最后,将未修改的边增加到 INF。
class Solution {

    private val INF = 1e9.toInt()

    fun modifiedGraphEdges(n: Int, edges: Array<IntArray>, source: Int, destination: Int, target: Int): Array<IntArray> {
        if (source !in 0 .. n - 1 || destination !in 0 .. n - 1) return edges
        if (source == destination || edges.isNullOrEmpty()) return edges
        // 建图(领接表,节点号 + 边号方便修改边权)
        val graph = Array(n) { ArrayList<IntArray>() }
        for ((i, edge) in edges.withIndex()) {
            graph[edge[0]].add(intArrayOf(edge[1], i))
            graph[edge[1]].add(intArrayOf(edge[0], i))
        }
        // 第一轮最短路
        val originDis = dijkstra1(graph, edges, source, destination)
        if (originDis[destination] > target) return emptyArray() // 无解
        // 第二轮最短路
        val delta = target - originDis[destination] // 需要补全的最短路
        val dis = dijkstra2(graph, edges, source, destination, delta, originDis)
        if (dis[destination] < target) return emptyArray() // 无解
        // 修改剩余边
        for (edge in edges) {
            if (edge[2] == -1) edge[2] = INF
        }
        return edges
    }

    // return:将 -1 视为 1,并计算从起点到终点的最短路
    private fun dijkstra1(graph:Array<ArrayList<IntArray>>, edges: Array<IntArray>, source :Int, destination:Int) : IntArray {
        val n = graph.size
        val visit = BooleanArray(n)
        val dis = IntArray(n) { INF }
        dis[source] = 0
        while (true) {
            // 寻找最短路长度最短的节点
            var x = -1
            for (i in 0 until n) {
                if (visit[i]) continue
                if (-1 == x || dis[i] < dis[x]) x = i
            }
            if (x == destination) break
            visit[x] = true // 标记
            // 松弛相邻边
            for (to in graph[x]) {
                var w = edges[to[1]][2]
                if (-1 == w) w = 1 // 视为 1
                if (dis[x] + w < dis[to[0]]) dis[to[0]] = dis[x] + w
            }
        }
        return dis
    }

    // 补全
    private fun dijkstra2(graph:Array<ArrayList<IntArray>>, edges: Array<IntArray>, source :Int, destination:Int, delta: Int, originDis:IntArray /* 首轮计算的最短路 */) : IntArray {
        val n = graph.size
        val visit = BooleanArray(n)
        val dis = IntArray(n) { INF }
        dis[source] = 0
        while (true) {
            // 寻找最短路长度最短的节点
            var x = -1
            for (i in 0 until n) {
                if (visit[i]) continue
                if (-1 == x || dis[i] < dis[x]) x = i
            }
            if (x == destination) break
            visit[x] = true // 标记
            // 松弛相邻边
            for (to in graph[x]) {
                var w = edges[to[1]][2]
                if (-1 == w) {
                    // 补全(两次 Dijkstra 只修改这里)
                    w = Math.max(delta - dis[x] + originDis[to[0]], 1) // 题目要求至少修改到 1
                    if (w >= 1) edges[to[1]][2] = w
                }
                if (dis[x] + w < dis[to[0]]) dis[to[0]] = dis[x] + w
            }
        }
        return dis
    }
}

复杂度分析:

  • 时间复杂度:$O(n^2)$ 两轮最短路算法;
  • 空间复杂度:$O(m)$ 图空间。

往期回顾

标签:周赛,21,val,05,Int,短路,target,复杂度,dis
From: https://www.cnblogs.com/pengxurui/p/17420374.html

相关文章

  • 【PHP兴趣部落-05】html table(表格)
    一、简介:表格由<table>标签来定义。每个表格均有若干行(由<tr>标签定义),每行被分割为若干单元格(由<td>标签定义)。字母td指表格数据(tabledata),即数据单元格的内容。数据单元格可以包含文本、图片、列表、段落、表单、水平线、表格等等。二、代码<!DOCTYPEhtml><html......
  • Python 4-05 sys
    syssys模块主要是针对与Python解释器相关的变量和方法,不是主机操作系统。导入方式:importsys属性及方法使用说明sys.argv获取命令行参数列表,第一个元素是程序本身sys.exit(n)退出Python程序,exit(0)表示正常退出。当参数非0时,会引发一个SystemExit异常,可以在程序中捕获该异......
  • Python 2-05 高阶函数
    一、函数式编程函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。而函数式编程(请注意多了一个“式”字)——FunctionalProgrammi......
  • Python 05 Selenium 等待
    等待WebDriver通常可以说有一个阻塞API。因为它是一个指示浏览器做什么的进程外库,而且web平台本质上是异步的,所以WebDriver不跟踪DOM的实时活动状态。大多数由于使用Selenium和WebDriver而产生的间歇性问题都与浏览器和用户指令之间的竞争条件有关。例如,用户指示浏览......
  • 上周热点回顾(5.15-5.21)
    热点随笔:· 我试图通过这篇文章告诉你,这行源码有多牛逼。 (why技术)· 纪念陈皓(左耳朵耗子) (陈硕)· 园子的商业化努力-AI人才服务:招募AI导师 (博客园团队)· 记一次.NET某医院门诊软件卡死分析 (一线码农)· 【趣话计算机底层技术】一个故事看懂各种锁 (轩辕之风)......
  • 05-译码器
    1.译码器译码器是编码的逆过程,在编码时,每一种二进制代码都赋予了特定的含义,即都代表了一个确定的信号或者是对象;把代码状态的特定含义翻译出来的过程叫做译码,实现译码操作的电路称为译码器,或者说,译码器可以将输入二机制代码的状态翻译成输出信号,以表示其原来含义的电路......
  • 1105. 模型基础
    一、Django的ORM简介1.ORM系统概念:对象关系映射(ObjectRelationalMapping,简称ORM)优势:不用直接编写SQL代码,只需像操作对象一样从数据库操作数据。2.django模型映射关系①模型类必须都写在app下的modles.py文件中②模型如果需要映射到数据库,所在的app必须被安装③一个......
  • 5.21打卡
     一、问题描述:一只兔子躲进了10 个环形分布的洞中的一个。狼在第一个洞中没有找到兔子,就隔一个洞,到第3个洞去找;也没有找到,就隔2个洞,到第6个洞去找;以后每次多一个洞去找兔子……这样下去,如果一直找不到兔子,请问兔子可能在哪个洞中?二、设计思路:首先定义一个数组a[11],其数组元素......
  • java学习日记20230521-HashTable
    存放的键值对k-v键和值都不能为空,否则会抛出NullPointException使用方法和HashMap一致线程安全,HashMap线程不安全继承的dictionary实现了Map接口底层是一个entry数组,初始化大小为11,临界值为8,第一次扩容为23,按照自己的扩容机制,2N+1 ......
  • 5、21
    一周没写了还是慢慢总结一下本周的收获part1:新知识:1)斜率优化(好像不会考),用斜率的思想求解最值(其实有点像线性规划),一般来讲核心是转化为维护一个坐标系上的凸包2)剪枝:1排除等效冗余2最优化剪枝3可行性剪枝4搜索顺序剪枝3)双端搜......