LeetCode 1633 - Percentage of Users Attended a Contest
This problem asks us to calculate the percentage of users who attended each contest. We are given two tables: Users and
Difficulty: 🟢 Easy
Topics: Database
Solution
Problem Understanding
This problem asks us to calculate the percentage of users who attended each contest. We are given two tables: Users and Register. The Users table contains all users, each with a unique user_id. The Register table contains records of which user registered for which contest, using contest_id and user_id.
The goal is to compute the number of unique users registered in each contest, divide it by the total number of users, multiply by 100, and round the result to two decimal places. The final output must be sorted first by percentage in descending order, and in the event of a tie, by contest_id in ascending order.
Important constraints and observations include that user_id is unique, (contest_id, user_id) is unique, and there are no missing values. Edge cases that could trip up naive solutions include contests with zero registrations, rounding issues, and ordering ties.
Approaches
The brute-force approach would iterate through each contest, count the number of users registered, calculate the percentage for each, and then sort the results. While straightforward, it requires scanning the entire Register table multiple times if implemented naively, which is inefficient for large datasets.
The optimal approach leverages SQL aggregation functions. The key insight is that we can count the number of users per contest using COUNT(user_id) grouped by contest_id, and compute the percentage using the total number of users obtained from Users. This avoids multiple scans and simplifies the solution into a single query. Rounding and ordering can then be applied at the end.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n * m) | O(k) | For each contest, count registrations iteratively; n = users, m = registrations, k = contests |
| Optimal | O(n + m) | O(k) | Aggregate with SQL GROUP BY, count per contest, and compute percentage in one pass |
Algorithm Walkthrough
- Count total users: Retrieve the total number of users from the
Userstable usingCOUNT(*). This represents the denominator for our percentage calculation. - Count users per contest: Aggregate the
Registertable bycontest_idusingCOUNT(user_id)to get the number of registered users per contest. - Compute percentage: For each contest, divide the count of registered users by the total user count and multiply by 100. Use rounding to ensure the result has two decimal places.
- Sort results: Order the final results first by
percentagein descending order, and then bycontest_idin ascending order for ties.
Why it works: Counting users per contest and dividing by the total guarantees accurate percentages because the total number of users is constant. SQL aggregation ensures each contest is handled independently, and ordering ensures correct presentation.
Python Solution
import sqlite3
def percentage_users_attended(db: sqlite3.Connection):
query = """
WITH total_users AS (
SELECT COUNT(*) AS total FROM Users
)
SELECT
r.contest_id,
ROUND(COUNT(r.user_id) * 100.0 / t.total, 2) AS percentage
FROM Register r
CROSS JOIN total_users t
GROUP BY r.contest_id
ORDER BY percentage DESC, contest_id ASC
"""
cursor = db.cursor()
cursor.execute(query)
return cursor.fetchall()
Explanation:
We first compute total_users as a CTE to hold the total number of users. Then, we aggregate the Register table by contest_id and compute the percentage for each contest. ROUND ensures the two-decimal requirement, and CROSS JOIN allows access to the total count without subqueries in each row. Finally, we sort the results per the problem requirements.
Go Solution
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"fmt"
)
func PercentageUsersAttended(db *sql.DB) ([]struct {
ContestID int
Percentage float64
}, error) {
query := `
WITH total_users AS (
SELECT COUNT(*) AS total FROM Users
)
SELECT
r.contest_id,
ROUND(COUNT(r.user_id) * 100.0 / t.total, 2) AS percentage
FROM Register r
CROSS JOIN total_users t
GROUP BY r.contest_id
ORDER BY percentage DESC, contest_id ASC
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var results []struct {
ContestID int
Percentage float64
}
for rows.Next() {
var contestID int
var percentage float64
if err := rows.Scan(&contestID, &percentage); err != nil {
return nil, err
}
results = append(results, struct {
ContestID int
Percentage float64
}{contestID, percentage})
}
return results, nil
}
Go-specific notes:
In Go, we define a slice of structs to hold results. db.Query executes the SQL, and we iterate over rows using rows.Next(). Scan maps the result columns into Go variables. We use ROUND in SQL to ensure two-decimal accuracy, avoiding any post-processing in Go.
Worked Examples
Using the example from the problem:
| contest_id | registered_users | percentage |
|---|---|---|
| 208 | 3 | 100.0 |
| 209 | 3 | 100.0 |
| 210 | 3 | 100.0 |
| 215 | 2 | 66.67 |
| 207 | 1 | 33.33 |
Steps:
- Total users = 3
- Count per contest: 208 -> 3, 209 -> 3, 210 -> 3, 215 -> 2, 207 -> 1
- Compute percentage:
(count / 3) * 100, round to 2 decimals - Sort by percentage descending and contest_id ascending for ties
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + m) | n = number of users, m = number of registrations; counting users and aggregating registrations are linear operations |
| Space | O(k) | k = number of contests; storing aggregated counts per contest |
This is optimal because we only scan the users table once and the register table once. Sorting is on the number of contests, which is typically much smaller than the total rows.
Test Cases
# test cases
# basic example
assert percentage_users_attended(db) == [(208, 100.0), (209, 100.0), (210, 100.0), (215, 66.67), (207, 33.33)]
# single user, single contest
assert percentage_users_attended(single_user_db) == [(1, 100.0)]
# contest with no users should not appear
assert percentage_users_attended(no_registration_db) == []
# tie on percentage, sorted by contest_id ascending
assert percentage_users_attended(tie_percentage_db) == [(101, 50.0), (102, 50.0)]
| Test | Why |
|---|---|
| Basic example | Validates main example logic and rounding |
| Single user | Edge case with 100% attendance |
| No registration | Contests with no users should not appear |
| Tie on percentage | Checks secondary sort order by contest_id |
Edge Cases
No registrations: If a contest has no registrations, it should not appear in the output. Using GROUP BY on the Register table naturally excludes such contests.
All users registered in a contest: This results in a percentage of 100. Correct rounding is crucial, and the solution handles this using ROUND.
Rounding precision: If the percentage has more than two decimals, rounding can lead to edge cases like 66.666... becoming 66.67. Using SQL's ROUND function guarantees two-decimal accuracy.
Tie in percentage: Multiple contests with the same percentage must be sorted by contest_id ascending. This is explicitly handled in the ORDER BY clause.
This approach ensures all edge cases are handled correctly without additional logic.