京东
距离上次 京东薪资"逆势上涨" 的新闻才过去 2 个多月,京东最近又发「涨薪公告」了。
京东科技宣布「技术研发岗」将给出平均 20 个月的薪酬,远高于云计算行业以往 16 薪左右的水平。
虽然钱还没发,但总算来了个和咱有强关联的消息(之前京东宣布加薪,大多都是和采购/销售等非技术相关的岗位),而且这个「平均 20 个月薪酬」还是挺有想象力的。
社交媒体上,有已收到录用通知的学生表示"大方到离谱"。
不过当我重新梳理今年「京东」的涨薪好消息之后,我开始有点慌了:
-
2024 年初,京东零售全员平均加薪不低于 20%; -
2024-01-01 起,京东采销等一线业务人员的年固定薪酬翻倍(平均涨幅 100%); -
2024-02-01 起,京东一线客服人员(超 2W 人)实现全年平均薪酬上涨 30%; -
2024-07-01 起,京东销售将用 18 个月时间,将年度固定薪酬从 16 薪提升至 20 薪,同时调整业绩激励上不封顶; -
2024 年 8 月,京东 2025 校园招聘全球启动,大幅上调校招薪资(算法岗平均起薪涨幅超 75%,硬件和设计等岗位起薪涨幅超 50%),开放 1.8W 个岗位招聘; -
2024-10-01,京东零售集团和职能体系将用两年时间实现 20 薪; -
2024-11-11,京东科技宣布「技术研发岗」将给出平均 20 个月的薪酬;
这消息的公布节奏,有点像是为了持续炒热某个话题一样 🤣🤣🤣
再细想一下,加薪消息的对外公布有助于企业形象没错,但这毕竟是京东内部的改革,有必要细化到每个 BU 薪资改革,都对外同步一次吗?
还是说这些消息对外公布的目的之一,就是为了上热搜,动摇一下那些正在 Offer 决策的候选人?
我不知道,但愿是我多虑了,希望到了年底,京东说到做到,能够给员工发放当时承诺的奖金。
对此,你怎么看?觉得京东这波能说到做到吗?
...
回归主题。
来一道和「京东-校招」相关的算法题。
题目描述
平台:LeetCode
题号:395
给你一个字符串 s
和一个整数 k
,请你找出 s
中的最长子串,要求该子串中的每一字符出现次数都不少于 k
,返回这一子串的长度。
示例 1:
输入:s = "aaabb", k = 3
输出:3
解释:最长子串为 "aaa" ,其中 'a' 重复了 3 次。
示例 2:
输入:s = "ababbc", k = 2
输出:5
解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。
提示:
-
-
s
仅由小写英文字母组成 -
枚举 + 双指针
其实看到这道题,我第一反应是「二分」,直接「二分」答案。
但是往下分析就能发现「二分」不可行,因为不具有二段性质。
也就是假设有长度 t
的一段区间满足要求的话,t + 1
长度的区间是否「一定满足」或者「一定不满足」呢?
显然并不一定,是否满足取决于 t + 1
个位置出现的字符在不在原有区间内。
举个🌰吧,假设我们已经画出来一段长度为 t
的区间满足要求(且此时 k > 1),那么当我们将长度扩成 t + 1
的时候(无论是往左扩还是往右扩):
-
如果新位置的字符在原有区间 「出现过」,那必然还是满足出现次数大于 k,这时候 「 t + 1
的长度满足要求」 -
如果新位置的字符在原有区间 「没出现过」,那新字符的出现次数只有一次,这时候 「 t + 1
的长度不满足要求」
因此我们无法是使用「二分」,相应的也无法直接使用「滑动窗口」思路的双指针。
因为双指针其实也是利用了二段性质,当一个指针确定在某个位置,另外一个指针能够落在某个明确的分割点,使得左半部分满足,右半部分不满足。
「那么还有什么性质可以利用呢?这时候要留意数据范围「数值小」的内容。」
题目说明了只包含小写字母(26 个,为有限数据),「我们可以枚举最大长度所包含的字符类型数量,答案必然是 [1, 26],即最少包含 1 个字母,最多包含 26 个字母。」
你会发现,「当确定了长度所包含的字符种类数量时,区间重新具有了二段性质。」
当我们使用双指针的时候:
-
右端点往右移动必然会导致字符类型数量增加(或不变) -
左端点往右移动必然会导致字符类型数量减少(或不变)
当然,我们还需要记录有多少字符符合要求(出现次数不少于 k
),当区间内所有字符都符合时更新答案。
Java 代码:
class Solution {
public int longestSubstring(String s, int k) {
int ans = 0;
int n = s.length();
char[] cs = s.toCharArray();
int[] cnt = new int[26];
for (int p = 1; p <= 26; p++) {
Arrays.fill(cnt, 0);
// tot 代表 [j, i] 区间所有的字符种类数量;sum 代表满足「出现次数不少于 k」的字符种类数量
for (int i = 0, j = 0, tot = 0, sum = 0; i < n; i++) {
int u = cs[i] - 'a';
cnt[u]++;
// 如果添加到 cnt 之后为 1,说明字符总数 +1
if (cnt[u] == 1) tot++;
// 如果添加到 cnt 之后等于 k,说明该字符从不达标变为达标,达标数量 + 1
if (cnt[u] == k) sum++;
// 当区间所包含的字符种类数量 tot 超过了当前限定的数量 p,那么我们要删除掉一些字母,即「左指针」右移
while (tot > p) {
int t = cs[j++] - 'a';
cnt[t]--;
// 如果添加到 cnt 之后为 0,说明字符总数-1
if (cnt[t] == 0) tot--;
// 如果添加到 cnt 之后等于 k - 1,说明该字符从达标变为不达标,达标数量 - 1
if (cnt[t] == k - 1) sum--;
}
// 当所有字符都符合要求,更新答案
if (tot == sum) ans = Math.max(ans, i - j + 1);
}
}
return ans;
}
}
C++ 代码:
class Solution {
public:
int longestSubstring(string s, int k) {
int ans = 0;
int n = s.length();
vector<int> cnt(26, 0);
for (int p = 1; p <= 26; p++) {
fill(cnt.begin(), cnt.end(), 0);
for (int i = 0, j = 0, tot = 0, sum = 0; i < n; i++) {
int u = s[i] - 'a';
cnt[u]++;
if (cnt[u] == 1) tot++;
if (cnt[u] == k) sum++;
while (tot > p) {
int t = s[j++] - 'a';
cnt[t]--;
if (cnt[t] == 0) tot--;
if (cnt[t] == k - 1) sum--;
}
if (tot == sum) ans = max(ans, i - j + 1);
}
}
return ans;
}
};
-
时间复杂度:枚举 种可能性,每种可能性会扫描一遍数组。复杂度为 -
空间复杂度:
总结 & 补充
总结
「当确定了窗口内所包含的字符数量时,区间重新具有了二段性质」。这是本题的滑动窗口解法和迄今为止做的滑动窗口题目的最大不同,本题需要手动增加限制,即限制窗口内字符种类。
补充
这里解释一下「为什么需要先枚举 26 种可能性」。
首先我们知道「「答案子串的左边界左侧的字符以及右边界右侧的字符一定不会出现在子串中,否则就不会是最优解」」。
但「如果我们只从该性质出发的话,朴素解法应该是使用一个滑动窗口,不断的调整滑动窗口的左右边界,使其满足「左边界左侧的字符以及右边界右侧的字符一定不会出现在窗口中」」,这实际上就是双指针解法,但是如果不先敲定(枚举)出答案所包含的字符数量的话,「这里的双指针是不具有单调性的」。
换句话说,只利用这一性质是没法完成逻辑的。
这时候我们面临的问题是:「性质是正确的,但是还无法直接利用」。
因此我们需要「先利用字符数量有限性(可枚举)作为切入点,使得「答案子串的左边界左侧的字符以及右边界右侧的字符一定不会出现在子串中」这一性质在双指针的实现下具有单调性」。也就是题解说的「让区间重新具有二段性质」。
然后遍历 26 种可能性(答案所包含的字符种类数量),「对每种可能性应用滑动窗口(由上述性质确保正确),可以得到每种可能性的最大值(局部最优),由所有可能性的最大值可以得出答案(全局最优)」。
点评
这道题的突破口分析其实和 1178. 猜字谜 类似。
解决思路:当我们采用常规的分析思路发现无法进行时,「要去关注一下数据范围中「数值小」的值。因为数值小其实是代表了「可枚举」,往往是解题或者降低复杂度的一个重要(甚至是唯一)的突破口。」
最后
巨划算的 LeetCode 会员优惠通道目前仍可用 ~
使用福利优惠通道 leetcode.cn/premium/?promoChannel=acoier,年度会员 有效期额外增加两个月,季度会员 有效期额外增加两周,更有超大额专属 🧧 和实物 🎁 福利每月发放。
我是宫水三叶,每天都会分享算法知识,并和大家聊聊近期的所见所闻。
欢迎关注,明天见。
更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉