sqlsql-servergreatest-n-per-group

List of all the Last People to Enter the Elevator


I came across this problem recently and have been unable to solve it using SQL. The problem appears to be an extension to the original LeetCode problem here

Here is the premise: There is a queue of people waiting to board an elevator. However, the elevator has a maximum weight limit of 1000 kilograms, so people may have to board the elevator in separate trips.

The sample table below shows information about people who are waiting to board. The column turn determines the order in which each person will board. I added the column weight_cumulative for ease of understanding, with the query SUM(weight) OVER (ORDER BY turn) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

name weight turn weight_cumulative
Alice 250 1 250
Bob 170 2 420
Alex 350 3 770
John 400 4 1170
Winston 500 5 1670
Marie 200 6 1870

I need to return the list of all people who are the last to board the elevator in their trips.

i.e. for the above example, the output would be

name
Alex
Winston
Marie

(In contrast, the original LeetCode problem only asks for the last person to enter the first and only trip. This is as simple finding the last person ordered by turn where weight_cumulative <= 1000)

The problem would be solved if I can create the column weight_cumulative_tripwise as below

name weight turn weight_cumulative weight_cumlative_tripwise
Alice 250 1 250 250
Bob 170 2 420 420
Alex 350 3 770 770
John 400 4 1170 400
Winston 500 5 1670 900
Marie 200 6 1870 200

But creating weight_cumulative_tripwise requires some column (like trip_number) that I can use as a partition to run my SUM() window function on top of, but I cannot figure out how to create trip_number without having weight_cumulative_tripwise first.

I am not an expert at recursive CTEs, but this looks a problem that can be solved recursively in a general programming language. Is there a way to do this problem without using recursive CTEs? Ideally using functions that are available in all popular flavors of SQL.


Solution

  • The naïve CTE version:

    WITH data AS (
        SELECT  *
        FROM
        (
            VALUES  (N'Alice', 250, 1, 250)
            ,   (N'Bob', 170, 2, 420)
            ,   (N'Alex', 350, 3, 770)
            ,   (N'John', 400, 4, 1170)
            ,   (N'Winston', 500, 5, 1670)
            ,   (N'Marie', 200, 6, 1870)
        ) t (name,weight,turn,weight_cumulative)
    
    )
    , cte_elevator AS (
        SELECT  name, weight, turn, weight AS total, 1 AS trip
        FROM    data d
        WHERE   turn = 1
        UNION ALL
        SELECT  d.name, d.weight, d.turn
        ,   CASE WHEN e.total + d.weight > 1000 THEN d.weight ELSE e.total + d.weight END
        ,   CASE WHEN e.total + d.weight > 1000 THEN trip + 1 ELSE trip END
        FROM    cte_elevator e
        INNER JOIN data d
            ON  d.turn = e.turn + 1
        )
    SELECT  *
    FROM    (
        SELECT  *
        ,   ROW_NUMBER() OVER(PARTITION BY trip ORDER BY turn DESC) AS sort
        FROM    cte_elevator
        ) x
    WHERE   x.sort = 1
    

    One creates a recursive cte that starts from turn = 1 and for every next person, one compares previous total weight + current person's weight to 1000, and increments the "trip" counter accordingly.

    To get the last person per trip, you can use the standard ROW_NUMBER function.

    Outputs:

    name weight turn total trip sort
    Alex 350 3 770 1 1
    Winston 500 5 900 2 1
    Marie 200 6 200 3 1