Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I generate a "Social Golfer" matrix for worker seating arrangement?

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).

like image 957
Frank R. Avatar asked Jul 24 '13 12:07

Frank R.


Video Answer


2 Answers

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
like image 94
Expedito Avatar answered Nov 15 '22 05:11

Expedito


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:

  • someone may be fired and can't participate in the next meeting;
  • HR hired 10 more workers for new project and all of them must participate in next meeting;
  • On next week starts renovation of dining room and only 20 tables would be available for next month.

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:

  • for each person maximize a number of persons to meet with
  • for each pair of persons minimize a number of meetings

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.

like image 35
ThinkJet Avatar answered Nov 15 '22 04:11

ThinkJet