Para Vectory! - O Grande Prêmio de Corridas de Vetores


39

O usuário CarpetPython publicou uma nova visão sobre esse problema, que coloca um foco muito maior nas soluções heurísticas, devido ao aumento do espaço de pesquisa. Pessoalmente, acho que esse desafio é muito melhor do que o meu, então considere tentar esse!

Vector racing é um jogo viciante que pode ser jogado com uma caneta e uma folha de papel quadriculado. Você desenha uma pista arbitrária no papel, define um começo e um fim e depois dirige seu carro de tamanho pontual de maneira baseada em turnos. Chegue ao fim o mais rápido possível, mas tome cuidado para não acabar em uma parede!

The Track

  • O mapa é uma grade bidimensional, onde cada célula possui coordenadas inteiras.
  • Você move nas células da grade.
  • Cada célula da grade faz parte da trilha ou é uma parede.
  • Exatamente uma célula da trilha é a coordenada inicial.
  • Pelo menos uma célula de rastreamento é designada como objetivo. A aterrissagem em qualquer um deles completa a corrida. Várias células de objetivo não estão necessariamente conectadas.

Dirigindo o carro

Seu carro começa em uma determinada coordenada e com vetor de velocidade (0, 0). Em cada turno, você pode ajustar cada componente da velocidade ±1ou deixá-lo como está. Em seguida, o vetor de velocidade resultante é adicionado à posição do seu carro.

Uma imagem pode ajudar! O círculo vermelho era sua posição no último turno. O círculo azul é sua posição atual. Sua velocidade é o vetor do círculo vermelho ao azul. Nesse turno, dependendo de como você ajusta sua velocidade, você pode mover-se para qualquer um dos círculos verdes.

                                    insira a descrição da imagem aqui

Se você cair em uma parede, você perde imediatamente.

Sua tarefa

Você adivinhou: escreva um programa que, dado uma pista de corrida como entrada, leve o carro a uma das células do gol no menor número de curvas possível. Sua solução deve ser capaz de lidar razoavelmente bem com trilhas arbitrárias e não ser otimizada especificamente para os casos de teste fornecidos.

Entrada

Quando seu programa é chamado, leia a partir de stdin :

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

targeté o número máximo de turnos que você pode fazer para concluir a faixa e timeo orçamento total de tempo para a faixa, em segundos (não necessariamente inteiro). Veja abaixo os detalhes sobre o tempo.

Para a faixa delimitada por nova linha, os seguintes caracteres são usados:

  • # - parede
  • S- o começo
  • *- um objetivo
  • . - todas as outras células da via (estrada)

Todas as células fora da n x mgrade são implicadas em paredes.

A origem das coordenadas está no canto superior esquerdo.

Aqui está um exemplo simples:

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

Usando indexação baseada em 0, a coordenada inicial seria (0,4).

Após cada movimento, você receberá mais informações:

x y
u v
time

Onde x, y, u, vsão todos os inteiros baseados em 0. (x,y)é sua posição atual e (u,v)sua velocidade atual. Observe que x+ue / ou y+vpode estar fora dos limites.

timeé o que resta do seu orçamento de tempo, em segundos. Sinta-se livre para ignorar isso. Isso é apenas para participantes que realmente querem levar sua implementação ao limite de tempo.

Quando o jogo terminar (porque você caiu em uma parede, saiu dos limites, excedeu as targetcurvas, ficou sem tempo ou atingiu a meta), você receberá uma linha vazia.

Saída

Para cada turno, escreva para stdout :

Δu Δv

onde Δue Δvsão cada uma de -1, 0, 1. Isso será adicionado (u,v)para determinar sua nova posição. Apenas para esclarecer, as instruções são as seguintes

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

Uma solução ideal para o exemplo acima seria

1 0
1 -1
1 0

Observe que o controlador não se conecta ao stderr ; portanto, você pode usá-lo para qualquer tipo de saída de depuração necessária ao desenvolver seu bot. Por favor, remova / comente qualquer saída desse tipo no código enviado.

Seu bot pode levar meio segundo para responder a cada turno. Para turnos que demoram mais, você terá um orçamento de tempo (por faixa) de target/2segundos. Sempre que uma curva demorar mais de meio segundo, o tempo adicional será subtraído do seu orçamento de tempo. Quando seu orçamento de tempo chegar a zero, a corrida atual será abortada.

Novo: Por razões práticas, tenho que definir um limite de memória (já que a memória parece ser mais limitadora do que o tempo para tamanhos razoáveis ​​de faixas). Portanto, terei que abortar qualquer execução de teste em que o piloto use mais de 1 GB de memória, conforme medido pelo Process Explorer como bytes privados .

Pontuação

Há uma referência de 20 faixas. Para cada faixa:

  • Se você completar a faixa, sua pontuação é o número de movimentos necessários para alcançar uma célula de objetivo dividida portarget .
  • Se você ficar sem tempo / memória ou não atingir a meta antes do targettérmino dos turnos ou você cair em uma parede / fora dos limites a qualquer momento, sua pontuação será 2.
  • Se o seu programa não é determinístico, sua pontuação é a média acima de 10 corridas nessa faixa (por favor, indique isso na sua resposta).

Sua pontuação geral é a soma das pontuações individuais das faixas. Menor pontuação ganha!

Além disso, todo participante pode (e é fortemente encorajado a) fornecer uma faixa de referência adicional , que será adicionada à lista oficial. As respostas anteriores serão reavaliadas, incluindo esta nova faixa. Isso é para garantir que nenhuma solução seja adaptada muito de perto aos casos de teste existentes e para dar conta de casos extremos interessantes que eu poderia ter esquecido (mas que você pode perceber).

Quebra de gravata

Agora que já existe uma solução ideal, esse provavelmente será o principal fator para a pontuação dos participantes.

Se houver um empate (devido a várias respostas resolverem todas as faixas da melhor maneira ou de outra forma), adicionarei casos de teste adicionais (maiores) para romper o empate. Para evitar qualquer viés humano ao criar esses desempatadores, eles serão gerados de maneira fixa:

  • Aumentarei o comprimento do lado nem 10comparação com a última faixa gerada dessa maneira. (Eu posso pular tamanhos se eles não quebrarem a gravata.)
  • A base é este gráfico vetorial
  • Isso será rasterizado na resolução desejada usando esse trecho do Mathematica .
  • O início está no canto superior esquerdo. Em particular, será a célula mais à esquerda da linha superior da extremidade da pista.
  • O objetivo está no canto inferior direito. Em particular, será a célula mais à direita da linha mais baixa do final da pista.
  • A targetvontade 4*n.

A trilha final do benchmark inicial já foi gerada assim, com n = 50.

O controlador

O programa que testa as submissões está escrito em Ruby e pode ser encontrado no GitHub junto com o arquivo de benchmark que utilizarei. Há também um exemplo de bot chamado randomracer.rblá, que simplesmente escolhe movimentos aleatórios. Você pode usar sua estrutura básica como ponto de partida para o seu bot ver como a comunicação funciona.

Você pode executar seu próprio bot em um arquivo de trilha de sua escolha da seguinte maneira:

ruby controller.rb track_file_name command to run your racer

por exemplo

ruby controller.rb benchmark.txt ruby randomracer.rb

O repositório também contém duas classes Point2De Track. Se o seu envio estiver escrito em Ruby, fique à vontade para reutilizá-los para sua conveniência.

Opções de linha de comando

Você pode adicionar opções de linha de comando -v, -s, -tantes do nome de arquivo do benchmark. Se você deseja usar vários comutadores, também é possível, por exemplo -vs,. Isto é o que eles fazem:

-v (detalhado): use isso para produzir um pouco mais de saída de depuração do controlador.

-s (silencioso): se você preferir manter o controle de sua posição e velocidade e não se importar com o orçamento de tempo, pode desativar essas três linhas de saída a cada turno (enviadas para sua apresentação) com esta bandeira.

-t(faixas): permite selecionar faixas individuais a serem testadas. Por exemplo -t "1,2,5..8,15", testaria apenas as faixas 1, 2, 5, 6, 7, 8 e 15. Muito obrigado ao Ventero por esse recurso e pelo analisador de opções.

Sua submissão

Em resumo, inclua o seguinte na sua resposta:

  • Sua pontuação.
  • Se você estiver usando aleatoriedade, indique isso, para que eu possa calcular sua pontuação em várias execuções.
  • O código para o seu envio.
  • A localização de um compilador ou intérprete gratuito para o seu idioma preferido, executado em uma máquina Windows 8.
  • Instruções de compilação, se necessário.
  • Uma sequência de linhas de comando do Windows para executar seu envio.
  • Se o seu envio exige a -sbandeira ou não.
  • (opcionalmente) Uma nova faixa solucionável que será adicionada ao benchmark. Eu determinarei um razoável targetpara sua faixa manualmente. Quando a faixa for adicionada à referência, eu a editarei com sua resposta. Reservo-me o direito de solicitar uma faixa diferente (caso você adicione uma faixa desproporcionalmente grande, inclua arte ASCII obscena na faixa etc.). Quando adiciono o caso de teste ao conjunto de benchmarks, substituirei a trilha na sua resposta por um link para a trilha no arquivo de benchmark para reduzir a confusão nesta postagem.

Como você pode ver, testarei todos os envios em uma máquina Windows 8. Se não houver absolutamente nenhuma maneira de fazer o seu envio ser executado no Windows, também posso experimentar uma VM do Ubuntu. Isso será consideravelmente mais lento, portanto, se você deseja maximizar o limite de tempo, verifique se o seu programa é executado no Windows.

Que o melhor motorista surja vetorial!

Mas eu quero jogar!

Se você quiser experimentar o jogo para ter uma idéia melhor, existe esta implementação . As regras usadas lá são um pouco mais sofisticadas, mas é semelhante o suficiente para ser útil, eu acho.

Entre os melhores

Última atualização: 01/09/2014, 21:29 UTC
Faixas de referência: 25
Tamanhos de desempatador: 290, 440

  1. 6.86688 - Kuroi Neko
  2. 8.73108 - user2357112 - segundo envio
  3. 9.86627 - nneonneo
  4. 10.66109 - user2357112 - 1º envio
  5. 12.49643 - Ray
  6. 40.0759 - pseudônimo117 (probabilístico)

Resultados detalhados do teste . (As pontuações para envios probabilísticos foram determinadas separadamente.)

Respostas:


5

C ++ 11 - 6.66109

Mais uma implementação de primeira pesquisa de abrangência, apenas otimizada.

Ele deve ser executado com a opção -s .
Sua entrada não é higienizada, portanto, faixas erradas podem transformá-la em abóbora.

Testei-o com o Microsoft Visual C ++ 2013, versão compilada com o sinalizador padrão / O2 (otimizar a velocidade).
FAZ Construir OK com g ++ e o Microsoft IDE.
Meu alocador de memória barebone é uma porcaria, então não espere que ele funcione com outras implementações STL do 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;
}

Resultados

 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

Performances

Essa linguagem C ++ ruim tem um talento especial para fazer você pular em bastidores apenas para mover um palito de fósforo. No entanto, você pode usá-lo para produzir um código relativamente rápido e com eficiência de memória.

Hashing

Aqui, a chave é fornecer uma boa tabela de hash para os nós. Esse é de longe o fator dominante para a velocidade de execução.
Duas implementações do unordered_set(GNU e Microsoft) produziram uma diferença de velocidade de execução de 30% (em favor do GNU, yay!).

A diferença não é realmente surpreendente, com os caminhões de código escondidos atrás unordered_set.

Por curiosidade, fiz algumas estatísticas sobre o estado final da tabela de hash.
Ambos os algoritmos terminam com quase a mesma proporção de bucket / elementos, mas a repartição varia:
para o desempatador de 290x290, o GNU obtém uma média de 1,5 elementos por bucket não vazio, enquanto a Microsoft está em 5,8 (!).

Parece que minha função de hash não é muito bem randomizada pela Microsoft ... Gostaria de saber se os caras em Redmond realmente fizeram o benchmark de seu STL, ou talvez meu caso de uso apenas favorece a implementação do GNU ...

Claro, minha função de hash não está nem perto do ideal. Eu poderia ter usado a mistura inteira usual com base em turnos múltiplos / muls, mas uma função eficiente de hash leva tempo para calcular.

Parece que o número de consultas da tabela de hash é muito alto comparado ao número de inserções. Por exemplo, no desempatador de 290x290, você tem cerca de 3,6 milhões de inserções para 22,7 milhões de consultas.
Nesse contexto, um hash subótimo, mas rápido, produz melhores desempenhos.

Alocação de memória

Fornecer um alocador de memória eficiente vem em segundo lugar. Melhorou o desempenho em cerca de 30%. Se vale a pena o código porcaria adicionado é discutível :).

A versão atual usa entre 40 e 55 bytes por nó.
Os dados funcionais requerem 24 bytes para um nó (4 coordenadas e 2 ponteiros).
Devido ao caso de teste insano de 100.000 linhas, as coordenadas precisam ser armazenadas em palavras de 4 bytes; caso contrário, você pode obter 8 bytes usando shorts (com um valor máximo de coordenadas de 32767). Os bytes restantes são consumidos principalmente pela tabela de hash do conjunto não ordenado. Isso significa que a manipulação de dados realmente consome um pouco mais do que a carga útil "útil".

E o vencedor é...

No meu PC no Win7, o desempatador (caso 23, 290x290) é resolvido pela pior versão (ou seja, compilada pela Microsoft) em cerca de 2,2 segundos, com um consumo de memória de cerca de 185 Mb.
Para comparação, o líder atual (código python pelo usuário2357112) leva um pouco mais de 30 segundos e consome cerca de 780 Mb.

Problemas do controlador

Não tenho certeza se posso codificar em Ruby para salvar minha vida.
No entanto, localizei e hackeei dois problemas do código do controlador:

1) leitura de mapa em track.rb

Com o ruby ​​1.9.3 instalado, o leitor de faixas resmungava por shift.to_inão estar disponível string.lines.
Após um longo período de pesquisa na documentação on-line do Ruby, desisti de strings e usei uma matriz intermediária, como a seguir (exatamente no início do arquivo):

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

2) matando fantasmas em controller.rb

Como outros pôsteres já observaram, o controlador às vezes tenta eliminar processos que já foram encerrados. Para evitar essas saídas de erro vergonhosas, simplesmente lidei com a exceção, da seguinte forma (em torno da linha 134):

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

Caso de teste

Para derrotar a abordagem de força bruta dos solucionadores de BFS, a pior pista é o oposto do mapa de 100.000 células: uma área completamente livre com o objetivo o mais longe possível do início.

Neste exemplo, um mapa de 100x400 com a meta no canto superior esquerdo e o início no canto inferior direito.

Este mapa tem uma solução em 28 turnos, mas um solucionador de BFS irá explorar milhões de estados para encontrá-lo (o meu contou 10.022.658 estados visitados, levou cerca de 12 segundos e atingiu o pico de 600 Mb!).

Com menos da metade da superfície do desempatador de 290x290, é necessário cerca de três vezes mais visitas ao nó. Por outro lado, um solucionador baseado em heurística / A * deve derrotá-lo facilmente.

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

Bônus: uma versão PHP equivalente (mas um pouco menos eficiente)

Foi com isso que comecei, antes que a lentidão da linguagem inerente me convencesse a usar C ++.
As tabelas de hash internas do PHP não parecem tão eficientes quanto as do Python, pelo menos nesse caso em particular :).

<?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 ... Meu alocador de barebone é um pouco barebone demais. Vou adicionar a porcaria necessária para fazê-lo funcionar com o g ++, então. Me desculpe por isso.

OK, está consertado. A versão do g ++ funciona até 30% mais rápido. Agora ele exibe algumas estatísticas no stderr. Sinta-se livre para comentar (das últimas linhas da fonte). Desculpe novamente pelo erro.

Tudo bem, ele funciona agora e eu reproduzi sua pontuação. É muito rápido! :) Adicionarei seu caso de teste ao benchmark, mas alterarei o destino para400 , pois está de acordo com a forma como eu determinei todos os outros alvos (exceto o desempate). Atualizarei a postagem principal assim que redirecionar todas as outras submissões.
Martin Ender

Atualizado os resultados. Não havia necessidade de desempate, porque todos os outros envios excediam o limite de memória em sua pista de teste. Parabéns por isso! :)
Martin Ender

Obrigado. Na verdade, esse desafio me deu a oportunidade de explorar essas tabelas de hash do STL. Embora eu odeie coragem em C ++, não posso deixar de ser morto por minha curiosidade. Miau! :).

10

C ++, 5.4 (determinístico, ideal)

Solução de programação dinâmica. Provavelmente ideal. Muito rápido: resolve todos os 20 casos de teste em 0,2s. Deve ser especialmente rápido em máquinas de 64 bits. Supõe que o quadro tenha menos de 32.000 lugares em cada direção (o que deve ser verdade).

Este piloto é um pouco incomum. Ele calcula o caminho ideal na linha de partida e depois executa o caminho calculado instantaneamente. Ele ignora o controle de tempo e assume que pode concluir a etapa de otimização no prazo (o que deve ser verdadeiro para qualquer hardware razoavelmente moderno). Em mapas excessivamente grandes, há uma pequena chance de o piloto falhar. Se você pode convencê-lo a segfault, obtém um ponto de brownie e eu o corrigirei para usar um loop explícito.

Compile com g++ -O3. Pode exigir C ++ 11 (para <unordered_map>). Para executar, basta executar o executável compilado (nenhum sinalizador ou opção é suportado; todas as entradas são obtidas no 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;
}

Resultados

 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

Novo Testcase


1
Bem, algo como isso era praticamente esperado. Simplesmente não há estado suficiente no quebra-cabeça para tornar a programação dinâmica inviável. Se eu entrar, vou precisar enviar um mapa que exija estratégias de pesquisa mais sofisticadas para resolver.
User2357112 suporta Monica

Como o seu piloto se sai no seu caso de teste?
User2357112 suporta Monica

0,14 (14 movimentos)
nneonneo 27/06

Esse tempo é gasto ou se move / alvo? Se é um movimento / alvo, como é o desempenho em termos de tempo?
User2357112 suporta Monica

1
Acho que encontrei um bug no código de prevenção de ciclos. Assume-se que para cada estado os alcances busca de um estado S, um caminho ideal não pode ser para voltar a S. Pode parecer que, se um caminho ideal não retorno a S, então o Estado não pode estar em um caminho ideal (desde que pudemos basta remover o loop ativado e obter um caminho mais curto), para que não nos importemos se obtemos um resultado muito alto para esse estado. No entanto, se um caminho ideal passar pelos estados A e B nessa ordem, mas a pesquisa primeiro encontrar A enquanto B ainda estiver na pilha, os resultados de A serão envenenados pela prevenção de loop.
User2357112 suporta Monica

6

Python 2 , determinístico, ideal

Aqui está o meu piloto. Eu não o testei no benchmark (ainda questionando qual versão e instalador do Ruby instalar), mas ele deve resolver tudo da melhor maneira possível e dentro do prazo. O comando para executá-lo é python whateveryoucallthefile.py. Precisa da -sbandeira do controlador.

# 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()

Depois de inspecionar o piloto de nneonneo (mas na verdade não testá-lo, já que também não tenho um compilador C ++), descobri que ele parece realizar uma pesquisa quase exaustiva no espaço de estados, não importa o quão próximo o objetivo ou o curto seja um caminho já foi encontrado. Também descobri que as regras de tempo significam a construção de um mapa com uma solução longa e complexa, que exige um prazo longo e chato. Assim, o envio do meu mapa é bem simples:

Novo Testcase

(O GitHub não pode exibir a linha longa. A faixa é *S.......[and so on].....)


Submissão adicional: Python 2, pesquisa bidirecional

Essa é uma abordagem que escrevi há cerca de dois meses atrás, ao tentar otimizar minha primeira apresentação. Para os casos de teste que existiam na época, ele não ofereceu uma melhoria, então eu não o enviei, mas, para o novo mapa de kuroi, ele parece apenas se espremer sob o limite de memória. Ainda estou esperando que o solucionador de kuroi supere isso, mas estou interessado em como ele se mantém.

# 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()

Isto às vezes falhar em caso 12 e 13. Não sei por que, desde as mensagens de erro são um pouco ... hostil
Ray

@ Ray Também recebo mensagens de erro, mas sempre recebo o resultado para elas. Eu acho que pode ser algo no meu controlador, porque parece que o controlador tenta matar o processo do piloto, embora já tenha terminado.
Martin Ender

@ m.buettner Encontrei o motivo, adicione -s, então tudo ficará bem.
Raio

@ Ray Oh sim, eu estou fazendo isso. Ainda recebo um erro nas faixas 13 e 14 quando o controlador está tentando interromper o processo, embora o resultado já esteja lá. Acho que devo investigar isso, mas não afeta a pontuação, por isso não me incomodei ainda.
Martin Ender

Infelizmente, tive que adicionar outra regra. A memória parece ser mais limitadora do que o tempo neste desafio, por isso tive que limitar o consumo de memória. Qualquer corrida em que seu piloto use mais de 1 GB de memória será abortada para o mesmo efeito que exceder o limite de tempo. Para o conjunto atual de faixas, sua pontuação não foi afetada por essa alteração. (Acho que você atingirá esse limite nos desempate n = 400.) Entre em contato se aplicar alguma otimização para que eu possa executar novamente os testes.
Martin Ender

3

Python 3: 6.49643 (ideal, BFS)

Para o antigo arquivo de referência de 20 casos, obteve uma pontuação de 5,35643. A solução da @nneonneo não é ótima, pois obteve a versão 5.4. Alguns erros talvez.

Esta solução usa o BFS para pesquisar o gráfico, cada estado de pesquisa está na forma de (x, y, dx, dy). Então eu uso um mapa para mapear de estados para distâncias. No pior dos casos, a complexidade do tempo e do espaço é O (n ^ 2 m ^ 2). Isso raramente acontecerá, pois a velocidade não será muito alta ou o piloto cairá. Na verdade, custou 3 segundos na minha máquina para concluir todos os 22 casos de teste.

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

# Resultados

± % 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

Sim, de acordo com o comentário do usuário2357112, há um erro na prevenção do ciclo do nneonneo. Até onde eu sei, a velocidade é limitada, o O(√n)que tornaria sua implementação O(n³)em grades quadradas (o mesmo que os outros, suponho). Vou adicionar um desempate para pontuar sua inscrição contra as de user2357112 mais tarde hoje.
Martin Ender

Btw, você planeja adicionar outro caso de teste?
Martin Ender

@ m.buettner Não, eu não tenho uma compreensão boa o suficiente para este jogo. Portanto, meu testcase não será interessante.
Raio

Infelizmente, tive que adicionar outra regra. A memória parece ser mais limitadora do que o tempo neste desafio, por isso tive que limitar o consumo de memória. Qualquer corrida em que seu piloto use mais de 1 GB de memória será abortada para o mesmo efeito que exceder o limite de tempo. Com essa regra, seu envio é o primeiro a exceder esse limite em um desempate de tamanho n=270, e é por isso que agora você está atrás dos outros dois envios "ideais". Dito isto, sua submissão também é a mais lenta das três, portanto seria a terceira de qualquer maneira, apenas com um desempate maior.
Martin Ender

Entre em contato se você aplicar alguma otimização para que eu possa executar novamente os testes.
Martin Ender

1

RandomRacer, ~ 40.0 (média de 10 execuções)

Não é que esse bot nunca termine uma faixa, mas definitivamente com muito menos frequência do que uma vez em 10 tentativas. (Eu recebo uma pontuação que não é do pior caso a cada 20 a 30 simulações.)

Isso serve principalmente como um caso de linha de base e para demonstrar uma possível implementação (Ruby) para um piloto:

# 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

Execute-o com

ruby controller.rb benchmark.txt ruby randomracer.rb

1

Corredor aleatório 2.0, ~ 31

Bem, isso não vai superar o solucionador ideal publicado, mas é uma ligeira melhora em um piloto aleatório. A principal diferença é que esse corredor apenas considera ir aleatoriamente onde não há um muro, a menos que fique sem lugares válidos para se mover e, se puder passar para um objetivo que vire, ele o fará. Ele também não será movido para permanecer no mesmo local, a menos que não haja outro movimento disponível (improvável, mas possível).

Implementado em Java, compilado com java8, mas Java 6 deve estar bem. Sem parâmetros de linha de comando. Há uma hierarquia muito boa de cluster, então acho que estou fazendo java corretamente.

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

Os resultados (o melhor caso que já vi)

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

Sim, consegui executá-lo, embora eu tenha que executá-lo no diretório em que o .classarquivo está por algum motivo (em vez do diretório em que o controlador está). Ping-me (com um comentário) se você decidir adicionar um caso de teste, para que eu possa adicioná-lo à referência. Sua pontuação foi de 33 em 10 corridas (consulte a tabela de classificação), mas isso pode mudar a cada nova pista de teste adicionada ao benchmark.
Martin Ender

Também consegui rodar a partir do outro diretório. Para aqueles que não estão familiarizados com Java na linha de comando:java -cp path/to/class/file VectorRacing
Martin Ender

Ah, sim, eu fiz muitas aulas (13, para ser exato). Eu estava sempre executando o seu script no meu diretório de classes, então não testei isso. Posso fazer um caso de teste, mas acho que tentarei fazer um piloto que não seja aleatório primeiro.
pseudonym117

Certo. Se o fizer, adicione-o como uma resposta separada. (E sinta-se livre para adicionar um caso de teste com cada um deles.)
Martin Ender

Infelizmente, tive que adicionar outra regra. A memória parece ser mais limitadora do que o tempo neste desafio, por isso tive que limitar o consumo de memória. Qualquer corrida em que seu piloto use mais de 1 GB de memória será abortada para o mesmo efeito que exceder o limite de tempo. Para o conjunto atual de faixas, sua pontuação não foi afetada por essa alteração (e provavelmente nunca será).
Martin Ender
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.