Calendario de partidos de tenis


Hay un número limitado de jugadores y un número limitado de canchas de tenis. En cada ronda, puede haber como máximo tantos partidos como canchas. Nadie juega 2 rondas sin un descanso. Todos juegan un partido contra los demás. Producir el horario que toma tan pocas rondas como sea posible. (Debido a la regla de que debe haber un descanso entre rondas para todos, puede haber una ronda sin partidos.) La salida para 5 jugadores y 2 canchas podría ser:

 |  1   2   3   4   5
-|------------------- 
2|  1   - 
3|  5   3   - 
4|  7   9   1   - 
5|  3   7   9   5   -

En este salida las columnas y filas son los números de jugador, y los números dentro de la matriz son los números redondos que compiten estos dos jugadores.

El problema es encontrar un algoritmo que pueda hacer esto para instancias más grandes en un tiempo factible. Se nos pidió hacer esto en Prolog, pero el código (pseudo) en cualquier idioma sería útil.

Mi primer intento fue un algoritmo codicioso, pero eso da resultados con demasiadas rondas. Luego sugerí una búsqueda iterativa de profundización en profundidad, que un amigo de la mía implementado, pero que todavía tomó demasiado tiempo en instancias tan pequeñas como 7 jugadores.

(Esto es de una vieja pregunta de examen. Nadie con quien hablé tenía ninguna solución.)

Author: Handcraftsman, 2011-01-20

6 answers

Prefacio

En Prolog, las restricciones CLP(FD) son la opción correcta para resolver tales tareas de programación.

Véase clpfd para más información.

En este caso, sugiero usar el poderoso global_cardinality/2 restricción para restringir el número de ocurrencias de cada ronda, dependiendo del número de pistas disponibles. Podemos usar la profundización iterativa para encontrar el número mínimo de admisibles ronda.

Los sistemas Prolog disponibles gratuitamente son suficientes para resolver la tarea satisfactoriamente. Los sistemas de grado comercial funcionarán docenas de veces más rápido.

Variante 1: Solución con SWI-Prolog

:- use_module(library(clpfd)).

tennis(N, Courts, Rows) :-
        length(Rows, N),
        maplist(same_length(Rows), Rows),
        transpose(Rows, Rows),
        Rows = [[_|First]|_],
        chain(First, #<),
        length(_, MaxRounds),
        numlist(1, MaxRounds, Rounds),
        pairs_keys_values(Pairs, Rounds, Counts),
        Counts ins 0..Courts,
        foldl(triangle, Rows, Vss, Dss, 0, _),
        append(Vss, Vs),
        global_cardinality(Vs, Pairs),
        maplist(breaks, Dss),
        labeling([ff], Vs).

triangle(Row, Vs, Ds, N0, N) :-
        length(Prefix, N0),
        append(Prefix, [-|Vs], Row),
        append(Prefix, Vs, Ds),
        N #= N0 + 1.

breaks([]).
breaks([P|Ps]) :- maplist(breaks_(P), Ps), breaks(Ps).

breaks_(P0, P) :- abs(P0-P) #> 1.

Consulta de ejemplo: 5 jugadores en 2 canchas:

?- time(tennis(5, 2, Rows)), maplist(writeln, Rows).
% 827,838 inferences, 0.257 CPU in 0.270 seconds (95% CPU, 3223518 Lips)
[-,1,3,5,7]
[1,-,5,7,9]
[3,5,-,9,1]
[5,7,9,-,3]
[7,9,1,3,-]

La tarea especificada, 6 jugadores en 2 canchas, resuelto bien dentro del límite de tiempo de 1 minuto:

?- time(tennis(6, 2, Rows)),
   maplist(format("~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+\n"), Rows).
% 6,675,665 inferences, 0.970 CPU in 0.977 seconds (99% CPU, 6884940 Lips)
  -  1  3  5  7 10
  1  -  6  9 11  3
  3  6  - 11  9  1
  5  9 11  -  2  7
  7 11  9  2  -  5
 10  3  1  7  5  -

Otro ejemplo: 7 jugadores en 5 canchas:

?- time(tennis(7, 5, Rows)),
   maplist(format("~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+\n"), Rows).
% 125,581,090 inferences, 17.476 CPU in 18.208 seconds (96% CPU, 7185927 Lips)
  -  1  3  5  7  9 11
  1  -  5  3 11 13  9
  3  5  -  9  1  7 13
  5  3  9  - 13 11  7
  7 11  1 13  -  5  3
  9 13  7 11  5  -  1
 11  9 13  7  3  1  -

Variante 2: Solución con SICStus Prolog

Con las siguientes definiciones adicionales de compatibilidad, el programa same también se ejecuta en SICStus Prolog:

:- use_module(library(lists)).
:- use_module(library(between)).

:- op(700, xfx, ins).

Vs ins D :- maplist(in_(D), Vs).

in_(D, V) :- V in D.

chain([], _).
chain([L|Ls], Pred) :-
        chain_(Ls, L, Pred).

chain_([], _, _).
chain_([L|Ls], Prev, Pred) :-
        call(Pred, Prev, L),
        chain_(Ls, L, Pred).

pairs_keys_values(Ps, Ks, Vs) :- keys_and_values(Ps, Ks, Vs).

foldl(Pred, Ls1, Ls2, Ls3, S0, S) :-
        foldl_(Ls1, Ls2, Ls3, Pred, S0, S).

foldl_([], [], [], _, S, S).
foldl_([L1|Ls1], [L2|Ls2], [L3|Ls3], Pred, S0, S) :-
        call(Pred, L1, L2, L3, S0, S1),
        foldl_(Ls1, Ls2, Ls3, Pred, S1, S).

time(Goal) :-
        statistics(runtime, [T0|_]),
        call(Goal),
        statistics(runtime, [T1|_]),
        T #= T1 - T0,
        format("% Runtime: ~Dms\n", [T]).

Diferencia importante: SICStus, siendo un Prolog de calidad comercial que viene con un sistema CLP(FD) serio, es mucho más rápido que SWI-Prolog en este caso de uso y otros similares.

La tarea especificada, 6 jugadores en 2 canchas:

?-   time(tennis(6, 2, Rows)),
     maplist(format("~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+\n"), Rows).
% Runtime: 34ms (!)
  -  1  3  5  7 10
  1  -  6 11  9  3
  3  6  -  9 11  1
  5 11  9  -  2  7
  7  9 11  2  -  5
 10  3  1  7  5  -

El ejemplo más grande:

| ?- time(tennis(7, 5, Rows)),
   maplist(format("~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+\n"), Rows).
% Runtime: 884ms
  -  1  3  5  7  9 11
  1  -  5  3  9  7 13
  3  5  -  1 11 13  7
  5  3  1  - 13 11  9
  7  9 11 13  -  3  1
  9  7 13 11  3  -  5
 11 13  7  9  1  5  -

Observaciones finales

En ambos systems, global_cardinality/3 le permite especificar opciones que alteran la fuerza de propagación de la restricción de cardinalidad global, lo que permite un filtrado más débil y potencialmente más eficiente. Elegir las opciones correctas para un ejemplo específico puede tener un impacto aún mayor que la elección del sistema Prolog.

 33
Author: mat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-17 11:11:00

Esto es muy similar al Problema del Torneo Itinerante , que se trata de programar equipos de fútbol. En TTP, pueden encontrar la solución óptima hasta solo 8 equipos. Cualquiera que rompa un récord continuo de 10 o más equipos, tiene mucho más fácil ser publicado en una revista de investigación.

Es NP difícil y el truco es utilizar meta-heurística, como búsqueda tabu, recocido simulado,... en lugar de fuerza bruta o rama y atado.

Echa un vistazo mi implementación con Drools Planner (código abierto, java). Aquí están las restricciones, debería ser sencillo reemplazarlas con restricciones como Nadie juega 2 rondas sin un descanso.

 12
Author: Geoffrey De Smet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-29 09:13:29

Cada jugador debe jugar al menos n - 1 partidos donde n es el número de jugadores. Así que el mínimo de rondas es 2 (n-1) - 1, ya que cada jugador necesita descansar un partido. El mínimo también está limitado por(n (n-1))/2 partidos totales divididos por el número de pistas. Usando el más pequeño de estos dos le da la longitud de la solución óptima. Entonces es cuestión de llegar a una buena fórmula de estimación más baja ((número de partidos+restos restantes)/canchas) y ejecutar A* search.

Como Geoffrey dicho esto, creo que el problema es NP Duro, pero meta-heurística como A * es muy aplicable.

 5
Author: LarsV,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-26 14:10:38

Solución Python:

import itertools

def subsets(items, count = None):
    if count is None:
        count = len(items)

    for idx in range(count + 1):
        for group in itertools.combinations(items, idx):
            yield frozenset(group)

def to_players(games):
    return [game[0] for game in games] + [game[1] for game in games]

def rounds(games, court_count):
    for round in subsets(games, court_count):
        players = to_players(round)
        if len(set(players)) == len(players):
            yield round

def is_canonical(player_count, games_played):
    played = [0] * player_count
    for players in games_played:
        for player in players:
            played[player] += 1

    return sorted(played) == played



def solve(court_count, player_count):
    courts = range(court_count)
    players = range(player_count)

    games = list( itertools.combinations(players, 2) )
    possible_rounds = list( rounds(games, court_count) )

    rounds_last = {}
    rounds_all = {}
    choices_last = {}
    choices_all = {}



    def update(target, choices, name, value, choice):
        try:
            current = target[name]
        except KeyError:
            target[name] = value
            choices[name] = choice
        else:
            if current > value:
                target[name] = value
                choices[name] = choice

    def solution(games_played, players, score, choice, last_players):
        games_played = frozenset(games_played)
        players = frozenset(players)

        choice = (choice, last_players)

        update(rounds_last.setdefault(games_played, {}), 
                choices_last.setdefault(games_played, {}), 
                players, score, choice)
        update(rounds_all, choices_all, games_played, score, choice)

    solution( [], [], 0, None, None)

    for games_played in subsets(games):
        if is_canonical(player_count, games_played):
            try:
                best = rounds_all[games_played]
            except KeyError:
                pass
            else:
                for next_round in possible_rounds:
                    next_games_played = games_played.union(next_round)

                    solution( 
                        next_games_played, to_players(next_round), best + 2,
                        next_round, [])

                for last_players, score in rounds_last[games_played].items():
                    for next_round in possible_rounds:
                        if not last_players.intersection( to_players(next_round) ):
                            next_games_played = games_played.union(next_round)
                            solution( next_games_played, to_players(next_round), score + 1,
                                    next_round, last_players)

    all_games = frozenset(games)


    print rounds_all[ all_games ]
    round, prev = choices_all[ frozenset(games) ]
    while all_games:
        print "X ", list(round)
        all_games = all_games - round
        if not all_games:
            break
        round, prev = choices_last[all_games][ frozenset(prev) ]

solve(2, 6)

Salida:

11
X  [(1, 2), (0, 3)]
X  [(4, 5)]
X  [(1, 3), (0, 2)]
X  []
X  [(0, 5), (1, 4)]
X  [(2, 3)]
X  [(1, 5), (0, 4)]
X  []
X  [(2, 5), (3, 4)]
X  [(0, 1)]
X  [(2, 4), (3, 5)]

Esto significa que tomará 11 rondas. La lista muestra los juegos que se jugarán en las rondas en orden inverso. (Aunque creo que el mismo horario funciona hacia adelante y hacia atrás.) Volveré y explicaré por qué tengo la oportunidad.

Obtiene respuestas incorrectas para una cancha, cinco jugadores.

 3
Author: Winston Ewert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-26 20:44:47

Algunos pensamientos, tal vez una solución...

Ampliando el problema a los jugadores de X y las canchas de Y, creo que podemos decir con seguridad que cuando se nos da la opción, debemos seleccionar a los jugadores con el menor número de partidos completados, de lo contrario corremos el riesgo de terminar con un jugador que solo puede jugar cada dos semanas y terminamos con muchas semanas vacías en el medio. Imagina el caso con 20 jugadores y 3 canchas. Podemos ver que durante la ronda 1 los jugadores 1-6 se reúnen, luego en la ronda 2 los jugadores 7-12 se reúnen, y en la ronda 3 podríamos reutilizar a los jugadores 1-6 dejando a los jugadores 13-20 hasta más tarde. Por lo tanto, creo que nuestra solución no puede ser codiciosa y debe equilibrar a los jugadores.

Con esa suposición, aquí hay un primer intento de una solución:

 1. Create master-list of all matches ([12][13][14][15][16][23][24]...[45][56].)
 2. While (master-list > 0) {
 3.     Create sub-list containing only eligible players (eliminate all players who played the previous round.)
 4.     While (available-courts > 0) {
 5.         Select match from sub-list where player1.games_remaining plus player2.games_remaining is maximized.
 6.         Place selected match in next available court, and 
 7.         decrement available-courts.
 8.         Remove selected match from master-list.
 9.         Remove all matches that contain either player1 or player2 from sub-list.
10.     } Next available-court
11.     Print schedule for ++Round.
12. } Next master-list

No puedo probar que esto produzca un calendario con el menor número de rondas, pero debería estar cerca. El paso que puede causar problemas es #5 (seleccionar partido que maximiza las partidas restantes del jugador. Puedo imaginar que podría haber un caso en el que es mejor elegir una partida que casi maximiza 'games_remaining' para dejar más opciones en la siguiente ronda.

La salida de este algoritmo sería algo así como:

Round    Court1    Court2
  1       [12]      [34]
  2       [56]       --
  3       [13]      [24]
  4        --        --
  5       [15]      [26]
  6        --        --
  7       [35]      [46]
  .         .         .

Una inspección detallada mostrará que en la ronda 5, si el partido en Court2 hubiera sido [23], entonces el partido [46] podría haberse jugado durante la ronda 6. Eso, sin embargo, no garantiza que no habrá un problema similar en una ronda posterior.

Estoy trabajando en otra solución, pero eso tendrá que espera a más tarde.

 1
Author: oosterwal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-21 17:40:39

No se si esto importa, los datos de ejemplo de "5 Jugadores y 2 Canchas" faltan otros tres partidos: [1,3], [2,4] y [3,5]. Basado en la instrucción: "Todo el mundo juega un partido contra todos los demás."

 0
Author: me2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-25 20:19:24