Skip to main content
Back to problems
#3401
Hard Database

Find circular gift exchange chains

Database
51.2% acceptance
Mar 31, 2026
3
2

No description available.

Solution

Pandas
Time O(n)
Space O(1)
LeetCode
solution.pandas
# Table: SecretSanta
#
# +-------------+------+
# | Column Name | Type |
# +-------------+------+
# | giver_id    | int  |
# | receiver_id | int  |
# | gift_value  | int  |
# +-------------+------+
# (giver_id, receiver_id) is the unique key for this table.
# Each row represents a record of a gift exchange between two employees, giver_id represents the employee who gives a gift, receiver_id represents the employee who receives the gift and gift_value represents the value of the gift given.
#
# Write a solution to find the total gift value and length of circular chains of Secret Santa gift exchanges:
#
# A circular chain is defined as a series of exchanges where:
#
# Each employee gives a gift to exactly one other employee.
#
# Each employee receives a gift from exactly one other employee.
#
# The exchanges form a continuous loop (e.g., employee A gives a gift to B, B gives to C, and C gives back to A).
#
# Return the result ordered by the chain length and total gift value of the chain in descending order.
#
# The result format is in the following example.
#
# Example 1:
# Input:
# SecretSanta table:
# +----------+-------------+------------+
# | giver_id | receiver_id | gift_value |
# +----------+-------------+------------+
# | 1        | 2           | 20         |
# | 2        | 3           | 30         |
# | 3        | 1           | 40         |
# | 4        | 5           | 25         |
# | 5        | 4           | 35         |
# +----------+-------------+------------+
# Output:
# +----------+--------------+------------------+
# | chain_id | chain_length | total_gift_value |
# +----------+--------------+------------------+
# | 1        | 3            | 90               |
# | 2        | 2            | 60               |
# +----------+--------------+------------------+
# Explanation:
# Chain 1 involves employees 1, 2, and 3:
# Employee 1 gives a gift to 2, employee 2 gives a gift to 3, and employee 3 gives a gift to 1.
# Total gift value for this chain = 20 + 30 + 40 = 90.
# Chain 2 involves employees 4 and 5:
# Employee 4 gives a gift to 5, and employee 5 gives a gift to 4.
# Total gift value for this chain = 25 + 35 = 60.
# The result table is ordered by the chain length and total gift value of the chain in descending order.

import pandas as pd


def find_gift_chains(secret_santa: pd.DataFrame) -> pd.DataFrame:
  if secret_santa.empty:
    return pd.DataFrame(columns=["chain_id", "chain_length", "total_gift_value"])

  # Build directed graph: giver -> receiver (each giver gives to exactly one person)
  next_node = dict(zip(secret_santa["giver_id"], secret_santa["receiver_id"]))
  gift_map = dict(
    zip(
      zip(secret_santa["giver_id"], secret_santa["receiver_id"]),
      secret_santa["gift_value"],
    )
  )

  visited = set()
  chains = []

  for start in next_node:
    if start in visited:
      continue

    path = []
    path_index = {}  # node -> index in path
    current = start

    while current not in visited and current not in path_index:
      path_index[current] = len(path)
      path.append(current)
      nxt = next_node.get(current)
      if nxt is None:
        current = None  # dead end
        break
      current = nxt

    if current is not None and current in path_index:
      # Found a directed cycle
      cycle = path[path_index[current] :]
      chain_len = len(cycle)
      total_value = sum(
        gift_map.get((cycle[i], cycle[(i + 1) % chain_len]), 0)
        for i in range(chain_len)
      )
      chains.append({"chain_length": chain_len, "total_gift_value": total_value})

    visited.update(path)

  if not chains:
    return pd.DataFrame(columns=["chain_id", "chain_length", "total_gift_value"])

  result = pd.DataFrame(chains)
  result = result.drop_duplicates(subset=["chain_length", "total_gift_value"])
  result = result.sort_values(
    ["chain_length", "total_gift_value"], ascending=[False, False]
  ).reset_index(drop=True)
  result["chain_id"] = range(1, len(result) + 1)
  return result[["chain_id", "chain_length", "total_gift_value"]]