字节"中毒"
7月31日,据新加坡多家当地媒体报道,当地时间周二下午(7月30日)字节跳动新加坡办公处疑似发生集体食物中毒事件。
多名员工在食堂吃了中式自助午餐后感到身体不适,至少130人出现肠胃不适症状,有员工在办公场所呕吐,甚至有吐到瘫倒在地起不来。
感到不适的员工主要食用了中式自助餐中的变质鸡肉,而当日肉类供应商为新加坡云海肴。
在确认有不少员工出现不适后,公司已楼设置临时医疗援助站点,并暂时封锁食堂内的自助餐区,以便对食堂提供的食物进行化验、对食堂进行深度清洁。
目前新加坡食品局和卫生部正在调查此事件。
...
回归主线。
来一道和「字节跳动」相关的算法原题。
题目描述
平台:LeetCode
题号:2304
给你一个下标从 0
开始的整数矩阵 grid
,矩阵大小为 m x n
,由从 0
到
你可以在此矩阵中,从一个单元格移动到下一行的任何其他单元格。
如果你位于单元格 ,且满足
,你可以移动到
,
, ...,
注意: 在最后一行中的单元格不能触发移动。
每次可能的移动都需要付出对应的代价,代价用一个下标从 0
开始的二维数组 moveCost
表示,该数组大小为 ,其中
moveCost[i][j]
是从值为 i
的单元格移动到下一行第 j
列单元格的代价。
从 grid
最后一行的单元格移动的代价可以忽略。
grid
一条路径的代价是:所有路径经过的单元格的值之和加上所有移动的代价之和 。从第一行任意单元格出发,返回到达最后一行任意单元格的最小路径代价。
示例 1:
示例 2:
提示:
grid
由从0
到m * n - 1
的不同整数组成
建新图 + 建虚拟点 + 堆优化 Dijkstra
注意:可以直接使用解法二的方法,但先认真看完本做法,再去看解法二,会有相当丝滑的体验。
每次移动,实际路径权值 = 经过边的权值 + 目的地的权值。
利用原图,构建新图:每个单元格视为一个点,除最后一行外,每个点对下一行的所有点连一条有向边,边权 = 原图中该边的权值 + 原图中该目的地的权值。
分析新图中的点边数量:
- 点:共
个点,数量为
- 边:不算最后一行,共
个点,这些点与下一行的每个点均有一条有向边,合计
条边,数量为
原问题转换为:求点 i
到 j
的最短路,其中点 i
所在位置为第 0 行,点 j
所在位置为第
这似乎是一个「多源汇最短路」问题?但求解多源汇最短路的 Floyd
算法是
实际上,我们也并不真的关心图中任意点之间的最短路,仅仅关心第一行到最后一行的最短路。
因此,我们可通过建立“虚拟源点”和“虚拟汇点”的方式,来将“多源汇最短路”问题转换为“单源最短路”问题。
具体的,我们创建一个“虚拟源点”,该点向所有第一行的点连权值为
问题进一步转化为:求“虚拟源点”到“虚拟汇点”的最短路。
至此,我们通过 建新图 -> 创建虚拟源汇点(转换为单源最短路)-> 套用单源最短路算法 解决本题。
将新图中点的数量记为 n
,边数记为 m
,朴素 Dijkstra
复杂度为 ,堆优化的
Dijkstra
的复杂度为 ,当
(相对稀疏)时,优先使用堆优化
Dijkstra
。
Java 代码:
C++ 代码:
Python 代码:
- 时间复杂度:
,其中
为新图中的点数
,
为新图中的边数
- 空间复杂度:
堆优化 Dijkstra
什么?你说你实在不想建新图,也不想搞什么虚拟点,就想用你心爱的 BFS
来做?!
我懂你意思,但那不叫 BFS
。
只是将「建新图」和「建虚拟点」的过程省掉,仍需要使用优先队列(堆)来每次取出当前“路径代价最小”的点来进行扩充,执行过程仍为堆优化 Dijkstra
的核心操作。
尤其所谓“省掉” 建新图 和 建虚拟点,真就字面上的“省掉”,并非不存在,因为两种做法思路是完全一致的。可简单列举「本解法」与「解法一」的对应关系:
- 起始往队列放入首行元素,对应了解法一的“建立虚拟源点”过程;
- 从队列中取元素出来扩充时,若当前元素所在行是最后一行时,用当前路径代价来更新答案,对应了解法一的“建立虚拟汇点”过程;
- 扩充时直接遍历列(即下一行的所有点),对应的解法一的“用原图边建新图”的过程。
Java 代码:
C++ 代码:
Python 代码:
- 时间复杂度:
,其中
为新图中的点数
,
为新图中的边数
- 空间复杂度:
原地模拟
什么?你说你连图论的方法都不想用,想就着题意做一遍?
可以。甚至当你调整更新方向,还能利用已有的 grid
,实现原地模拟。
具体的,我们将“从上往下走”调整为“从下往上走”,这样可以确保当我们使用底下一行 来更新当前行
时,所用到的
Java 代码:
C++ 代码:
Python 代码:
TypeScript 代码:
- 时间复杂度:
,其中
和
分别代表给定
grid
的长宽 - 空间复杂度: