Искал какой-нибудь простой способ генерирования перестановок, по возможности, без рекурсии. Наткнулся на алгоритм Нарайаны (статья в Википедии: https://ru.wikipedia.org/wiki....D%D1%8B ).
По ссылке приведены реализации на Си и Паскале. Изучил их, переложил на VBA, с возможностью использования массивов не только с основанием 0, как в исходных текстах, но и с более привычным для некоторых, включая меня, основанием 1.
Делюсь достигнутым: [vba]
Код
Dim a() As Integer
Sub GeneratePermutations()
Dim obs As Integer, n As Integer, j As Long
n = 6 'количество элементов
'управление основанием массива a(): либо от 0 до n-1, либо от 1 до n obs = 1 'здесь можно задать либо 0, либо 1 (а-ля Option Base)
'начальная расстановка: элементы = натуральные числа от 1 до N по возрастанию Select Case obs Case 0: ReDim a(0 To n - 1): For j = 0 To n - 1: a(j) = j + 1: Next j Case 1: ReDim a(1 To n): For j = 1 To n: a(j) = j: Next j End Select 'следующую строку раскомментировать для тестирования случая остановки на строке 400 'a(1) = 4: a(2) = 2: a(3) = 5: a(4) = 3: a(5) = 6: a(6) = 1
'собственно генерирование всех перестановок j = 0 Do j = j + 1 'очередная расстановка: вывод на лист (или можно запоминать в какой-нибудь массив) Cells(j, 1).Resize(1, n) = a Loop Until NarayanaNextPerm(n, obs) = 0
End Sub
Function NarayanaNextPerm(n As Integer, Optional obs As Integer = 0) As Integer
Dim i As Integer, k As Integer, t As Integer, tmp As Integer
'Шаг 1 k = n - 2 + obs Do While k >= 0 + obs If a(k) < a(k + 1) Then Exit Do k = k - 1 Loop
If k = -1 + obs Then NarayanaNextPerm = 0 'сигнал об окончании внешнего цикла генерирования Exit Function End If
'Шаг 2 t = n - 1 + obs Do While t >= k + 1 And a(k) >= a(t) t = t - 1 Loop tmp = a(k): a(k) = a(t): a(t) = tmp
'Шаг 3 For i = k + 1 To (n + k + obs) \ 2 t = n + k + obs - i tmp = a(i): a(i) = a(t): a(t) = tmp Next i NarayanaNextPerm = i
End Function
[/vba] Сердцем алгоритма является функция NarayanaNextPerm, которая для текущей заданной расстановки, хранящейся в элементах внешнего массива a(), генерирует следующую расстановку (перестановку). Для генерации всех перестановок следует стартовать с начального монотонно возрастающего порядка элементов: от 1 до N. И затем дойти до конца процесса, получив ровно обратную последовательность - от N до 1.
Для 6 элементов (исходно: 1,2,3,4,5,6) процедура GeneratePermutations создает 720 строк (6!). Интересно, что процесс генерирования можно прервать в определенный момент, скажем, достигнув значения j = 400. Этому значению соответствует следующее состояние элементов массива a(1-6): 4,2,5,3,6,1. Если затем начать процесс с этого состояния, то он дойдёт до конца, создав оставшиеся 320 комбинаций.
Искал какой-нибудь простой способ генерирования перестановок, по возможности, без рекурсии. Наткнулся на алгоритм Нарайаны (статья в Википедии: https://ru.wikipedia.org/wiki....D%D1%8B ).
По ссылке приведены реализации на Си и Паскале. Изучил их, переложил на VBA, с возможностью использования массивов не только с основанием 0, как в исходных текстах, но и с более привычным для некоторых, включая меня, основанием 1.
Делюсь достигнутым: [vba]
Код
Dim a() As Integer
Sub GeneratePermutations()
Dim obs As Integer, n As Integer, j As Long
n = 6 'количество элементов
'управление основанием массива a(): либо от 0 до n-1, либо от 1 до n obs = 1 'здесь можно задать либо 0, либо 1 (а-ля Option Base)
'начальная расстановка: элементы = натуральные числа от 1 до N по возрастанию Select Case obs Case 0: ReDim a(0 To n - 1): For j = 0 To n - 1: a(j) = j + 1: Next j Case 1: ReDim a(1 To n): For j = 1 To n: a(j) = j: Next j End Select 'следующую строку раскомментировать для тестирования случая остановки на строке 400 'a(1) = 4: a(2) = 2: a(3) = 5: a(4) = 3: a(5) = 6: a(6) = 1
'собственно генерирование всех перестановок j = 0 Do j = j + 1 'очередная расстановка: вывод на лист (или можно запоминать в какой-нибудь массив) Cells(j, 1).Resize(1, n) = a Loop Until NarayanaNextPerm(n, obs) = 0
End Sub
Function NarayanaNextPerm(n As Integer, Optional obs As Integer = 0) As Integer
Dim i As Integer, k As Integer, t As Integer, tmp As Integer
'Шаг 1 k = n - 2 + obs Do While k >= 0 + obs If a(k) < a(k + 1) Then Exit Do k = k - 1 Loop
If k = -1 + obs Then NarayanaNextPerm = 0 'сигнал об окончании внешнего цикла генерирования Exit Function End If
'Шаг 2 t = n - 1 + obs Do While t >= k + 1 And a(k) >= a(t) t = t - 1 Loop tmp = a(k): a(k) = a(t): a(t) = tmp
'Шаг 3 For i = k + 1 To (n + k + obs) \ 2 t = n + k + obs - i tmp = a(i): a(i) = a(t): a(t) = tmp Next i NarayanaNextPerm = i
End Function
[/vba] Сердцем алгоритма является функция NarayanaNextPerm, которая для текущей заданной расстановки, хранящейся в элементах внешнего массива a(), генерирует следующую расстановку (перестановку). Для генерации всех перестановок следует стартовать с начального монотонно возрастающего порядка элементов: от 1 до N. И затем дойти до конца процесса, получив ровно обратную последовательность - от N до 1.
Для 6 элементов (исходно: 1,2,3,4,5,6) процедура GeneratePermutations создает 720 строк (6!). Интересно, что процесс генерирования можно прервать в определенный момент, скажем, достигнув значения j = 400. Этому значению соответствует следующее состояние элементов массива a(1-6): 4,2,5,3,6,1. Если затем начать процесс с этого состояния, то он дойдёт до конца, создав оставшиеся 320 комбинаций.Gustav
[поскольку в одно сообщение не поместился, продолжаю в этом...]
В процессе работы демонстрационной процедуры GeneratePermutations на рабочий лист выводятся генерируемые перестановки. Однако, строк для вывода всех комбинациий становится недостаточно уже при n = 10 - требуется 10! = 3 628 800 строк, что больше максимального кол-ва строк на листе 2^20 = 1 048 576. Для Excel до версии 2007 ограничение, разумеется, наступает гораздо раньше.
В следующей процедуре GeneratePermutations2 учтена возможность достижения последней строки листа, либо произвольного заданного ограничения rmax. Достигнув предельной строки, процесс вывода продолжается с первой строки того же листа, с отступом через пустой столбец вправо от уже выведенных перестановок, т.е. происходит такое "подворачивание" вывода. Группы столбцов, между которыми делаются пустые столбцы, я назвал "грядками", а сами пустые столбцы - "межами".
[vba]
Код
Sub GeneratePermutations2()
'вариант GeneratePermutations с "подворачиванием" на листе 'понадобится при n > 9, если комбинации хочется вывести на рабочий лист
Dim obs As Integer, n As Integer, r As Long, rmax As Long, c As Integer
n = 10 'количество элементов
'управление основанием массива a(): либо от 0 до n-1, либо от 1 до n obs = 1 'здесь можно задать либо 0, либо 1 (а-ля Option Base) rmax = 2 ^ 20 'предел "подворачивания"
'начальная расстановка: элементы = натуральные числа от 1 до N по возрастанию Select Case obs Case 0: ReDim a(0 To n - 1): For r = 0 To n - 1: a(r) = r + 1: Next r Case 1: ReDim a(1 To n): For r = 1 To n: a(r) = r: Next r End Select
r = 0: c = 1 Do r = r + 1 If r > Rows.Count Or r > rmax Then r = 1: c = c + n + 1 End If 'очередная расстановка: вывод на лист (с "подворачиванием") Cells(r, c).Resize(1, n) = a Loop Until NarayanaNextPerm(n, obs) = 0
End Sub
[/vba] Процедура GeneratePermutations2 генерирует перестановки для 10 элементов, выводя первые 2^20 строк в первую "грядку", далее полностью заполняются еще две "грядки" и, наконец, четвертая "грядка" заполняется лишь по строку 483072. Итого 2^20*3+483072 = 3 628 800 строк. При n = 10 процедура работает порядка 4-х минут.
[поскольку в одно сообщение не поместился, продолжаю в этом...]
В процессе работы демонстрационной процедуры GeneratePermutations на рабочий лист выводятся генерируемые перестановки. Однако, строк для вывода всех комбинациий становится недостаточно уже при n = 10 - требуется 10! = 3 628 800 строк, что больше максимального кол-ва строк на листе 2^20 = 1 048 576. Для Excel до версии 2007 ограничение, разумеется, наступает гораздо раньше.
В следующей процедуре GeneratePermutations2 учтена возможность достижения последней строки листа, либо произвольного заданного ограничения rmax. Достигнув предельной строки, процесс вывода продолжается с первой строки того же листа, с отступом через пустой столбец вправо от уже выведенных перестановок, т.е. происходит такое "подворачивание" вывода. Группы столбцов, между которыми делаются пустые столбцы, я назвал "грядками", а сами пустые столбцы - "межами".
[vba]
Код
Sub GeneratePermutations2()
'вариант GeneratePermutations с "подворачиванием" на листе 'понадобится при n > 9, если комбинации хочется вывести на рабочий лист
Dim obs As Integer, n As Integer, r As Long, rmax As Long, c As Integer
n = 10 'количество элементов
'управление основанием массива a(): либо от 0 до n-1, либо от 1 до n obs = 1 'здесь можно задать либо 0, либо 1 (а-ля Option Base) rmax = 2 ^ 20 'предел "подворачивания"
'начальная расстановка: элементы = натуральные числа от 1 до N по возрастанию Select Case obs Case 0: ReDim a(0 To n - 1): For r = 0 To n - 1: a(r) = r + 1: Next r Case 1: ReDim a(1 To n): For r = 1 To n: a(r) = r: Next r End Select
r = 0: c = 1 Do r = r + 1 If r > Rows.Count Or r > rmax Then r = 1: c = c + n + 1 End If 'очередная расстановка: вывод на лист (с "подворачиванием") Cells(r, c).Resize(1, n) = a Loop Until NarayanaNextPerm(n, obs) = 0
End Sub
[/vba] Процедура GeneratePermutations2 генерирует перестановки для 10 элементов, выводя первые 2^20 строк в первую "грядку", далее полностью заполняются еще две "грядки" и, наконец, четвертая "грядка" заполняется лишь по строку 483072. Итого 2^20*3+483072 = 3 628 800 строк. При n = 10 процедура работает порядка 4-х минут.Gustav
Интересно будет потестить на скорость данный алгоритм и рекурсии, предполагаю, что рекурсии могут оказаться быстрее. Проверил, рекурсия, которая есть у меня, работает раза в два медленнее.
Есть собственные разработки по генерации следующей перестановки в лексикографическом порядке (реализовывал сам, получилось похоже). Сравню по скорости. Сравнил, скорость аналогична на 10-20% мой алгоритм медленнее
Т.к. мои реализации медленнее опубликованного здесь алгоритма Нарайаны, то опубликовывать их не буду
Интересно будет потестить на скорость данный алгоритм и рекурсии, предполагаю, что рекурсии могут оказаться быстрее. Проверил, рекурсия, которая есть у меня, работает раза в два медленнее.
Есть собственные разработки по генерации следующей перестановки в лексикографическом порядке (реализовывал сам, получилось похоже). Сравню по скорости. Сравнил, скорость аналогична на 10-20% мой алгоритм медленнее
Т.к. мои реализации медленнее опубликованного здесь алгоритма Нарайаны, то опубликовывать их не будуMCH
Сообщение отредактировал MCH - Пятница, 29.08.2014, 21:41
Набрался наглости и внес коррективы в код Константина: изменил типы, убрал obs, немного оптимизировал вычисления в циклах Это дало ускорение более чем в 1,5 раза от первоначального кода (на моем компьютере: 13,5 сек. против первоначальных 21,55 сек.)
[vba]
Код
Option Explicit
Private prmArr&()
Function MyNarayanaNextPerm&(n&) Dim i&, k&, t&, tmp&
For k = n - 1 To 1 Step -1 If prmArr(k) < prmArr(k + 1) Then Exit For Next k
If k Then t = n While t > k And prmArr(k) >= prmArr(t) t = t - 1 Wend tmp = prmArr(k): prmArr(k) = prmArr(t): prmArr(t) = tmp t = n For i = k + 1 To (n + k) \ 2 tmp = prmArr(i): prmArr(i) = prmArr(t): prmArr(t) = tmp t = t - 1 Next i MyNarayanaNextPerm = i End If End Function
Sub GeneratePermutations0() Dim n&, i&, tmr!
n = 11 ReDim prmArr&(1 To n) For i = 1 To n: prmArr(i) = i: Next i 'i = 0 tmr = Timer Do 'i = i + 1 'Cells(i, 1).Resize(1, n) = prmArr Loop While MyNarayanaNextPerm(n) Debug.Print Timer - tmr End Sub
[/vba]
Набрался наглости и внес коррективы в код Константина: изменил типы, убрал obs, немного оптимизировал вычисления в циклах Это дало ускорение более чем в 1,5 раза от первоначального кода (на моем компьютере: 13,5 сек. против первоначальных 21,55 сек.)
[vba]
Код
Option Explicit
Private prmArr&()
Function MyNarayanaNextPerm&(n&) Dim i&, k&, t&, tmp&
For k = n - 1 To 1 Step -1 If prmArr(k) < prmArr(k + 1) Then Exit For Next k
If k Then t = n While t > k And prmArr(k) >= prmArr(t) t = t - 1 Wend tmp = prmArr(k): prmArr(k) = prmArr(t): prmArr(t) = tmp t = n For i = k + 1 To (n + k) \ 2 tmp = prmArr(i): prmArr(i) = prmArr(t): prmArr(t) = tmp t = t - 1 Next i MyNarayanaNextPerm = i End If End Function
Sub GeneratePermutations0() Dim n&, i&, tmr!
n = 11 ReDim prmArr&(1 To n) For i = 1 To n: prmArr(i) = i: Next i 'i = 0 tmr = Timer Do 'i = i + 1 'Cells(i, 1).Resize(1, n) = prmArr Loop While MyNarayanaNextPerm(n) Debug.Print Timer - tmr End Sub
Прошло 10 лет. Скинем 10% от времени кода МСН? [vba]
Код
Option Explicit
Private A&()
Function MyNarayanaNextPerm&(N&) Dim I&, J&, L&, T&
For J = N - 1 To 1 Step -1 If A(J) < A(J + 1) Then T = A(J): Exit For Next J
If J Then L = N While T > A(L) L = L - 1 Wend A(J) = A(L): A(L) = T L = N For I = (J + 1) To ((N + J) \ 2) T = A(I): A(I) = A(L): A(L) = T L = L - 1 Next I MyNarayanaNextPerm = I End If End Function
Sub GeneratePermutations0() Dim N&, I&, tmr!
N = 11 ReDim A&(1 To N) For I = 1 To N: A(I) = I: Next I I = 0 tmr = Timer Do ' i = i + 1 ' Cells(i, 1).Resize(1, n) = A Loop While MyNarayanaNextPerm(N) Debug.Print Timer - tmr End Sub
[/vba]
Прошло 10 лет. Скинем 10% от времени кода МСН? [vba]
Код
Option Explicit
Private A&()
Function MyNarayanaNextPerm&(N&) Dim I&, J&, L&, T&
For J = N - 1 To 1 Step -1 If A(J) < A(J + 1) Then T = A(J): Exit For Next J
If J Then L = N While T > A(L) L = L - 1 Wend A(J) = A(L): A(L) = T L = N For I = (J + 1) To ((N + J) \ 2) T = A(I): A(I) = A(L): A(L) = T L = L - 1 Next I MyNarayanaNextPerm = I End If End Function
Sub GeneratePermutations0() Dim N&, I&, tmr!
N = 11 ReDim A&(1 To N) For I = 1 To N: A(I) = I: Next I I = 0 tmr = Timer Do ' i = i + 1 ' Cells(i, 1).Resize(1, n) = A Loop While MyNarayanaNextPerm(N) Debug.Print Timer - tmr End Sub