|
| 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