Генерация комбинаций из набора пар без повторения элементов

28

У меня есть набор пар. Каждая пара имеет форму (x, y), так что x, y принадлежат целым числам из диапазона [0,n).

Итак, если n равно 4, то у меня есть следующие пары:

(0,1) (0,2) (0,3)
(1,2) (1,3) 
(2,3) 

У меня уже есть пары. Теперь я должен построить комбинацию, используя n/2пары, чтобы ни одно из целых чисел не повторялось (другими словами, каждое целое число появляется хотя бы один раз в финальной комбинации). Ниже приведены примеры правильной и неправильной комбинации для лучшего понимания.

 1. (0,1)(1,2) [Invalid as 3 does not occur anywhere]
 2. (0,2)(1,3) [Correct]
 3. (1,3)(0,2) [Same as 2]

Может кто-нибудь предложить мне способ генерировать все возможные комбинации, как только у меня есть пары.

Анкит
источник
Возможно, используя 2d массив для представления ваших пар. Допустимые комбинации соответствуют выбору из n ячеек массива, так что каждая строка и столбец содержит ровно 1 выбранную ячейку.
Джо
4
Вы говорите, что вход - это набор всех пар? Если это так, то вы должны просто сказать, что ввод просто . N
rgrig
2
Всегда ли ? Если нет, утверждения «ни одно из целых чисел не повторяется» и «каждое целое число появляется хотя бы один раз в окончательной комбинации» противоречивы. N
Дмитрий Кордубан
1
та же проблема, что и у @rgrig: все входные неупорядоченные пары или произвольный набор возможных пар? Если это все пары, то вы можете просто сказать, что ввод , нет необходимости давать список. N
Каве
1
Вы заинтересованы в генерации всех совершенных совпадений графа по точкам, определенным вашим исходным набором пар. Более того, кажется, что вы берете этот граф за полный график этих точек. Ваш вопрос будет яснее, если вы упомянули это. Есть ( n - 1 ) ! ! : = 1 × 3 × 5 × × ( n - 1 ) таких совпадений. N(N-1)!!знак равно1×3×5××(N-1)
Марк ван Леувен

Ответы:

14

Один прямой путь - это рекурсивная процедура, которая выполняет следующие действия при каждом вызове. Входные данные для процедуры - это список пар, которые уже были выбраны, и список всех пар.

  1. Вычислите наименьшее число, которое еще не включено в список ввода. Для первого вызова это будет 0, конечно, потому что пары не были выбраны.
  2. Если все числа покрыты, у вас есть правильная комбинация, распечатайте ее и верните предыдущий шаг. В противном случае наименьшее число, которое будет обнаружено, является целью, к которой мы будем стремиться.
  3. Ищите пары, ища способ покрыть целевое число. Если его нет, просто вернитесь к предыдущему уровню рекурсии.
  4. Если есть способ покрыть целевое число, выберите первый способ и рекурсивно вызовите всю процедуру снова, с только что выбранной парой добавьте в список выбранных пар.
  5. Когда это вернется, ищите следующий способ, чтобы покрыть целевое число парой, не перекрывая ранее выбранную пару. Если вы найдете один, выберите его и снова рекурсивно вызовите следующую процедуру.
  6. Продолжайте шаги 4 и 5, пока не будет больше способов покрыть целевое число. Просмотрите весь список пар. Когда больше нет правильного выбора, вернитесь к предыдущему уровню рекурсии.

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

           корень
             |
     ----------------
     | | |
   (0,1) (0,2) (0,3)
     | | |
   (2,3) (1,3) (1,2)

В этом примере все пути через дерево фактически дают правильные коллекции, но, например, если бы мы пропустили пару (1,2), то самый правый путь имел бы только один узел и соответствовал бы неудаче поиска на шаге 3.

Поисковые алгоритмы этого типа могут быть разработаны для многих похожих задач перечисления всех объектов определенного типа.


Было высказано предположение, что, возможно, ОП означало, что на входе присутствуют все пары, а не только их набор, как говорится в вопросе. В этом случае алгоритм намного проще, потому что больше нет необходимости проверять, какие пары разрешены. Нет необходимости создавать множество всех пар; следующий псевдокод будет делать то, что спросил OP. Здесь - входной номер, «список» начинается с пустого списка, а «охватываемый» - это массив длиной n, инициализированный равным 0. Его можно сделать несколько более эффективным, но это не является моей непосредственной целью.nn

sub cover {
  i = 0;
  while ( (i < n) && (covered[i] == 1 )) {
   i++;
  }
  if ( i == n ) { print list; return;}
  covered[i] = 1;
  for ( j = 0; j < n; j++ ) {
    if ( covered[j] == 0 ) {
      covered[j] = 1;
      push list, [i,j];
      cover();
      pop list;
      covered[j] = 0;
    }
  }
  covered[i] = 0;
}
Карл Муммерт
источник
Это должно работать, но это, вероятно, не самый эффективный способ сделать это.
Джо
2
В конце концов, смысл как-то перечислить пути этого дерева. Если количество пар во входном списке намного меньше, чем количество возможных пар, алгоритм такого рода будет совершенно эффективен, особенно если некоторые хеш-таблицы используются, чтобы помочь запомнить, какие числа уже были покрыты на каждом шаге, так что это может быть запрошено в постоянное время.
Карл Маммерт
Если в списке используются указатели, то «Танцевальные ссылки Кнута» стоит посмотреть. При возврате формы рекурсивный вызов и необходимо восстановить предыдущее состояние списка.
uli
10

Вы можете решить это итеративно. Предположим, у вас есть все решения для диапазона [ 0 , n ) . Тогда вы можете легко построить решения S n + 2 из S n . Размер растет очень быстро с n , поэтому лучше написать генератор, а не хранить все наборы в памяти, см. Пример Python ниже.SN[0,N)SN+2SNN

def pairs(n):
    if (n%2==1 or n<2):
        print("no solution")
        return
    if (n==2):
        yield(  [[0,1]]  )
    else:
        Sn_2 = pairs(n-2) 
        for s in Sn_2:
            yield( s + [[n-2,n-1]] )
            for i in range(n/2-1):
                sn = list(s)
                sn.remove(s[i])
                yield( sn + [ [s[i][0], n-2] , [s[i][1], n-1] ] )
                yield( sn + [ [s[i][1], n-2] , [s[i][0], n-1] ] )

Вы можете перечислить все пары, позвонив

for x in pairs(6):
   print(x)
mitchus
источник
6

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

В связи с этим есть хороший опрос Проппа, в котором описывается прогресс (до 1999 года). Некоторые идеи из этой статьи и соответствующие ссылки могут оказаться полезными. TL; DR - это сложно :)

--- начало старого ответа

Обратите внимание, что вы просите перечислить все возможные идеальные совпадения на двудольном графе. Для этого существует множество различных алгоритмов, и, в частности, один из более свежих - из ISAAC 2001 .

Основная идея состоит в том, чтобы найти одно идеальное соответствие с использованием сетевых потоков, а затем неоднократно изменять его с помощью чередующихся циклов (дополнительную информацию см. В главе любого учебника по алгоритмам, посвященной сетевым потокам).

Суреш
источник
Двудольный граф состоит из двух наборов с заданными метками [0, n), и существует ребро (i, j) тогда и только тогда, когда (i! = J)
Джо
NКN
2
перманент вычисляет ответ. но ОП хочет их перечислить
Суреш
все они изоморфны из-за структуры графа, поэтому было бы неплохо подумать о применении перестановок (но проблема в том, что они будут создавать дубликаты).
Каве
4

Каждая пара, которую вы выбираете, удаляет два ряда, из которых вы больше не можете выбрать. Эта идея может быть использована для настройки рекурсивного алгоритма (в Scala):

def combine(pairs : Seq[(Int,Int)]) : Seq[Seq[(Int, Int)]] = pairs match {
  case Seq() => Seq()
  case Seq(p) => Seq(Seq(p))
  case _ => {
    val combinations = pairs map { case (a,b) => {
      val others = combine(pairs filter { case (c,d) =>
        a != c && a != d && b != c && b != d
      })

      others map { s => ((a,b) +: s) }
    }}

    combinations.flatten map { _.sorted } distinct
  }
}

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

Рафаэль
источник
Не приведет ли это также к возврату комбинаций, которые не включают каждое число, но которые не могут быть расширены, потому что в исходной последовательности нет пар, которые могли бы их расширить? Если так, то эти комбинации должны быть отфильтрованы.
Карл Маммерт
N2N
(0,1)Nзнак равно4
Да. Но, как я уже сказал, мой ответ касается сценария, предлагаемого ОП, то есть не произвольных входных данных.
Рафаэль
Когда я читаю оригинальный вопрос, речь идет о произвольном наборе пар, ОП никогда не говорит, что все пары возможны. Но я согласен, что ОП может быть более ясным об этом.
Карл Маммерт
4

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

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

Дополнительным осложнением в этой конкретной задаче является желание получить только комбинации длины n / 2 (максимальная длина). Это не сложно сделать, если мы выберем хорошую стратегию сортировки. Например, как указано в ответе Карла Маммета, если мы рассмотрим лексикографическую сортировку (сверху вниз, слева направо на диаграмме в вопросе), мы получим стратегию всегда брать следующий элемент так, чтобы его первая цифра была наименьшее количество неиспользованных номеров

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

hugomg
источник
3

Я не уверен, если это то, что вы спрашиваете, но, как я понимаю, у вас есть все (N2)[N]знак равно{1,,N}[N]NКNN

[N]перм(КN)

#п-сомпLеTе КNN!2N2

[N]

Кава
источник