Gdzie poszedł ten zarodek?

21

Wprowadzenie

Jesteś biologiem badającym wzorce ruchowe bakterii. Twój zespół badawczy ma ich kilka na szalce Petriego, a ty rejestrujesz ich aktywność. Niestety, jesteś poważnie niedofinansowany i nie możesz sobie pozwolić na kamerę wideo, więc po prostu rób zdjęcia anteny w regularnych odstępach czasu. Twoim zadaniem jest stworzenie programu, który śledzi ruch zarazków z tych zdjęć.

Wkład

Twoje dane wejściowe to dwie tablice 2D znaków w dowolnym rozsądnym formacie, reprezentujące kolejne zdjęcia płytki Petriego. W obu tablicach znak .reprezentuje pustą przestrzeń i Oreprezentuje zarodek (możesz wybrać dowolne dwa różne znaki, jeśli chcesz). Również tablica „po” jest uzyskiwana z tablicy „przed” poprzez przesunięcie niektórych zarazków o jeden krok w jednym z czterech głównych kierunków; w szczególności tablice mają ten sam kształt. Zarazki poruszają się jednocześnie, więc jeden z nich może przenieść się na przestrzeń, która już zawierała inny zarazek, jeśli zejdzie z drogi. Gwarantuje się, że granice tablicy „przed” zawierają tylko puste spacje i że istnieje co najmniej jeden zarodek. Dlatego następująca poprawna para danych wejściowych:

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

Wydajność

Twój wynik to pojedyncza tablica 2D znaków w tym samym formacie co dane wejściowe. Uzyskuje się go z tablicy „przed” poprzez zamianę zarazków, które przesunęły się na jedną >^<v, w zależności od kierunku ruchu (możesz tutaj użyć dowolnych 4 różnych znaków). Może być kilka możliwych wyników, ale powinieneś podać tylko jedno z nich. W powyższym przykładzie jedną z możliwych poprawnych danych wyjściowych jest

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

Na wyjściu dozwolony jest niepotrzebny ruch, a zarazki mogą zamieniać się miejscami, więc obowiązuje również:

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

Zasady i punktacja

Możesz napisać pełny program lub funkcję. Wygrywa najniższa liczba bajtów, a standardowe luki są niedozwolone.

Interesują mnie stosunkowo wydajne algorytmy, ale nie chcę całkowicie blokować brutalnego wymuszania. Z tego powodu istnieje premia w wysokości -75% za rozwiązanie ostatniego przypadku testowego w ciągu 10 minut na nowoczesnym procesorze (nie jestem w stanie przetestować większości rozwiązań, więc po prostu wam tutaj ufam). Oświadczenie: Wiem, że istnieje szybki algorytm (wyszukaj „problem z rozłącznymi ścieżkami”), ale sam go nie wdrożyłem.

Dodatkowe przypadki testowe

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
źródło
Dla pewności zarazki mogą poruszać się tylko o jedną lub zero komórek, prawda?
Domino
@JacqueGoupil Tak, zgadza się. Każdy z nich >^<vodpowiada ruchowi dokładnie jednego kroku w odpowiednim kierunku.
Zgarb
Jeszcze nie próbowałem go rozwiązać, ale oto narzędzie do budowania większej liczby przypadków testowych :) jsfiddle.net/xd2xns64/embedded/result
Domino
Och, ostrożnie, istnieje szansa, że ​​skrypt zapętli się na zawsze, jeśli spróbuje przesunąć wszystkie komórki o krawędź, ale wtedy komórki krawędzi nie mają dokąd pójść.
Domino

Odpowiedzi:

3

Oktawa, 494 496 bajtów - premia 372 bajtów = 124 bajty

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

Ta odpowiedź wciąż wymaga gry w golfa, ale chciałem uzyskać wyjaśnienie bez golfisty.

Widziałem to jako problem z ograniczeniami związanymi z ograniczeniami, więc wybrałem moją ulubioną heurystyczną wyszukiwarkę lokalną, Min-konflikty . Chodzi o to, biorąc pod uwagę początkowe umiejscowienie każdego zarodka w osiągalnym miejscu docelowym, wybierz losowy zarodek, który zajmuje tę samą komórkę docelową co jeden lub więcej innych zarazków i przenieś go do prawidłowej komórki, w której jest już minimum innych zarazków. Powtarzaj w razie potrzeby, aż miejsce docelowe będzie zgodne z celem.

Co ciekawe, nie gwarantuje się, że algorytm ten zakończy się (na przykład, jeśli cel jest nieosiągalny, będzie kontynuowany na czas nieokreślony), ale jeśli zostanie zakończony, gwarantuje się prawidłowe rozwiązanie.

Oto kod:

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

Dane wyjściowe dla ostatniego przypadku testowego:

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

Średni czas, który upłynął, wynosił mniej niż 9 sekund 1 sekunda * na 5-letnim Core i5, co kwalifikuje się do premii.

Staram się, aby to działało na ideone, ale mam problemy, które moim zdaniem dotyczą zakresu, w sposobie, w jaki obsługuje zagnieżdżone funkcje. (Oto niedziałający link ideone w celach informacyjnych: http://ideone.com/mQSwgZ )
Kod na ideone teraz działa. Musiałem wymusić wszystkie zmienne na globalne, co nie było konieczne, aby uruchamiać je lokalnie.

* W mojej nie golfowej wersji miałem notatkę, że jeden z kroków był nieefektywny, więc spróbowałem sprawdzić, czy mogę przyspieszyć wykonanie i dla 2 dodanych bajtów czas wykonania jest teraz krótszy niż sekunda. Kod i przykładowe dane wyjściowe zostały zaktualizowane, a dane wejściowe ideone zostały zmienione na ostatni przypadek testowy.

zlewka
źródło
3

Python, 1171 bajtów - 878,25 bajta premii = 292,75 bajtów

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

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

Ostatni test trwa średnio od 1 do 8 sekund, kwalifikując się do otrzymania premii.

To jest moje pierwsze zgłoszenie do gry w golfa, więc prawdopodobnie nie jest to najlepszy program do gry w golfa. Niemniej było to interesujące wyzwanie i bardzo mi się podobało. @Beaker zasługuje na wzmiankę o przypomnieniu, że wyszukiwania oparte na heurystyce to coś. Zanim opublikował swoje rozwiązanie i zainspirował mnie do ponownego opracowania, moje poszukiwania brutalnej siły były zbyt długie, aby zakwalifikować się do premii za ostatni przypadek testowy (było to rzędu 69! Iteracji, czyli liczby 99-cyfrowej .. .).

Nie chciałem od razu kopiować rozwiązania Beakera, więc zdecydowałem się użyć symulowanego wyżarzania dla mojej heurystyki wyszukiwania. Wydaje się, że ten problem jest wolniejszy niż minimalny konflikt (prawdopodobnie dlatego, że jest to algorytm optymalizacji, a nie ograniczenie satysfakcji), ale wciąż jest w granicach 10 minut. Miał też tę zaletę, że był dość mały, jeśli chodzi o kod. Wydałem o wiele więcej bajtów na przekształcenie problemu niż na znalezienie rozwiązania.

Wyjaśnienie

Moje rozwiązanie jest prawdopodobnie dość nieefektywne bajtowo, ale miałem problemy z konceptualizacją tego, jak rozwiązać problem w obecnej postaci, więc ostatecznie musiałem go przekształcić w inny problem, który był dla mnie łatwiejszy do zrozumienia. Uświadomiłem sobie, że dla każdej komórki na siatce są cztery możliwości:

  • Nie miał zarazków przed ani po, co oznacza, że ​​możemy to zignorować
  • Miał zarazek wcześniej, ale nie później, co oznacza, że ​​musimy znaleźć ruch.
  • Nie miał wcześniej zarodków, tylko jeden po nich, co oznacza również, że musimy znaleźć ruch.
  • Miał zarazek przed i po, co oznacza, że ​​możemy znaleźć dla niego ruch, ale znowu może nie.

Po rozłożeniu danych na te klasy mogłem dalej przekształcić problem. Natychmiast stało się dla mnie oczywiste, że muszę znaleźć sposób na zarodek z zestawu „przed, ale nie po” do zestawu w zestawie „po, ale nie przed”. Co więcej, zarazki mogą poruszać się tylko o jedną przestrzeń, więc jedynym sposobem na oddziaływanie na odległe komórki jest „popychanie” nieprzerwanej ścieżki zarazków do tej komórki. Oznaczało to, że problemem stało się znalezienie ścieżek rozłącznych X wierzchołków na wykresie, gdzie każda komórka z zarodkiem była wierzchołkiem na tym wykresie, a krawędzie reprezentowały sąsiednie komórki.

Rozwiązałem ten problem, budując najpierw wyjaśniony powyżej wykres. Następnie wyliczyłem każdą możliwą ścieżkę z każdej komórki w przed i każdej komórki w po, a następnie losowo przypisałem każdą komórkę w przed jedną z możliwych ścieżek. W końcu użyłem symulowanego wyżarzania, aby częściowo losowo mutować potencjalne rozwiązanie, aż w końcu znajdę rozwiązanie, które nie ma konfliktów dla żadnej ze ścieżek.

Wersja z adnotacjami

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

źródło