A Vectory! - Il Gran Premio di Vector Racing


39

L'utente CarpetPython ha pubblicato una nuova interpretazione di questo problema che pone una maggiore attenzione alle soluzioni euristiche, a causa di un maggiore spazio di ricerca. Personalmente penso che la sfida sia molto più bella della mia, quindi prova a provarla!

La corsa vettoriale è un gioco avvincente che può essere giocato con una penna e un foglio di carta a quadretti. Disegni una pista arbitraria sulla carta, definisci l'inizio e la fine e poi guidi la tua auto a punti in modo a turni. Arriva alla fine il più velocemente possibile, ma fai attenzione a non finire in un muro!

La traccia

  • La mappa è una griglia bidimensionale, in cui ogni cella ha coordinate intere.
  • Ti sposti sulle celle della griglia.
  • Ogni cella della griglia fa parte della traccia o è un muro.
  • Esattamente una cella di traccia è la coordinata iniziale.
  • Almeno una cella di traccia è designata come obiettivo. L'atterraggio su uno di questi completa la gara. Più celle obiettivo non sono necessariamente collegate.

Sterzare la macchina

La tua auto parte da una data coordinata e con il vettore di velocità (0, 0). In ogni turno, puoi regolare ciascun componente della velocità ±1o lasciarlo così com'è. Quindi, il vettore di velocità risultante viene aggiunto alla posizione della tua auto.

Un'immagine può aiutare! Il cerchio rosso era la tua posizione nell'ultima curva. Il cerchio blu è la posizione corrente. La tua velocità è il vettore dal cerchio rosso al cerchio blu. In questo turno, a seconda di come si regola la velocità, è possibile spostarsi su uno dei cerchi verdi.

                                    inserisci qui la descrizione dell'immagine

Se atterri in un muro, perdi immediatamente.

Il tuo compito

Hai indovinato: scrivi un programma che, dato un circuito come input, guida la macchina verso una delle celle obiettivo nel minor numero di curve possibile. La tua soluzione dovrebbe essere in grado di gestire ragionevolmente bene le tracce arbitrarie e non essere specificamente ottimizzata per i casi di test forniti.

Ingresso

Quando viene invocato il tuo programma, leggi da stdin :

target
n m
[ASCII representation of an n x m racetrack]
time

targetè il numero massimo di turni che è possibile eseguire per completare la traccia ed timeè il budget di tempo totale per la traccia, in secondi (non necessariamente intero). Vedi sotto per i dettagli sui tempi.

Per la traccia delimitata da newline, vengono utilizzati i seguenti caratteri:

  • # - muro
  • S- l' inizio
  • *- un obiettivo
  • . - tutte le altre celle del binario (es. Strada)

Tutte le celle esterne alla n x mgriglia fornite sono implicite come pareti.

L'origine delle coordinate si trova nell'angolo in alto a sinistra.

Qui c'è un semplice esempio:

8
4.0
9 6
###...***
###...***
###...***
......###
S.....###
......###

Usando l'indicizzazione basata su 0, la coordinata iniziale sarebbe (0,4).

Dopo ogni mossa riceverai ulteriori input:

x y
u v
time

Dove x, y, u, vsono tutti gli interi 0-based. (x,y)è la tua posizione attuale ed (u,v)è la tua velocità attuale. Nota che x+ue / o y+vpotrebbe essere fuori dai limiti.

timeè tutto ciò che resta del budget temporale, in pochi secondi. Sentiti libero di ignorare questo. Questo è solo per i partecipanti che vogliono davvero portare la loro implementazione al limite di tempo.

Quando il gioco termina (perché sei atterrato in un muro, hai superato i limiti, superato i targetturni, esaurito il tempo o raggiunto l'obiettivo), riceverai una linea vuota.

Produzione

Per ogni turno, scrivi a stdout :

Δu Δv

dove Δue Δvciascuno è uno dei -1, 0, 1. Questo verrà aggiunto (u,v)per determinare la tua nuova posizione. Giusto per chiarire, le indicazioni sono le seguenti

(-1,-1) ( 0,-1) ( 1,-1)
(-1, 0) ( 0, 0) ( 1, 0)
(-1, 1) ( 0, 1) ( 1, 1)

Una soluzione ottimale per l'esempio sopra sarebbe

1 0
1 -1
1 0

Nota che il controller non si collega a stderr , quindi sei libero di usarlo per qualsiasi tipo di output di debug di cui potresti aver bisogno durante lo sviluppo del tuo bot. Tuttavia, rimuovi / commenta qualsiasi output nel codice inviato.

Il bot può impiegare mezzo secondo per rispondere in ogni singolo turno. Per turni che richiedono più tempo, avrai un budget di tempo (per traccia) di target/2secondi. Ogni volta che un turno impiega più di mezzo secondo, il tempo aggiuntivo verrà sottratto dal budget. Quando il tuo budget cronometrato raggiunge lo zero, la gara corrente verrà interrotta.

Novità: per motivi pratici, devo impostare un limite di memoria (poiché la memoria sembra essere più limitante del tempo per dimensioni di traccia ragionevoli). Pertanto, dovrò interrompere qualsiasi test in cui il pilota utilizza più di 1 GB di memoria misurata da Process Explorer come byte privati .

punteggio

C'è un benchmark di 20 tracce. Per ogni traccia:

  • Se completi la traccia, il tuo punteggio è il numero di mosse necessarie per raggiungere una cella obiettivo divisa pertarget .
  • Se esaurisci il tempo / la memoria o non raggiungi l'obiettivo prima che targetsiano trascorsi i turni o atterri in un muro / fuori limite in qualsiasi momento, il tuo punteggio è 2.
  • Se il tuo programma non è deterministico, il tuo punteggio è la media di oltre 10 corse su quella traccia (per favore, specifica questo nella tua risposta).

Il tuo punteggio complessivo è la somma dei punteggi dei singoli brani. Il punteggio più basso vince!

Inoltre, ogni partecipante può (ed è fortemente incoraggiato a) fornire una traccia benchmark aggiuntiva , che verrà aggiunta all'elenco ufficiale. Le risposte precedenti verranno rivalutate includendo questa nuova traccia. Questo per assicurarsi che nessuna soluzione sia adattata troppo da vicino ai casi di test esistenti e per tenere conto di casi limite interessanti che potrei aver perso (ma che potresti individuare).

Rottura del legame

Ora che esiste già una soluzione ottimale, questo sarà probabilmente il fattore principale per i punteggi dei partecipanti.

Se c'è un pareggio (a causa di diverse risposte che risolvono tutte le tracce in modo ottimale o meno), aggiungerò ulteriori casi di test (più grandi) per spezzare il pareggio. Per evitare qualsiasi pregiudizio umano durante la creazione di questi tie-breaker, questi saranno generati in modo fisso:

  • Io aumentare la lunghezza del lato ndal 10rispetto per l'ultimo brano generato in questo modo. (Potrei saltare le taglie se non si rompono la cravatta.)
  • La base è questa grafica vettoriale
  • Questo verrà rasterizzato alla risoluzione desiderata usando questo frammento di Mathematica .
  • L'inizio è nell'angolo in alto a sinistra. In particolare, sarà la cella più a sinistra della riga più in alto di quella fine della traccia.
  • L'obiettivo è nell'angolo in basso a destra. In particolare, sarà la cella più a destra della riga più in basso di quella fine della traccia.
  • La targetvolontà 4*n.

La traccia finale del benchmark iniziale è stata già generata in questo modo, con n = 50.

Il controller

Il programma che verifica i contributi è scritto in Ruby e può essere trovato su GitHub insieme al file di riferimento che userò. C'è anche un bot di esempio chiamato randomracer.rblì, che raccoglie semplicemente mosse casuali. Puoi usare la sua struttura di base come punto di partenza per il tuo bot per vedere come funziona la comunicazione.

Puoi eseguire il tuo bot su un file di traccia a tua scelta come segue:

ruby controller.rb track_file_name command to run your racer

per esempio

ruby controller.rb benchmark.txt ruby randomracer.rb

Il repository contiene anche due classi Point2De Track. Se la tua richiesta è scritta in Ruby, sentiti libero di riutilizzarli per tua comodità.

Opzioni della riga di comando

È possibile aggiungere opzioni della riga di comando -v, -s, -tprima che il nome del file del benchmark. Se si desidera utilizzare più switch, si può anche fare, per esempio, -vs. Questo è ciò che fanno:

-v (dettagliato): utilizzare questo per produrre un po 'più di output di debug dal controller.

-s (silenzioso): se preferisci tenere traccia della tua posizione e della tua velocità e non ti preoccupi del budget di tempo, puoi disattivare quelle tre linee di output ogni turno (inviato alla tua presentazione) con questo flag.

-t(tracce): consente di selezionare singole tracce da testare. Ad esempio -t "1,2,5..8,15"testerebbe solo le tracce 1, 2, 5, 6, 7, 8 e 15. Grazie mille a Ventero per questa funzione e per il parser delle opzioni.

La tua presentazione

In sintesi, includere quanto segue nella risposta:

  • Il tuo punteggio.
  • Se stai usando la casualità, ti preghiamo di indicarlo, in modo che io possa calcolare la media del tuo punteggio su più corse.
  • Il codice per l'invio.
  • La posizione di un compilatore o interprete gratuito per la tua lingua preferita che gira su un computer Windows 8.
  • Istruzioni per la compilazione, se necessario.
  • Una stringa della riga di comando di Windows per eseguire l'invio.
  • Se la tua richiesta richiede la -sbandiera o meno.
  • (facoltativo) Una nuova traccia risolvibile che verrà aggiunta al benchmark. Determinerò targetmanualmente un ragionevole per la tua traccia. Quando la traccia viene aggiunta al benchmark, la modificherò in base alla tua risposta. Mi riservo il diritto di chiederti una traccia diversa (nel caso in cui aggiungi una traccia sproporzionatamente grande, includi un'oscena arte ASCII nella traccia, ecc.). Quando aggiungo il test case al set di benchmark, sostituirò la traccia nella tua risposta con un link alla traccia nel file di benchmark per ridurre il disordine in questo post.

Come puoi vedere, testerò tutti gli invii su un computer Windows 8. Se non c'è assolutamente alcun modo per far funzionare la tua presentazione su Windows, posso anche provare su una macchina virtuale Ubuntu. Questo sarà notevolmente più lento, quindi se vuoi massimizzare il limite di tempo, assicurati che il tuo programma funzioni su Windows.

Possa il miglior pilota emergere vettoriale!

Ma io voglio giocare!

Se vuoi provare tu stesso il gioco per farti un'idea migliore, c'è questa implementazione . Le regole usate lì sono leggermente più sofisticate, ma è abbastanza simile per essere utile, credo.

Classifica

Ultimo aggiornamento: 01/09/2014, 21:29 UTC
Tracce nel benchmark: 25
dimensioni del demolitore: 290, 440

  1. 6.86688 - Kuroi Neko
  2. 8.73108 - user2357112 - 2a presentazione
  3. 9.86627 - nneonneo
  4. 10.66109 - user2357112 - 1a presentazione
  5. 12.49643 - Ray
  6. 40.0759 - pseudonimo117 (probabilistico)

Risultati dei test dettagliati . (I punteggi per gli invii probabilistici sono stati determinati separatamente.)

Risposte:


5

C ++ 11 - 6.66109

Ancora un'altra ampia implementazione di ricerca, solo ottimizzata.

Deve essere eseguito con l' opzione -s .
Il suo input non è affatto sterilizzato, quindi tracce errate potrebbero trasformarlo in una zucca.

L'ho provato con Microsoft Visual C ++ 2013, build di rilascio con il flag predefinito / O2 (ottimizza per la velocità).
Costruisce OK con g ++ e Microsoft IDE.
Il mio allocatore di memoria barebone è un pezzo di merda, quindi non aspettarti che funzioni con altre implementazioni STL di unordered_set!

#include <cstdint>
#include <iostream>
#include <fstream>
#include <sstream>
#include <queue>
#include <unordered_set>

#define MAP_START 'S'
#define MAP_WALL  '#'
#define MAP_GOAL  '*'

#define NODE_CHUNK_SIZE   100 // increasing this will not improve performances
#define VISIT_CHUNK_SIZE 1024 // increasing this will slightly reduce mem consumption at the (slight) cost of speed

#define HASH_POS_BITS 8 // number of bits for one coordinate
#define HASH_SPD_BITS (sizeof(size_t)*8/2-HASH_POS_BITS)

typedef int32_t tCoord; // 32 bits required to overcome the 100.000 cells (insanely) long challenge

// basic vector arithmetics
struct tPoint {
    tCoord x, y;
    tPoint(tCoord x = 0, tCoord y = 0) : x(x), y(y) {}
    tPoint operator+ (const tPoint & p) { return tPoint(x + p.x, y + p.y); }
    tPoint operator- (const tPoint & p) { return tPoint(x - p.x, y - p.y); }
    bool operator== (const tPoint & p) const { return p.x == x && p.y == y;  }
};

// a barebone block allocator. Improves speed by about 30%
template <class T, size_t SIZE> class tAllocator
{
    T * chunk;
    size_t i_alloc;
    size_t m_alloc;
public:
    typedef T                 value_type;
    typedef value_type*       pointer;
    typedef const value_type* const_pointer;
    typedef std::size_t       size_type;
    typedef value_type&       reference;
    typedef const value_type& const_reference;
    tAllocator()                                              { m_alloc = i_alloc = SIZE; }
    template <class U> tAllocator(const tAllocator<U, SIZE>&) { m_alloc = i_alloc = SIZE; }
    template <class U> struct rebind { typedef tAllocator<U, SIZE> other; };
    pointer allocate(size_type n, const_pointer = 0)
    {
        if (n > m_alloc) { i_alloc = m_alloc = n; }      // grow max size if request exceeds capacity
        if ((i_alloc + n) > m_alloc) i_alloc = m_alloc;  // dump current chunk if not enough room available
        if (i_alloc == m_alloc) { chunk = new T[m_alloc]; i_alloc = 0; } // allocate new chunk when needed
        T * mem = &chunk[i_alloc];
        i_alloc += n;
        return mem;
    }
    void deallocate(pointer, size_type) { /* memory is NOT released until process exits */ }
    void construct(pointer p, const value_type& x) { new(p)value_type(x); }
    void destroy(pointer p) { p->~value_type(); }
};

// a node in our search graph
class tNode {
    static tAllocator<tNode, NODE_CHUNK_SIZE> mem; // about 10% speed gain over a basic allocation
    tNode * parent;
public:
    tPoint pos;
    tPoint speed;
    static tNode * alloc (tPoint pos, tPoint speed, tNode * parent) { return new (mem.allocate(1)) tNode(pos, speed, parent); }
    tNode (tPoint pos = tPoint(), tPoint speed = tPoint(), tNode * parent = nullptr) : parent(parent), pos(pos), speed(speed) {}
    bool operator== (const tNode& n) const { return n.pos == pos && n.speed == speed; }
    void output(void)
    {
        std::string output;
        tPoint v = this->speed;
        for (tNode * n = this->parent ; n != nullptr ; n = n->parent)
        {
            tPoint a = v - n->speed;
            v = n->speed;
            std::ostringstream ss;  // a bit of shitty c++ text I/O to print elements in reverse order
            ss << a.x << ' ' << a.y << '\n';
            output = ss.str() + output;
        }
        std::cout << output;
    }
};
tAllocator<tNode, NODE_CHUNK_SIZE> tNode::mem;

// node queueing and storing
static int num_nodes = 0;
class tNodeJanitor {
    // set of already visited nodes. Block allocator improves speed by about 20%
    struct Hasher { size_t operator() (tNode * const n) const 
    {
        int64_t hash = // efficient hashing is the key of performances
            ((int64_t)n->pos.x   << (0 * HASH_POS_BITS))
          ^ ((int64_t)n->pos.y   << (1 * HASH_POS_BITS))
          ^ ((int64_t)n->speed.x << (2 * HASH_POS_BITS + 0 * HASH_SPD_BITS))
          ^ ((int64_t)n->speed.y << (2 * HASH_POS_BITS + 1 * HASH_SPD_BITS));
        return (size_t)((hash >> 32) ^ hash);
        //return (size_t)(hash);
    }
    };
    struct Equalizer { bool operator() (tNode * const n1, tNode * const n2) const
        { return *n1 == *n2; }};
    std::unordered_set<tNode *, Hasher, Equalizer, tAllocator<tNode *, VISIT_CHUNK_SIZE>> visited;
    std::queue<tNode *> queue; // currently explored nodes queue
public:
    bool empty(void) { return queue.empty();  }
    tNode * dequeue() { tNode * n = queue.front(); queue.pop(); return n; }
    tNode * enqueue_if_new (tPoint pos, tPoint speed = tPoint(0,0), tNode * parent = nullptr)
    {
        tNode signature (pos, speed);
        tNode * n = nullptr;
        if (visited.find (&signature) == visited.end()) // the classy way to check if an element is in a set
        {
            n = tNode::alloc(pos, speed, parent);
            queue.push(n);
            visited.insert (n);
num_nodes++;
        }
        return n;
    }
};

// map representation
class tMap {
    std::vector<char> cell;
    tPoint dim; // dimensions
public:
    void set_size(tCoord x, tCoord y) { dim = tPoint(x, y); cell.resize(x*y); }
    void set(tCoord x, tCoord y, char c) { cell[y*dim.x + x] = c; }
    char get(tPoint pos)
    {
        if (pos.x < 0 || pos.x >= dim.x || pos.y < 0 || pos.y >= dim.y) return MAP_WALL;
        return cell[pos.y*dim.x + pos.x];
    }
    void dump(void)
    {
        for (int y = 0; y != dim.y; y++)
        {
            for (int x = 0; x != dim.x; x++) fprintf(stderr, "%c", cell[y*dim.x + x]);
            fprintf(stderr, "\n");
        }
    }
};

// race manager
class tRace {
    tPoint start;
    tNodeJanitor border;
    static tPoint acceleration[9];
public:
    tMap map;
    tRace ()
    {
        int target;
        tCoord sx, sy;
        std::cin >> target >> sx >> sy;
        std::cin.ignore();
        map.set_size (sx, sy);
        std::string row;
        for (int y = 0; y != sy; y++)
        {
            std::getline(std::cin, row);
            for (int x = 0; x != sx; x++)
            {
                char c = row[x];
                if (c == MAP_START) start = tPoint(x, y);
                map.set(x, y, c);
            }
        }
    }

    // all the C++ crap above makes for a nice and compact solver
    tNode * solve(void)
    {
        tNode * initial = border.enqueue_if_new (start);
        while (!border.empty())
        {
            tNode * node = border.dequeue();
            tPoint p = node->pos;
            tPoint v = node->speed;
            for (tPoint a : acceleration)
            {
                tPoint nv = v + a;
                tPoint np = p + nv;
                char c = map.get(np);
                if (c == MAP_WALL) continue;
                if (c == MAP_GOAL) return new tNode (np, nv, node);
                border.enqueue_if_new (np, nv, node);
            }
        }
        return initial; // no solution found, will output nothing
    }
};
tPoint tRace::acceleration[] = {
    tPoint(-1,-1), tPoint(-1, 0), tPoint(-1, 1),
    tPoint( 0,-1), tPoint( 0, 0), tPoint( 0, 1),
    tPoint( 1,-1), tPoint( 1, 0), tPoint( 1, 1)};

#include <ctime>
int main(void)
{
    tRace race;
    clock_t start = clock();
    tNode * solution = race.solve();
    std::cerr << "time: " << (clock()-start)/(CLOCKS_PER_SEC/1000) << "ms nodes: " << num_nodes << std::endl;
    solution->output();
    return 0;
}

risultati

 No.       Size     Target   Score     Details
-------------------------------------------------------------------------------------
  1       37 x 1        36   0.22222   Racer reached goal at ( 36, 0) in 8 turns.
  2       38 x 1        37   0.24324   Racer reached goal at ( 37, 0) in 9 turns.
  3       33 x 1        32   0.25000   Racer reached goal at ( 32, 0) in 8 turns.
  4       10 x 10       10   0.40000   Racer reached goal at ( 7, 7) in 4 turns.
  5        9 x 6         8   0.37500   Racer reached goal at ( 6, 0) in 3 turns.
  6       15 x 7        16   0.37500   Racer reached goal at ( 12, 4) in 6 turns.
  7       17 x 8        16   0.31250   Racer reached goal at ( 14, 0) in 5 turns.
  8       19 x 13       18   0.27778   Racer reached goal at ( 0, 11) in 5 turns.
  9       60 x 10      107   0.14953   Racer reached goal at ( 0, 6) in 16 turns.
 10       31 x 31      106   0.23585   Racer reached goal at ( 27, 0) in 25 turns.
 11       31 x 31      106   0.24528   Racer reached goal at ( 15, 15) in 26 turns.
 12       50 x 20       50   0.24000   Racer reached goal at ( 49, 10) in 12 turns.
 13      100 x 100    2600   0.01385   Racer reached goal at ( 50, 0) in 36 turns.
 14       79 x 63      242   0.24380   Racer reached goal at ( 3, 42) in 59 turns.
 15       26 x 1        25   0.32000   Racer reached goal at ( 25, 0) in 8 turns.
 16       17 x 1        19   0.52632   Racer reached goal at ( 16, 0) in 10 turns.
 17       50 x 1        55   0.34545   Racer reached goal at ( 23, 0) in 19 turns.
 18       10 x 7        23   0.34783   Racer reached goal at ( 1, 3) in 8 turns.
 19       55 x 55       45   0.17778   Racer reached goal at ( 50, 26) in 8 turns.
 20      101 x 100     100   0.14000   Racer reached goal at ( 99, 99) in 14 turns.
 21   100000 x 1         1   1.00000   Racer reached goal at ( 0, 0) in 1 turns.
 22       50 x 50      200   0.05500   Racer reached goal at ( 47, 46) in 11 turns.
 23      290 x 290    1160   0.16466   Racer reached goal at ( 269, 265) in 191 turns.
-------------------------------------------------------------------------------------
TOTAL SCORE:                 6.66109

Prestazioni

Quel pessimo linguaggio C ++ ha un talento per farti saltare attraverso i cerchi solo per muovere un fiammifero. Tuttavia, puoi utilizzarlo per produrre codice relativamente veloce ed efficiente in termini di memoria.

hashing

Qui la chiave è fornire una buona tabella hash per i nodi. Questo è di gran lunga il fattore dominante per la velocità di esecuzione.
Due implementazioni di unordered_set(GNU e Microsoft) hanno prodotto una differenza di velocità di esecuzione del 30% (a favore di GNU, yay!).

La differenza non è davvero sorprendente, con i camion carichi di codice nascosti dietro unordered_set.

Per curiosità, ho fatto alcune statistiche sullo stato finale della tabella hash.
Entrambi gli algoritmi finiscono con quasi lo stesso rapporto bucket / elementi, ma la ripartizione varia:
per il tie breaker 290x290, GNU ottiene una media di 1,5 elementi per bucket non vuoto, mentre Microsoft è a 5,8 (!).

Sembra che la mia funzione di hashing non sia molto ben randomizzata da Microsoft ... Mi chiedo se i ragazzi di Redmond abbiano davvero confrontato il loro STL, o forse il mio caso d'uso favorisce solo l'implementazione GNU ...

Certo, la mia funzione di hashing non è affatto ottimale. Avrei potuto usare il solito mix di numeri interi basato su shift / muls multipli, ma una funzione hash-efficiente richiede tempo per il calcolo.

Sembra che il numero di query della tabella hash sia molto elevato rispetto al numero di inserimenti. Ad esempio, nel tie breaker 290x290, sono presenti circa 3,6 milioni di inserimenti per 22,7 milioni di query.
In questo contesto, un hashing non ottimale ma veloce produce prestazioni migliori.

Allocazione della memoria

Fornire un efficiente allocatore di memoria è il secondo. Ha migliorato le prestazioni di circa il 30%. Se vale la pena aggiungere il codice merda aggiunto è discutibile :).

La versione corrente utilizza tra 40 e 55 byte per nodo.
I dati funzionali richiedono 24 byte per un nodo (4 coordinate e 2 puntatori).
A causa del folle test di 100.000 linee, le coordinate devono essere memorizzate in parole di 4 byte, altrimenti si potrebbero guadagnare 8 byte usando i cortocircuiti (con un valore massimo delle coordinate di 32767). I byte rimanenti sono per lo più consumati dalla tabella hash dell'insieme non ordinato. Significa che la gestione dei dati consuma effettivamente un po 'di più rispetto al payload "utile".

E il vincitore è...

Sul mio PC con Win7, il tie breaker (caso 23, 290x290) è risolto dalla versione peggiore (ovvero compilata da Microsoft) in circa 2,2 secondi, con un consumo di memoria di circa 185 Mb.
Per fare un confronto, l'attuale leader (codice Python di user2357112) impiega poco più di 30 secondi e consuma circa 780 Mb.

Problemi del controller

Non sono del tutto sicuro di poter codificare in Ruby per salvarmi la vita.
Tuttavia, ho individuato e modificato due problemi dal codice del controller:

1) lettura della mappa in track.rb

Con ruby ​​1.9.3 installato, il lettore di tracce graccherebbe per shift.to_inon essere disponibile per string.lines.
Dopo un lungo giro nella documentazione di Ruby online ho rinunciato alle stringhe e ho usato invece un array intermedio, in questo modo (proprio all'inizio del file):

def initialize(string)
    @track = Array.new;
    string.lines.each do |line|
        @track.push (line.chomp)
    end

2) uccidere i fantasmi controller.rb

Come altri poster hanno già notato, il controller a volte cerca di uccidere i processi che sono già usciti. Per evitare questi output di errori vergognosi, ho semplicemente gestito l'eccezione, in questo modo (attorno alla riga 134):

if silent
    begin # ;!;
        Process.kill('KILL', racer.pid)
    rescue Exception => e
    end

Caso di prova

Per sconfiggere l'approccio della forza bruta dei solutori di BFS, la traccia peggiore è l'opposto della mappa di 100.000 celle: un'area completamente libera con l'obiettivo il più lontano possibile dall'inizio.

In questo esempio, una mappa 100x400 con l'obiettivo nell'angolo in alto a sinistra e l'inizio in basso a destra.

Questa mappa ha una soluzione in 28 turni, ma un solutore BFS esplorerà milioni di stati per trovarla (la mia ha contato 10.022.658 stati visitati, ha impiegato circa 12 secondi e ha raggiunto il picco a 600 Mb!).

Con meno della metà della superficie del demolitore 290x290, richiede circa 3 volte più visite ai nodi. D'altra parte, un solutore euristico / basato su A * dovrebbe sconfiggerlo facilmente.

30
100 400
*...................................................................................................
....................................................................................................
                          < 400 lines in all >
....................................................................................................
....................................................................................................
...................................................................................................S

Bonus: una versione equivalente (ma un po 'meno efficiente) di PHP

Questo è ciò con cui ho iniziato, prima che la lentezza del linguaggio intrinseco mi convincesse a usare il C ++.
Le tabelle hash interne di PHP non sembrano efficienti come quelle di Python, almeno in questo caso particolare :).

<?php

class Trace {
    static $file;
    public static $state_member;
    public static $state_target;
    static function msg ($msg)
    {
        fputs (self::$file, "$msg\n");
    }

    static function dump ($var, $msg=null)
    {
        ob_start();
        if ($msg) echo "$msg ";
        var_dump($var);
        $dump=ob_get_contents();
        ob_end_clean();
        fputs (self::$file, "$dump\n");
    }

    function init ($fname)
    {
        self::$file = fopen ($fname, "w");
    }
}
Trace::init ("racer.txt");

class Point {
    public $x;
    public $y;

    function __construct ($x=0, $y=0)
    {
        $this->x = (float)$x;
        $this->y = (float)$y;
    }

    function __toString ()
    {
        return "[$this->x $this->y]";
    }

    function add ($v)
    {
        return new Point ($this->x + $v->x, $this->y + $v->y);
    }

    function vector_to ($goal)
    {
        return new Point ($goal->x - $this->x, $goal->y - $this->y);
    }
}

class Node {
    public $posx  , $posy  ;
    public $speedx, $speedy;
    private $parent;

    public function __construct ($posx, $posy, $speedx, $speedy, $parent)
    {
        $this->posx = $posx;
        $this->posy = $posy;
        $this->speedx = $speedx;
        $this->speedy = $speedy;
        $this->parent = $parent;
    }

    public function path ()
    {
        $res = array();
        $v = new Point ($this->speedx, $this->speedy);
        for ($node = $this->parent ; $node != null ; $node = $node->parent)
        {
            $nv = new Point ($node->speedx, $node->speedy);
            $a = $nv->vector_to ($v);
            $v = new Point ($node->speedx, $node->speedy);
            array_unshift ($res, $a);
        }
        return $res;
    }
}

class Map {

    private static $target;       // maximal number of turns
    private static $time;         // time available to solve
    private static $sx, $sy;      // map dimensions
    private static $cell;         // cells of the map
    private static $start;        // starting point
    private static $acceleration; // possible acceleration values

    public static function init ()
    {
        // read map definition
        self::$target = trim(fgets(STDIN));
        list (self::$sx, self::$sy) = explode (" ", trim(fgets(STDIN)));
        self::$cell = array();
        for ($y = 0 ; $y != self::$sy ; $y++) self::$cell[] = str_split (trim(fgets(STDIN)));
        self::$time = trim(fgets(STDIN));

        // get starting point
        foreach (self::$cell as $y=>$row)
        {
            $x = array_search ("S", $row);
            if ($x !== false)
            {
                self::$start = new Point ($x, $y);
Trace::msg ("start ".self::$start);
                break;
            }
        }

        // compute possible acceleration values
        self::$acceleration = array();
        for ($x = -1 ; $x <= 1 ; $x++)
        for ($y = -1 ; $y <= 1 ; $y++)
        {
            self::$acceleration[] = new Point ($x, $y);
        }
    }

    public static function solve ()
    {
        $now = microtime(true);
        $res = array();
        $border = array (new Node (self::$start->x, self::$start->y, 0, 0, null));
        $present = array (self::$start->x." ".self::$start->y." 0 0" => 1);
        while (count ($border))
        {
if ((microtime(true) - $now) > 1)
{
Trace::msg (count($present)." nodes, ".round(memory_get_usage(true)/1024)."K");
$now = microtime(true);
}
            $node = array_shift ($border);
//Trace::msg ("node $node->pos $node->speed");
            $px = $node->posx;
            $py = $node->posy;
            $vx = $node->speedx;
            $vy = $node->speedy;
            foreach (self::$acceleration as $a)
            {
                $nvx = $vx + $a->x;
                $nvy = $vy + $a->y;
                $npx = $px + $nvx;
                $npy = $py + $nvy;
                if ($npx < 0 || $npx >= self::$sx || $npy < 0 || $npy >= self::$sy || self::$cell[$npy][$npx] == "#")
                {
//Trace::msg ("invalid position $px,$py $vx,$vy -> $npx,$npy");
                    continue;
                }
                if (self::$cell[$npy][$npx] == "*")
                {
Trace::msg ("winning position $px,$py $vx,$vy -> $npx,$npy");
                    $end = new Node ($npx, $npy, $nvx, $nvy, $node);
                    $res = $end->path ();
                    break 2;
                }
//Trace::msg ("checking $np $nv");
                $signature = "$npx $npy $nvx $nvy";
                if (isset ($present[$signature])) continue;
//Trace::msg ("*** adding $np $nv");
                $border[] = new Node ($npx, $npy, $nvx, $nvy, $node);
                $present[$signature] = 1;
            }
        }
        return $res;
    }
}

ini_set("memory_limit","1000M");
Map::init ();
$res = Map::solve();
//Trace::dump ($res);
foreach ($res as $a) echo "$a->x $a->y\n";
?>

erf ... Il mio allocatore barebone è solo un po 'troppo barebone. Aggiungerò la merda necessaria per farlo funzionare con g ++, quindi. Mi dispiace per quello.

OK, è stato risolto. La versione g ++ funziona addirittura del 30% più velocemente. Ora fornisce alcune statistiche su stderr. Sentiti libero di commentarlo (dalle ultime righe della fonte). Scusami ancora per l'errore.

Bene, ora funziona e ho riprodotto la tua partitura. È dannatamente veloce! :) Aggiungerò il tuo test case al benchmark, ma cambierò l'obiettivo in 400, in quanto è in linea con il modo in cui ho determinato tutti gli altri target (tranne il tie breaker). Aggiornerò il post principale dopo aver eseguito nuovamente tutte le altre comunicazioni.
Martin Ender,

Aggiornato i risultati. Non è stato necessario un pareggio, poiché tutti gli altri invii superano il limite di memoria sulla pista di prova. Complimenti per quello! :)
Martin Ender,

Grazie. In realtà questa sfida mi ha dato l'occasione di scavare in queste tabelle hash STL. Anche se odio il coraggio del C ++, non posso fare a meno di essere ucciso dalla mia curiosità. Miao! :).

10

C ++, 5.4 (deterministico, ottimale)

Soluzione di programmazione dinamica. Sicuramente ottimale. Molto veloce: risolve tutti i 20 test in 0,2 secondi. Dovrebbe essere particolarmente veloce su macchine a 64 bit. Suppone che il tabellone sia inferiore a 32.000 posti in ogni direzione (che dovrebbe essere vero).

Questo corridore è un po 'insolito. Calcola il percorso ottimale sulla linea di partenza, quindi esegue immediatamente il percorso calcolato. Ignora il controllo del tempo e presume che possa completare la fase di ottimizzazione in tempo (che dovrebbe essere vero per qualsiasi hardware ragionevolmente moderno). Su mappe troppo grandi, c'è una piccola possibilità che il pilota possa segfault. Se riesci a convincerlo a segfault, ottieni un punto brownie e lo riparerò per usare un ciclo esplicito.

Compila con g++ -O3. Potrebbe richiedere C ++ 11 (per <unordered_map>). Per eseguire, basta eseguire l'eseguibile compilato (non sono supportati flag o opzioni; tutto l'input è preso su stdin).

#include <unordered_map>
#include <iostream>
#include <string>
#include <vector>
#include <sstream>

#include <cstdint>

#define MOVES_INF (1<<30)

union state {
    struct {
        short px, py, vx, vy;
    };
    uint64_t val;
};

struct result {
    int nmoves;
    short dvx, dvy;
};

typedef std::unordered_map<uint64_t, result> cache_t;
int target, n, m;
std::vector<std::string> track;
cache_t cache;

static int solve(uint64_t val) {
    cache_t::iterator it = cache.find(val);
    if(it != cache.end())
        return it->second.nmoves;

    // prevent recursion
    result res;
    res.nmoves = MOVES_INF;
    cache[val] = res;

    state cur;
    cur.val = val;
    for(int dvx = -1; dvx <= 1; dvx++) for(int dvy = -1; dvy <= 1; dvy++) {
        state next;
        next.vx = cur.vx + dvx;
        next.vy = cur.vy + dvy;
        next.px = cur.px + next.vx;
        next.py = cur.py + next.vy;
        if(next.px < 0 || next.px >= n || next.py < 0 || next.py >= m)
            continue;
        char c = track[next.py][next.px];
        if(c == '*') {
            res.nmoves = 1;
            res.dvx = dvx;
            res.dvy = dvy;
            break;
        } else if(c == '#') {
            continue;
        } else {
            int score = solve(next.val) + 1;
            if(score < res.nmoves) {
                res.nmoves = score;
                res.dvx = dvx;
                res.dvy = dvy;
            }
        }
    }

    cache[val] = res;
    return res.nmoves;
}

bool solve_one() {
    std::string line;
    float time;

    std::cin >> target;
    // std::cin >> time; // uncomment to use "time" control
    std::cin >> n >> m;
    if(!std::cin)
        return false;
    std::cin.ignore(); // skip newline at end of "n m" line

    track.clear();
    track.reserve(m);

    for(int i=0; i<m; i++) {
        std::getline(std::cin, line);
        track.push_back(line);
    }

    cache.clear();

    state cur;
    cur.vx = cur.vy = 0;
    for(int y=0; y<m; y++) for(int x=0; x<n; x++) {
        if(track[y][x] == 'S') {
            cur.px = x;
            cur.py = y;
            break;
        }
    }

    solve(cur.val);

    int sol_len = 0;
    while(track[cur.py][cur.px] != '*') {
        cache_t::iterator it = cache.find(cur.val);
        if(it == cache.end() || it->second.nmoves >= MOVES_INF) {
            std::cerr << "Failed to solve at p=" << cur.px << "," << cur.py << " v=" << cur.vx << "," << cur.vy << std::endl;
            return true;
        }

        int dvx = it->second.dvx;
        int dvy = it->second.dvy;
        cur.vx += dvx;
        cur.vy += dvy;
        cur.px += cur.vx;
        cur.py += cur.vy;
        std::cout << dvx << " " << dvy << std::endl;
        sol_len++;
    }

    //std::cerr << "Score: " << ((float)sol_len) / target << std::endl;

    return true;
}

int main() {
    /* benchmarking: */
    //while(solve_one())
    //    ;

    /* regular running */
    solve_one();
    std::string line;
    while(std::cin) std::getline(std::cin, line);

    return 0;
}

risultati

 No.    Size     Target   Score     Details
-------------------------------------------------------------------------------------
  1    37 x 1        36   0.22222   Racer reached goal at ( 36, 0) in 8 turns.
  2    38 x 1        37   0.24324   Racer reached goal at ( 37, 0) in 9 turns.
  3    33 x 1        32   0.25000   Racer reached goal at ( 32, 0) in 8 turns.
  4    10 x 10       10   0.40000   Racer reached goal at ( 7, 7) in 4 turns.
  5     9 x 6         8   0.37500   Racer reached goal at ( 6, 0) in 3 turns.
  6    15 x 7        16   0.37500   Racer reached goal at ( 12, 4) in 6 turns.
  7    17 x 8        16   0.31250   Racer reached goal at ( 15, 0) in 5 turns.
  8    19 x 13       18   0.27778   Racer reached goal at ( 1, 11) in 5 turns.
  9    60 x 10      107   0.14953   Racer reached goal at ( 2, 6) in 16 turns.
 10    31 x 31      106   0.25472   Racer reached goal at ( 28, 0) in 27 turns.
 11    31 x 31      106   0.24528   Racer reached goal at ( 15, 15) in 26 turns.
 12    50 x 20       50   0.24000   Racer reached goal at ( 49, 10) in 12 turns.
 13   100 x 100    2600   0.01385   Racer reached goal at ( 50, 0) in 36 turns.
 14    79 x 63      242   0.26860   Racer reached goal at ( 3, 42) in 65 turns.
 15    26 x 1        25   0.32000   Racer reached goal at ( 25, 0) in 8 turns.
 16    17 x 1        19   0.52632   Racer reached goal at ( 16, 0) in 10 turns.
 17    50 x 1        55   0.34545   Racer reached goal at ( 23, 0) in 19 turns.
 18    10 x 7        23   0.34783   Racer reached goal at ( 1, 3) in 8 turns.
 19    55 x 55       45   0.17778   Racer reached goal at ( 52, 26) in 8 turns.
 20    50 x 50      200   0.05500   Racer reached goal at ( 47, 46) in 11 turns.
-------------------------------------------------------------------------------------
TOTAL SCORE:              5.40009

Nuovo Testcase


1
Beh, qualcosa del genere era praticamente previsto. Non c'è abbastanza stato nel puzzle per rendere impossibile la programmazione dinamica. Se entro, dovrò presentare una mappa che richiede strategie di ricerca più sofisticate da risolvere.
user2357112 supporta Monica il

Come si comporta il tuo pilota sul tuo test case?
user2357112 supporta Monica il

0,14 (14 mosse)
nneonneo,

Quel tempo è impiegato o si muove / bersaglio? Se si tratta di mosse / bersaglio, come si comporta in termini di tempo?
user2357112 supporta Monica il

1
Penso di aver trovato un bug nel codice di prevenzione del ciclo. Si assume che per ogni stato i tratti di ricerca da uno stato S, un percorso ottimale non può essere tornare a S. Potrebbe sembrare che se un percorso ottimale non ritorno a S, allora lo stato non può essere su un percorso ottimale (poiché abbiamo potuto basta rimuovere il ciclo in cui è attivo e ottenere un percorso più breve), quindi non ci importa se otteniamo un risultato troppo elevato per quello stato. Tuttavia, se un percorso ottimale passa attraverso gli stati A e B in quell'ordine, ma la ricerca trova prima A mentre B è ancora in pila, i risultati di A vengono avvelenati dalla prevenzione del loop.
user2357112 supporta Monica il

6

Python 2 , deterministico, ottimale

Ecco il mio pilota. Non l'ho testato sul benchmark (ancora preoccupandomi della versione e dell'installer di Ruby da installare), ma dovrebbe risolvere tutto in modo ottimale e entro il limite di tempo. Il comando per eseguirlo è python whateveryoucallthefile.py. Ha bisogno della -sbandiera del controller.

# Breadth-first search.
# Future directions: bidirectional search and/or A*.

import operator
import time

acceleration_options = [(dvx, dvy) for dvx in [-1, 0, 1] for dvy in [-1, 0, 1]]

class ImpossibleRaceError(Exception): pass

def read_input(line_source=raw_input):
    # We don't use the target.
    target = int(line_source())

    width, height = map(int, line_source().split())
    input_grid = [line_source() for _ in xrange(height)]

    start = None
    for i in xrange(height):
        for j in xrange(width):
            if input_grid[i][j] == 'S':
                start = i, j
                break
        if start is not None:
            break

    walls = [[cell == '#' for cell in row] for row in input_grid]
    goals = [[cell == '*' for cell in row] for row in input_grid]

    return start, walls, goals

def default_bfs_stop_threshold(walls, goals):
    num_not_wall = sum(sum(map(operator.not_, row)) for row in walls)
    num_goals = sum(sum(row) for row in goals)
    return num_goals * num_not_wall

def bfs(start, walls, goals, stop_threshold=None):
    if stop_threshold is None:
        stop_threshold = default_bfs_stop_threshold(walls, goals)

    # State representation is (x, y, vx, vy)
    x, y = start
    initial_state = (x, y, 0, 0)
    frontier = {initial_state}
    # Visited set is tracked by a map from each state to the last move taken
    # before reaching that state.
    visited = {initial_state: None}

    while len(frontier) < stop_threshold:
        if not frontier:
            raise ImpossibleRaceError

        new_frontier = set()
        for x, y, vx, vy in frontier:
            for dvx, dvy in acceleration_options:
                new_vx, new_vy = vx+dvx, vy+dvy
                new_x, new_y = x+new_vx, y+new_vy
                new_state = (new_x, new_y, new_vx, new_vy)

                if not (0 <= new_x < len(walls) and 0 <= new_y < len(walls[0])):
                    continue
                if walls[new_x][new_y]:
                    continue
                if new_state in visited:
                    continue

                new_frontier.add(new_state)
                visited[new_state] = dvx, dvy

                if goals[new_x][new_y]:
                    return construct_path_from_bfs(new_state, visited)
        frontier = new_frontier

def construct_path_from_bfs(goal_state, best_moves):
    reversed_path = []
    current_state = goal_state
    while best_moves[current_state] is not None:
        move = best_moves[current_state]
        reversed_path.append(move)

        x, y, vx, vy = current_state
        dvx, dvy = move
        old_x, old_y = x-vx, y-vy # not old_vx or old_vy
        old_vx, old_vy = vx-dvx, vy-dvy
        current_state = (old_x, old_y, old_vx, old_vy)
    return reversed_path[::-1]

def main():
    t = time.time()

    start, walls, goals = read_input()
    path = bfs(start, walls, goals, float('inf'))
    for dvx, dvy in path:
        # I wrote the whole program with x pointing down and y pointing right.
        # Whoops. Gotta flip things for the output.
        print dvy, dvx

if __name__ == '__main__':
    main()

Dopo aver ispezionato il corridore di nneonneo (ma in realtà non testarlo, dal momento che non ho un compilatore C ++), ho scoperto che sembra eseguire una ricerca quasi esauriente dello spazio dello stato, non importa quanto vicino l'obiettivo o quanto breve è già stato trovato un percorso. Ho anche scoperto che le regole del tempo significano che la costruzione di una mappa con una soluzione lunga e complessa richiede un limite di tempo lungo e noioso. Pertanto, la mia presentazione della mappa è piuttosto semplice:

Nuovo Testcase

(GitHub non può visualizzare la linea lunga. La traccia è *S.......[and so on].....)


Presentazione aggiuntiva: Python 2, ricerca bidirezionale

Questo è un approccio che ho scritto circa due mesi fa, quando ho cercato di ottimizzare la mia prima presentazione. Per i casi di test che esistevano al momento, non offriva un miglioramento, quindi non l'ho presentato, ma per la nuova mappa di Kuroi, sembra appena spremere sotto il limite di memoria. Mi aspetto ancora che il risolutore di Kuroi possa batterlo, ma sono interessato a come regge.

# Bidirectional search.
# Future directions: A*.

import operator
import time

acceleration_options = [(dvx, dvy) for dvx in [-1, 0, 1] for dvy in [-1, 0, 1]]

class ImpossibleRaceError(Exception): pass

def read_input(line_source=raw_input):
    # We don't use the target.
    target = int(line_source())

    width, height = map(int, line_source().split())
    input_grid = [line_source() for _ in xrange(height)]

    start = None
    for i in xrange(height):
        for j in xrange(width):
            if input_grid[i][j] == 'S':
                start = i, j
                break
        if start is not None:
            break

    walls = [[cell == '#' for cell in row] for row in input_grid]
    goals = [[cell == '*' for cell in row] for row in input_grid]

    return start, walls, goals

def bfs_to_bidi_threshold(walls, goals):
    num_not_wall = sum(sum(map(operator.not_, row)) for row in walls)
    num_goals = sum(sum(row) for row in goals)
    return num_goals * (num_not_wall - num_goals)

class GridBasedGoalContainer(object):
    '''Supports testing whether a state is a goal state with `in`.

    Does not perform bounds checking.'''
    def __init__(self, goal_grid):
        self.goal_grid = goal_grid
    def __contains__(self, state):
        x, y, vx, vy = state
        return self.goal_grid[x][y]

def forward_step(state, acceleration):
    x, y, vx, vy = state
    dvx, dvy = acceleration

    new_vx, new_vy = vx+dvx, vy+dvy
    new_x, new_y = x+new_vx, y+new_vy

    return (new_x, new_y, new_vx, new_vy)

def backward_step(state, acceleration):
    x, y, vx, vy = state
    dvx, dvy = acceleration

    old_x, old_y = x-vx, y-vy
    old_vx, old_vy = vx-dvx, vy-dvy

    return (old_x, old_y, old_vx, old_vy)

def bfs(start, walls, goals):
    x, y = start
    initial_state = (x, y, 0, 0)
    initial_frontier = {initial_state}
    visited = {initial_state: None}

    goal_state, frontier, visited = general_bfs(
        frontier=initial_frontier,
        visited=visited,
        walls=walls,
        goalcontainer=GridBasedGoalContainer(goals),
        stop_threshold=float('inf'),
        step_function=forward_step
    )

    return construct_path_from_bfs(goal_state, visited)

def general_bfs(
        frontier,
        visited,
        walls,
        goalcontainer,
        stop_threshold,
        step_function):

    while len(frontier) <= stop_threshold:
        if not frontier:
            raise ImpossibleRaceError

        new_frontier = set()
        for state in frontier:
            for accel in acceleration_options:
                new_state = new_x, new_y, new_vx, new_vy = \
                        step_function(state, accel)

                if not (0 <= new_x < len(walls) and 0 <= new_y < len(walls[0])):
                    continue
                if walls[new_x][new_y]:
                    continue
                if new_state in visited:
                    continue

                new_frontier.add(new_state)
                visited[new_state] = accel

                if new_state in goalcontainer:
                    return new_state, frontier, visited
        frontier = new_frontier
    return None, frontier, visited

def max_velocity_component(n):
    # It takes a distance of at least 0.5*v*(v+1) to achieve a velocity of
    # v in the x or y direction. That means the map has to be at least
    # 1 + 0.5*v*(v+1) rows or columns long to accomodate such a velocity.
    # Solving for v, we get a velocity cap as follows.
    return int((2*n-1.75)**0.5 - 0.5)

def solver(
        start,
        walls,
        goals,
        mode='bidi'):

    x, y = start
    initial_state = (x, y, 0, 0)
    initial_frontier = {initial_state}
    visited = {initial_state: None}
    if mode == 'bidi':
        stop_threshold = bfs_to_bidi_threshold(walls, goals)
    elif mode == 'bfs':
        stop_threshold = float('inf')
    else:
        raise ValueError('Unsupported search mode: {}'.format(mode))

    goal_state, frontier, visited = general_bfs(
        frontier=initial_frontier,
        visited=visited,
        walls=walls,
        goalcontainer=GridBasedGoalContainer(goals),
        stop_threshold=stop_threshold,
        step_function=forward_step
    )

    if goal_state is not None:
        return construct_path_from_bfs(goal_state, visited)

    # Switching to bidirectional search.

    not_walls_or_goals = []
    goal_list = []
    for x in xrange(len(walls)):
        for y in xrange(len(walls[0])):
            if not walls[x][y] and not goals[x][y]:
                not_walls_or_goals.append((x, y))
            if goals[x][y]:
                goal_list.append((x, y))
    max_vx = max_velocity_component(len(walls))
    max_vy = max_velocity_component(len(walls[0]))
    reverse_visited = {(goal_x, goal_y, goal_x-prev_x, goal_y-prev_y): None
                        for goal_x, goal_y in goal_list
                        for prev_x, prev_y in not_walls_or_goals
                        if abs(goal_x-prev_x) <= max_vx
                        and abs(goal_y - prev_y) <= max_vy}
    reverse_frontier = set(reverse_visited)
    while goal_state is None:
        goal_state, reverse_frontier, reverse_visited = general_bfs(
            frontier=reverse_frontier,
            visited=reverse_visited,
            walls=walls,
            goalcontainer=frontier,
            stop_threshold=len(frontier),
            step_function=backward_step
        )
        if goal_state is not None:
            break
        goal_state, frontier, visited = general_bfs(
            frontier=frontier,
            visited=visited,
            walls=walls,
            goalcontainer=reverse_frontier,
            stop_threshold=len(reverse_frontier),
            step_function=forward_step
        )
    forward_path = construct_path_from_bfs(goal_state, visited)
    backward_path = construct_path_from_bfs(goal_state,
                                            reverse_visited,
                                            step_function=forward_step)
    return forward_path + backward_path[::-1]

def construct_path_from_bfs(goal_state,
                            best_moves,
                            step_function=backward_step):
    reversed_path = []
    current_state = goal_state
    while best_moves[current_state] is not None:
        move = best_moves[current_state]
        reversed_path.append(move)
        current_state = step_function(current_state, move)
    return reversed_path[::-1]

def main():
    start, walls, goals = read_input()
    t = time.time()
    path = solver(start, walls, goals)
    for dvx, dvy in path:
        # I wrote the whole program with x pointing down and y pointing right.
        # Whoops. Gotta flip things for the output.
        print dvy, dvx

if __name__ == '__main__':
    main()

Questo a volte fallisce nei casi 12 e 13. Non so perché, poiché i messaggi di errore sono in qualche modo ... ostili
Ray

@Ray ricevo anche messaggi di errore, ma ottengo sempre comunque il risultato per quelli. Penso che potrebbe piuttosto essere qualcosa nel mio controller, perché sembra che il controller cerchi di uccidere il processo racer anche se è già finito.
Martin Ender,

@ m.buettner Ho trovato il motivo, aggiungi -s quindi andrà bene.
Ray

@Ray Oh sì, lo sto facendo. Ottengo ancora un errore sulle tracce 13 e 14 quando il controller sta tentando di terminare il processo sebbene il risultato sia già lì. Immagino che dovrei esaminarlo, ma non influisce sul punteggio, quindi non mi sono ancora preoccupato.
Martin Ender,

Sfortunatamente, ho dovuto aggiungere un'altra regola. La memoria sembra essere più limitante del tempo in questa sfida, quindi ho dovuto stabilire un limite al consumo di memoria. Qualsiasi corsa in cui il tuo pilota utilizza più di 1 GB di memoria verrà interrotta con lo stesso effetto del superamento del limite di tempo. Per l'attuale serie di tracce, il tuo punteggio non è stato influenzato da questa modifica. (Penso che raggiungerai quel limite per i tie-break in giro n = 400.) Per favore fatemi sapere se applicate delle ottimizzazioni, così posso rieseguire i test.
Martin Ender,

3

Python 3: 6.49643 (ottimale, BFS)

Per il vecchio file di riferimento di 20 casi, ha ottenuto un punteggio di 5,35643. La soluzione di @nneonneo non è ottimale poiché ha ottenuto 5.4. Alcuni bug forse.

Questa soluzione usa BFS per cercare il grafico, ogni stato di ricerca è nella forma di (x, y, dx, dy). Quindi uso una mappa per mappare dagli stati alle distanze. Nel peggiore dei casi, la complessità del tempo e dello spazio è O (n ^ 2 m ^ 2). Ciò accadrà raramente poiché la velocità non sarà troppo elevata o il pilota si schianterà. In realtà, sulla mia macchina è costato 3 secondi per completare tutti i 22 test.

from collections import namedtuple, deque
import itertools

Field = namedtuple('Map', 'n m grids')

class Grid:
    WALL = '#'
    EMPTY = '.'
    START = 'S'
    END = '*'

def read_nums():
    return list(map(int, input().split()))

def read_field():
    m, n = read_nums()
    return Field(n, m, [input() for i in range(n)])

def find_start_pos(field):
    return next((i, j)
        for i in range(field.n) for j in range(field.m)
        if field.grids[i][j] == Grid.START)

def can_go(field, i, j):
    return 0 <= i < field.n and 0 <= j < field.m and field.grids[i][j] != Grid.WALL

def trace_path(start, end, prev):
    if end == start:
        return
    end, step = prev[end]
    yield from trace_path(start, end, prev)
    yield step

def solve(max_turns, field, time):
    i0, j0 = find_start_pos(field)
    p0 = i0, j0, 0, 0
    prev = {}
    que = deque([p0])
    directions = list(itertools.product((-1, 0, 1), (-1, 0, 1)))

    while que:
        p = i, j, vi, vj = que.popleft()
        for dvi, dvj in directions:
            vi1, vj1 = vi + dvi, vj + dvj
            i1, j1 = i + vi1, j + vj1
            if not can_go(field, i1, j1):
                continue
            p1 = i1, j1, vi1, vj1
            if p1 in prev:
                continue
            que.append(p1)
            prev[p1] = p, (dvi, dvj)
            if field.grids[i1][j1] == Grid.END:
                return trace_path(p0, p1, prev)
    return []

def main():
    for dvy, dvx in solve(int(input()), read_field(), float(input())):
        print(dvx, dvy)

main()

# risultati

± % time ruby controller.rb benchmark.txt python ../mybfs.py                                                                                                                                                                             !9349
["benchmark.txt", "python", "../mybfs.py"]

Running 'python ../mybfs.py' against benchmark.txt

 No.       Size     Target   Score     Details
-------------------------------------------------------------------------------------
  1       37 x 1        36   0.22222   Racer reached goal at ( 36, 0) in 8 turns.
  2       38 x 1        37   0.24324   Racer reached goal at ( 37, 0) in 9 turns.
  3       33 x 1        32   0.25000   Racer reached goal at ( 32, 0) in 8 turns.
  4       10 x 10       10   0.40000   Racer reached goal at ( 7, 7) in 4 turns.
  5        9 x 6         8   0.37500   Racer reached goal at ( 6, 0) in 3 turns.
  6       15 x 7        16   0.37500   Racer reached goal at ( 12, 4) in 6 turns.
  7       17 x 8        16   0.31250   Racer reached goal at ( 14, 0) in 5 turns.
  8       19 x 13       18   0.27778   Racer reached goal at ( 0, 11) in 5 turns.
  9       60 x 10      107   0.14953   Racer reached goal at ( 0, 6) in 16 turns.
 10       31 x 31      106   0.23585   Racer reached goal at ( 27, 0) in 25 turns.
 11       31 x 31      106   0.24528   Racer reached goal at ( 15, 15) in 26 turns.
 12       50 x 20       50   0.24000   Racer reached goal at ( 49, 10) in 12 turns.
 13      100 x 100    2600   0.01385   Racer reached goal at ( 50, 0) in 36 turns.
 14       79 x 63      242   0.24380   Racer reached goal at ( 3, 42) in 59 turns.
 15       26 x 1        25   0.32000   Racer reached goal at ( 25, 0) in 8 turns.
 16       17 x 1        19   0.52632   Racer reached goal at ( 16, 0) in 10 turns.
 17       50 x 1        55   0.34545   Racer reached goal at ( 23, 0) in 19 turns.
 18       10 x 7        23   0.34783   Racer reached goal at ( 1, 3) in 8 turns.
 19       55 x 55       45   0.17778   Racer reached goal at ( 50, 26) in 8 turns.
 20      101 x 100     100   0.14000   Racer reached goal at ( 99, 99) in 14 turns.
 21   100000 x 1         1   1.00000   Racer reached goal at ( 0, 0) in 1 turns.
 22       50 x 50      200   0.05500   Racer reached goal at ( 47, 46) in 11 turns.
-------------------------------------------------------------------------------------
TOTAL SCORE:                 6.49643

ruby controller.rb benchmark.txt python ../mybfs.py  3.06s user 0.06s system 99% cpu 3.146 total

Sì, secondo il commento di user2357112 c'è un bug nella prevenzione del ciclo di nneonneo. Per quanto ne so, la velocità è limitata da O(√n)ciò che renderebbe la tua implementazione O(n³)su griglie quadrate (uguale alle altre, suppongo). Aggiungerò un pareggio per segnare la tua richiesta rispetto a quella dell'utente2357112 più tardi oggi.
Martin Ender,

A proposito, hai intenzione di aggiungere un altro caso di test?
Martin Ender,

@ m.buettner No, non ho una comprensione abbastanza buona per questo gioco. Quindi la mia testcase non sarà interessante.
Ray

Sfortunatamente, ho dovuto aggiungere un'altra regola. La memoria sembra essere più limitante del tempo in questa sfida, quindi ho dovuto stabilire un limite al consumo di memoria. Qualsiasi corsa in cui il tuo pilota utilizza più di 1 GB di memoria verrà interrotta con lo stesso effetto del superamento del limite di tempo. Con questa regola, il tuo invio è il primo a superare quel limite per un pareggio di dimensioni n=270, motivo per cui ora sei dietro gli altri due invii "ottimali". Detto questo, la tua richiesta è anche la più lenta delle tre, quindi sarebbe stata comunque terza, solo con un pareggio più grande.
Martin Ender,

Per favore fatemi sapere se applicate delle ottimizzazioni, così posso rieseguire i test.
Martin Ender,

1

RandomRacer, ~ 40.0 (media su 10 corse)

Non è questo bot mai finisca una traccia, ma sicuramente molto meno spesso di una volta su 10 tentativi. (Ottengo un punteggio non peggiore ogni 20-30 simulazioni circa.)

Questo serve principalmente a fungere da caso di base e per dimostrare una possibile implementazione (Ruby) per un pilota:

# Parse initial input
target = gets.to_i
size = gets.split.map(&:to_i)
track = []
size[1].times do
    track.push gets
end
time_budget = gets.to_f

# Find start position
start_y = track.find_index { |row| row['S'] }
start_x = track[start_y].index 'S'

position = [start_x, start_y]
velocity = [0, 0]

while true
    x = rand(3) - 1
    y = rand(3) - 1
    puts [x,y].join ' '
    $stdout.flush

    first_line = gets
    break if !first_line || first_line.chomp.empty?

    position = first_line.split.map(&:to_i)
    velocity = gets.split.map(&:to_i)
    time_budget = gets.to_f
end

Eseguilo con

ruby controller.rb benchmark.txt ruby randomracer.rb

1

Random Racer 2.0, ~ 31

Bene, questo non batterà il risolutore ottimale pubblicato, ma è un leggero miglioramento su un pilota casuale. La differenza principale è che questo corridore prenderà in considerazione la possibilità di andare casualmente dove non c'è un muro, a meno che non si esaurisca in posti validi per muoversi, e se può spostarsi verso un obiettivo in quel turno, lo farà. Inoltre, non si muoverà per rimanere nello stesso punto, a meno che non ci siano altre mosse disponibili (improbabile, ma possibile).

Implementato in Java, compilato con java8, ma Java 6 dovrebbe andare bene. Nessun parametro della riga di comando. C'è un discreto ammasso di gerarchie, quindi penso che sto facendo java nel modo giusto.

import java.util.Scanner;
import java.util.Random;
import java.util.ArrayList;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

public class VectorRacing   {
    private static Scanner in = new Scanner(System.in);
    private static Random rand = new Random();
    private static Track track;
    private static Racer racer;
    private static int target;
    private static double time;
    public static void main(String[] args)  {
        init();
        main_loop();
    }
    private static void main_loop() {
        Scanner linescan;
        String line;
        int count = 0,
            x, y, u, v;

        while(!racer.lost() && !racer.won() && count < target)  {
            Direction d = racer.think();
            racer.move(d);
            count++;
            System.out.println(d);

            line = in.nextLine();
            if(line.equals("")) {
                break;
            }
            linescan = new Scanner(line);
            x = linescan.nextInt();
            y = linescan.nextInt();
            linescan = new Scanner(in.nextLine());
            u = linescan.nextInt();
            v = linescan.nextInt();
            time = Double.parseDouble(in.nextLine());

            assert x == racer.location.x;
            assert y == racer.location.y;
            assert u == racer.direction.x;
            assert v == racer.direction.y;
        }
    }
    private static void init()  {
        target = Integer.parseInt(in.nextLine());
        int width = in.nextInt();
        int height = Integer.parseInt(in.nextLine().trim());
        String[] ascii = new String[height];
        for(int i = 0; i < height; i++) {
            ascii[i] = in.nextLine();
        }
        time = Double.parseDouble(in.nextLine());
        track = new Track(width, height, ascii);
        for(int y = 0; y < ascii.length; y++)   {
            int x = ascii[y].indexOf("S");
            if( x != -1)    {
                racer = new RandomRacer(track, new Location(x, y));
                break;
            }
        }
    }

    public static class RandomRacer extends Racer   {
        public RandomRacer(Track t, Location l) {
            super(t, l);
        }
        public Direction think()    {
            ArrayList<Pair<Location, Direction> > possible = this.getLocationsCanMoveTo();
            if(possible.size() == 0)    {
                return Direction.NONE;
            }
            Pair<Location, Direction> ret = null;
            do  {
                ret = possible.get(rand.nextInt(possible.size()));
            }   while(possible.size() != 1 && ret.a.equals(this.location));
            return ret.b;
        }
    }

    // Base things
    public enum Direction   {
        NORTH("0 -1"), SOUTH("0 1"), EAST("1 0"), WEST("-1 0"), NONE("0 0"),
        NORTH_EAST("1 -1"), NORTH_WEST("-1 -1"), SOUTH_EAST("1 1"), SOUTH_WEST("-1 1");

        private final String d;
        private Direction(String d) {this.d = d;}
        public String toString()    {return d;}
    }
    public enum Cell    {
        WALL('#'), GOAL('*'), ROAD('.'), OUT_OF_BOUNDS('?');

        private final char c;
        private Cell(char c)    {this.c = c;}
        public String toString()    {return "" + c;}
    }

    public static class Track   {
        private Cell[][] track;
        private int target;
        private double time;
        public Track(int width, int height, String[] ascii) {
            this.track = new Cell[width][height];
            for(int y = 0; y < height; y++) {
                for(int x = 0; x < width; x++)  {
                    switch(ascii[y].charAt(x))  {
                        case '#':   this.track[x][y] = Cell.WALL; break;
                        case '*':   this.track[x][y] = Cell.GOAL; break;
                        case '.':
                        case 'S':   this.track[x][y] = Cell.ROAD; break;
                        default:    System.exit(-1);
                    }
                }
            }
        }
        public Cell atLocation(Location loc)    {
            if(loc.x < 0 || loc.x >= track.length || loc.y < 0 || loc.y >= track[0].length) return Cell.OUT_OF_BOUNDS;
            return track[loc.x][loc.y];
        }

        public String toString()    {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            PrintStream ps = new PrintStream(bos);
            for(int y = 0; y < track[0].length; y++)    {
                for(int x = 0; x < track.length; x++)   {
                    ps.append(track[x][y].toString());
                }
                ps.append('\n');
            }
            String ret = bos.toString();
            ps.close();
            return ret;
        }
    }

    public static abstract class Racer  {
        protected Velocity tdir;
        protected Location tloc;
        protected Track track;
        public Velocity direction;
        public Location location;

        public Racer(Track track, Location start)   {
            this.track = track;
            direction = new Velocity(0, 0);
            location = start;
        }
        public boolean canMove() throws GoHereDammitException {return canMove(Direction.NONE);}
        public boolean canMove(Direction d) throws GoHereDammitException    {
            tdir = new Velocity(direction);
            tloc = new Location(location);
            tdir.add(d);
            tloc.move(tdir);
            Cell at = track.atLocation(tloc);
            if(at == Cell.GOAL) {
                throw new GoHereDammitException();
            }
            return at == Cell.ROAD;
        }
        public ArrayList<Pair<Location, Direction> > getLocationsCanMoveTo()    {
            ArrayList<Pair<Location, Direction> > ret = new ArrayList<Pair<Location, Direction> >(9);
            for(Direction d: Direction.values())    {
                try {
                    if(this.canMove(d)) {
                        ret.add(new Pair<Location, Direction>(tloc, d));
                    }
                }   catch(GoHereDammitException e)  {
                    ret.clear();
                    ret.add(new Pair<Location, Direction>(tloc, d));
                    return ret;
                }
            }
            return ret;
        }
        public void move()  {move(Direction.NONE);}
        public void move(Direction d)   {
            direction.add(d);
            location.move(direction);
        }
        public boolean won()    {
            return track.atLocation(location) == Cell.GOAL;
        }
        public boolean lost()   {
            return track.atLocation(location) == Cell.WALL || track.atLocation(location) == Cell.OUT_OF_BOUNDS;
        }
        public String toString()    {
            return location + ", " + direction;
        }
        public abstract Direction think();

        public class GoHereDammitException extends Exception    {
            public GoHereDammitException()  {}
        }
    }

    public static class Location extends Point  {
        public Location(int x, int y)   {
            super(x, y);
        }
        public Location(Location l) {
            super(l);
        }
        public void move(Velocity d)    {
            this.x += d.x;
            this.y += d.y;
        }
    }

    public static class Velocity extends Point  {
        public Velocity(int x, int y)   {
            super(x, y);
        }
        public Velocity(Velocity v) {
            super(v);
        }
        public void add(Direction d)    {
            if(d == Direction.NONE) return;
            if(d == Direction.NORTH || d == Direction.NORTH_EAST || d == Direction.NORTH_WEST)  this.y--;
            if(d == Direction.SOUTH || d == Direction.SOUTH_EAST || d == Direction.SOUTH_WEST)  this.y++;
            if(d == Direction.EAST || d == Direction.NORTH_EAST || d == Direction.SOUTH_EAST)   this.x++;
            if(d == Direction.WEST || d == Direction.NORTH_WEST || d == Direction.SOUTH_WEST)   this.x--;
        }
    }

    public static class Point   {
        protected int x, y;
        protected Point(int x, int y)   {
            this.x = x;
            this.y = y;
        }
        protected Point(Point c)    {
            this.x = c.x;
            this.y = c.y;
        }
        public int getX()   {return x;}
        public int getY()   {return y;}
        public String toString()    {return "(" + x + ", " + y + ")";}
        public boolean equals(Point p)  {
            return this.x == p.x && this.y == p.y;
        }
    }

    public static class Pair<T, U>  {
        public T a;
        public U b;
        public Pair(T t, U u)   {
            a=t;b=u;
        }
    }
}

I risultati (il caso migliore che abbia mai visto)

Running 'java VectorRacing' against ruby-runner/benchmark.txt

 No.    Size     Target   Score     Details
-------------------------------------------------------------------------------------
  1    37 x 1        36   0.38889   Racer reached goal at ( 36, 0) in 14 turns.
  2    38 x 1        37   0.54054   Racer reached goal at ( 37, 0) in 20 turns.
  3    33 x 1        32   0.62500   Racer reached goal at ( 32, 0) in 20 turns.
  4    10 x 10       10   0.40000   Racer reached goal at ( 9, 8) in 4 turns.
  5     9 x 6         8   0.75000   Racer reached goal at ( 6, 2) in 6 turns.
  6    15 x 7        16   2.00000   Racer did not reach the goal within 16 turns.
  7    17 x 8        16   2.00000   Racer hit a wall at position ( 8, 2).
  8    19 x 13       18   0.44444   Racer reached goal at ( 16, 2) in 8 turns.
  9    60 x 10      107   0.65421   Racer reached goal at ( 0, 6) in 70 turns.
 10    31 x 31      106   2.00000   Racer hit a wall at position ( 25, 9).
 11    31 x 31      106   2.00000   Racer hit a wall at position ( 8, 1).
 12    50 x 20       50   2.00000   Racer hit a wall at position ( 27, 14).
 13   100 x 100    2600   2.00000   Racer went out of bounds at position ( 105, 99).
 14    79 x 63      242   2.00000   Racer went out of bounds at position (-2, 26).
 15    26 x 1        25   0.32000   Racer reached goal at ( 25, 0) in 8 turns.
 16    17 x 1        19   2.00000   Racer went out of bounds at position (-2, 0).
 17    50 x 1        55   2.00000   Racer went out of bounds at position ( 53, 0).
 18    10 x 7        23   2.00000   Racer went out of bounds at position ( 10, 2).
 19    55 x 55       45   0.33333   Racer reached goal at ( 4, 49) in 15 turns.
 20    50 x 50      200   2.00000   Racer hit a wall at position ( 14, 7).
-------------------------------------------------------------------------------------
TOTAL SCORE:             26.45641

Sì, l'ho fatto funzionare, anche se ho dovuto eseguirlo dalla directory in cui il .classfile è per qualche motivo (invece della directory in cui si trova il controller). Esegui il ping (con un commento) se decidi di aggiungere una testcase, quindi posso aggiungerla al benchmark. Il tuo punteggio è stato di circa 33 su 10 prove (vedi classifica), ma questo potrebbe cambiare con ogni nuova pista di prova che viene aggiunta al benchmark.
Martin Ender,

Ah, ha funzionato anche dall'altra directory. Per coloro che non hanno familiarità con Java sulla riga di comando:java -cp path/to/class/file VectorRacing
Martin Ender,

Ah, sì, ho fatto un sacco di lezioni (13, per l'esattezza). Ho sempre eseguito il tuo script dalla mia directory di classi, quindi non l'ho provato. Potrei fare un caso di prova, ma penso che proverò a fare un pilota che non sia prima casuale.
pseudonimo117,

Sicuro. Se lo fai, ti preghiamo di aggiungerlo come risposta separata. (E sentiti libero di aggiungere un caso di test con ciascuno di essi.)
Martin Ender,

Sfortunatamente, ho dovuto aggiungere un'altra regola. La memoria sembra essere più limitante del tempo in questa sfida, quindi ho dovuto stabilire un limite al consumo di memoria. Qualsiasi corsa in cui il tuo pilota utilizza più di 1 GB di memoria verrà interrotta con lo stesso effetto del superamento del limite di tempo. Per l'attuale serie di tracce, il tuo punteggio non è stato influenzato da questo cambiamento (e probabilmente non lo sarà mai).
Martin Ender,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.