Куда ушел этот зародыш?


21

Вступление

Вы биолог, изучающий закономерности движения бактерий. У вашей исследовательской группы их куча в чашке Петри, а вы записываете их активность. К сожалению, вы серьезно недофинансированы и не можете позволить себе видеокамеру, поэтому вы просто фотографируете блюдо через равные промежутки времени. Ваша задача - составить программу, которая отслеживает движения микробов по этим картинкам.

вход

Ваши входные данные представляют собой два двумерных массива символов в любом приемлемом формате, представляющих последовательные изображения чашки Петри. В обоих массивах символ .представляет собой пустое пространство и Oпредставляет собой зародыш (вы можете выбрать любые два различных символа, если хотите). Кроме того, массив «после» получается из массива «до» путем перемещения некоторых микробов на один шаг в одном из четырех основных направлений; в частности, массивы имеют одинаковую форму. Микробы движутся одновременно, поэтому один из них может переместиться в пространство, в котором уже содержится другой микроб, если он уходит с дороги. Гарантируется, что границы массива «before» содержат только пустые пробелы и что есть хотя бы один зародыш. Таким образом, следующее является допустимой парой входных данных:

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

Выход

Ваш вывод представляет собой один двумерный массив символов в том же формате, что и входные данные. Он получается из массива «before» путем замены тех микробов, которые переместились, на один из >^<v, в зависимости от направления движения (вы также можете использовать любые 4 различных символа здесь). Возможных выходов может быть несколько, но вы должны указать только один из них. В приведенном выше примере одним из возможных правильных выходных данных является

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

В выходных данных допускается ненужное перемещение, и микробы могут поменяться местами, поэтому действует также следующее:

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

Правила и оценки

Вы можете написать полную программу или функцию. Побеждает меньшее количество байтов, и стандартные лазейки запрещены.

Мне интересны относительно эффективные алгоритмы, но я не хочу полностью запрещать грубое принуждение. По этой причине есть бонус -75% за решение последнего теста в течение 10 минут на современном процессоре (я не могу тестировать большинство решений, поэтому я просто доверю вам здесь). Отказ от ответственности: я знаю, что существует быстрый алгоритм (поиск «проблема непересекающихся путей»), но я сам не реализовал его.

Дополнительные тестовые случаи

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

Просто чтобы быть уверенным, микробы могут двигаться только на одну или ноль клеток, верно?
Домино

@JacqueGoupil Да, это правильно. Каждый из них >^<vсоответствует движению ровно на один шаг в соответствующем направлении.
Згарб

Я еще не пытался решить ее, но вот инструмент для создания дополнительных тестовых случаев :) jsfiddle.net/xd2xns64/embedded/result
Domino

О, осторожно, есть шанс, что скрипт будет зацикливаться вечно, если он попытается переместить все ячейки к краю, но тогда краевым ячейкам некуда идти.
Домино

Ответы:


3

Октава, 494 496 байтов - бонус 372 байта = 124 байта

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

В этом ответе еще предстоит много поиграть в гольф, но я хотел получить объяснение без загадок.

Я видел это как проблему удовлетворения ограничений, поэтому я выбрал свою любимую эвристику локального поиска, Min-конфликты . Идея заключается в том, чтобы с учетом начального размещения каждого зародыша в достижимом месте назначения выбрать случайный зародыш, который занимает ту же ячейку назначения, что и один или несколько других зародышей, и переместить его в действительную ячейку, в которой уже есть минимум других зародышей. Повторите по мере необходимости, пока размещение не соответствует цели.

Интересно, что этот алгоритм не гарантированно завершится (если цель недостижима, он будет продолжаться, например, бесконечно), но если он завершится, он гарантированно даст правильное решение.

Вот код:

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

Выход для последнего теста:

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

Среднее затраченное время составляло менее 9 секунд 1 секунда * на 5-летнем Core i5, который претендовал на бонус.

Я пытаюсь заставить это работать на ideone, но у меня, как мне кажется, возникают проблемы с тем, как он обрабатывает вложенные функции. (Вот ссылка на нерабочий ideone для справки: http://ideone.com/mQSwgZ )
Код на ideone теперь работает. Мне пришлось принудительно настроить все переменные на глобальный, что не требовало их локального запуска.

* В моей версии без заглядывания было замечено, что один из шагов был неэффективным, поэтому я попытался проверить, могу ли я ускорить выполнение, и для 2 добавленных байтов время выполнения теперь меньше секунды. Код и пример выходных данных были обновлены, а входные данные для ideone были заменены на последний контрольный пример.


3

Python, 1171 байт - бонус 878,25 байт = 292,75 байт

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

Идеальная ссылка: http://ideone.com/0Ylmwq

В среднем занимает от 1 до 8 секунд на последнем тестовом примере, что дает право на бонус.

Это моя первая подача кода в гольф, так что, вероятно, это не самая лучшая игра в гольф. Тем не менее, это был интересный вызов, и мне это очень понравилось. @Beaker заслуживает упоминания за напоминание о том, что эвристический поиск - это вещь. До того, как он опубликовал свое решение и вдохновил меня на повторную проверку, мой перебор был слишком долгим, чтобы претендовать на бонус в последнем тестовом примере (это было порядка 69! Итераций, что является 99-значным числом). .).

Я не хотел прямо копировать решение Beaker, поэтому я решил использовать имитацию отжига для своей эвристики поиска. Эта проблема кажется медленнее минимального конфликта (вероятно, потому что это алгоритм оптимизации, а не алгоритм удовлетворения ограничений), но он все еще находится в пределах 10-минутной отметки. Он также имел преимущество в том, что был довольно маленьким, с точки зрения кода. Я потратил на преобразование проблемы гораздо больше байтов, чем на ее решение.

объяснение

Мое решение, вероятно, довольно неэффективно, но у меня возникли проблемы с концепцией, как решить проблему как есть, и поэтому мне пришлось трансформировать ее в другую проблему, которую мне было легче понять. Я понял, что для каждой ячейки в сетке есть четыре варианта:

  • У него не было микробов ни до, ни после, а это значит, что мы можем его игнорировать
  • У него был зародыш до, но не после, что означает, что мы должны найти для него ход.
  • Раньше у него не было микробов, а один после, что также означает, что мы должны найти для него ход.
  • У него был зародыш до и после, что означает, что нам, возможно, придется найти движение, но с другой стороны, возможно, нет.

После разложения данных на эти классы я смог еще больше трансформировать проблему. Для меня сразу стало очевидным, что мне нужно было найти способ доставки микробов из набора «до, но не после» в клетку из набора «после, но не до». Кроме того, микробы могут перемещаться только в одно пространство, поэтому единственный способ для них воздействовать на более отдаленные клетки - это «проталкивать» непрерывный путь микробов в эту клетку. Это означало, что проблема заключалась в поиске X непересекающихся вершин на графе, где каждая ячейка с зародышем была вершиной в указанном графе, а ребра представляли соседние ячейки.

Я решил эту проблему, сначала построив график, описанный выше. Затем я перечислил все возможные пути из каждой ячейки в «До» и каждой ячейки в «После», а затем случайным образом назначил каждой ячейке в «До» один из возможных путей. Наконец, я использовал имитационный отжиг для полуслучайной мутации потенциального решения, пока в конце концов не нашел решение, не имеющее конфликтов ни для одного из путей.

Аннотированная версия

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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.