Каков оптимальный алгоритм разрезания еврейских ногтей?

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

Значительный процент нашей потенциальной базы пользователей, вероятно, будет евреем и, по-видимому, существует традиция не обрезать ногти на ногах (или ногти) в последовательном порядке

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

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

Вот как мы решили указать пальцы ног:

5 4 3 2 1  1 2 3 4 5
Left foot  Right foot

Я написал код для решения проблемы, но используемый алгоритм является субоптимальным: на самом деле производительность худшего случая O (∞). Способ его работы сопоставим с bogosort. Ниже приведено упрощение псевдокода фактического используемого кода:

function GenerateRandomSequence
   sequence = Array[5]
   foreach (item in sequence)
       item = RandomNumberBetween(1,5)
   return sequence

function GetToenailCuttingOrder
   while (true)
      sequence = GenerateRandomSequence()
      if (!AllItemsAreUnique(sequence))
         continue
      if (NoTwoAdjacentItemsHaveConsecutiveNumbers(sequence))
         return sequence

do
    leftFootSequence = GetToenailCuttingOrder()
    rightFootSequence = GetToenailCuttingOrder()
until (leftFootSequence != rightFootSequence &&
       leftFootSequence != leftFootSequenceFromLastRun &&
       rightFootSequence != rightFootSequenceFromLastRun)

В принципе, он генерирует случайные последовательности и проверяет, соответствуют ли они критериям. Если он не соответствует критериям, он начинается. Это не занимает смехотворно много времени, но это очень непредсказуемо.

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

Ответ 1

Вы можете генерировать все возможные последовательности резания ногтей без ограничений, а затем отфильтровывать все последовательности, нарушающие еврейское правило. К счастью, у людей только пять пальцев на ногу *, поэтому их всего 5!= 120 неограниченных последовательностей.

Пример Python:

#seq is only valid when consecutive elements in the list differ by at least two.
def isValid(seq):
    for i in range(len(seq)-1):
        a = seq[i]
        b = seq[i+1]
        if abs(a-b) == 1:
            return False
    return True


from itertools import ifilter, permutations
validseqs = ifilter(isValid, permutations([1,2,3,4,5]))
for i in validseqs:
    print i

(1, 3, 5, 2, 4)
(1, 4, 2, 5, 3)
(2, 4, 1, 3, 5)
(2, 4, 1, 5, 3)
(2, 5, 3, 1, 4)
(3, 1, 4, 2, 5)
(3, 1, 5, 2, 4)
(3, 5, 1, 4, 2)
(3, 5, 2, 4, 1)
(4, 1, 3, 5, 2)
(4, 2, 5, 1, 3)
(4, 2, 5, 3, 1)
(5, 2, 4, 1, 3)
(5, 3, 1, 4, 2)

Чтобы применить правило "нет повторов одной и той же последовательности", вы можете просто выбрать четыре из приведенных выше последовательностей и использовать их поочередно. Единственный улов здесь в том, что если вы считаете два больших пальца "последовательными", то вы не можете выбрать две последовательности, которые заканчиваются и начинаются с 1, соответственно.

* Возможно, вы захотите сделать переменную numberOfToesPerFoot, поэтому ее можно легко изменить позже, если у любого из ваших клиентов будет меньше пальцев, чем вы ожидаете, или больше.

Ответ 2

Существует конечное число последовательностей, которые удовлетворяют вашим требованиям.

  • Сгенерировать все перестановки {1,2,3,4,5}. Всего 120.
  • Отклонить те, которые не удовлетворяют требованиям, и сохранить оставшийся набор (Постоянно).
  • Случайно выберите две разные последовательности. Помните, какие из них вы использовали в прошлый раз.

EDIT: если речь идет не только о носках, но и о некоторой случайной проблеме, когда набор может быть намного больше 5, пространство последовательностей становится очень большим, и вероятность повторения одной и той же последовательности на второй ноге становится очень маленькой, Поэтому случайная генерация последовательностей и отказ от них, если они совпадают, - хорошая идея. Генерирование случайных последовательностей в соответствии с некоторым правилом, например "прыжок по двум или трем, а затем заполнение пробелов", вероятно, будет быстрее, чем генерация случайных перестановок и тестирования, и вероятность перекрытия будет по-прежнему небольшим, если число "пальцев" велико.

Ответ 3

На самом деле, мне нравится ваш оригинальный алгоритм.

Поскольку 14 из 120 перестановок работают, 106 из 120 нет. Таким образом, каждая проверка имеет вероятность сбоя в 106/120.

Это означает, что ожидаемое число отказов:

1*(106/120) + 2*(106/120)^2 + 3*(106/120)^3 + ...

Не слишком сложно суммировать эту бесконечную серию:

S       = 1*x + 2*x^2 + 3*x^3 + ...

Умножить на x:

x*S     =       1*x^2 + 2*x^3 + ...

Вычитание:

S - x*S = x + x^2 + x^3 + ...

Умножьте на x снова и снова вычтите:

x*S - x^2*S = x^2 + x^3 + ...
S - 2*x*S + x^2S = x
S*(1-x)^2 = x
S = x/(1-x)^2

Так как x = 106/120, S = 64,9.

Итак, в среднем для вашего решения требуется всего 65 итераций.

Какова вероятность того, что потребуется, скажем, тысяча итераций?

Ну, вероятность провала какой-либо одной итерации равна 104/120, поэтому вероятность отказа 1000 итераций равна (104/120) ^ 1000, что составляет примерно 10 ^ (- 63). То есть вы никогда не увидите, что это произойдет в вашей жизни и, вероятно, не в жизни Вселенной.

Нет предварительно вычисленных таблиц, легко адаптируется к разным количествам пальцев/пальцев ног/рук/ног, легко адаптируется к различным наборам правил... Что не нравится? Экспоненциальный распад - замечательная вещь.

[обновление]

Упс, я неправильно понял исходную форму... Поскольку мои вероятности не суммируются с 1.: -)

Правильное выражение для ожидаемого числа отказов:

0*(14/120) + 1*(106/120)*(14/120) + 2*(106/120)^2*(14/120) + ...

(Например, чтобы получить ровно два отказа, вам нужно два отказа, за которыми следует успех. Два отказа имеют вероятность (106/120) ^ 2, один успех имеет вероятность (14/120), умножьте их вместе, чтобы получить вес для термина "2".)

Таким образом, мой S выключен в 1 раз (1-x) (т.е. 14/120). Фактическое ожидаемое число отказов равно x/(1-x) = 106/14 = 7.57. Таким образом, в среднем требуется всего 8-9 итераций, чтобы найти решение (7.5 отказов плюс один успех).

Моя математика для случая "1000 неудач" по-прежнему верна.

Ответ 4

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

Вы можете создавать перестановки намного лучше, чем вы это делаете. Вам не нужно делать отбор проб. Используйте Shuffle Fisher Yates на первоначально отсортированной перестановке (1, 2,.. 5), и у вас будет случайная перестановка. http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle

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

Ответ 5

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

Extract[#,Position[Times @@ ([email protected]#-1)&/@ Differences/@ #, [email protected], 1][[2 ;;]]]  
         &@ [email protected]@5

Некоторые объяснения:

[email protected]@5 Calculates all permutations of {1, 2, 3, 4, 5}

Differences          Calculate the differences between adjacent elements
                     (we wish to discard all lists containing +1 or -1)

Times @@ ([email protected]#-1)   Abs turns the -1s into +1s, and then to zeros by subtracting
                     one, then TIMES multiplies all elements, giving zero when 
                     the original result of "Differences" contained a +1 or a -1

Position ... [email protected] Returns the position of the non zero results

Extract              Returns the original elements according to the calculated 
                     positions

Конечный результат:

{{1, 3, 5, 2, 4}, {1, 4, 2, 5, 3}, {2, 4, 1, 3, 5}, {2, 4, 1, 5, 3}, 
 {2, 5, 3, 1, 4}, {3, 1, 4, 2, 5}, {3, 1, 5, 2, 4}, {3, 5, 1, 4, 2}, 
 {3, 5, 2, 4, 1}, {4, 1, 3, 5, 2}, {4, 2, 5, 1, 3}, {4, 2, 5, 3, 1}, 
 {5, 2, 4, 1, 3}, {5, 3, 1, 4, 2}}

Edit

Или, что труднее объяснить, но короче:

Reap[ Table[ If[Times @@ ([email protected]@i - 1) != 0, [email protected]],
           {i, [email protected]@5}]][[2, 1]]

Ответ 6

На самом деле нет причин вводить случайность в эту проблему. Есть только 14 последовательностей, которые удовлетворяют этой проблеме, и, безусловно, некоторый порядок этих последовательностей лучше всего удовлетворяет эстетическому смыслу, который вы пытаетесь приспособить. Таким образом, вы должны просто свести эту проблему к алгоритму для выбора последовательности из этих 14, возможно, в заранее заданном порядке.

Выполнение Javascript алгоритма поиска 14:

function swap (a, i, j) {
  var temp = a[i]
  a[i]=a[j]
  a[j]=temp
}

function permute (b, n, a) {
  if (n==4) {
    b.push(a.slice(0)) //copy array
  }
  else {
    for (var i = n; i < 5; i++) {
      swap(a,n,i)
      permute(b, n+1, a)
      swap(a,n,i)
    }
  }
}

var a = [1,2,3,4,5]
var b = []
var c = []

permute(b,0,a)

for (var i = 1; i < b.length-1; i++) {
  var good = true
  for (var j = 0; j < b[i].length; j++) {
    if (Math.abs(b[i][j-1]-b[i][j]) < 2 || Math.abs(b[i][j]-b[i][j+1]) < 2) {
      good = false
    }
  }
  if (good) c.push(b[i].join(''))
}

console.log(c)

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