참고 : http://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order
사전식 순열 생성 함수
어떤 원소들의 순열을 구하는 많은 알고리듬이 있다. 파이썬의 itertools
모듈은 순열, 조합 생성을 하는 permutations()
, combinations()
등의 함수를 제공한다. 이 블로그에서도 이 외에 특정한 집합에 대해 N 번째 순열을 구하는 문제도 다뤄본 적이 있다. 오늘은 특정한 수열이 주어졌을 때, 해당 수열의 원소들로 구성되는 사전식 순열에서 다음 번 순열을 구하는 문제를 생각해보자.
사전식 순열이란 특정 집합의 원소들로 이루어진 모든 순열을 정렬하여 순차적으로 나타내는 순열을 말하는 것이다. 예를 들어 [1,2,3,4]는 오름차순으로 정리되어 있으므로 사전식 순열에서는 가장 처음에 등장한다. 그리고 그 다음은 [1,2,4,3], [1,4,2,3], [1,4,3,2], … 의 순으로 진행하여 [4,3,2,1]에 이르게 될 것이다.
이러한 순열과 관련하여서는 itertools.permutations()
를 사용하거나 N번째 순열을 찾는 함수는 여러 번 소개한 적이 있다. 오늘은 임의의 순열이 주어졌을 때 그 다음 번 순열을 찾아야 하는 문제로 좀 차이가 있다. 이는 특정한 상태로부터 그 다음 상태의 사전식 순열을 찾는 알고리듬이 필요하다는 뜻이다.
한가지 예를 들어보자. 순열 [4, 1, 5, 3, 2] 의 바로 다음 번에 나타나는 사전식 순열은 무엇일까? (답은 [4, 1, 3, 5] 이다.) 이 질문에 답할 수 있는 알고리듬은 이미 14세기에 인도의 수학자 나라야나 판디타가 고안한 것이 있다.
a[k] < a[k+1]
인 가장 큰 인덱스 k를 찾는다. 만약 이를 찾을 수 없다면, 수열이 역순으로 정렬된 것으로 사전식 순열의 맨 마지막 항이 될 것이다.k
를 찾았다면k
이후의 인덱스에서a[k]
보다 큰 값을 가진 가장 먼 인덱스l
을 찾는다.k
와l
위치의 값을 교환한다.- 그런다음 k보다 큰 인덱스의 값들을 역순으로 배치한다.
위 알고리듬에 의해 [4, 1, 5, 3, 2] 의 값의 다음 번 순열을 찾아보자.
- 증가하는 마지막 인덱스는 1이다. (a[1] => 1)
- 1보다 큰 값을 가지는 가장 먼 인덱스는 4이다. (a[4] => 2)
- a[1]과 a[4]의 값을 바꾼다. ([4, 1, 5, 3, 2] → [4, 2, 5, 3, 1])
- a[1]다음의 3개 숫자를 역순으로 뒤집는다. (🤸♂️[4, 2, 1, 3, 5])
한 번 더 해보자.
- 증가하는 마지막 인덱스는 3이다. (a[3] => 3)
- 3보다 큰 값을 가지는 가장 먼 인덱스는 4이다. (a[4] => 5)
- a[1]과 a[4] 자리의 값을 바꾼다. ([ 4, 2, 1, 5, 3 ])
- a[2] 이후의 3개 숫자를 역순으로 뒤집는다. ([ 4, 2, 1, 5, 3 ])
이 방법은 잘 작동하는 것 같다. 그럼 이를 코드로 구현해보자.
# Narayana Pandit's lexicographical permutation
from typing import List, Any
def nextPermutation(xs: List[Any]):
k = max(i for (i, x) in enumerate(xs[:-1]) if x < xs[i+1])
l = max(k + i for (i, x) in enumerate(xs[k:]) if x >= xs[k])
xs[k], xs[l] = xs[l], xs[k]
xs[k+1:] = xs[:k:-1]
그런데 xs가 내림차순으로 정렬된 상태(사전식 순열의 마지막)라면 k 값을 구할 수 없게 된다. max()
함수에 빈 인자가 전달되기 때문에 ValueError
가 발생한다. 따라서 이 때에는 다시 뒤집어서 내림차순으로 정렬된 상태로 만들어주면 된다.
def nextPermutation(xs: List[Any]):
k = len(xs) - 2
try:
k = max(i for (i, x) in enumerate(xs[:-1]) if x < xs[i+1])
except ValueError:
xs[:]= xs[::-1]
return
l = max(k + i for (i, x) in enumerate(xs[k:]) if x >= xs[k])
xs[k], xs[l] = xs[l], xs[k]
xs[k+1:] = xs[:k:-1]
이제 이 함수를 사용해서 주어진 연속열의 순열을 차례로 생성해주는 제너레이터 함수를 작성해보자. itertools.permutations 함수와 하는 일이 거의 같다.
from typing import Iterable, Any
def permutations(xs: Iterable):
ws = list(xs)
while True:
try:
yield ws[:]
k = max(i for (i, (x, y)) in enumerate(zip(ws, ws[1:])) if x < y)
l = max(k + i for (i, x) in enumerate(ws[k:]) if x >= ws[k])
ws[k], ws[l] = ws[l], ws[k]
ws[k+1:] = ws[:k:-1]
except Exception as e:
print(e)
return