Wo ist dieser Keim geblieben?

21

Einführung

Sie sind Biologe und untersuchen die Bewegungsmuster von Bakterien. Ihr Forschungsteam hat ein paar davon in einer Petrischale und Sie zeichnen deren Aktivität auf. Leider sind Sie stark unterfinanziert und können sich keine Videokamera leisten. Machen Sie daher in regelmäßigen Abständen ein Foto des Gerichts. Ihre Aufgabe ist es, ein Programm zu erstellen, das die Bewegungen der Keime aus diesen Bildern nachzeichnet.

Eingang

Ihre Eingaben sind zwei 2D-Zeichenfelder in jedem vernünftigen Format, die aufeinanderfolgende Bilder der Petrischale darstellen. In beiden Arrays stellt das Zeichen ein .Leerzeichen und Oeinen Keim dar (Sie können zwei beliebige Zeichen auswählen, wenn Sie möchten). Außerdem wird das "After" -Array aus dem "Before" -Array erhalten, indem einige Keime um einen Schritt in eine der vier Hauptrichtungen bewegt werden. Insbesondere haben die Arrays die gleiche Form. Die Keime bewegen sich gleichzeitig, sodass sich einer von ihnen in ein Feld bewegen kann, das bereits einen anderen Keim enthielt, wenn er aus dem Weg geht. Es wird garantiert, dass die Ränder des "Vorher" -Arrays nur Leerzeichen enthalten und dass es mindestens einen Keim gibt. Das Folgende ist also ein gültiges Eingangspaar:

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

Ausgabe

Ihre Ausgabe ist ein einzelnes 2D-Array von Zeichen im selben Format wie die Eingaben. Es wird aus dem "Vorher" -Array erhalten, indem die bewegten Keime >^<vje nach Bewegungsrichtung durch eines der folgenden ersetzt werden (Sie können hier auch 4 verschiedene Zeichen verwenden). Es kann mehrere mögliche Ausgaben geben, aber Sie sollten nur eine davon angeben. Im obigen Beispiel ist eine mögliche korrekte Ausgabe

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

Unnötige Bewegung ist in der Ausgabe erlaubt und Keime können Plätze tauschen, so dass auch Folgendes gilt:

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

Regeln und Wertung

Sie können ein vollständiges Programm oder eine Funktion schreiben. Die niedrigste Byteanzahl gewinnt, und Standardlücken sind nicht zulässig.

Ich bin an relativ effizienten Algorithmen interessiert, aber ich möchte das Brute Forcing nicht ganz verbieten. Aus diesem Grund gibt es einen Bonus von -75% für die Lösung des letzten Testfalls innerhalb von 10 Minuten auf einer modernen CPU (ich kann die meisten Lösungen nicht testen, daher vertraue ich Ihnen hier nur). Haftungsausschluss: Ich weiß, dass ein schneller Algorithmus existiert (Suche nach "Disjoint Paths Problem"), habe ihn aber nicht selbst implementiert.

Zusätzliche Testfälle

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
quelle
Nur um sicher zu sein, können sich Keime nur um eine oder null Zellen bewegen, oder?
Domino
@JacqueGoupil Ja, das stimmt. Jedes >^<ventspricht einer Bewegung von genau einem Schritt in die jeweilige Richtung.
Zgarb
Ich habe es noch nicht versucht, aber hier ist ein Tool, um weitere Testfälle zu erstellen :) jsfiddle.net/xd2xns64/embedded/result
Domino
Oh, Vorsicht, es besteht die Möglichkeit, dass das Skript eine Endlosschleife ausführt, wenn versucht wird, alle Zellen an eine Kante zu verschieben, die Kantenzellen dann aber nirgendwo hin müssen.
Domino

Antworten:

3

Oktave, 494 496 Bytes - 372 Bytes Bonus = 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

Es gibt noch viel zu tun mit dieser Antwort, aber ich wollte die ungelöste Erklärung einholen.

Ich sah dies als ein Problem der Beschränkungszufriedenheit, also ging ich zu meiner bevorzugten Heuristik für die lokale Suche über, Min-Konflikte . Die Idee ist, bei einer Startplatzierung mit jedem Keim in einem erreichbaren Ziel einen zufälligen Keim auszuwählen, der dieselbe Zielzelle wie ein oder mehrere andere Keime belegt, und ihn in eine gültige Zelle zu verschieben, in der bereits ein Minimum an anderen Keimen vorhanden ist. Wiederholen Sie diesen Vorgang so oft, bis die Platzierung dem Ziel entspricht.

Interessanterweise ist nicht garantiert, dass dieser Algorithmus beendet wird (wenn das Ziel nicht erreichbar ist, wird es beispielsweise auf unbestimmte Zeit fortgesetzt), aber wenn er beendet wird, wird garantiert, dass eine gültige Lösung erstellt wird.

Hier ist der Code:

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

Ausgabe für den letzten Testfall:

>> 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.

Die durchschnittliche verstrichene Zeit auf einem 5 Jahre alten Core i5, der sich für den Bonus qualifiziert, betrug weniger als 9 Sekunden und 1 Sekunde *.

Ich versuche, dies auf ideone zum Laufen zu bringen, aber ich habe, wie ich glaube, Probleme mit der Art und Weise, wie verschachtelte Funktionen behandelt werden. (Hier ist der nicht funktionierende ideone-Link als Referenz: http://ideone.com/mQSwgZ )
Der Code für ideone funktioniert jetzt. Ich musste alle Variablen auf global setzen, was es unnötig machte, sie lokal auszuführen.

* Ich hatte eine Notiz in meiner ungolfed Version, dass einer der Schritte ineffizient war, also habe ich versucht, die Ausführung zu beschleunigen, und für 2 hinzugefügte Bytes beträgt die Ausführungszeit jetzt weniger als eine Sekunde. Die Code- und Beispielausgabe wurde aktualisiert und die Eingabe in ideone wurde auf den letzten Testfall geändert.

Becherglas
quelle
3

Python, 1171 Bytes - 878,25 Bytes Bonus = 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

Ideone-Link: http://ideone.com/0Ylmwq

Nimmt im Durchschnitt zwischen 1 und 8 Sekunden im letzten Testfall in Anspruch und qualifiziert sich für den Bonus.

Dies ist meine erste Code-Golf-Einsendung, es ist also wahrscheinlich nicht das beste Programm, das es gibt. Trotzdem war es eine interessante Herausforderung und ich habe es sehr genossen. @Beaker verdient eine Erwähnung, um mich daran zu erinnern, dass heuristische Suchen eine Sache sind. Bevor er seine Lösung veröffentlichte und mich dazu inspirierte, meine zu wiederholen, war meine Brute-Force-Suche zu lang, um für den Bonus im letzten Testfall zu qualifizieren (es lag in der Größenordnung von 69! -Iterationen, was einer 99-stelligen Zahl entspricht). .).

Ich wollte Beakers Lösung nicht direkt kopieren, also entschied ich mich für simuliertes Tempern für meine Suchheuristik. Es scheint langsamer als ein Min-Konflikt für dieses Problem zu sein (wahrscheinlich, weil es sich eher um einen Optimierungsalgorithmus als um einen Algorithmus zur Erfüllung von Einschränkungen handelt), aber es liegt immer noch deutlich innerhalb der 10-Minuten-Marke. Es hatte auch den Vorteil, dass es in Bezug auf den Code ziemlich klein war. Ich habe viel mehr Bytes für die Transformation des Problems aufgewendet als für die Suche nach einer Lösung.

Erläuterung

Meine Lösung ist wahrscheinlich byteweise ziemlich ineffizient, aber ich hatte Probleme damit, mir vorzustellen, wie ich das Problem so lösen soll, wie es ist, und musste es schließlich in ein anderes Problem umwandeln, das für mich leichter zu verstehen war. Ich habe festgestellt, dass es für jede Zelle im Raster vier Möglichkeiten gibt:

  • Es hatte vorher oder nachher keinen Keim, was bedeutet, dass wir es ignorieren können
  • Es hatte einen Keim davor, aber nicht danach, was bedeutet, dass wir einen Zug dafür finden müssen.
  • Es hatte vorher keinen Keim, aber einen nachher, was auch bedeutet, dass wir einen Zug dafür finden müssen.
  • Es hatte vorher und nachher einen Keim, was bedeutet, dass wir vielleicht einen Zug dafür finden müssen, aber andererseits vielleicht auch nicht.

Nachdem ich die Daten in diese Klassen zerlegt hatte, konnte ich das Problem weiter transformieren. Mir war sofort klar, dass ich einen Weg finden musste, um einen Keim aus dem Set "vorher aber nicht nachher" in eine Zelle im Set "nachher aber nicht nachher" zu liefern. Außerdem können sich Keime nur um ein Feld bewegen, sodass sie nur dann auf weiter entfernte Zellen einwirken können, wenn sie einen ungebrochenen Pfad von Keimen in diese Zelle "hineinschieben". Dies bedeutete, dass das Problem darin bestand, in einem Graphen X durch Scheitelpunkte getrennte Pfade zu finden, wobei jede Zelle mit einem Keim ein Scheitelpunkt in dem Graphen war und die Kanten benachbarte Zellen darstellten.

Ich habe das Problem gelöst, indem ich zuerst das oben erläuterte Diagramm erstellt habe. Ich habe dann jeden möglichen Pfad von jeder Zelle in Before und jeder Zelle in After aufgezählt und dann jeder Zelle in Before zufällig einen ihrer möglichen Pfade zugewiesen. Schließlich habe ich simuliertes Tempern verwendet, um die potenzielle Lösung halb zufällig zu mutieren, bis ich schließlich eine Lösung gefunden habe, die für keinen der Pfade Konflikte aufweist.

Kommentierte Version

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

quelle