Skip to content

Commit 0b07fa7

Browse files
committed
Add: Add 2025/1/26
1 parent acfe55f commit 0b07fa7

File tree

2 files changed

+345
-0
lines changed

2 files changed

+345
-0
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# 2127. Maximum Employees to Be Invited to a Meeting
2+
3+
A company is organizing a meeting and has a list of `n` employees, waiting to be invited.
4+
They have arranged for a large circular table, capable of seating any number of employees.
5+
6+
The employees are numbered from `0` to `n - 1`.
7+
Each employee has a favorite person, and they will attend the meeting only if they can sit next to their favorite person at the table.
8+
The favorite person of an employee is not themselves.
9+
10+
Given a 0-indexed integer array `favorite`, where `favorite[i]` denotes the favorite person of the $i_{th}$ employee,
11+
return the maximum number of employees that can be invited to the meeting.
12+
13+
## 基礎思路
14+
我們可以將問題轉化為一個「有向圖」(directed graph) 的問題:
15+
- 節點 (node) 代表員工。
16+
- 每個員工指向自己最喜歡的人 (favorite[i]),形成有向邊。
17+
18+
在一張圓桌上,如果一組員工能圍成一個「迴圈」(cycle),那麼就能彼此相鄰。
19+
所以有兩種情形可以幫我們找最大可邀請人數:
20+
1. **尋找圖中最長的迴圈 (cycle)**
21+
2. **尋找所有「互相喜歡」的 2-cycle (雙人互相指向) 加上各自延伸進來的「鏈」(chain) 長度後,再把它們合計起來。**
22+
23+
> Tips
24+
> - 若整個圖中最大的迴圈長度大於所有 2-cycle 加它們外圍鏈的總合,答案就是最大迴圈。
25+
> - 否則,我們就選擇所有 2-cycle 配上它們的鏈,並把總長加起來。
26+
> - 兩者取其最大值即為答案。
27+
28+
## 解題步驟
29+
30+
以下步驟對應到主要程式邏輯,並附上重點程式片段說明。
31+
32+
---
33+
34+
### Step 1: 找出圖中最大的迴圈長度
35+
36+
```typescript
37+
function findLargestCycle(favorite: number[]): number {
38+
const n = favorite.length;
39+
const visited: boolean[] = Array(n).fill(false);
40+
let maxCycleLength = 0;
41+
42+
for (let i = 0; i < n; i++) {
43+
if (visited[i]) continue; // 若已拜訪過,略過
44+
45+
const currentPath: number[] = [];
46+
let currentNode = i;
47+
48+
// 走訪直到遇到一個拜訪過的節點為止
49+
while (!visited[currentNode]) {
50+
visited[currentNode] = true;
51+
currentPath.push(currentNode);
52+
currentNode = favorite[currentNode];
53+
}
54+
55+
// currentNode 是第一次出現的重複節點,找出迴圈起始
56+
for (let j = 0; j < currentPath.length; j++) {
57+
if (currentPath[j] === currentNode) {
58+
// 計算迴圈長度 = (整條路徑長度 - 迴圈開始索引 j)
59+
const cycleLength = currentPath.length - j;
60+
maxCycleLength = Math.max(maxCycleLength, cycleLength);
61+
break;
62+
}
63+
}
64+
}
65+
66+
return maxCycleLength;
67+
}
68+
```
69+
- 使用 `visited` 陣列記錄哪些節點已經處理過,減少重複計算。
70+
- 遇到重複節點即為迴圈入口,從那裡算出迴圈長度。
71+
72+
---
73+
74+
### Step 2: 找出所有 2-cycle 及其「鏈」長度總合
75+
76+
這一步專門處理「互相喜歡」(2-cycle) 的情形,並計算對應的鏈(一條單向路徑) 能夠延伸多長。
77+
78+
#### Step 2.1 計算每個節點的入度 (inDegree)
79+
```typescript
80+
function calculateChainsForMutualFavorites(favorite: number[]): number {
81+
const n = favorite.length;
82+
const inDegree: number[] = Array(n).fill(0);
83+
const longestChain: number[] = Array(n).fill(1); // 每個節點至少自身長度 1
84+
85+
// 計算入度
86+
for (const person of favorite) {
87+
inDegree[person]++;
88+
}
89+
90+
// ...
91+
}
92+
```
93+
- `inDegree[i]` 表示「有多少人最喜歡 i」。
94+
- 之後能找到入度為 0 的節點,當作「鏈」的最開頭。
95+
96+
#### Step 2.2 使用類拓撲排序 (Topological Sort) 更新鏈長度
97+
```typescript
98+
function calculateChainsForMutualFavorites(favorite: number[]): number {
99+
// 2.1 計算每個節點的入度 (inDegree)
100+
101+
// 尋找所有入度為 0 的節點,當作初始化 queue
102+
const queue: number[] = [];
103+
for (let i = 0; i < n; i++) {
104+
if (inDegree[i] === 0) {
105+
queue.push(i);
106+
}
107+
}
108+
109+
// 不斷地從 queue 取出節點,將其鏈長度更新到下一個節點
110+
while (queue.length > 0) {
111+
const currentNode = queue.pop()!;
112+
const nextNode = favorite[currentNode];
113+
114+
// 若把 currentNode 接在 nextNode 前面,可以使 longestChain[nextNode] 更長
115+
longestChain[nextNode] = Math.max(longestChain[nextNode], longestChain[currentNode] + 1);
116+
117+
// 因為使用了 currentNode 這條邊,所以 nextNode 的入度要減 1
118+
inDegree[nextNode]--;
119+
120+
// 如果 nextNode 的入度歸 0,則放入 queue
121+
if (inDegree[nextNode] === 0) {
122+
queue.push(nextNode);
123+
}
124+
}
125+
126+
// ...
127+
}
128+
```
129+
- 這種方法能有效找出「鏈」的最大長度,因為每個節點都會最終把它的最大鏈長度「傳遞」給後繼節點。
130+
131+
#### Step 2.3 對於每個 2-cycle,將兩邊的鏈長度加總
132+
```typescript
133+
function calculateChainsForMutualFavorites(favorite: number[]): number {
134+
// 2.1 計算每個節點的入度 (inDegree)
135+
136+
// 2.2 使用類拓撲排序 (Topological Sort) 更新鏈長度
137+
138+
let totalContribution = 0;
139+
// 檢查 (i, favorite[i]) 是否形成互相喜歡 (2-cycle)
140+
for (let i = 0; i < n; i++) {
141+
const j = favorite[i];
142+
// 確保只算一次 (i < j),且 i 與 j 互相指向
143+
if (j !== i && i === favorite[j] && i < j) {
144+
totalContribution += longestChain[i] + longestChain[j];
145+
}
146+
}
147+
return totalContribution;
148+
}
149+
```
150+
- 互相喜歡即是 i -> j 與 j -> i;對此組合,我們把它們兩邊延伸進來的鏈長度相加。
151+
- 多個不同的 2-cycle 不會互相重疊,所以可以直接累加其貢獻。
152+
153+
---
154+
155+
### Step 3: 取最大值
156+
157+
`maximumInvitations()` 主函式中,我們分別呼叫上述兩個函式,並選取「最大迴圈長度」與「所有 2-cycle 貢獻總合」的較大者作為答案。
158+
159+
```typescript
160+
function maximumInvitations(favorite: number[]): number {
161+
// 1) 找出最大的迴圈
162+
const largestCycle = findLargestCycle(favorite);
163+
164+
// 2) 找出 2-cycle + 其鏈貢獻總合
165+
const totalChains = calculateChainsForMutualFavorites(favorite);
166+
167+
// 取兩者較大者
168+
return Math.max(largestCycle, totalChains);
169+
}
170+
```
171+
172+
---
173+
174+
## 時間複雜度
175+
1. **找最大迴圈**
176+
- 對每個節點做 DFS/走訪,平均為 $O(n)$,因為每個節點只會被走訪一次。
177+
2. **計算 2-cycle + 鏈貢獻**
178+
- 先計算所有節點的入度 $O(n)$。
179+
- 使用 queue 做類拓撲排序,整體也在 $O(n)$ 範圍。
180+
- 最後再檢查 2-cycle 也只需 $O(n)$。
181+
182+
因此整體時間複雜度約為 $O(n)$。
183+
184+
> $O(n)$
185+
186+
## 空間複雜度
187+
- 需要使用 `visited``inDegree``longestChain` 等陣列,各佔用 $O(n)$。
188+
- `currentPath``queue` 亦會在過程中最多使用到 $O(n)$ 大小。
189+
190+
綜合來看,空間複雜度為 $O(n)$。
191+
192+
> $O(n)$
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* Finds the maximum number of people that can be invited based on:
3+
* 1) The length of the largest cycle in the graph.
4+
* 2) The total length of chains attached to all 2-cycles (pairs who favor each other).
5+
*
6+
* @param favorite - An array where each index i represents a person, and favorite[i] is the person i "favors".
7+
* @returns The maximum between the largest cycle length and the total contributions from 2-cycles and their chains.
8+
*/
9+
function maximumInvitations(favorite: number[]): number {
10+
// 1) Find the longest cycle in the graph.
11+
const largestCycle = findLargestCycle(favorite);
12+
13+
// 2) Calculate the sum of chain lengths for all mutual-favorite pairs (2-cycles).
14+
const totalChains = calculateChainsForMutualFavorites(favorite);
15+
16+
// The final answer is the larger of these two values.
17+
return Math.max(largestCycle, totalChains);
18+
}
19+
20+
/**
21+
* Finds the length of the largest cycle in the "favorite" graph.
22+
* A cycle means starting from some node, if you follow each person's 'favorite',
23+
* eventually you come back to the starting node.
24+
*
25+
* @param favorite - The array representing the directed graph: person -> favorite[person].
26+
* @returns The length of the largest cycle found in this graph.
27+
*/
28+
function findLargestCycle(favorite: number[]): number {
29+
const n = favorite.length;
30+
31+
// This array will track which nodes have been visited.
32+
// Once visited, we don't need to start a cycle check from those nodes again.
33+
const visited: boolean[] = Array(n).fill(false);
34+
let maxCycleLength = 0; // Keep track of the longest cycle length found.
35+
36+
// Iterate over each node to ensure we explore all potential cycles.
37+
for (let i = 0; i < n; ++i) {
38+
// If a node has already been visited, skip it because we have already explored its cycle.
39+
if (visited[i]) {
40+
continue;
41+
}
42+
43+
// This list will store the path of the current exploration to detect where a cycle starts.
44+
const currentPath: number[] = [];
45+
let currentNode = i;
46+
47+
// Move to the next node until you revisit a node (detect a cycle) or hit an already visited node.
48+
while (!visited[currentNode]) {
49+
// Mark the current node as visited and store it in the path.
50+
visited[currentNode] = true;
51+
currentPath.push(currentNode);
52+
53+
// Jump to the node that 'currentNode' favors.
54+
currentNode = favorite[currentNode];
55+
}
56+
57+
// currentNode is now a node we've seen before (end of the cycle detection).
58+
// We need to find where that node appeared in currentPath to determine the cycle length.
59+
for (let j = 0; j < currentPath.length; ++j) {
60+
if (currentPath[j] === currentNode) {
61+
// The cycle length is the distance from j to the end of currentPath.
62+
// Because from j back to the end is where the cycle loops.
63+
const cycleLength = currentPath.length - j;
64+
maxCycleLength = Math.max(maxCycleLength, cycleLength);
65+
break;
66+
}
67+
}
68+
}
69+
70+
return maxCycleLength;
71+
}
72+
73+
/**
74+
* This function focuses on pairs of people who are mutual favorites (2-cycles),
75+
* and calculates how many extra people can be attached in "chains" leading into these pairs.
76+
*
77+
* Explanation:
78+
* - A "2-cycle" means person A favors person B, and person B favors person A.
79+
* - A "chain" is a path of people leading into one node of the 2-cycle.
80+
* For example, if X favors A, and Y favors X, and so on, eventually leading into A,
81+
* that's a chain that ends at A.
82+
* - We'll use topological sorting here to find the longest chain length for each node.
83+
* That helps us figure out how many people can be attached before we reach a 2-cycle.
84+
*
85+
* @param favorite - The array representing the graph: person -> favorite[person].
86+
* @returns The total "chain" contributions added by all 2-cycles combined.
87+
*/
88+
function calculateChainsForMutualFavorites(favorite: number[]): number {
89+
const n = favorite.length;
90+
91+
// inDegree[i] will store how many people favor person i.
92+
// We'll use this to find "starting points" of chains (where inDegree is 0).
93+
const inDegree: number[] = Array(n).fill(0);
94+
95+
// longestChain[i] represents the longest chain length ending at node i.
96+
// We start at 1 because each node itself counts as a chain of length 1 (just itself).
97+
const longestChain: number[] = Array(n).fill(1);
98+
99+
// First, compute the in-degree for every node.
100+
for (const person of favorite) {
101+
inDegree[person] += 1;
102+
}
103+
104+
// We will use a queue to perform a topological sort-like process.
105+
// Initially, any node that has no one favoring it (inDegree = 0) can be a "start" of a chain.
106+
const queue: number[] = [];
107+
for (let i = 0; i < n; ++i) {
108+
if (inDegree[i] === 0) {
109+
queue.push(i);
110+
}
111+
}
112+
113+
// Process nodes in the queue:
114+
// Remove a node from the queue, then update its target's longest chain and reduce that target’s inDegree.
115+
// If the target’s inDegree becomes 0, it means we've resolved all paths leading into it, so we push it into the queue.
116+
while (queue.length > 0) {
117+
// Take a node with no unresolved incoming edges.
118+
const currentNode = queue.pop()!;
119+
// The node that currentNode directly favors.
120+
const nextNode = favorite[currentNode];
121+
122+
// Update the longest chain for nextNode:
123+
// The best chain that can end in currentNode is longestChain[currentNode].
124+
// So if we extend currentNode's chain by 1, we might get a longer chain for nextNode.
125+
longestChain[nextNode] = Math.max(longestChain[nextNode], longestChain[currentNode] + 1);
126+
127+
// Now we've accounted for this edge, so reduce the inDegree of nextNode by 1.
128+
inDegree[nextNode] -= 1;
129+
130+
// If nextNode now has no more unresolved incoming edges, push it into the queue for processing.
131+
if (inDegree[nextNode] === 0) {
132+
queue.push(nextNode);
133+
}
134+
}
135+
136+
let totalContribution = 0;
137+
// Now we look for 2-cycles: pairs (i, favorite[i]) where each favors the other.
138+
// In code, that means i === favorite[favorite[i]] (i points to someone who points back to i).
139+
// We add the chain lengths that lead into each side of the pair.
140+
for (let i = 0; i < n; ++i) {
141+
const j = favorite[i];
142+
143+
// Check if (i, j) forms a mutual-favorites pair (2-cycle).
144+
// We add the condition i < j so that each pair is only counted once.
145+
if (j !== i && i === favorite[j] && i < j) {
146+
// We sum up the chain from i and the chain from j.
147+
// This represents the total number of unique people that can be included from both sides of the 2-cycle.
148+
totalContribution += longestChain[i] + longestChain[j];
149+
}
150+
}
151+
152+
return totalContribution;
153+
}

0 commit comments

Comments
 (0)