如何为工人座位安排生成“社交高尔夫球手”矩阵?

时间:2013-07-24 12:17:59

标签: c# sql oracle select permutation

此挑战是Social Golfer Problem方案。我有一个有320人的公司。我最近实施了一个按目标管理(MBO)计划,其中每个工作人员都被分配了每月完成的目标。其中一个反复出现的目标是按时到达工作,每天早上参加30分钟的咖啡和dounut会议。会议在我们的餐厅举行,共有50张桌子。每张桌子最多可容纳8人。每个工作日,正负80个座位是空的,因为目前最大容量为400个。我想生成一个循环座位安排,以便每个人可以轮流与其他人会面和协作。

(编辑)规则:每个工作日需要8人独特。一个人不能再与他们过去坐过的其他人坐在一起,直到所有可能的排列都已用完为止。

编辑:所需结果的一个示例是:

Day 1: 

Table 1 will seat worker numbers 1,2,3,4,5,6,7,8.
Table 2 will seat worker numbers 9.10,11,12,13,14,15,16
...
Table 50 will seat worker numbers 313,314,315,316,317,318,319,320


**NOTE:**
(So, the next workday and thereafter, workers 1 through 8 cannot ever be seated with 
any other workers from that same set until all possible permutations have been
exhausted).

Day 2:

Table 1 will seat worker numbers 1,17,18,19,20,21,22,23
Table 2 will seat worker numbers 2,10,24,25,26,27,28,29
...
Table 50 will seat worker numbers 305,306,307,308,309,310,311,312


Day N: 
.
.
...
.

在所有可能的唯一集合(排列)都已用尽之前,所有工作者编号(元素)都不能在8个工作程序的每个集合(数组)中重复。然后循环重新开始,也许是移动元素,只有这样,一个工人才能和他们以前见过的另一个工人坐在一起。然后,我想通过电子邮件向每位工作人员发送电子邮件,告知他们在下一个工作日分配了哪些表格。每个工人在到达桌子前都不知道还有谁在他们指定的桌子上坐着。只有我会有完整的座位安排名单。 (这是一种“音乐椅”游戏)

这不是例外情况或学校作业。使用APL编程语言的朋友告诉我她可以使用一行代码生成所需的结果,但我们只使用基于SQL的DBMS(IBM Informix 11.70和Oracle 11)。

所以我有一个包含以下列的SQL表:

employee.id        INT      {unique primary key}
employee.FullName  VARCHAR
...

以下一行APL编程代码生成矩阵排列:

pmat2←{{,[⍳2]↑(⊂⊂⎕io,1+⍵)⌷¨⍒¨↓∘.=⍨⍳1+1↓⍴⍵}⍣⍵⍉⍪⍬}

在SQL中,我是否可以使用一个SELECT语句生成所需的结果,是否需要多个SELECT INTO TEMP语句,或者是获取所需结果所需的存储过程?

我的SELECT语句或SP应该是什么样的?

编辑:如果SQL无法实现所需的结果,是否可以使用c#等3GL来完成?

4 个答案:

答案 0 :(得分:4)

这就是所谓的“Social Golfer Problem”,虽然它已经用APL完成,而不是单行。这实际上是一个非常棘手的问题,所以我很难想象它可以用数据库查询来完成。网上有很多关于这个主题和一些在线计算器的文献。

修改

您的APL代码只是创建一个排列矩阵。例如,如果输入以下内容:

pmat2←{{,[⍳2]↑(⊂⊂⎕io,1+⍵)⌷¨⍒¨↓∘.=⍨⍳1+1↓⍴⍵}⍣⍵⍉⍪⍬}
pmat2 3

您将获得以下矩阵:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

根据维基百科:

循环赛锦标赛(或全部全部锦标赛)是一项“竞赛”,其中每位参赛者轮流与所有其他参赛者会面。

根据Markus Triska的主题论文:

社交高尔夫球手问题(SGP)是一个组合优化问题。任务是在g组p个球员中安排g×p个高尔夫球手w周,这样就不会有两个高尔夫球手在同一组中多次出场。

数学上存在很大差异。一场循环赛将涉及两人一组,所以如果你有9名参赛者,则需要在8轮比赛中进行36场比赛。对于社交高尔夫球手,您可以按三分组进行分组,并且需要在4轮中进行12场比赛:

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

答案 1 :(得分:3)

问题
如果问题是安排会议的真正任务,那么提出问题就会出现一些错误 这是因为工人的数量,甚至一些可用的桌子和座位都不是基本的物理常数:

  • 某人可能被解雇,无法参加下次会议;
  • 人力资源部门为新项目雇用了10名工人,他们都必须参加下一次会议;
  • 下周开始翻新餐厅,下个月只有20张桌子。

所以问题听起来像这样:"我们需要在接下来的5-10个工作日安排会议,以便尽可能多的人会见他们之前和之前没有说话的人尽可能少的人与另一个人交谈两次以上"。

因此,问题不在于产生一整套排列。问题是关于下一次N次会议的最佳规划。

<强>理论
问题可以归类为通用mathematical optimization problem。对于那类问题,我们的目标是找到最佳解决方案,作为函数的参数值集合,为目标函数提供最大值或最小值。
要制定问题,我们必须找到问题的根源:

  • 为每个人提供最大化的人数
  • 每对人员最少会议次数

每个目标都谈到了一对人之间的对话,所以我们必须根据&#34; meet&#34;来制定问题。 将P表示为人数,将i in [1..P]j in [1..P]表示为人员索引 将M表示为会议数量,将m in [1 .. M]表示为会议号码 然后让我们介绍a(i,j,m) | i < j, i in [1..P], j in [1..P], m in [1..M]作为两人在具体会议上会面的事实。 之后,可以为问题制定目标函数和边界条件。

数学方法
请注意,确切的解决方案(任何人只有一次见到另一个人,直到周期结束)才有可能在非常罕见的情况下 这是NP完全类问题,最佳匹配公式是满足1度共度条件的k均匀超图中完美匹配的优化问题&#34;。
对于进一步的理论研究,您可以在Mathematics提出问题或检查latest works available以进行k均匀超图分区,例如: "Polynomial-time perfect matchings in dense hypergraphs"

解决方案必须准确(P-1)/(T-1)=(320-1)/(8-1)=45.5714285714个会议,因为每次遇到7个人和#34;其他人&#34;根据问题的条件,在一对人遇到两次之前可以召开45次会议。

在StackOverflow(link)上已经有类似的问题和良好的答案。请注意,此算法会留下空白位置,因为要将所有人完全放置到seats * prime = person_count并选择41作为素数。 以下是使用此解决方案(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

实用方法
从实际的角度来看,我们不需要理论证明的最佳解决方案。如果一个人不止一次遇到另一个人并不是什么大问题,那么可以停下来接近最佳解决方案。
如果我们开始逐个将人员放在会议和桌子上,试图保持每对人的交叉数尽可能低,那么就可以根据经验规则生成这样的解决方案。
有许多可能的策略,其中一个说明如下。

出于演示目的,我使用Oracle,因为此数据库存在于问号标签中,并且可以在SQLFiddle站点上找到。

示例数据库架构设置包括三个表:

person - 包含工人名单的表格;

person_pair - 包含所有唯一工人对的列表和每对的交集计数的表,完全floor((P*P)/2) - floor(P/2)行。在P = 320的情况下,它保存51040行。

meeting - 包含每次会议中每个人的展示位置信息的表格。

由于SQLFiddle网站上的资源消耗限制以及保持结果数据集可观察,因此限制为20的工作人员的示例代码数量和4的席位数量。

以下是方案设置和填充的代码。请查看注释以了解有关表字段的更多信息。

-- 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;

填写初始数据(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
/

生成会议

策略由两部分组成:
1.按特定顺序选择人员;
2.将人员列在最合适的桌子上。

在选择列表中安排人员尝试尽早将人们聚会以前,并将其放在不同的表格中。

配置人员更为棘手,在该阶段的主要目的是最大限度地增加首次会议的次数,并尽量减少重复会议的次数。因此,它接近于优化问题构造正确目标函数的问题,在大多数现实案例中都是非平凡的。

我选择这个标准:

对于每张桌子计算两个因素:&#34;有吸引力&#34;(A) - 为什么将人放在那张桌子上&#34;驱逐&#34;(R) - 为什么那个人不能坐在那张桌子上 这个因素组成了得到决赛桌安排因素:
-A*A - (if A=0 then 0 else R/2) + R
&#34;有吸引力的&#34;因子计算为当前人员之前未与之会面的人数 &#34;驱避&#34;因子计算为当前人与已经在桌旁的所有人的会议次数之和。

很可能它不是那么好,但足够用于示例目的。 例如,可以扩展公式以考虑自上次会议以来已经过了多少时间。

您可以尝试构建良好的表达式,以便自己选择表格。

接下来是生成会议的代码。

代码(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;  

多一点理论

生成的解决方案可以用作某些multicriteria optimization methods的起点,但要使用它,您必须对问题进行良好的正式表述。

希望上述所有内容都能帮助您解决问题。

答案 2 :(得分:-2)

我实际上并不知道这是否有效,但你可以只创建一个表并插入个人表(只有id就足够了)8次交叉连接,其中where子句在第二次连接中排除员工.id(第二栏)!= employee.id(第一栏)。在第三次交叉连接中,您将需要employee.id(第三列)!= employee.id(第二列).....

在我看来会产生所有组合。然后你只需要随机选择并保存它,所以你不要再选择它了。

答案 3 :(得分:-2)

在SQL中,答案实际上非常简单,需要2个表,一个表定义员工,另一个表定义表席。如:

表:员工

列:

EmployeeID - 必须是唯一标识符。 员工姓名 ActiveEmployee - (Y / N) 等

表:座位

列:

SeatID - 必须是唯一标识符。 TableNumber TableSeatNumber 等

现在定义一个未加入标准的查询,称为笛卡尔积,通常是不合需要的结果,但在这种情况下并不是一些数据仓库实现。

Select EmployeeID, SeatID from Employees, Seating where ActiveEmployee = 'Y' order by TableSeatNumber, TableNumber;

这将为您提供每个座位的每位员工的结果。该排序首先在不同的表格中为整个人口产生不同的席位。如果您的员工人数有很大的营业额,那么将结果与历史进行比较,然后从笛卡尔积中否定该实例。

如果您想更多地混淆座位,可以使用排序顺序的其他选项,例如唯一字段。

希望这有帮助。