Robots! Ramassez ces cornichons!

10

Il me semble que je me suis mis un peu dans le pétrin. Au sens propre. J'ai laissé tomber un tas de cornichons sur le sol et maintenant ils sont tous éparpillés! J'ai besoin de vous pour m'aider à les collecter tous. Oh, ai-je mentionné que j'avais un tas de robots à ma disposition? (Ils sont également tous dispersés un peu partout; je suis vraiment mal à organiser les choses.)

Vous devez prendre connaissance sous la forme de:

P.......
..1..2..
.......P
........
P3PP...4
.......P

c'est-à-dire, plusieurs lignes de ., P(cornichon) ou un chiffre (ID du robot). (Vous pouvez supposer que chaque ligne est de longueur égale, complétée par ..) Vous pouvez entrer ces lignes sous forme de tableau, ou slurp à partir de STDIN, ou lire dans une seule ligne séparée par des virgules, ou lire un fichier, ou faire ce que vous voulez tiens à prendre l'entrée.

Votre sortie doit être sous forme de nlignes, où nest l'ID de robot le plus élevé. (Les identifiants des robots seront toujours séquentiels à partir de 1.) Chaque ligne contiendra le chemin du robot, formé des lettres L(gauche), R(droite), U(haut) et D(bas). Par exemple, voici un exemple de sortie pour ce puzzle:

LLU
RDR
LRRR
D

Il peut également être

LLU RDR LRRR D

Ou

["LLU","RDR","LRRR","D"]

Ou tout format que vous souhaitez, tant que vous pouvez dire quelle est la solution censée être.

Votre objectif est de trouver la sortie optimale, qui est celle qui a le moins d'étapes. Le nombre de pas est compté comme le plus grand nombre de pas de tous les robots. Par exemple, l'exemple ci-dessus comportait 4 étapes. Notez qu'il peut y avoir plusieurs solutions, mais il vous suffit d'en générer une seule.

Notation:

  • Votre programme sera exécuté avec chacun des 5 cas de test (générés aléatoirement).
  • Vous devez ajouter les étapes de chaque course, et ce sera votre score.
  • Le score cumulatif total le plus bas gagnera.
  • Vous ne pouvez pas coder en dur pour ces entrées spécifiques. Votre code devrait également fonctionner pour toute autre entrée.
  • Les robots peuvent se traverser.
  • Votre programme doit être déterministe, c'est-à-dire la même sortie pour chaque exécution. Vous pouvez utiliser un générateur de nombres aléatoires, tant qu'il est prédéfini et produit systématiquement les mêmes nombres multiplateforme.
  • Votre code doit s'exécuter dans les 3 minutes pour chacune des entrées. (De préférence beaucoup moins.)
  • En cas d'égalité, la plupart des votes positifs l'emporteront.

Voici les cas de test. Ils ont été générés au hasard avec un petit script Ruby que j'ai écrit.

P.......1.
..........
P.....P...
..P.......
....P2....
...P.P....
.PP..P....
....P....P
PPPP....3.
.P..P.P..P

....P.....
P....1....
.P.....PP.
.PP....PP.
.2.P.P....
..P....P..
.P........
.....P.P..
P.....P...
.3.P.P....

..P..P..P.
..1....P.P
..........
.......2P.
...P....P3
.P...PP..P
.......P.P
..P..P..PP
..P.4P..P.
.......P..

..P...P...
.....P....
PPPP...P..
..P.......
...P......
.......P.1
.P..P....P
P2PP......
.P..P.....
..........

......PP.P
.P1..P.P..
......PP..
P..P....2.
.P.P3.....
....4..P..
.......PP.
..P5......
P.....P...
....PPP..P

Bonne chance et ne laissez pas les cornichons rester trop longtemps, sinon ils se gâteront!


Oh, et pourquoi les cornichons, demandez-vous?

Pourquoi pas?

Poignée de porte
la source
3
Il n'y a aucun moyen raisonnable de montrer que vous avez réellement trouvé la "sortie optimale" car il s'agit essentiellement d'un problème de vendeur ambulant et de NP complet.
Wally
@Wally Hmm, c'est? Peut-être que quelqu'un devrait trouver les étapes minimales pour le cas de test fourni, puis toutes les réponses peuvent être basées sur cela.
Poignée de porte
2
Le cas de test est probablement assez petit pour un minimum de force brute - si quelqu'un voulait le faire. Et / ou tous ceux qui répondent pourraient dire ce qu'ils ont obtenu pour le cas de test et vous pourriez avoir besoin d'autres réponses pour au moins correspondre à ce minimum.
Wally
3
Les robots peuvent-ils se traverser? Sinon, quelles sont les restrictions temporelles sur l'interprétation des chemins?
Peter Taylor
1
@Gareth Le problème avec cela est que les scores ne seront pas connus jusqu'à ce que les tests soient révélés, puis les soumissions après cela verront déjà les tests.
Poignée de porte

Réponses:

6

Rubis, 15 + 26 + 17 + 26 + 17 = 101

Un robot trouve des cornichons!

D'accord, voici une base pour démarrer les gens, en utilisant l'algorithme super-naïf suivant:

  • Chaque tick, chaque robot agira dans l'ordre numérique, en effectuant les étapes suivantes:
    • Identifiez le cornichon le plus proche (distance de Manhattan) qu'aucun autre robot ne cible.
    • Déterminez vers quels carrés adjacents vous pouvez vous déplacer.
    • Choisissez l'un de ces carrés, en préférant les directions qui le rapprochent du cornichon sélectionné.

Voici à quoi cela ressemble pour le cas de test n ° 1:

Exemple animé pour TC1

Évidemment, ce n'est pas très bon mais c'est un début!

Code:

Tile = Struct.new(:world, :tile, :x, :y) do
    def passable?
        tile =~ /\.|P/
    end

    def manhattan_to other
        (self.x - other.x).abs + (self.y - other.y).abs
    end

    def directions_to other
        directions = []
        directions << ?U if self.y > other.y
        directions << ?D if self.y < other.y
        directions << ?L if self.x > other.x
        directions << ?R if self.x < other.x
        directions
    end

    def one_step direction
        nx,ny = case direction
            when ?U then [self.x, self.y - 1]
            when ?D then [self.x, self.y + 1]
            when ?L then [self.x - 1, self.y]
            when ?R then [self.x + 1, self.y]
        end

        self.world[nx,ny]
    end

    def move direction
        destination = one_step(direction)
        raise "can't move there" unless destination && destination.passable?

        destination.tile, self.tile = self.tile, ?.
    end
end

class World
    DIRECTIONS = %w(U D L R)

    def initialize s
        @board = s.split.map.with_index { |l,y| l.chars.map.with_index { |c,x| Tile.new(self, c, x, y) }}
        @width = @board[0].length
    end

    def [] x,y
        y >= 0 && x >= 0 && y < @board.size && x < @board[y].size && @board[y][x]
    end

    def robots
        tiles_of_type(/[0-9]/).sort_by { |t| t.tile }
    end

    def pickles
        tiles_of_type ?P
    end

    def tiles_of_type type
        @board.flatten.select { |t| type === t.tile }
    end

    def inspect
        @board.map { |l| l.map { |t| t.tile }*'' }*?\n
    end
end

gets(nil).split("\n\n").each do |input|
    w = World.new(input)
    steps = Hash[w.robots.map { |r| [r.tile, []] }]
    while (pickles_remaining = w.pickles).size > 0
        current_targets = Hash.new(0)

        w.robots.each do |r|
            target_pickle = pickles_remaining.min_by { |p| [current_targets[p], r.manhattan_to(p)] }

            possible_moves = World::DIRECTIONS.select { |d| t = r.one_step(d); t && t.passable? }
            raise "can't move anywhere" if possible_moves.empty?

            direction = (r.directions_to(target_pickle) & possible_moves).first || possible_moves[0]

            current_targets[target_pickle] += 1
            steps[r.tile] << direction
            r.move(direction)
        end
    end

    puts steps.values.map &:join
    p steps.values.map { |v| v.size }.max
end

Prend l'entrée sur STDIN dans exactement le format du bloc de code de cas de test dans la question d'origine. Voici ce qu'il imprime pour ces cas de test:

DDLLDLLLLULLUUD
LDLRRDDLDLLLLDR
URDDLLLLLULLUUL
15
ULDLDDLDRRRURRURDDDDDDDLLL
UUULDDRDRRRURRURDLDDDDLDLL
ULUURURRDDRRRRUUUDDDDLDLLL
26
URRRDRUDDDDLLLDLL
RUUUDLRRDDDLLLDLL
LDRDDLDDLLLLLLLUU
RUUURDRDDLLLLLUUU
17
DULLUUUUULDLDLLLLLDDRUUUUR
UDLRRRURDDLLLUUUUURDRUUUUD
26
LDDLDUUDDDUDDDDDR
ULUULDDDDDRDRDDDR
LULLDUUDDDRDRDDDR
UUUURDUURRRRDDDDD
LDLLUDDRRRRRRUDRR
17
Paul Prestidge
la source
1

Python, 16 + 15 + 14 + 20 + 12 = 77

Je n'ai pas vraiment d'expérience de problèmes de type vendeur itinérant, mais j'avais un peu de temps à disposition, alors j'ai pensé que je pourrais essayer. Il essaie essentiellement d'attribuer à chaque bot certains cornichons en le parcourant lors d'une course préliminaire où ils se dirigent vers les plus proches d'eux et les plus éloignés des autres. Il force ensuite brutalement le moyen le plus efficace pour chaque bot de collecter ses cornichons alloués.

Je n'ai vraiment aucune idée de la viabilité de cette méthode, mais je soupçonne qu'elle ne fonctionnerait pas bien pour les plus grandes cartes avec moins de bots (la quatrième planche prend déjà parfois plus de deux minutes).

Code:

def parse_input(string):
    pickles = []
    size = len(string) - string.count('\n')
    poses = [None] * (size - string.count('.') - string.count('P'))
    for y,line in enumerate(string.strip().split('\n')):
        for x,char in enumerate(line):
            if char == '.':
                continue
            elif char == 'P':
                pickles.append((x,y))
            else:
                poses[int(char)-1] = (x,y)
    return pickles, poses

def move((px,py),(tx,ty)):
    if (px,py) == (tx,ty):
        return (px,py)
    dx = tx-px
    dy = ty-py
    if abs(dx) <= abs(dy):
        if dy < 0:
            return (px,py-1)
        else:
            return (px,py+1)
    else:
        if dx < 0:
            return (px-1,py)
        else:
            return (px+1,py)

def distance(pos, pickle):
    return abs(pos[0]-pickle[0]) + abs(pos[1]-pickle[1])

def calc_closest(pickles,poses,index):
    distances = [[distance(pos,pickle) for pickle in pickles] for pos in poses]
    dist_diffs = []
    for i, pickle_dists in enumerate(distances):
        dist_diffs.append([])
        for j, dist in enumerate(pickle_dists):
            other = [d[j] for d in distances[:i]+distances[i+1:]]
            dist_diffs[-1].append(min(other)-dist)

    sorted = pickles[:]
    sorted.sort(key = lambda ppos: -dist_diffs[index][pickles.index(ppos)])
    return sorted

def find_best(items,level):
    if level == 0:
        best = (None, None)
        for rv, rest in find_best(items[1:],level+1):
            val = distance(items[0],rest[0]) + rv
            if best[0] == None or val < best[0]:
                best = (val, [items[0]] + rest)
        return best

    if len(items) == 1:
        return [(0,items[:])]

    size = len(items)
    bests = []
    for i in range(size):
        best = (None, None)
        for rv, rest in find_best(items[:i]+items[i+1:],level+1):
            val = distance(items[i],rest[0]) + rv
            if best[0] == None or val < best[0]:
                best = (val, [items[i]] + rest)
        if best[0] != None:
            bests.append(best)
    return bests

def find_best_order(pos,pickles):
    if pickles == []:
        return 0,[]
    best = find_best([pos]+pickles,0)
    return best

def walk_path(pos,path):
    history = ''
    while path:
        npos = move(pos, path[0])
        if npos == path[0]:
            path.remove(path[0])

        if npos[0] < pos[0]:
            history += 'L'
        elif npos[0] > pos[0]:
            history += 'R'
        elif npos[1] < pos[1]:
            history += 'U'
        elif npos[1] > pos[1]:
            history += 'D'
        pos = npos
    return history

def find_paths(input_str):
    pickles, poses = parse_input(input_str)                 ## Parse input string and stuff
    orig_pickles = pickles[:]
    orig_poses = poses[:]
    numbots = len(poses)

    to_collect = [[] for i in range(numbots)]               ## Will make a list of the pickles each bot should go after
    waiting = [True] * numbots
    targets = [None] * numbots
    while pickles:
        while True in waiting:                              ## If any bots are waiting for a new target
            index = waiting.index(True)
            closest = calc_closest(pickles,poses,index)     ## Prioritizes next pickle choice based upon how close they are RELATIVE to other bots
            tar = closest[0]

            n = 0
            while tar in targets[:index]+targets[index+1:]:                 ## Don't target the same pickle!
                other_i = (targets[:index]+targets[index+1:]).index(tar)
                dist_s = distance(poses[index],tar)
                dist_o = distance(poses[other_i],tar)
                if dist_s < dist_o:
                    waiting[other_i] = True
                    break

                n += 1
                if len(closest) <= n:
                    waiting[index] = False
                    break
                tar = closest[n]

            targets[index] = tar
            waiting[index] = False      

        for i in range(numbots):                            ## Move everything toward targets  (this means that later target calculations will not be based on the original position)
            npos = move(poses[i], targets[i])
            if npos != poses[i]:
                poses[i] = npos
            if npos in pickles:
                to_collect[i].append(npos)
                pickles.remove(npos)
                for t, target in enumerate(targets):
                    if target == npos:
                        waiting[t] = True               

    paths = []
    sizes = []

    for i,pickle_group in enumerate(to_collect):                    ## Lastly brute force the most efficient way for each bot to collect its allotted pickles
        size,path = find_best_order(orig_poses[i],pickle_group)
        sizes.append(size)
        paths.append(path)
    return max(sizes), [walk_path(orig_poses[i],paths[i]) for i in range(numbots)]

def collect_pickles(boards):
    ## Collect Pickles!
    total = 0
    for i,board in enumerate(boards):
        result = find_paths(board)
        total += result[0]
        print "Board "+str(i)+": ("+ str(result[0]) +")\n"
        for i,h in enumerate(result[1]):
            print '\tBot'+str(i+1)+': '+h
        print

    print "Total Score: " + str(total)

boards = """
P.......1.
..........
P.....P...
..P.......
....P2....
...P.P....
.PP..P....
....P....P
PPPP....3.
.P..P.P..P

....P.....
P....1....
.P.....PP.
.PP....PP.
.2.P.P....
..P....P..
.P........
.....P.P..
P.....P...
.3.P.P....

..P..P..P.
..1....P.P
..........
.......2P.
...P....P3
.P...PP..P
.......P.P
..P..P..PP
..P.4P..P.
.......P..

..P...P...
.....P....
PPPP...P..
..P.......
...P......
.......P.1
.P..P....P
P2PP......
.P..P.....
..........

......PP.P
.P1..P.P..
......PP..
P..P....2.
.P.P3.....
....4..P..
.......PP.
..P5......
P.....P...
....PPP..P
""".split('\n\n')

collect_pickles(boards)

Production:

Board 0: (16)

    Bot1: DLDLLLLDLLULUU
    Bot2: LDLDLLDDLDRURRDR
    Bot3: URDDLLLULULURU

Board 1: (15)

    Bot1: ULRDRDRRDLDDLUL
    Bot2: DDURURULLUUL
    Bot3: ULRRDRRRURULRR

Board 2: (14)

    Bot1: URRRDDDDDRLLUL
    Bot2: UUURDRDDLD
    Bot3: DDDLDDLUUU
    Bot4: RULLLDUUUL

Board 3: (20)

    Bot1: DLULUUUUULDLLLULDDD
    Bot2: LURDDURRDRUUUULUULLL

Board 4: (12)

    Bot1: LDDLDR
    Bot2: ULUULRRR
    Bot3: LUURURDR
    Bot4: RRRDRDDDR
    Bot5: LLDLRRRDRRRU

Total Score: 77
KSab
la source