owned this note
owned this note
Published
Linked with GitHub
# 2353.Design a Food Rating System <span class="tag" data-diff="medium" />
{%hackmd RN5D4nggQRO8wzNqxuvlNw %}
## 題目
Design a food rating system that can do the following:
- Modify the rating of a food item listed in the system.
- Return the highest-rated food item for a type of cuisine in the system.
Implement the `FoodRatings` class:
- `FoodRatings(String[] foods, String[] cuisines, int[] ratings)` Initializes the system. The food items are described by `foods`, `cuisines` and `ratings`, all of which have a length of `n`.
- `foods[i]` is the name of the $i^{th}$ food,
- `cuisines[i]` is the type of cuisine of the $i^{th}$ food, and
- `ratings[i]` is the initial rating of the $i^{th}$ food.
- `void changeRating(String food, int newRating)` Changes the rating of the food item with the name `food`.
- `String highestRated(String cuisine)` Returns the name of the food item that has the highest rating for the given type of `cuisine`. If there is a tie, return the item with the **lexicographically smaller** name.
Note that a string `x` is lexicographically smaller than string `y` if `x` comes before `y` in dictionary order, that is, either `x` is a prefix of `y`, or if `i` is the first position such that `x[i] != y[i]`, then `x[i]` comes before `y[i]` in alphabetic order.
### Example 1:
#### Input
```
["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"]]
```
#### Output
```
[null, "kimchi", "ramen", null, "sushi", null, "ramen"]
```
#### Explanation
````
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"); // return "kimchi"
// "kimchi" is the highest rated korean food with a rating of 9.
foodRatings.highestRated("japanese"); // return "ramen"
// "ramen" is the highest rated japanese food with a rating of 14.
foodRatings.changeRating("sushi", 16); // "sushi" now has a rating of 16.
foodRatings.highestRated("japanese"); // return "sushi"
// "sushi" is the highest rated japanese food with a rating of 16.
foodRatings.changeRating("ramen", 16); // "ramen" now has a rating of 16.
foodRatings.highestRated("japanese"); // return "ramen"
// Both "sushi" and "ramen" have a rating of 16.
// However, "ramen" is lexicographically smaller than "sushi".
````
### Constraints:
- $1 \le n \le 2 \times 10^4$
- `n == foods.length == cuisines.length == ratings.length`
- `1 <= foods[i].length, cuisines[i].length <= 10`
- `foods[i]`, `cuisines[i]` consist of lowercase English letters.
- $1 \le ratings[i] \le 10^8$
- All the strings in `foods` are **distinct**.
- `food` will be the name of a food item in the system across all calls to `changeRating`.
- `cuisine` will be a type of cuisine of **at least one** food item in the system across all calls to `highestRated`.
- At most $2 \times 10^4$ calls **in total** will be made to `changeRating` and `highestRated`.
## 思路
最初的想法是建一個hash map來存每個食物的評分,由於他的食物會有不同的風味(cuisine),並且題目的要求是要每個風味的最高分,所以在hash map的設計上,會將每個cuisine做各自的table來存放,另外為了在更新rating的時後能快速地找到該食物在哪一個cuisine table,所以額外maintain一張cuisineMap來記錄每個食物的風味。
最後在取得每種風味的最高評分時,則用`reduce`去找出table中rating最高的食物並回傳。
```typescript
class FoodRatings {
private tables: Record<string, Record<string, number>> = {};
private cuisineMap: Record<string, string> = {};
constructor(foods: string[], cuisines: string[], ratings: number[]) {
foods.forEach((f, i) => {
this.tables[cuisines[i]] = this.tables[cuisines[i]] ? this.tables[cuisines[i]] : {};
this.tables[cuisines[i]][f] = ratings[i];
this.cuisineMap[f] = cuisines[i];
});
}
changeRating(food: string, newRating: number): void {
this.tables[this.cuisineMap[food]][food] = newRating;
}
highestRated(cuisine: string): string {
if (!this.tables[cuisine]) return null;
const result = Object.entries(this.tables[cuisine]).reduce((max, curr) => {
if (curr[1] > max[1]) return curr;
if (curr[1] === max[1]) return max[0] < curr[0] ? max : curr;
return max;
}, ['', -1]);
return result[0];
}
}
```
<center><b>It works, but too slow.</b></center>
<br>
這的確是最簡單的做法,但如果這樣就這要解決了,就配不上他medium的難度了,果不其然,最後在其中一筆測資TLE了,仔細分析了一下上面的code,在 `changeRating` 的時候由於是直接修改hash map的值,所以理論上所需的時間複雜度是 $O(1)$ 所以最大的問題是出在 `highestRated` 的部分,在取得最高評分的食物時,需要loop過該風味底下的所有食物,才能找出最大值,所花費的時間複雜度是 $O(n)$,而且該結果並不會保存起來,等於每次call這個function的時候都需要重新找一遍,即便兩次function call之間rating並沒有改變。
### Heap
要解決這個問題,可以用 `heap` 這個資料結構來作為儲存rating的table
> heap 是一種完全二元樹(complete binary tree),且當heap是max heap時,能保證父節點一定比左右子節點還要大
一般的heap在每次insert時,會調整tree以滿足上面的條件,而heap調整的作法是會一層一層往下(上)去進行比較與交換,故其時間複雜度與樹高有關所以是$O(logn)$。
:::info
由於binary tree每個node最多只會有2個children,所以第 $i$ 層最多會有 $2^i$ 個node,假設樹高為 $h$,全部有 $n$ 個node,則
$$(1+2^1+...+2^h)=2^{h+1}-1=n$$故$$h=log_2{(n+1)}-1=O(logn)$$
:::
我們先定義每個node的資料型態如下:
```typescript
type Rating = { food: string, rating: number };
```
並定義一個class來實現我們的heap,並實作以下功能;其中我們的heap,由於是complete binary tree故很適合用array來保存。
```typescript
class Heap {
private heap: Rating[] = [];
constructor(){}
private adjustDown(startIdx: number):void {...}
private adjustUp(startIdx: number):void {...}
insert(node: Rating):void {...}
update(food: string, rating: number): void {...}
getMax(): Rating {...}
}
```
以下依序來講每個method的實作:
#### Insert and adjust (up)
首先是`insert`,這個method會用來在初期建構Heap,他會接收一個node並插入到array的最後面,也就是樹中的最後一個子節點的位置,之後進行調整。
```typescript
insert(node: Rating):void {
this.heap.push(node);
let idx = this.heap.length - 1;
this.adjustUp(idx);
}
```
```graphviz
graph hierarchy {
node [color=black,shape=circle]
edge [color=black]
root -- n1;
root -- n2;
n1 -- n3;
n1 -- n4;
n2 -- n5;
n6[style=dashed]
n2 -- n6[style=dashed];
}
```
當呼叫`adjustUp`時,會從給定的node index`idx`去往上調整max heap,調整的方法為:檢查parent,若parent 小於 current node,則switch兩個node,而後再一直與parent進行比較,直到他小於parent或是已經到達root。
以上圖為例,insert n6之後,先與n2進行比較,若$n_6>n_2$則交換兩者,並再跟root比較;若否則停下來。交換後並不需要再跟n5比較,因為n2已經保證會比n5大了,若n6大於n2則必定會大於n5。
```typescript
private adjustUp(startIdx: number) {
let idx = startIdx;
while (idx > 0) {
const jdx = Math.floor((idx - 1) / 2);
if ( this.heap[jdx].rating > this.heap[idx].rating) break;
if (
this.heap[jdx].rating === this.heap[idx].rating
&& this.heap[jdx].food < this.heap[idx].food
) break;
[this.heap[idx] ,this.heap[jdx]] = [this.heap[jdx], this.heap[idx]];
idx = jdx;
}
}
```
:::info
在0-index的array中,第 $i$ 個node的parent其index是 $\lfloor{{i - 1}\over 2}\rfloor$
:::
在上述程式碼中,要注意題目的條件,若兩者rating相同,則name的文字順序較小者為大,另外在javascript中可以很簡單的用array decompose去做兩個變數的交換
```javascript
[a, b] = [b, a] // Switch a and b
```
#### Update and adjust (down)
而`Update`則是用來修改某一個食物的rating,這個function一般不會出現在heap的資料結構中,一般的heap只會支援delete max(min)。
```typescript
update(food: string, rating: number): void {
const idx = this.heap.findIndex((x) => x.food === food);
if (idx < 0) return;
this.heap[idx].rating = rating;
this.adjustUp(idx);
this.adjustDown(idx);
}
```
在update的時候,首先loop過heap找到要被更新的node index並更新該node,之後調整tree,`adjustUp`前面已經提過了,這裡就不再贅述,而另一個調整程式`adjustDown`則是從目標node往下調整,看左右兩邊的child是否比自身大,若是,則與較大者交換,直到2個children node都比自身小或是已經到了leaf node。
```typescript=
private adjustDown(startIdx: number) {
const currentNode:Rating = this.heap[startIdx];
let childIdx = startIdx * 2 + 1;
let done = false;
while (childIdx < this.heap.length && !done) {
if (this.heap[childIdx + 1]) {
if (this.heap[childIdx + 1].rating > this.heap[childIdx].rating) {
childIdx += 1;
} else if (
this.heap[childIdx + 1].rating === this.heap[childIdx].rating
&& this.heap[childIdx + 1].food < this.heap[childIdx].food
) {
childIdx += 1;
}
}
if (currentNode.rating > this.heap[childIdx].rating) {
done = true;
} else if (
currentNode.rating === this.heap[childIdx].rating
&& currentNode.food < this.heap[childIdx].food
) {
done = true;
} else {
[this.heap[Math.floor((childIdx - 1) / 2)], this.heap[childIdx]]
= [this.heap[childIdx], this.heap[Math.floor((childIdx - 1) / 2)]]
childIdx = childIdx * 2 + 1;
}
}
}
```
:::info
在0-index的array中,第 $i$ 個node的left child其index是 $2i + 1$,而right child的index是 $2(i+1)$
:::
在第3行,我們先選取left child,接著進入迴圈,檢查是否有children且比較還沒結束(`!done`);在第7 ~ 16行,會檢查是否有right child且若right child比left child大的話,則選定其為比較的對象,接者開始進行比較;若此時current node的值已經比兩個children中最大者大了,則將done設為true來跳出迴圈,否則令其跟child交換,並將childIdx的指標指向互換後的left child。
雖然`update`看起來call了兩個調整程式,但其實只有一個會真的執行,因為update後,該node如果比parent大,就一定會比其children還要大,則在`adjustDone`的第一個回圈內就會被終止了;反之,若比children小,就會比parent還小,則`adjustUp`也不會執行。
#### GetMax
最後是`getMax`這個method,這是用來找出heap中最大值的function,由於heap的特性(parent一定大於child),且在每次insert及update的時候都會調整以滿足該特性,故這個method非常簡單只要回傳`heap[0]`即可,時間複雜度是 $O(1)$
```typescript
getMax(): Rating {
return this.heap[0];
}
```
最後將我們實作的Heap class拿到main function裡面作為存放rating的資料結構,就可以大大的提升效能
```typescript
class FoodRatings {
private table: Record<string, Heap> = {};
private cuisineMap: Record<string, string> = {};
constructor(foods: string[], cuisines: string[], ratings: number[]) {
foods.forEach((f, i) => {
const rating: Rating = { food: f, rating: ratings[i] };
if (!this.table[cuisines[i]]) this.table[cuisines[i]] = new Heap();
this.table[cuisines[i]].insert(rating);
this.cuisineMap[f] = cuisines[i];
});
}
changeRating(food: string, newRating: number): void {
this.table[this.cuisineMap[food]].update(food, newRating);
}
highestRated(cuisine: string): string {
return this.table[cuisine].getMax().food;
}
}
```
在`table`這個變數中,我們一樣對每個cuisine建立一個Heap來存放rating,construct的時候,則呼叫`insert`來建構heap,`changeRating`時用Heap內建的update function去更新heap,最後在`highestRated`的時候用`getMax`來取得rating最高的食物。