It may be easier to first build each column in its own array, then transpose the result. For example, consider if the data were arranged as:
$d = array(
array(array('Speaker One', 'Room 1', 18)),
array(array('Speaker Two', 'Room 2', 9), array('Speaker Five', 'Room 2', 9)),
array(array('Speaker Three', 'Room 3', 6), array('Speaker Four', 'Room 3', 6), array('Speaker Six', 'Room 3', 6)),
);
It is of course an array. Each element is an array corresponding to one column of the final table (I guess that's the same thing as one room). Each element of a column array is one cell, and is itself an array of information relevant to one booking - speaker name, room number, and number of time slots the booking occupies (which is of course simply the duration divided by five minutes - this number is the rowspan for the cell representing the booking). Objects might be used to represent cells/bookings and columns/rooms instead of just arrays if you already have smarter data structures for those purposes. The contents of a cell need not be limited to actual bookings; an "empty" cell can be used to hold a room empty for a duration, and the last column in the HTML table would correspond to a final row of the above array looking something like [font=monospace]array(array('9:00 AM', 1), array('9:05 AM', 1), ...)[/font] so that the individual slots can be listed.
To turn it into something more easily traversed while building the HTML table there's a trick I found in the manual:
array_unshift($d, null);
$t = call_user_func_array('array_map', $d);
The result of that code on the above array is
$t = array(
array(array('Speaker One', 'Room 1', 18), array('Speaker Two', 'Room 2', 9), array('Speaker Three', 'Room 3', 6)),
array(null, array('Speaker Five', 'Room 2', 9), array('Speaker Four', 'Room 3', 6)),
array(null, null, array('Speaker Six', 'Room 3', 6))
);
And it's easy to use a pair of nested foreach loops (the outer one to loop over the rows, the inner one to loop over the cells in a row) and output whatever is appropriate for each cell as it is reached (using that 18 or 9 or whatever as the value of the cell's rowspan attribute). Just skip the nulls - they represent the cells that were obliterated by being spanned over by cells higher up.