Peter Larsson Blog

Patron Saint of Lost Yaks

Celko Stumper - The Class Scheduling Problem

Joe Celko has posted a new Stumper - The Class Scheduling Problem
here http://www.simple-talk.com/sql/t-sql-programming/celkos-sql-stumper-the-class-scheduling-problem/

Here is one suggestion to solve the problem. It's linear in time so it should be very fast. It's based on the "Descending order" approach, and the "paths" columns are used to store valid room and classes.

-- Initialize and find the valid combinations
DECLARE  @HowManySeatsFree INT = 0 -- Set to zero for maximum seating, and to 1 for letting in late pupils.

DECLARE
  @Source TABLE
        (
            room_nbr CHAR(2),
            class_nbr CHAR(2),
            recID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED
        )

INSERT      @Source
            (
                room_nbr,
                class_nbr
            )
SELECT      r.room_nbr,
            c.class_nbr
FROM        dbo.Rooms AS r
INNER JOIN  dbo.Classes AS c ON c.class_size <= r.room_size - @HowManySeatsFree
ORDER BY    r.room_size DESC,
            c.class_size DESC

-- Iterate the possibilities and return the unique answers
;WITH cteYak(recID, room_nbr, roomPath, class_nbr, classPath, isPresent)
AS (
    SELECT  recID,
            room_nbr,
            '/' + CAST(room_nbr AS VARCHAR(MAX)) + '/' AS roomPath, -- List of taken rooms
            class_nbr,
            '/' + CAST(class_nbr AS VARCHAR(MAX)) + '/' AS classPath, -- List of taken classes
            CAST(0 AS BIGINT)
    FROM    @Source
    WHERE   recID = 1

    UNION ALL

    SELECT      recID,
                room_nbr,
                CASE isPresent -- If room never encountered before (isPresent=0), take it!
                    WHEN 0 THEN roompath + CAST(room_nbr AS VARCHAR(MAX)) + '/'
                    ELSE roompath
                END AS roompath,
                class_nbr,
                CASE isPresent -- If class never encountered before (isPresent=0), take it!
                    WHEN 0 THEN classpath + CAST(class_nbr AS VARCHAR(MAX)) + '/'
                    ELSE classpath
                END AS classpath,
                isPresent
    FROM    (
                SELECT      s.recID,
                            s.room_nbr,
                            y.roomPath,
                            s.class_nbr,
                            y.classpath,
                            CHARINDEX('/' + CAST(s.room_nbr AS VARCHAR(MAX)) + '/', y.roompath)
                            + CHARINDEX('/' + CAST(s.class_nbr AS VARCHAR(MAX)) + '/', y.classpath) AS isPresent -- See if room or class is already taken. If so, isPresent is greater than 0, otherwise it will be 0.
                FROM        @Source AS s
                INNER JOIN  cteYak AS y ON y.recID + 1 = s.recID
        ) AS d
)
SELECT  room_nbr,
        class_nbr
FROM    cteYak
WHERE   isPresent = 0 -- Only present the combination never taken/found before
OPTION  (MAXRECURSION 0) -- Allow up to 32767 possible combinations.

Legacy Comments


Joe Celko
2010-01-23
re: Celko Stumper - The Class Scheduling Problem
I see you went for the (class_size <= room_size) rather than the (class_size < room_size) T-JOIN. Can you put in some commentary? Why did you put an ORDER BY on an INSERT INTO Statement? I thinhk you are the first answer, by the way.

Peso
2010-01-24
re: Celko Stumper - The Class Scheduling Problem
I did put an ORDER BY to ensure matches between class size and room size are done in "proper" order because of the nature of CTE.
The recID value holds a "ticket" for which order to check combinations.

Peso
2010-01-24
re: Celko Stumper - The Class Scheduling Problem
Oh, and the REASON for using ORDER BY is to simulate a MERGE JOIN becuase I believe this task will be faster when items are pre-sorted, much like a MERGE JOIN need to.

Peso
2010-01-24
re: Celko Stumper - The Class Scheduling Problem
Oh, and the REASON for using ORDER BY is to simulate a MERGE JOIN becuase I believe this task will be faster when items are pre-sorted, much like a MERGE JOIN need to. It also makes sure I start with the largest class and end with the smallest class.

Transact Charlie
2010-01-25
re: Celko Stumper - The Class Scheduling Problem
Hmmmm. looks eerily familiar.

Always enjoy checking this blog.

All the best, Charlie.

ehorn
2010-02-24
re: Celko Stumper - The Class Scheduling Problem
Just spotted this one... here is mine...
[code]
--------------------------------------------------------------------------
-- build a results table
--------------------------------------------------------------------------

CREATE TABLE results
(
id INT IDENTITY(0,1) PRIMARY KEY,
class_nbr CHAR(2),
class_size INT,
room_nbr CHAR(2),
room_size INT
)

INSERT results
SELECT
class_nbr,
class_size,
room_nbr,
room_size
FROM
Classes c
JOIN Rooms r ON r.room_size >= c.class_size

--------------------------------------------------------------------------
--build a function to find the min unused room size
--------------------------------------------------------------------------
GO
CREATE FUNCTION dbo.udfGetMinUnusedRoomsize (@id INT, @class_nbr CHAR(2))
RETURNS INT
AS
BEGIN
DECLARE @room_size int;

SELECT @room_size = MIN(room_size)
FROM results
WHERE class_nbr = @class_nbr AND room_nbr NOT IN (SELECT room_nbr WHERE id < @id)

RETURN @room_size
END
GO

--------------------------------------------------------------------------
--result
--------------------------------------------------------------------------

SELECT
c.class_nbr,
c.class_size,
r.room_nbr,
r.room_size
FROM
Classes c
JOIN
(
SELECT
c.class_nbr,
MIN(dbo.udfGetMinUnusedRoomsize(c.id, c.class_nbr)) AS min_room_size
FROM
results c
GROUP BY
c.class_nbr
) d ON d.class_nbr = c.class_nbr
JOIN Rooms r ON r.room_size = d.min_room_size
ORDER BY c.class_size DESC

--------------------------------------------------------------------------
--clean up
--------------------------------------------------------------------------

DROP TABLE results
DROP FUNCTION dbo.udfGetMinUnusedRoomsize


[/code]