EDIT: I am looking for an APL function, or MS Access VBA function, which takes as arguments the total number of employees, total number of dinning tables, and number of employees per dinning table for generating rotating seating assignments.
This challenge is a Social Golfer Problem scenario. I have a company with 280 persons. I recently implemented a Management By Objectives (MBO) program where each worker is assigned goals to be completed on a monthly basis. One of the recurring goals is to arrive on time at work to attend a 30 minute coffee and dounut meeting each morning. The meeting is held in our dinning hall which has 50 tables. Each table can seat up to 12 persons maximum, however we are only using 6 per table because of the Covid pandemic.
I want to generate unique sets of seating arrangement for each dinning table so that each person can meet and collaborate with every other person on a rotating basis until all unique sets are exhausted. Then the cycle starts all over where two or more employees might be seated at the same table again.
(EDIT) RULE: Unique sets of 6 people are required for each workday. A person cannot be seated again with other persons they have sat with in the past until all possible permutations have been exhausted.
EDIT: An example of the desired result is:
Day 1:
Table 1 will seat worker numbers 1,2,3,4,5,6.
Table 2 will seat worker numbers 7,8,9,10,11,12.
...
Table 50 will seat worker numbers 275,276,277,278,279,280.
Day 2:
Table 1 will seat worker numbers 7,13,19,26,33,40.
Table 2 will seat worker numbers 14,20,27,34,41,48
...
NOTE: (So, the next workday and thereafter, workers 1 through 6 cannot ever be seated together at the same table with any other workers from that same set until all possible permutations have been exhausted).
That's called the "Social Golfer Problem," and though it has been accomplish with APL, not with a single line. It's actually a very difficult problem, so I'm having a hard time imagining that it could be done with a database query. There's lot's of literature online about the subject and some online calculators.
EDIT:
Your APL code simply creates a matrix of permutations. For example, if you enter the following:
pmat2←{{,[⍳2]↑(⊂⊂⎕io,1+⍵)⌷¨⍒¨↓∘.=⍨⍳1+1↓⍴⍵}⍣⍵⍉⍪⍬}
pmat2 3
You get the following matrix:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
According to Wikipedia:
A round-robin tournament (or all-play-all tournament) is a competition "in which each contestant meets all other contestants in turn".
According to Markus Triska in his master thesis on the subject:
The Social Golfer Problem (SGP) is a combinatorial optimisation problem. The task is to schedule g × p golfers in g groups of p players for w weeks such that no two golfers play in the same group more than once.
Mathematically there's a big difference. A round-robin tournament involves groups of two, so if you have 9 contestants, it would require 36 matches in 8 rounds. With the social golfer, you can group them by threes, and it would require 12 matches in 4 rounds:
6 4 8 1 8 3 1 9 6 9 5 8
3 9 7 4 2 9 4 3 5 4 7 1
5 1 2 5 7 6 8 7 2 6 3 2
The problem
If the problem is a real task to schedule meetings then there are some mistakes in posing a question.
It's because number of workers and even a number of available tables and seats not a fundamental physical constant:
So problem sounds like this: "We need to schedule meetings for next 5-10 working days in a such a way that as many persons as possible meet with persons that they didn't talk before and as low persons as possible talk with another persons twice and more".
Therefore the problem isn't about generating a full set of permutations. Problem is about optimal planning for next N meetings.
Theory
Problem can be classified as generic mathematical optimization problem. For that class of problems we have a goal to find optimal solution presented as set of argument value(s) for function(s) which provides maximum or minimum value for objective function(s).
To formulate a problem we must to find the root of the question:
Each of that goals talks about conversations between one pair of persons so we must formulate a problem in terms of "meet".
Denote P
as number of persons and i in [1..P]
and j in [1..P]
as person indexes.
Denote M
as quantity of meetings and m in [1 .. M]
as meeting number.
Then let's introduce a(i,j,m) | i < j, i in [1..P], j in [1..P], m in [1..M]
as a fact of meeting between two persons on concrete meeting.
After that it's possible to formulate an objective function and bounding conditions for the problem.
Math approach
Please note, that the exact solution (anyone meet another person only one time until cycle finished) possible only in very rare cases.
This is NP-complete class problem and best matched formulation is "optimization problem of perfect matching in k-uniform hypergraphs satisfying a 1-degree co-degree condition".
For further theory research you can ask a question at Mathematics or examine latest works available for k-uniform hypergraph partitioning, e.g. "Polynomial-time perfect matchings in dense hypergraphs"
Solution must have exactly (P-1)/(T-1)=(320-1)/(8-1)=45.5714285714
meetings because every time person meets 7 others and "others" number is 319. So it can be 45 meetings according conditions of the question before some pair of persons meets twice.
There are a similar question with good answer already on StackOverflow (link). Note that this algorithm leaves empty places, because for full placement of all persons it requires to seats * prime = person_count
and 41 chosen as prime.
Below is query using this solution (SQLFiddle).
with params as (
select
320 n, -- number of persons
8 k, -- number of seats per table
41 p -- least prime which greather or equal n/k
from dual
),
person_set as (
select level person_id from dual connect by level <= (select n from params)
),
person_map as (
select
person_id,
mod( mod(person_id, p.k * p.p), p.k ) x,
trunc( mod(person_id, p.k * p.p) / p.k ) y
from person_set, params p
),
meetings as (
select (level-1) meeting_no
from dual
connect by level <= (select least(k*p, (n-1)/(k-1)) from params)
),
seats as (
select (level-1) seat_no
from dual
connect by level <= (select k from params)
),
tables as (
select (level-1) table_no
from dual
connect by level <= (select p from params)
),
meeting_plan as (
select --+ ordered use_nl(seats tables)
meeting_no,
seat_no,
table_no,
(
select
person_id
from
person_map
where
x = seat_no
and
y = mod(meeting_no*seat_no + table_no, p.p)
) person_id
from
meetings, seats, tables, params p
)
select
meeting_no,
table_no,
max(case when seat_no = 0 then person_id else null end) seat1,
max(case when seat_no = 1 then person_id else null end) seat2,
max(case when seat_no = 2 then person_id else null end) seat3,
max(case when seat_no = 3 then person_id else null end) seat4,
max(case when seat_no = 4 then person_id else null end) seat5,
max(case when seat_no = 5 then person_id else null end) seat6,
max(case when seat_no = 6 then person_id else null end) seat7,
max(case when seat_no = 7 then person_id else null end) seat8
from meeting_plan
group by meeting_no, table_no
order by meeting_no, table_no
Practical approach
From practical point of view we don't need exactly optimal solution with theoretical proof. If one person meet another more than once it's not a big deal, so it's possible to stop at nearly optimal solution.
Such a solution can be generated on basis of empirical rules if we start to place persons one by one to meetings and tables trying to keep number of intersection for each pair of persons as low as possible.
There are many strategies of placing possible and one of them illustrated below.
For demonstration purposes I use Oracle because this database present in question tags and it's available at SQLFiddle site.
Example database schema setup includes three tables:
person
- table with list of workers;
person_pair
- table with list of all unique pairs of workers and count of intersection for each pair, totally floor((P*P)/2) - floor(P/2)
rows. In case of P
=320 it holds 51040 rows.
meeting
- table with placement information for each person on each meeting.
In example code number of workers limited to 20
and number of seats to 4
because of resource consumption limits on SQLFiddle site and to keep result datasets observable.
Below is code for scheme setup and fill. Please look through the comments to find out more about table fields.
-- List of persons
create table person(
person_id number not null -- Unique person identifier.
);
-- primary key
alter table person add constraint pk_person primary key (person_id) using index;
-- List of all possible unique person pairs
create table person_pair(
person1_id number not null, -- 1st person from pair, refers person table.
person2_id number not null, -- 2nd person from pair, refers person table.
-- person1_id always less than person2_id.
meet_count number -- how many times persons in pair meet each other.
);
-- primary key
alter table person_pair add constraint pk_person_pair primary key (person1_id, person2_id) using index;
-- indexes for search
alter table person_pair add constraint idx_pair2 unique (person2_id, person1_id) using index;
-- Placement information for meetings
create table meeting(
meeting_number number not null, -- sequential meeting number
table_number number not null, -- table number
person_id number not null, -- person placed on that table and meeting
seat_no number -- seat number
);
-- primary key: person can seat on the same table only once in one meeting
alter table meeting add constraint pk_meeting primary key (meeting_number, table_number, person_id) using index;
-- disallow duplicate seats on the same table during one meeting
alter table meeting add constraint miting_unique_seat unique (meeting_number, table_number, seat_no) using index;
-- person can participate in meeting only once
alter table meeting add constraint miting_unique_person unique (meeting_number, person_id) using index;
Fill initial data (SQLFiddle):
begin
-- Fill persons list with initial data
insert into person(person_id)
select level from dual connect by level <=20;
-- generate person pairs
insert into
person_pair(person1_id, person2_id, meet_count)
select
p1.person_id,
p2.person_id,
0
from
person p1,
person p2
where
p1.person_id < p2.person_id
;
end;
/
select * from person order by person_id
/
select * from person_pair order by person1_id, person2_id
/
Generating meetings
Strategy consist of 2 parts:
1. Select persons in specific order;
2. Place persons from list one-by-one at most appropriate table.
Arranging people in selection list is attempt to place persons who meet before many times before as early as possible and place it at separate tables.
Placing persons are more tricky and main purpose at that stage is to maximize number of first meetings and minimize number of repeated meetings. So it's close to problem of construction of proper objective function for optimization problem, what is non-trivial in most of a real world cases.
I choose this criteria:
For each table counted two factors: "attractive"(A
) - why place person at that table and "repellent"(R
) - why person can't seat on that table.
This factor composed toghether to get final table arranging factor:-A*A - (if A=0 then 0 else R/2) + R
"Attractive" factor counted as number of persons already placed at the table with which current person not meet before.
"Repellent" factor counted as sum of number of meetings of current person with all persons already at the table.
Very probably it not so good as it can, but enough for purposes of example. For example formula can be extended to take into account how much time has been passed since the last meeting.
You can experiment with building good expression for choosing table on your own.
Next is code for generation of meetings.
Code (SQLFiddle)
declare
vMeetingNumber number; -- number of current meeting
vNotMeetPairCount number; -- number of pairs not meet before
vTableCapacity number := 4; -- number of places at one table
vTableCount number; -- number of tables
begin
-- get next meeting number for case of continous generation
select nvl(max(meeting_number),0) + 1 into vMeetingNumber from meeting;
-- count minimum required table number
select ceil(count(1)/vTableCapacity) into vTableCount from person;
-- number of remaining pairs who don't meet before
select count(1) into vNotMeetPairCount
from person_pair
where meet_count < 1;
-- Generate new meetings while not all persons meet each other
while (vNotMeetPairCount > 0) loop
-- select list of persons to place
for cPersons in (
with person_meets as (
select
pp.person1_id, pp.person2_id, pp.meet_count,
( row_number() over (
order by pp.meet_count desc, pp.person1_id
)
) row_priority
from
person_pair pp
)
select person_id from (
select person_id, sum(pair_meet_count*pair_meet_count) pair_meetings from (
select person1_id person_id, meet_count pair_meet_count from person_meets
union all
select person2_id person_id, meet_count pair_meet_count from person_meets
)
group by person_id
)
order by pair_meetings desc
) loop
-- add current person to most applicable table
insert into meeting(meeting_number, table_number, person_id, seat_no)
select
vMeetingNumber, table_number, cPersons.person_id, seat_no
from (
with available_tables as (
select
table_number, places_occupied
from (
select
t.table_number,
(
select count(1)
from meeting m
where
m.meeting_number = vMeetingNumber
and
m.table_number = t.table_number
) places_occupied
from (
select level table_number
from dual connect by level <= vTableCount
) t
)
where places_occupied < vTableCapacity
)
select
table_number,
seat_no,
( row_number() over ( order by
-attractor_factor*attractor_factor - decode(attractor_factor,0,0,repellent_factor/2) + repellent_factor
)
) row_priority
from (
select
t.table_number,
t.places_occupied + 1 seat_no,
(
select
count(1)
from
meeting m,
person_pair pp
where
m.table_number = t.table_number
and
m.meeting_number = vMeetingNumber
and
pp.person1_id = least(m.person_id, cPersons.person_id)
and
pp.person2_id = greatest(m.person_id, cPersons.person_id)
and
pp.meet_count = 0
) attractor_factor,
(
select
nvl(sum(meet_count),0)
from
meeting m,
person_pair pp
where
m.table_number = t.table_number
and
m.meeting_number = vMeetingNumber
and
pp.person1_id = least(m.person_id, cPersons.person_id)
and
pp.person2_id = greatest(m.person_id, cPersons.person_id)
and
pp.meet_count > 0
) repellent_factor,
1 random_factor --trunc(dbms_random.value(0,1000000)) random_factor
from
available_tables t
)
)
where
row_priority = 1
;
end loop;
-- Update number of meets
update person_pair
set meet_count = meet_count + 1
where
(person1_id, person2_id) in (
select
m1.person_id person1_id,
m2.person_id person2_id
from
meeting m1,
meeting m2
where
m1.meeting_number = vMeetingNumber
and
m2.meeting_number = vMeetingNumber
and
m1.table_number = m2.table_number
and
m1.person_id < m2.person_id
)
;
-- advice to next meeting
vMeetingNumber := vMeetingNumber + 1;
-- count pairs who don't meet before
select count(1) into vNotMeetPairCount
from person_pair
where meet_count < 1;
end loop;
end;
A little bit more theory
Generated solution can be used as start point for some multicriteria optimization methods, but to use it you must have a good formal formulation of the problem.
Hope that all stated above helps you to resolve the problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With