多隆
这两天,国内 IT 圈最火的消息:阿里"扫地僧"蔡景现(花名 多隆)从阿里离职。
虽然多隆的故事,确实在早些年头,有所耳闻,但估计大多数读者,还是通过这次事件,才正式知晓这号人物。
首先,澄清一些基本事实,上面的蓝字,是网传的说法,里面有些基本的错误。
说是"扫地僧",但多隆是 P11。
什么概念?对标 M6,集团副总裁级别。
说是"从阿里离职",但大概率是"提前光荣退休"。
作为一个陪伴阿里巴巴成长的"首位程序员",财务自由,那是最基本不过的事情。
多隆的离职,不少自媒体的口吻,多少带着惋惜的意味。
真就看得我一愣一愣的。
或许是网络上,少数关于多隆的照片,过于朴实,强化了大家对这位"亿万富豪"的刻板印象:
但这可真不是普通程序员 🤣🤣🤣
多隆,1994 级杭州大学,2000 年硕士毕业加入阿里,中间(2014~2023)还当过几年阿里合伙人,身价肯定过亿。
多隆的离职,对 IT 圈的意义,并不在于这件事本身。
而是在于,这(或许是)国内大厂最后一位「凭借技术/不靠带团队」做到高 P 职位的纯正程序员,退隐了。
一个很残忍的现实:几乎所有大厂,考虑到年龄等各方面因素,如果程序员做到一定年头,而又不往"管理"层面发展,去带团队,基本很难做到「贡献」与「收入」相匹配,很快就会被列入"优化名单"。
如今多隆的退隐(阿里就职 25 年),可能宣告了"国内大厂最后一位高 P 技术角儿"这一物种的正式灭绝。
...
回归主题。
来一道和「阿里巴巴」相关的算法题。
题目描述
平台:LeetCode
题号:907
给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。
由于答案可能很大,因此返回答案模 。
示例 1:
输入:arr = [3,1,2,4]
输出:17
解释:
子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
示例 2:
输入:arr = [11,81,94,43,3]
输出:444
提示:
单调栈 + 数学
原问题为求所有子数组的最小值之和。
统计所有子数组需要枚举左右端点,复杂度为 ,对于每个子数组,我们还需要通过线性扫描的方式找到其最小值,复杂度为 ,因此朴素解法的整体复杂度为 ,题目给定数据范围为 ,会 TLE。
由于我们是从子数组中取最小值来进行累加,即参与答案构成的每个数必然某个具体的 。
因此我们可以将原问题转化为「考虑统计每个 对答案的贡献」。
对于某一个 而言,我们考虑其能够作为哪些子数组的最小值。
我们可以想象以 为中心,分别往两端进行拓展,只要新拓展的边界不会改变「 为当前子数组的最小值」的性质即可。
换句话说,我们需要找到 作为最小值的最远左右边界,即找到 左右最近一个比其小的位置 l 和 r。
在给定序列中,找到任意 最近一个比其大/小的位置,可使用「单调栈」进行求解。
到这里,我们会自然想到,通过单调栈的方式,分别预处理除 l 和 r 数组:
l[i] = loc含义为下标i左边最近一个比arr[i]小的位置是loc(若在 左侧不存在比其小的数,则loc = -1)r[i] = loc含义为下标i右边最近一个比arr[i]小的位置是loc(若在 左侧不存在比其小的数,则loc = n)
当我们预处理两数组后,通过简单「乘法原理」即可统计以 为最小值时,子数组的个数:
- 包含 的子数组左端点个数为 个
- 包含 的子数组右端点个数为 个
子数组的个数 子数组最小值 ,即是当前 对答案的贡献:。
统计所有 对答案的贡献即是最终答案,但我们忽略了「当 arr 存在重复元素,且该元素作为子数组最小值时,最远左右端点的边界越过重复元素时,导致重复统计子数组」的问题。
我们不失一般性的举个 🌰 来理解(下图):
为了消除这种重复统计,我们可以将「最远左右边界」的一端,从「严格小于」调整为「小于等于」,从而实现半开半闭的效果。
Java 代码:
class Solution {
int MOD = (int)1e9+7;
public int sumSubarrayMins(int[] arr) {
int n = arr.length, ans = 0;
int[] l = new int[n], r = new int[n];
Arrays.fill(l, -1); Arrays.fill(r, n);
Deque<Integer> d = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
while (!d.isEmpty() && arr[d.peekLast()] >= arr[i]) r[d.pollLast()] = i;
d.addLast(i);
}
d.clear();
for (int i = n - 1; i >= 0; i--) {
while (!d.isEmpty() && arr[d.peekLast()] > arr[i]) l[d.pollLast()] = i;
d.addLast(i);
}
for (int i = 0; i < n; i++) {
int a = i - l[i], b = r[i] - i;
ans += a * 1L * b % MOD * arr[i] % MOD;
ans %= MOD;
}
return ans;
}
}
C++ 代码:
class Solution {
public:
int MOD = 1e9 + 7;
int sumSubarrayMins(vector<int>& arr) {
int n = arr.size(), ans = 0;
vector<int> l(n, -1), r(n, n);
stack<int> d;
for (int i = 0; i < n; i++) {
while (!d.empty() && arr[d.top()] >= arr[i]) {
r[d.top()] = i;
d.pop();
}
d.push(i);
}
while (!d.empty()) d.pop();
for (int i = n - 1; i >= 0; i--) {
while (!d.empty() && arr[d.top()] > arr[i]) {
l[d.top()] = i;
d.pop();
}
d.push(i);
}
for (int i = 0; i < n; i++) {
long long a = i - l[i], b = r[i] - i;
ans = (ans + a * b % MOD * arr[i] % MOD) % MOD;
}
return ans;
}
};
Python 代码:
class Solution:
def sumSubarrayMins(self, arr: List[int]) -> int:
n, ans = len(arr), 0
l, r = [-1] * n, [n] * n
stk = []
for i in range(n):
while stk and arr[stk[-1]] >= arr[i]:
r[stk.pop()] = i
stk.append(i)
stk = []
for i in range(n - 1, -1, -1):
while stk and arr[stk[-1]] > arr[i]:
l[stk.pop()] = i
stk.append(i)
for i in range(n):
a, b = i - l[i], r[i] - i
ans += a * b * arr[i]
return ans % (10 ** 9 + 7)
TypeScript 代码:
const MOD = 1000000007
function sumSubarrayMins(arr: number[]): number {
let n = arr.length, ans = 0
const l = new Array<number>(n).fill(-1), r = new Array<number>(n).fill(n)
const stk = new Array<number>(n).fill(0)
let he = 0, ta = 0
for (let i = 0; i < n; i++) {
while (he < ta && arr[stk[ta - 1]] >= arr[i]) r[stk[--ta]] = i
stk[ta++] = i
}
he = ta = 0
for (let i = n - 1; i >= 0; i--) {
while (he < ta && arr[stk[ta - 1]] > arr[i]) l[stk[--ta]] = i
stk[ta++] = i
}
for (let i = 0; i < n; i++) {
const a = i - l[i], b = r[i] - i
ans += a * b % MOD * arr[i] % MOD
ans %= MOD
}
return ans
}
- 时间复杂度:
- 空间复杂度:
优化
实际上,当我们从栈中弹出某个 时,其右边界必然是导致其弹出的 arr[r](当前所遍历到的元素),而 若存在左边界,必然是位于 栈中的前一位置,即 弹出后的新栈顶元素(若不存在物理左边界,则左边界为 )。
Java 代码:
class Solution {
int MOD = (int)1e9+7;
public int sumSubarrayMins(int[] arr) {
int n = arr.length, ans = 0;
Deque<Integer> d = new ArrayDeque<>();
for (int r = 0; r <= n; r++) {
int t = r < n ? arr[r] : 0;
while (!d.isEmpty() && arr[d.peekLast()] >= t) {
int cur = d.pollLast();
int l = d.isEmpty() ? -1 : d.peekLast();
int a = cur - l, b = r - cur;
ans += a * 1L * b % MOD * arr[cur] % MOD;
ans %= MOD;
}
d.addLast(r);
}
return ans;
}
}
C++ 代码:
class Solution {
public:
int MOD = 1e9 + 7;
int sumSubarrayMins(vector<int>& arr) {
int n = arr.size(), ans = 0;
deque<int> d;
for (int r = 0; r <= n; r++) {
int t = (r < n) ? arr[r] : 0;
while (!d.empty() && arr[d.back()] >= t) {
int cur = d.back();
d.pop_back();
int l = d.empty() ? -1 : d.back();
long long a = cur - l, b = r - cur;
ans = (ans + a * b % MOD * arr[cur] % MOD) % MOD;
}
d.push_back(r);
}
return ans;
}
};
Python 代码:
class Solution:
def sumSubarrayMins(self, arr: List[int]) -> int:
n, ans = len(arr), 0
stk = []
for r in range(n + 1):
t = arr[r] if r < n else 0
while stk and arr[stk[-1]] >= t:
cur = stk.pop()
l = stk[-1] if stk else -1
a, b = cur - l, r - cur
ans += a * b * arr[cur]
stk.append(r)
return ans % (10 ** 9 + 7)
TypeScript 代码:
const MOD = 1000000007
function sumSubarrayMins(arr: number[]): number {
let n = arr.length, ans = 0
const stk = new Array<number>(n).fill(0)
let he = 0, ta = 0
for (let r = 0; r <= n; r++) {
const t = r < n ? arr[r] : 0
while (he < ta && arr[stk[ta - 1]] >= t) {
const cur = stk[--ta]
const l = he < ta ? stk[ta - 1] : -1
const a = cur - l, b = r - cur
ans += a * b % MOD * arr[cur] % MOD
ans %= MOD
}
stk[ta++] = r
}
return ans
}
- 时间复杂度:
- 空间复杂度: