¿A dónde fue ese germen?

21

Introducción

Usted es biólogo y estudia los patrones de movimiento de las bacterias. Su equipo de investigación tiene un montón de ellos en una placa de Petri, y usted está registrando su actividad. Desafortunadamente, no tiene fondos suficientes y no puede pagar una cámara de video, por lo que simplemente toma una foto del plato a intervalos regulares. Su tarea es hacer un programa que rastree los movimientos de los gérmenes a partir de estas imágenes.

Entrada

Sus entradas son dos matrices 2D de caracteres en cualquier formato razonable, que representan imágenes consecutivas de la placa de Petri. En ambas matrices, el carácter .representa un espacio vacío y Orepresenta un germen (puede elegir dos caracteres distintos si lo desea). Además, la matriz "después" se obtiene de la matriz "antes" moviendo algunos gérmenes un paso en una de las cuatro direcciones cardinales; en particular, las matrices tienen la misma forma. Los gérmenes se mueven simultáneamente, por lo que uno de ellos puede moverse a un espacio que ya contenía otro germen, si se mueve fuera del camino. Se garantiza que los bordes de la matriz "antes" contienen solo espacios vacíos y que hay al menos un germen. Por lo tanto, el siguiente es un par válido de entradas:

Before  After
......  ......
.O..O.  ....O.
.OO.O.  .OO.O.
......  ..O...

Salida

Su salida es una única matriz 2D de caracteres en el mismo formato que las entradas. Se obtiene de la matriz "antes" al reemplazar los gérmenes que se han movido con uno de ellos >^<v, dependiendo de la dirección del movimiento (también puede usar 4 caracteres distintos aquí). Puede haber varias salidas posibles, pero debe dar solo una de ellas. En el ejemplo anterior, una salida correcta posible es

......
.v..O.
.>v.O.
......

Se permite un movimiento innecesario en la salida y los gérmenes pueden intercambiar lugares, por lo que lo siguiente también es válido:

......
.v..v.
.>v.^.
......

Reglas y puntaje

Puede escribir un programa completo o una función. El conteo de bytes más bajo gana, y las lagunas estándar no se permiten.

Estoy interesado en algoritmos relativamente eficientes, pero no quiero prohibir por completo la fuerza bruta. Por esta razón, hay una bonificación del -75% para resolver el último caso de prueba en 10 minutos en una CPU moderna (no puedo probar la mayoría de las soluciones, así que confiaré en ti aquí). Descargo de responsabilidad: sé que existe un algoritmo rápido (buscar "problema de rutas disjuntas"), pero no lo he implementado yo mismo.

Casos de prueba adicionales

Before
......
.O..O.
..OO..
......
After
......
..O...
...OO.
..O...
Possible output
......
.>..v.
..vO..
......

Before
.......
.OOOOO.
.O..OO.
.OO..O.
.OOOOO.
.......
After
.......
..OOOOO
.O...O.
.O...O.
.OOOOOO
....O..
Possible output
.......
.>>>>>.
.O..>v.
.Ov..v.
.O>>v>.
.......

Before
..........
.OOO..OOO.
.OOOOOOOO.
.OOO..OOO.
..........
After
..O.......
.OOO..O.O.
..OOOOOOOO
.O.O..OOO.
.......O..
Possible output
..........
.>^O..O>v.
.^O>>>vO>.
.O>^..>vO.
..........

Before
............
.OO..OOOOOO.
.OO......OO.
...OOOOOO...
.O.OOOOOO.O.
...OOOOOO...
.OOOOOOOOOO.
............
After
..........O.
.OO..OOOOO..
.O...O...O..
.O.OOOOOOO..
.O.OOOOOO..O
...OO..OO...
....OOOOOOOO
.OOO........
Possible output
............
.OO..v<<<<^.
.v<......^<.
...OOO>>>...
.O.OOO^OO.>.
...OOv^OO...
.vvvO>>>>>>.
............

Before
................
.OOOOOO.OOOOOOO.
..OO..OOOOOOOOO.
.OOO..OOOO..OOO.
..OOOOOOOO..OOO.
.OOOOOOOOOOOOOO.
................
After
................
..OOOOO.OOOOOOOO
..OO..OOOOOOOOO.
..OO..OOOO..OOOO
..OOOOOOOO..OOO.
..OOOOOOOOOOOOOO
................
Possible output
................
.>>>>>v.>>>>>>>.
..OO..>>^>>>>>v.
.>>v..OOO^..OO>.
..O>>>>>>^..OOO.
.>>>>>>>>>>>>>>.
................

Before
..............................
.OOO.O.O.....O.....O.O.O..O...
..OOO.O...O..OO..O..O.O.......
.....O......O..O.....O....O...
.O.OOOOO......O...O..O....O...
.OO..O..OO.O..OO..O..O....O...
..O.O.O......OO.OO..O..OO.....
..O....O..O.OO...OOO.OOO...O..
.....O..OO......O..O...OO.OO..
........O..O........OO.O.O....
..O.....OO.....OO.OO.......O..
.O.....O.O..OO.OO....O......O.
..O..OOOO..O....OO..........O.
.O..O...O.O....O..O....O...OO.
....O...OO..O.......O.O..OO...
........O.O....O.O....O.......
.OO.......O.OO..O.......O..O..
....O....O.O.O...OOO..O.O.OO..
.OO..OO...O.O.O.O.O...OO...O..
..............................
After
..............................
.OOOOO.......OO.....O..O......
...OO..O...O...O....OO....O...
....O.O......O..OO...OO...O...
.OO.OOOO......OO..O..O........
O.O.OO..O..O..O..OO...O...OO..
.OO.....O....OO.O..O.OO.O.....
......O.....O.....OOO.OO...O..
....O..OOOO..O..O..O.O.O.OO...
..O......O.O........O...O.O...
.O.....OOO.....OO.OO...O...O..
.......OOO..O.O.O...........O.
.O...O.....O...OOOO..O.O....O.
.O..O.O..O.....O......O....OO.
....O..O..O.O......O.....O....
........OOO....O......O..O....
.OO......O..OO..OOO.....O..O..
..O.O....OO..O...OO...O...OO..
.O..OO....O..O...O.O.O.OO.....
..............O............O..
Possible output
..............................
.OOO.O.v.....>.....>.v.O..v...
..>>^.v...>..^>..v..O.v.......
.....<......>..>.....O....O...
.O.<O><O......O...O..O....v...
.<O..O..v<.O..O^..O..>....>...
..<.^.v......OO.O^..>..<O.....
..^....v..v.Ov...>>^.<OO...O..
.....<..OO......O..O...Ov.v<..
........>..O........O^.v.^....
..^.....Ov.....OO.OO.......O..
.^.....^.^..O>.vO....v......O.
..<..Ov^^..O....><..........O.
.O..O...>.v....O..^....^...OO.
....O...<v..O.......<.^..v<...
........O.O....O.v....O.......
.OO.......<.Ov..O.......O..O..
....O....O.<.^...O^v..O.v.OO..
.O^..<<...O.>.v.>.^...<O...v..
..............................
Zgarb
fuente
Solo para estar seguro, los gérmenes solo pueden moverse por una o cero celdas, ¿verdad?
Domino
@JacqueGoupil Sí, eso es correcto. Cada uno >^<vcorresponde a un movimiento de exactamente un paso en la dirección respectiva.
Zgarb
No intenté resolverlo todavía, pero aquí hay una herramienta para construir más casos de prueba :) jsfiddle.net/xd2xns64/embedded/result
Domino
Oh, cuidado, existe la posibilidad de que el script se repita para siempre si intenta mover todas las celdas contra un borde, pero luego las celdas del borde no tienen a dónde ir.
Domino

Respuestas:

3

Octava, 494 496 bytes - Bonificación de 372 bytes = 124 bytes

function o=G(b,a)
y='.O<^v>';s=(b>46)+0;t=a>46;v=t;f=s;t(:,2:end,2)=t(:,1:end-1);t(2:end,:,3)=t(1:end-1,:,1);t(1:end-1,:,4)=t(2:end,:,1);t(:,1:end-1,5)=t(:,2:end,1);t=reshape(t,[],5);m=size(s,1);p=[0 -m -1 1 m];
function z(n)
f(n+p(s(n)))--;q=find(t(n,:));w=n+p(q);d=min(f(w));q=q(f(w)==d);j=randi(numel(q));s(n)=q(j);f(n+p(q(j)))++;end
for g=find(s)' z(g);end
while any((f~=v)(:)) L=find(s);k=zeros(size(s));for h=L' k(h)=f(h+p(s(h)));end;c=find(k>1);g=c(randi(numel(c)));z(g);end
o = y(s+1);end

Todavía hay mucho golf por hacer en esta respuesta, pero quería obtener la explicación sin fundamento.

Vi esto como un problema de satisfacción de restricciones, así que elegí mi heurística de búsqueda local favorita, Min-conflict . La idea es que, dada una ubicación inicial con cada germen en un destino accesible, seleccione un germen aleatorio que ocupe la misma celda de destino que uno o más de otros gérmenes y muévalo a una celda válida que ya tenga un mínimo de otros gérmenes. Repita según sea necesario hasta que la colocación coincida con el objetivo.

Curiosamente, no se garantiza que este algoritmo termine (si el objetivo es inalcanzable, continuará indefinidamente, por ejemplo), pero si termina, se garantiza que producirá una solución válida.

Aquí está el código:

function output = germs(before, after)

%before = ['......';'.O..O.';'.OO.O.';'......'];
%after = ['......';'....O.';'.OO.O.';'..O...'];

symbs = '.O<^v>';
start = (before > 46) + 0;                   %should be called current_board
target = after > 46;                         %destinations on current cell == O
goal = target;
conflicts = start;
target(:, 2:end,2) = target(:, 1:end-1);     %destinations on cell to left
target(2:end, :,3) = target(1:end-1, :,1);   %destinations on cell above
target(1:end-1, :,4) = target(2:end, :,1);   %destinations on cell below
target(:, 1:end-1,5) = target(:, 2:end,1);   %destinations on cell to right
target=reshape(target,[],5);
m = size(start,1);                           %number of rows = offset to previous/next column
offsets = [0 -m -1 1 m];                     %offsets of neighbors from current index


function moveGerm(n)
   conflicts(n+offsets(start(n)))--;         %take germ off board
   move = find(target(n, :));                %get valid moves for this germ
   neighbors = n + offsets(move);            %valid neighbors = current position + offsets
   minVal = min(conflicts(neighbors));       %minimum number of conflicts for valid moves
   move = move(conflicts(neighbors)==minVal);
   mi = randi(numel(move));                  %choose a random move with minimum conflicts
   start(n) = move(mi);                      %add move type to board
   conflicts(n + offsets(move(mi)))++;       %add a conflict on the cell we move to
end

% Generate an initial placement
for g = find(start)'
   moveGerm(g);                              %make sure all germs are moved to valid cells
end

% Repeat until board matches goal
while any((conflicts ~= goal)(:))
   germList = find(start);                   %list of all our germs
   cost = zeros(size(start));                %calculate conflicts for each germ
   for h = germList'
      cost(h) = conflicts(h + offsets(start(h)));
   end
   conflicted = find(cost > 1);              %find those germs that occupy the same cell as another
   g = conflicted(randi(numel(conflicted))); %choose a random germ to move
   moveGerm(g);
end

output = symbs(start+1);                     %use moves as indices into symbol array for output

end

Salida para el último caso de prueba:

>> gtest
ans =

..............................
.OO>.O.v.....>.....>.v.O..v...
..>^O.v...>..^>..v..O.v.......
.....v......>..>.....O....O...
.O.<^<OO......>...O..O....v...
.<O..O..v<.O..^<..O..>....>...
..<.^.v......OO.O^..<..<O.....
..^....v..v.Ov...>>>.^OO...O..
.....<..OO......O..O...Ov.<<..
........>..O........O^.v.>....
..^.....OO.....OO.OO.......O..
.^.....^.O..O>.vO....v......O.
..<..Ov^^..O....OO..........O.
.O..O...>.v....O..^....^...OO.
....O...<v..O.......<.^..v<...
........O.O....O.v....O.......
.OO.......<.OO..O.......O..O..
....O....O.<.O...O^<..O.v.OO..
.O^..<<...O.>.v.>.>...<O...v..
..............................

Elapsed time is 0.681691 seconds.

El tiempo promedio transcurrido fue inferior a 9 segundos 1 segundo * en un Core i5 de 5 años, calificando para el bono.

Estoy tratando de hacer que esto funcione en ideone, pero estoy teniendo lo que creo que son problemas de alcance con la forma en que maneja las funciones anidadas. (Aquí está el enlace ideone que no funciona para referencia: http://ideone.com/mQSwgZ )
El código en ideone ahora está funcionando. Tuve que forzar todas las variables a global, lo que era innecesario ejecutarlo localmente.

* Tuve una nota en mi versión no escrita de que uno de los pasos era ineficiente, así que lo intenté para ver si podía acelerar la ejecución y para 2 bytes adicionales el tiempo de ejecución ahora es menos de un segundo. El código y la salida de muestra se han actualizado y la entrada en ideone se ha cambiado al último caso de prueba.

cubilete
fuente
3

Python, 1171 bytes - 878.25 bytes de bonificación = 292.75 bytes

from itertools import *;from random import *;R=range;L=len;O=choice;G='O'
def A(a,b):a.append(b)
def D(y,z):
 a=[];b=[];c=[]
 for i in R(L(y)):
  A(c,[])
  for j in R(L(y[0])):
   k=[(i,j),y[i][j]==G,z[i][j]==G,[],0];A(c[i],k)
   for l,m in [(0,1),(1,0)]:
    try:
     n=c[i-l][j-m]
     if k[2]&n[1]:A(n[3],k)
     if k[1]&n[2]:A(k[3],n)
    except:pass
   if k[1]&~k[2]:A(a,k)
   elif k[2]&~k[1]:A(b,k)
 d={}
 for i in a:
  j=[[i]]
  while j:
   k=j.pop();l=[e[0] for e in k]
   while True:
    m=k[-1];n=[o for o in m[3] if o[0] not in l]
    if not n:
     if m in b:A(d.setdefault(i[0],[]),k)
     break
    for o in n[1:]:p=k[:];A(p,o);A(j,p)
    A(k,n[0]);A(l,n[0][0])
 e={}
 for i in a:e[i[0]]=O(d[i[0]])
 def E():return sum(any(k in j for k in i) for i,j in combinations(e.values(),2))
 f=E()
 for i in count():
  t=3**-i/L(a);j=O(a);k=e[j[0]];e[j[0]]=O(d[j[0]]);l=E()
  if not l:break
  else:
   if l>f and random()>t:e[j[0]]=k
   else:f=l
 for i in e.values():
  for j in R(L(i)-1):i[j][4]=i[j+1]
 for i in c:
  for j in R(L(i)):
   k=i[j]
   if 1&~k[1]:i[j]='.'
   elif not k[4]:i[j]=G
   else:l,m=k[0];n,o=k[4][0];i[j]='v>^<'[abs((l-n+1)+2*(m-o))]
 return c

Enlace de ideona: http://ideone.com/0Ylmwq

Toma de 1 a 8 segundos en promedio en el último caso de prueba, calificando para el bono.

Esta es mi primera presentación de código de golf, por lo que probablemente no sea el mejor programa de golf que existe. Sin embargo, fue un desafío interesante y lo disfruté bastante. @Beaker merece una mención por recordarme que las búsquedas basadas en heurística son una cosa. Antes de que publicara su solución y me inspirara a rehacer la mía, mi búsqueda de fuerza bruta fue muuuucho más larga para calificar para la bonificación en el último caso de prueba (¡fue del orden de 69 iteraciones, que es un número de 99 dígitos ... .).

No quería copiar directamente la solución de Beaker, así que decidí usar recocido simulado para mi búsqueda heurística. Parece más lento que el conflicto mínimo para este problema (probablemente porque es un algoritmo de optimización en lugar de uno de restricción-satisfacción), pero todavía está dentro de los 10 minutos. También tenía el beneficio de ser bastante pequeño, en cuanto a código. Gasté muchos más bytes en transformar el problema que en encontrar una solución.

Explicación

Probablemente mi solución sea bastante ineficiente en cuanto a bytes, pero tuve problemas para conceptualizar cómo resolver el problema tal como está y, por lo tanto, terminé teniendo que transformarlo en un problema diferente que me resultó más fácil de entender. Me di cuenta de que hay cuatro posibilidades para cada celda en la cuadrícula:

  • No tenía germen antes ni después, lo que significa que podemos ignorarlo
  • Tenía un germen antes pero no después, lo que significa que tenemos que encontrar un movimiento para ello.
  • No tenía germen antes, sino uno después, lo que también significa que tenemos que encontrar un movimiento para ello.
  • Tenía un germen antes y después, lo que significa que podríamos tener que encontrar un movimiento para ello, pero quizás no.

Después de descomponer los datos en esas clases, pude transformar aún más el problema. Para mí fue inmediatamente obvio que tenía que encontrar una manera de suministrar un germen del conjunto "antes pero no después" a una celda en el conjunto "después pero no antes". Además, los gérmenes solo pueden moverse un espacio, por lo que la única forma de que afecten a las células más alejadas es "empujando" una ruta ininterrumpida de gérmenes hacia esa célula. Eso significaba que el problema se convirtió en encontrar X caminos separados de vértices en un gráfico, donde cada celda con un germen era un vértice en dicho gráfico, y los bordes representaban celdas adyacentes.

Resolví ese problema que al construir primero el gráfico explicado anteriormente. Luego enumeré cada ruta posible desde cada celda en Antes y cada celda en Después, luego asigné aleatoriamente cada celda en Antes una de sus posibles rutas. Finalmente, utilicé el recocido simulado para mutar semialeatoriamente la solución potencial hasta que finalmente encuentro una solución que no tenga conflictos para ninguno de los caminos.

Versión anotada

from itertools import *;from random import *;

# redefine some built-in functions to be shorter
R=range;L=len;O=choice;G='O'
def A(a,b):a.append(b)

# The function itself.  Input is in the form of two 2d arrays of characters, one each for before and after.
def D(y,z):
 # Declare the Before-but-not-after set, the After-but-not-before set, and a temp cell array
 # (the cells are temporarily stored in a 2d array because I need to be able to locate neighbors)
 a=[];b=[];c=[]

 # Build the graph
 for i in R(L(y)):
  # Append a row to the 2d temp array
  A(c,[])

  for j in R(L(y[0])):
   # Define the interesting information about the cell, then add it to the temp array
   # The cell looks like this: [position, does it have a germ before?, does it have a germ after?, list of neighbors with germs, final move]
   k=[(i,j),y[i][j]==G,z[i][j]==G,[],0];A(c[i],k)
   for l,m in [(0,1),(1,0)]:
    # Fill up the neighbors by checking the above and left cell, then mutually assigning edges
    try:
     n=c[i-l][j-m]
     if k[2]&n[1]:A(n[3],k)
     if k[1]&n[2]:A(k[3],n)
    except:pass

   # Decide if it belongs in the Before or After set
   if k[1]&~k[2]:A(a,k)
   elif k[2]&~k[1]:A(b,k)

 # For each cell in the before set, define ALL possible paths from it (this is a big number of paths if the grid is dense with germs)
 # This uses a bastard form of depth-first search where different paths can cross each other, but no path will cross itself
 d={}
 for i in a:
  j=[[i]]  # Define the initial stack of incomplete paths as the starting node.
  while j:
   # While the stack is not empty, pop an incomplete path of the stack and finish it
   k=j.pop();l=[e[0] for e in k]
   while True:
    # Set the list of next possible moves to the neighbors of the current cell,
    # ignoring any that are already in the current path.
    m=k[-1];n=[o for o in m[3] if o[0] not in l]

    # If there are no more moves, save the path if it ends in an After cell and break the loop
    if not n:
     if m in b:A(d.setdefault(i[0],[]),k)
     break

    # Otherwise, set the next move in this path to be the first move,
    # then split off new paths and add them to the stack for every other move
    for o in n[1:]:p=k[:];A(p,o);A(j,p)
    A(k,n[0]);A(l,n[0][0])

 # Perform simulated annealing to calculate the solution
 e={}
 for i in a:e[i[0]]=O(d[i[0]])  # Randomly assign paths for the first potential solution

 # Define a function for calculating the number of conflicts between all paths, then do the initial calculation for the initial potential solution
 def E():return sum(any(k in j for k in i) for i,j in combinations(e.values(),2))
 f=E()

 # Do the annealing
 for i in count():
  # The "temperature" for simulated annealing is calculated as 3^-i/len(Before set).
  # 3 was chosen as an integer approximation of e, and the function e^(-i/len) itself was chosen because
  # it exponentially decays, and does so slower for larger problem sets
  t=3**-i/L(a)

  j=O(a)              # Pick a random Before cell to change
  k=e[j[0]]           # Save it's current path
  e[j[0]]=O(d[j[0]])  # Replace the current path with a new one, randomly chosen
  l=E()               # Recalculate the number of conflicts

  if not l:break  # If there are no conflicts, we have a valid solution and can terminate
  else:           # Otherwise check the temperature to see if we keep the new move
   if l>f and random()>t:e[j[0]]=k  # Always keep the move if it's better, and undo it with probability 1 - T if it's worse
   else:f=l                         # If we don't undo, remember the new conflict count

 # Set each of the cells' final moves based on the paths
 for i in e.values():
  for j in R(L(i)-1):i[j][4]=i[j+1]

 # Build the output in the form of a 2d array of characters
 # Reuse the temp 2d array from step since its the right size
 for i in c:
  for j in R(L(i)):
   k=i[j]
   # Cells that are empty in the before array are always empty in the output
   if 1&~k[1]:i[j]='.'
   # Cells that aren't empty and don't have a move are always germs in the output
   elif not k[4]:i[j]=G
   # Otherwise draw the move
   else:l,m=k[0];n,o=k[4][0];i[j]='v>^<'[abs((l-n+1)+2*(m-o))]
 return c

fuente