来自0x3f【从周赛中学算法 - 2022 年周赛题目总结(下篇)】:https://leetcode.cn/circle/discuss/WR1MJP/
模拟指按照题目要求实现相应代码,一般暴力即可通过。
某些模拟题会有点复杂,这也能很好地锻炼实现代码的能力。
常见于周赛第一题(约占 90%)、第二题(约占 40%)和第三题(约占 19%)。
题目 | 难度 |
---|---|
2437. 有效时间的数目 | 1427 |
2349. 设计数字容器系统 | 1540 |
2456. 最流行的视频创作者 | 1548 |
2409. 统计共同度过的日子数 | 1562 |
2512. 奖励最顶尖的 K 名学生 | 1636 |
2502. 设计内存分配器 | 1746 |
🔺2353. 设计食物评分系统 | 1782 |
文章目录
- 灵神-从周赛中学算法(模拟)
- [2437. 有效时间的数目](https://leetcode.cn/problems/number-of-valid-clock-times/)
- 🎃[2349. 设计数字容器系统](https://leetcode.cn/problems/design-a-number-container-system/)
- [2456. 最流行的视频创作者](https://leetcode.cn/problems/most-popular-video-creator/)
- [2409. 统计共同度过的日子数](https://leetcode.cn/problems/count-days-spent-together/)(区间求交集问题)
- [2512. 奖励最顶尖的 K 名学生](https://leetcode.cn/problems/reward-top-k-students/)
- [2502. 设计内存分配器](https://leetcode.cn/problems/design-memory-allocator/)
- 🎃[2353. 设计食物评分系统](https://leetcode.cn/problems/design-a-food-rating-system/)
灵神-从周赛中学算法(模拟)
2437. 有效时间的数目
难度简单14
给你一个长度为 5
的字符串 time
,表示一个电子时钟当前的时间,格式为 "hh:mm"
。最早 可能的时间是 "00:00"
,最晚 可能的时间是 "23:59"
。
在字符串 time
中,被字符 ?
替换掉的数位是 未知的 ,被替换的数字可能是 0
到 9
中的任何一个。
请你返回一个整数 answer
,将每一个 ?
都用 0
到 9
中一个数字替换后,可以得到的有效时间的数目。
示例 1:
输入:time = "?5:00"
输出:2
解释:我们可以将 ? 替换成 0 或 1 ,得到 "05:00" 或者 "15:00" 。注意我们不能替换成 2 ,因为时间 "25:00" 是无效时间。所以我们有两个选择。
示例 2:
输入:time = "0?:0?"
输出:100
解释:两个 ? 都可以被 0 到 9 之间的任意数字替换,所以我们总共有 100 种选择。
示例 3:
输入:time = "??:??"
输出:1440
解释:小时总共有 24 种选择,分钟总共有 60 种选择。所以总共有 24 * 60 = 1440 种选择。
提示:
time
是一个长度为5
的有效字符串,格式为"hh:mm"
。"00" <= hh <= "23"
"00" <= mm <= "59"
- 字符串中有的数位是
'?'
,需要用0
到9
之间的数字替换。
方法一:模拟,还是wa了两次(注意没有24点几几)
class Solution {
public int countTime(String time) {
String[] s = time.split(":");
int res = 1;
String hour = s[0];
if(hour.charAt(0) == '?' && hour.charAt(1) == '?') res *= 24;
else if(hour.charAt(0) != '?' && hour.charAt(1) != '?') res *= 1;
else if(hour.charAt(0) == '?'){
if(hour.charAt(1)-'0' < 4) res *= 3;
else res *= 2;
} else{ // hour.charAt(1) == '?'
if(hour.charAt(0)-'0' <= 1) res *= 10;
else res *= 4;
}
String minute = s[1];
if(minute.charAt(0) == '?' && minute.charAt(1) == '?') res *= 60;
else if(minute.charAt(0) != '?' && minute.charAt(1) != '?') res *= 1;
else if(minute.charAt(0) == '?'){
res *= 6;
} else{
res *= 10;
}
return res;
}
}
方法二:反向思考
反向思考,不用if else 判断情况,而是枚举[0,period]每位数字,判断time能否组成
class Solution {
public int countTime(String time) {
return count(time.substring(0, 2), 24) * count(time.substring(3), 60);
}
// 反向思考,不用if else 判断情况,而是枚举[0,period]每位数字,判断time能否组成
public int count(String time, int period){
char[] s = time.toCharArray();
int ans = 0;
for(int i = 0; i < period; i++){
if((s[0] == '?' || i / 10 == s[0] - '0') &&
(s[1] == '?' || i % 10 == s[1] - '0'))
ans += 1;
}
return ans;
}
}
🎃2349. 设计数字容器系统
难度中等22
设计一个数字容器系统,可以实现以下功能:
- 在系统中给定下标处 插入 或者 替换 一个数字。
- 返回 系统中给定数字的最小下标。
请你实现一个 NumberContainers
类:
NumberContainers()
初始化数字容器系统。void change(int index, int number)
在下标index
处填入number
。如果该下标index
处已经有数字了,那么用number
替换该数字。int find(int number)
返回给定数字number
在系统中的最小下标。如果系统中没有number
,那么返回-1
。
示例:
输入:
["NumberContainers", "find", "change", "change", "change", "change", "find", "change", "find"]
[[], [10], [2, 10], [1, 10], [3, 10], [5, 10], [10], [1, 20], [10]]
输出:
[null, -1, null, null, null, null, 1, null, 2]
解释:
NumberContainers nc = new NumberContainers();
nc.find(10); // 没有数字 10 ,所以返回 -1 。
nc.change(2, 10); // 容器中下标为 2 处填入数字 10 。
nc.change(1, 10); // 容器中下标为 1 处填入数字 10 。
nc.change(3, 10); // 容器中下标为 3 处填入数字 10 。
nc.change(5, 10); // 容器中下标为 5 处填入数字 10 。
nc.find(10); // 数字 10 所在的下标为 1 ,2 ,3 和 5 。因为最小下标为 1 ,所以返回 1 。
nc.change(1, 20); // 容器中下标为 1 处填入数字 20 。注意,下标 1 处之前为 10 ,现在被替换为 20 。
nc.find(10); // 数字 10 所在下标为 2 ,3 和 5 。最小下标为 2 ,所以返回 2 。
提示:
1 <= index, number <= 109
- 调用
change
和find
的 总次数 不超过105
次。
class NumberContainers {
Map<Integer, TreeSet<Integer>> num_idxs; //key为对应数字,val为索引列表
Map<Integer, Integer> idx_num;//key为索引,val为数字
public NumberContainers() {
num_idxs = new HashMap<>();
idx_num = new HashMap<>();
}
public void change(int index, int num) {
if(!idx_num.containsKey(index)){
idx_num.put(index, num);
}else{
int t = idx_num.get(index);
idx_num.put(index, num);
num_idxs.get(t).remove(index);
if(num_idxs.get(t).size() == 0) num_idxs.remove(t);
}
if(!num_idxs.containsKey(num)){
num_idxs.put(num, new TreeSet<>());
}
num_idxs.get(num).add(index);
}
public int find(int num) {
return num_idxs.containsKey(num) ? num_idxs.get(num).first() : -1;
}
}
2456. 最流行的视频创作者
难度中等19
给你两个字符串数组 creators
和 ids
,和一个整数数组 views
,所有数组的长度都是 n
。平台上第 i
个视频者是 creator[i]
,视频分配的 id 是 ids[i]
,且播放量为 views[i]
。
视频创作者的 流行度 是该创作者的 所有 视频的播放量的 总和 。请找出流行度 最高 创作者以及该创作者播放量 最大 的视频的 id 。
- 如果存在多个创作者流行度都最高,则需要找出所有符合条件的创作者。
- 如果某个创作者存在多个播放量最高的视频,则只需要找出字典序最小的
id
。
返回一个二维字符串数组 answer
,其中 answer[i] = [creatori, idi]
表示 creatori
的流行度 最高 且其最流行的视频 id 是 idi
,可以按任何顺序返回该结果*。*
示例 1:
输入:creators = ["alice","bob","alice","chris"], ids = ["one","two","three","four"], views = [5,10,5,4]
输出:[["alice","one"],["bob","two"]]
解释:
alice 的流行度是 5 + 5 = 10 。
bob 的流行度是 10 。
chris 的流行度是 4 。
alice 和 bob 是流行度最高的创作者。
bob 播放量最高的视频 id 为 "two" 。
alice 播放量最高的视频 id 是 "one" 和 "three" 。由于 "one" 的字典序比 "three" 更小,所以结果中返回的 id 是 "one" 。
示例 2:
输入:creators = ["alice","alice","alice"], ids = ["a","b","c"], views = [1,2,2]
输出:[["alice","b"]]
解释:
id 为 "b" 和 "c" 的视频都满足播放量最高的条件。
由于 "b" 的字典序比 "c" 更小,所以结果中返回的 id 是 "b" 。
提示:
n == creators.length == ids.length == views.length
1 <= n <= 105
1 <= creators[i].length, ids[i].length <= 5
creators[i]
和ids[i]
仅由小写英文字母组成0 <= views[i] <= 105
双哈希模拟:①创作者;总流行度 ②创作者; 视频数[视频下标,视频播放量]
- !!!在这种int累加的情况下,要考虑int溢出问题
class Solution {
public List<List<String>> mostPopularCreator(String[] creators, String[] ids, int[] views) {
Map<String, Double> creat_popu = new HashMap<>(); // 创作者;流行度
double maxpopu = 0; // 最大流行度
Map<String, List<int[]>> creat_ids = new HashMap<>(); // 创作者; 视频数[视频下标,视频播放量]
for(int i = 0; i < creators.length; i++){
String creat = creators[i];
double v = (double)views[i];
creat_popu.put(creat, creat_popu.getOrDefault(creat, 0.0) + v);
maxpopu = Math.max(maxpopu, creat_popu.get(creat));
List<int[]> tmp = creat_ids.getOrDefault(creat, new ArrayList<>());
tmp.add(new int[]{i, (int)v});
creat_ids.put(creat, tmp);
}
List<List<String>> res = new ArrayList<>();
for(Map.Entry<String, Double> entry : creat_popu.entrySet()){
if(entry.getValue() == maxpopu) {
List<int[]> list = creat_ids.get(entry.getKey());
// 按照播放量从大到小排序,相同的话则比较视频的字典序,注意这里用ids数组来对下标进行排序
Collections.sort(list, (a, b) -> a[1] == b[1] ? ids[a[0]].compareTo(ids[b[0]]) : b[1] - a[1]);
List<String> cur = new ArrayList<>();
cur.add(entry.getKey());
cur.add(ids[list.get(0)[0]]);
res.add(cur);
}
}
return res;
}
}
2409. 统计共同度过的日子数(区间求交集问题)
难度简单9
Alice 和 Bob 计划分别去罗马开会。
给你四个字符串 arriveAlice
,leaveAlice
,arriveBob
和 leaveBob
。Alice 会在日期 arriveAlice
到 leaveAlice
之间在城市里(日期为闭区间),而 Bob 在日期 arriveBob
到 leaveBob
之间在城市里(日期为闭区间)。每个字符串都包含 5 个字符,格式为 "MM-DD"
,对应着一个日期的月和日。
请你返回 Alice和 Bob 同时在罗马的天数。
你可以假设所有日期都在 同一个 自然年,而且 不是 闰年。每个月份的天数分别为:[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
。
示例 1:
输入:arriveAlice = "08-15", leaveAlice = "08-18", arriveBob = "08-16", leaveBob = "08-19"
输出:3
解释:Alice 从 8 月 15 号到 8 月 18 号在罗马。Bob 从 8 月 16 号到 8 月 19 号在罗马,他们同时在罗马的日期为 8 月 16、17 和 18 号。所以答案为 3 。
示例 2:
输入:arriveAlice = "10-01", leaveAlice = "10-31", arriveBob = "11-01", leaveBob = "12-31"
输出:0
解释:Alice 和 Bob 没有同时在罗马的日子,所以我们返回 0 。
提示:
- 所有日期的格式均为
"MM-DD"
。 - Alice 和 Bob 的到达日期都 早于或等于 他们的离开日期。
- 题目测试用例所给出的日期均为 非闰年 的有效日期。
区间求交集问题:max(min(b,d)-max(a,c)+1,0)
Alice的到达的时间就相当于a,它的离开时间就是b。同理,Bob的到达时间就是c,离开时间就是d。一开始我们不知道abcd谁大谁小,但是我们肯定是求[a,b]与[c,d]之间的交集。我们可以把具体日期变成一个数字表示它的长度,这里我们就把日期变成今年到这天共经历了多少天。四个日期都变成纯数字后,我们就可以用一个公式来求交集:max{0,min{b,d}-max{a,c}}
class Solution {
private static final int[] month = new int[]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
public int countDaysTogether(String arriveAlice, String leaveAlice, String arriveBob, String leaveBob) {
int aa = get(arriveAlice);
int al = get(leaveAlice);
int ba = get(arriveBob);
int bl = get(leaveBob);
return Math.max(Math.min(al, bl) - Math.max(aa, ba) + 1, 0);
}
public int get(String time){
int res = 0;
String[] s = time.split("-");
for(int i = 0; i < Integer.parseInt(s[0])-1; i++){
res += month[i];
}
res += Integer.parseInt(s[1]);
return res;
}
}
纯模拟:
class Solution {
private static final int[] month = new int[]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
public int countDaysTogether(String arriveAlice, String leaveAlice, String arriveBob, String leaveBob) {
int Alicearr = countday(arriveAlice);
int Alicelea = countday(leaveAlice);
int Bobarr = countday(arriveBob);
int Boblea = countday(leaveBob);
// 如果 A离开时间 < B到达时间,则说明没有交集
if(Alicelea < Bobarr || Boblea < Alicearr) return 0;
// A到达时间小于B到达时间(A来的早)
if(Alicearr < Bobarr){
int m = Math.min(Alicelea, Boblea);
// AB早离开的时间 - 晚到的人的天数
return m - Bobarr + 1;
}else{
int m = Math.min(Alicelea, Boblea);
return m - Alicearr + 1;
}
}
public int countday(String time){
int res = 0;
String[] s = time.split("-");
for(int i = 0; i < Integer.parseInt(s[0])-1; i++){
res += month[i];
}
res += Integer.parseInt(s[1]);
return res;
}
}
2446. 判断两个事件是否存在冲突
难度简单17
给你两个字符串数组 event1
和 event2
,表示发生在同一天的两个闭区间时间段事件,其中:
event1 = [startTime1, endTime1]
且event2 = [startTime2, endTime2]
事件的时间为有效的 24 小时制且按 HH:MM
格式给出。
当两个事件存在某个非空的交集时(即,某些时刻是两个事件都包含的),则认为出现 冲突 。
如果两个事件之间存在冲突,返回 true
;否则,返回 false
。
示例 1:
输入:event1 = ["01:15","02:00"], event2 = ["02:00","03:00"]
输出:true
解释:两个事件在 2:00 出现交集。
示例 2:
输入:event1 = ["01:00","02:00"], event2 = ["01:20","03:00"]
输出:true
解释:两个事件的交集从 01:20 开始,到 02:00 结束。
示例 3:
输入:event1 = ["10:00","11:00"], event2 = ["14:00","15:00"]
输出:false
解释:两个事件不存在交集。
提示:
evnet1.length == event2.length == 2.
event1[i].length == event2[i].length == 5
startTime1 <= endTime1
startTime2 <= endTime2
- 所有事件的时间都按照
HH:MM
格式给出
class Solution {
// 区间求交集问题:max(min(b,d)-max(a,c)+1,0)
public boolean haveConflict(String[] event1, String[] event2) {
int[] e1 = change(event1);
int[] e2 = change(event2);
int res = Math.max(Math.min(e1[1], e2[1]) - Math.max(e1[0], e2[0]) + 1, 0);
if(res == 0) return false;
return true;
}
public int[] change(String[] e){
int[] res = new int[2];
for(int i = 0; i < e.length; i++){
String[] s = e[i].split(":");
int hour = Integer.valueOf(s[0]);
int min = Integer.valueOf(s[1]);
int cnt = 0;
while(hour-- > 0){
cnt += 60;
}
cnt += min;
res[i] = cnt;
}
return res;
}
}
一行写法:
https://leetcode.cn/problems/determine-if-two-events-have-conflict/solution/shi-jian-duan-zhong-die-de-pan-duan-yi-t-zfh9/
class Solution {
public boolean haveConflict(String[] event1, String[] event2) {
// s1 e1 s2 e2 ; s1 < e2 and s2 < e1
return event1[0].compareTo(event2[1]) <= 0 && event2[0].compareTo(event1[1]) <= 0;
}
}
python
class Solution(object):
def haveConflict(self, event1, event2):
return event1[0] <= event2[1] and event2[0] <= event1[1]
2512. 奖励最顶尖的 K 名学生
难度中等2
给你两个字符串数组 positive_feedback
和 negative_feedback
,分别包含表示正面的和负面的词汇。不会 有单词同时是正面的和负面的。
一开始,每位学生分数为 0
。每个正面的单词会给学生的分数 加 3
分,每个负面的词会给学生的分数 减 1
分。
给你 n
个学生的评语,用一个下标从 0 开始的字符串数组 report
和一个下标从 0 开始的整数数组 student_id
表示,其中 student_id[i]
表示这名学生的 ID ,这名学生的评语是 report[i]
。每名学生的 ID 互不相同。
给你一个整数 k
,请你返回按照得分 从高到低 最顶尖的 k
名学生。如果有多名学生分数相同,ID 越小排名越前。
示例 1:
输入:positive_feedback = ["smart","brilliant","studious"], negative_feedback = ["not"], report = ["this student is studious","the student is smart"], student_id = [1,2], k = 2
输出:[1,2]
解释:
两名学生都有 1 个正面词汇,都得到 3 分,学生 1 的 ID 更小所以排名更前。
示例 2:
输入:positive_feedback = ["smart","brilliant","studious"], negative_feedback = ["not"], report = ["this student is not studious","the student is smart"], student_id = [1,2], k = 2
输出:[2,1]
解释:
- ID 为 1 的学生有 1 个正面词汇和 1 个负面词汇,所以得分为 3-1=2 分。
- ID 为 2 的学生有 1 个正面词汇,得分为 3 分。
学生 2 分数更高,所以返回 [2,1] 。
提示:
1 <= positive_feedback.length, negative_feedback.length <= 104
1 <= positive_feedback[i].length, negative_feedback[j].length <= 100
positive_feedback[i]
和negative_feedback[j]
都只包含小写英文字母。positive_feedback
和negative_feedback
中不会有相同单词。n == report.length == student_id.length
1 <= n <= 104
report[i]
只包含小写英文字母和空格' '
。report[i]
中连续单词之间有单个空格隔开。1 <= report[i].length <= 100
1 <= student_id[i] <= 109
student_id[i]
的值 互不相同 。1 <= k <= n
class Solution {
// TOP-K : 求从高到低,大根堆
public List<Integer> topStudents(String[] positive_feedback, String[] negative_feedback, String[] report, int[] student_id, int k) {
Set<String> pos = new HashSet<>();
Set<String> neg = new HashSet<>();
for(String pf : positive_feedback){
pos.add(pf);
}
for(String nf : negative_feedback){
neg.add(nf);
}
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]);
for(int i = 0; i < report.length; i++){
String[] rep = report[i].split(" ");
int score = 0;
for(String s : rep){
if(pos.contains(s)) score += 3;
if(neg.contains(s)) score -= 1;
}
pq.add(new int[]{score, student_id[i]});
}
List<Integer> list = new ArrayList<>();
for(int i = 0; i < k; i++){
list.add(pq.poll()[1]);
}
return list;
}
}
2502. 设计内存分配器
难度中等14
给你一个整数 n
,表示下标从 0 开始的内存数组的大小。所有内存单元开始都是空闲的。
请你设计一个具备以下功能的内存分配器:
- 分配 一块大小为
size
的连续空闲内存单元并赋 idmID
。 - 释放 给定 id
mID
对应的所有内存单元。
注意:
- 多个块可以被分配到同一个
mID
。 - 你必须释放
mID
对应的所有内存单元,即便这些内存单元被分配在不同的块中。
实现 Allocator
类:
Allocator(int n)
使用一个大小为n
的内存数组初始化Allocator
对象。int allocate(int size, int mID)
找出大小为size
个连续空闲内存单元且位于 最左侧 的块,分配并赋 idmID
。返回块的第一个下标。如果不存在这样的块,返回-1
。int free(int mID)
释放 idmID
对应的所有内存单元。返回释放的内存单元数目。
示例:
输入
["Allocator", "allocate", "allocate", "allocate", "free", "allocate", "allocate", "allocate", "free", "allocate", "free"]
[[10], [1, 1], [1, 2], [1, 3], [2], [3, 4], [1, 1], [1, 1], [1], [10, 2], [7]]
输出
[null, 0, 1, 2, 1, 3, 1, 6, 3, -1, 0]
解释
Allocator loc = new Allocator(10); // 初始化一个大小为 10 的内存数组,所有内存单元都是空闲的。
loc.allocate(1, 1); // 最左侧的块的第一个下标是 0 。内存数组变为 [1, , , , , , , , , ]。返回 0 。
loc.allocate(1, 2); // 最左侧的块的第一个下标是 1 。内存数组变为 [1,2, , , , , , , , ]。返回 1 。
loc.allocate(1, 3); // 最左侧的块的第一个下标是 2 。内存数组变为 [1,2,3, , , , , , , ]。返回 2 。
loc.free(2); // 释放 mID 为 2 的所有内存单元。内存数组变为 [1, ,3, , , , , , , ] 。返回 1 ,因为只有 1 个 mID 为 2 的内存单元。
loc.allocate(3, 4); // 最左侧的块的第一个下标是 3 。内存数组变为 [1, ,3,4,4,4, , , , ]。返回 3 。
loc.allocate(1, 1); // 最左侧的块的第一个下标是 1 。内存数组变为 [1,1,3,4,4,4, , , , ]。返回 1 。
loc.allocate(1, 1); // 最左侧的块的第一个下标是 6 。内存数组变为 [1,1,3,4,4,4,1, , , ]。返回 6 。
loc.free(1); // 释放 mID 为 1 的所有内存单元。内存数组变为 [ , ,3,4,4,4, , , , ] 。返回 3 ,因为有 3 个 mID 为 1 的内存单元。
loc.allocate(10, 2); // 无法找出长度为 10 个连续空闲内存单元的空闲块,所有返回 -1 。
loc.free(7); // 释放 mID 为 7 的所有内存单元。内存数组保持原状,因为不存在 mID 为 7 的内存单元。返回 0 。
提示:
1 <= n, size, mID <= 1000
- 最多调用
allocate
和free
方法1000
次
class Allocator {
int[] nums;
int n;
public Allocator(int n) {
this.n = n;
nums = new int[n];
}
public int allocate(int size, int mID) {
// 找到大小 >= size 的连续空闲块
int left = 0, right = 0;
while(right < n && right - left < size){
if(nums[right] != 0){
left = right + 1;
}
right++;
}
if(right - left < size) return -1;
for(int i = left; i < right; i++){
nums[i] = mID;
}
return left;
}
public int free(int mID) {
int res = 0;
for(int i = 0; i < n; i++){
if(nums[i] == mID){
res++;
nums[i] = 0;
}
}
return res;
}
}
/**
* Your Allocator object will be instantiated and called as such:
* Allocator obj = new Allocator(n);
* int param_1 = obj.allocate(size,mID);
* int param_2 = obj.free(mID);
*/
🎃2353. 设计食物评分系统
难度中等29
设计一个支持下述操作的食物评分系统:
- 修改 系统中列出的某种食物的评分。
- 返回系统中某一类烹饪方式下评分最高的食物。
实现 FoodRatings
类:
-
FoodRatings(String[] foods, String[] cuisines, int[] ratings)
初始化系统。食物由foods、cuisines和ratings
描述,长度均为n
foods[i]
是第i
种食物的名字。cuisines[i]
是第i
种食物的烹饪方式。ratings[i]
是第i
种食物的最初评分。
-
void changeRating(String food, int newRating)
修改名字为food
的食物的评分。 -
String highestRated(String cuisine)
返回指定烹饪方式cuisine
下评分最高的食物的名字。如果存在并列,返回 字典序较小 的名字。
注意,字符串 x
的字典序比字符串 y
更小的前提是:x
在字典中出现的位置在 y
之前,也就是说,要么 x
是 y
的前缀,或者在满足 x[i] != y[i]
的第一个位置 i
处,x[i]
在字母表中出现的位置在 y[i]
之前。
示例:
输入
["FoodRatings", "highestRated", "highestRated", "changeRating", "highestRated", "changeRating", "highestRated"]
[[["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]], ["korean"], ["japanese"], ["sushi", 16], ["japanese"], ["ramen", 16], ["japanese"]]
输出
[null, "kimchi", "ramen", null, "sushi", null, "ramen"]
解释
FoodRatings foodRatings = new FoodRatings(["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]);
foodRatings.highestRated("korean"); // 返回 "kimchi"
// "kimchi" 是分数最高的韩式料理,评分为 9 。
foodRatings.highestRated("japanese"); // 返回 "ramen"
// "ramen" 是分数最高的日式料理,评分为 14 。
foodRatings.changeRating("sushi", 16); // "sushi" 现在评分变更为 16 。
foodRatings.highestRated("japanese"); // 返回 "sushi"
// "sushi" 是分数最高的日式料理,评分为 16 。
foodRatings.changeRating("ramen", 16); // "ramen" 现在评分变更为 16 。
foodRatings.highestRated("japanese"); // 返回 "ramen"
// "sushi" 和 "ramen" 的评分都是 16 。
// 但是,"ramen" 的字典序比 "sushi" 更小。
提示:
1 <= n <= 2 * 104
n == foods.length == cuisines.length == ratings.length
1 <= foods[i].length, cuisines[i].length <= 10
foods[i]
、cuisines[i]
由小写英文字母组成1 <= ratings[i] <= 108
foods
中的所有字符串 互不相同- 在对
changeRating
的所有调用中,food
是系统中食物的名字。 - 在对
highestRated
的所有调用中,cuisine
是系统中 至少一种 食物的烹饪方式。 - 最多调用
changeRating
和highestRated
总计2 * 104
次
Map<String, Pair<Integer, String>>
:记录每个食物名称对应食物评分和烹饪方式
Map<String, TreeSet<Pair<Integer, String>>>
:记录每个烹饪对应的食物评分和食物名字集合
法一:哈希表套平衡树
- 在暴力的基础上对料理对应食物评分排序使用
TreeSet
保存,通过TreeSet.first()
可以获得第一个元素
class FoodRatings {
// 记录每个食物名称对应食物评分和烹饪方式
Map<String, Pair<Integer, String>> fs = new HashMap<>();
// 记录每个烹饪对应的食物评分和食物名字集合
Map<String, TreeSet<Pair<Integer, String>>> cs = new HashMap<>();
public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
for(int i = 0; i < foods.length; i++){
String f = foods[i], c = cuisines[i];
int r = ratings[i];
fs.put(f, new Pair<>(r, c));
// JDK1.8以后:computeIfAbsent 相当于GetOrDefault的简写
cs.computeIfAbsent(c, k -> new TreeSet<>((a, b) ->
// 按照食物评分排序,评分相同则按照名称字典序排序
!Objects.equals(a.getKey(), b.getKey()) ? b.getKey() - a.getKey() : a.getValue().compareTo(b.getValue())))
.add(new Pair<>(r, f));
}
}
public void changeRating(String food, int newRating) {
// 先在cs中删掉旧数据,然后将新排名和food加入到cs和fs中
Pair<Integer, String> e = fs.get(food);
TreeSet<Pair<Integer, String>> s = cs.get(e.getValue());
s.remove(new Pair<>(e.getKey(), food)); // 移除旧数据
s.add(new Pair<>(newRating, food)); // 添加新数据
fs.put(food, new Pair<>(newRating, e.getValue())); // 更新评分
}
public String highestRated(String cuisine) {
// TreeSet.first() : 返回TreeSet中当前的第一个(最低)元素
// lower、floor、ceiling 和 higher 分别返回小于、小于等于、大于等于、大于给定元素的元素,如果不存在这样的元素,则返回 null。
return cs.get(cuisine).first().getValue();
}
}
法二:懒删除堆
- 添加元素时直接加入优先队列,访问元素时,弹出堆顶食物评分与实际评分不相等的值
class FoodRatings {
// 记录每个食物名称对应食物评分和烹饪方式
Map<String, Pair<Integer, String>> fs = new HashMap<>();
// 记录每个烹饪对应的食物评分和食物名字集合
Map<String, Queue<Pair<Integer, String>>> cs = new HashMap<>();
public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
for(int i = 0; i < foods.length; i++){
String f = foods[i], c = cuisines[i];
int r = ratings[i];
fs.put(f, new Pair<>(r, c));
// JDK1.8以后:computeIfAbsent 相当于GetOrDefault的简写
cs.computeIfAbsent(c, k -> new PriorityQueue<>((a, b) ->
// 按照食物评分排序,评分相同则按照名称字典序排序
!Objects.equals(a.getKey(), b.getKey()) ? b.getKey() - a.getKey() : a.getValue().compareTo(b.getValue())))
.add(new Pair<>(r, f));
}
}
public void changeRating(String food, int newRating) {
// 先在cs中删掉旧数据,然后将新排名和food加入到cs和fs中
Pair<Integer, String> e = fs.get(food);
cs.get(e.getValue()).offer(new Pair<>(newRating, food));// 直接添加新数据,后面 highestRated 再删除旧的
fs.put(food, new Pair<>(newRating, e.getValue())); // 更新评分
}
public String highestRated(String cuisine) {
Queue<Pair<Integer, String>> q = cs.get(cuisine);
// 堆顶的食物评分不等于其实际值,弹出
while (!Objects.equals(q.peek().getKey(), fs.get(q.peek().getValue()).getKey()))
q.poll();
return q.peek().getValue();
}
}
/**
* Your FoodRatings object will be instantiated and called as such:
* FoodRatings obj = new FoodRatings(foods, cuisines, ratings);
* obj.changeRating(food,newRating);
* String param_2 = obj.highestRated(cuisine);
*/
暴力模拟,超时:
class FoodRatings {
Map<String, String> food_cui = new HashMap<>(); // food, cui
Map<String, Integer> food_rating = new HashMap<>(); // food, rating
Map<String, List<int[]>> cui_food = new HashMap<>(); // cuisines, food[id,ranting]
String[] foods;
public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
this.foods = foods;
for(int i = 0; i < foods.length; i++){
String food = foods[i], cui = cuisines[i];
int rat = ratings[i];
food_rating.put(food, rat);
food_cui.put(food, cui);
List<int[]> tmp = cui_food.getOrDefault(cui, new ArrayList<>());
tmp.add(new int[]{i, rat});
Collections.sort(tmp, (a, b) -> a[1] == b[1] ? foods[a[0]].compareTo(foods[b[0]]) : b[1] - a[1]);
cui_food.put(cui, tmp);
}
}
public void changeRating(String food, int newRating) {
int oldrat = food_rating.get(food);
food_rating.put(food, newRating);
List<int[]> cur = cui_food.get(food_cui.get(food));
for(int i = 0; i < cur.size(); i++){
int idx = cur.get(i)[0];
if(foods[idx].equals(food)){
cur.remove(i);
cur.add(new int[]{idx, newRating});
break;
}
}
Collections.sort(cur, (a, b) -> a[1] == b[1] ? foods[a[0]].compareTo(foods[b[0]]) : b[1] - a[1]);
cui_food.put(food_cui.get(food), cur);
return;
}
public String highestRated(String cuisine) {
List<int[]> cur = cui_food.get(cuisine);
return foods[cur.get(0)[0]];
}
}
/**
* Your FoodRatings object will be instantiated and called as such:
* FoodRatings obj = new FoodRatings(foods, cuisines, ratings);
* obj.changeRating(food,newRating);
* String param_2 = obj.highestRated(cuisine);
*/