[영리한 프로그래밍을 위한 알고리즘] #7 순환_멱집합

2 minute read


멱집합(powerset)

부분집합을 원소로 하는 집합을 멱집합이라고 한다.

예)
임의의 집합 data = {a, b, c, d}가 있을 때
data의 멱집합 P(data) =
{ {∅}, {a}, {b}, {c}, {d}, {a, b}, {a, c}, {a, d}, {b, c}, {b, d}, {c, d}, {a, b, c}, {a, b, d}, {a, c, d}, {b, c, d}, {a, b, c, d} }

멱집합의 개수를 구하는 공식은 각 원소들이 포함될 경우와 포함되지 않을 경우 2가지이므로 모든 원소의 개수가 n일 때 2n으로 멱집합의 개수를 구할 수 있다.


recursion thinking

{a, b, c, d, e, f} 모든 부분집합을 나열하려면 a를 제외한 {b, c, d, e, f}의 모든 부분집합들을 나열하고, {b, c, d, e, f}의 모든 부분집합에 {a}를 추가한 집합들을 나열한다.

  • a를 포함하지 않는 경우 {b, c, d, e, f} 5개 원소, 25
  • 위의 경우의 수에서 a가 포함되는 경우를 추가, 25

따라서 25 + 25 = 26의 결과와 같다.

즉 어떤 집합의 모든 부분집합을 구하기 위해서는 그 집합에서 원소하나를 제외한 나머지의 모든 부분집합을 구할 수 있으면 원래 집합에 대한 모든 부분집합도 구할 수 있다.

  • {b, c, d, e, f}의 모든 부분집합에 {a}를 추가한 집합들을 나열하려면

  • {c, d, e, f}의 모든 부분집합들에 {a}를 추가한 집합들을 나열하고, {c, d, e, f}의 모든 부분집합에 {a, b}를 추가한 집합들을 나열한다.

이 규칙으로 recursion 표현이 가능하다.


알고리즘

powerSet(S)
if S is an empty set
  print nothing;
else
  let t be the first element of S;
  find all subsets of S-{t} by calling powerSet(S-{t});
  return all of them; 

2n 개의 집합을 반환하기 때문에 메모리 문제가 발생할 위험이 있다. 그렇다고 반환 없이 출력만 실행시키기에는 집합에 다른 집합을 추가할 수 가 없게 된다.

그렇기 때문에 집합을 저장하기 위한 변수를 만들어 준다.

powerSet(P, S)
if S is an empty set
  print P;
else
  let t be the first element of S;
  powerSet(P, S-{t});
  powerSet(P  {t}, S - {t});

S의 멱집합을 구한 후 각각에 집합 P를 합집합한 뒤 출력한다.
recursion 함수가 두 개의 집합을 매개변수로 받도록 설계해야 한다는 의미로 두 번째 집합의 모든 부분집합들에 첫번째 집합을 합집합하여 출력한다.

코드로 적용시킨다.

private static char data[] = {'a', 'b', 'c', 'd', 'e', 'f'};
private static int n=data.length;
private static boolean [] include = new boolean [n];

public static void powerSet(int k) {
  if (k == n) {
    for (int i = 0; i < n; i++)
      if (include[i]) System.out.print(data[i] + " ");
    System.out.printIn();
    return;
  }
  include[k]=false;
  powerSet(k+1);
  include[k] = true;
  powerSet(k+1);
}

data[k], … , data[n-1]의 멱집합을 구한 후 각각에 include[i] = true, i = 0, … , k-1 인 원소를 추가하여 출력한다.

처음 이 함수를 호출할 때는 powerSet(0)로 호출하며 P는 공집합이고 S는 전체집합이 된다.

상태공간트리
이 문제도 상태공간트리를 사용해서 모든 경우의 수를 구조화 시킨 다음 탐색을 통하여 답을 구하는 방법이 가능하다.

state_space_tree

private static char data[] = {'a', 'b', 'c', 'd', 'e',};
private static int n = data.length;
//트리상에서 현재 나의 위치를 표현한다.
private static boolean [] include = new boolean [n];

public static void powerSet(int k) {
  // 만약 내 위치가 리프 노드라면 동작수행
  if (k == n) {
    for (int i = 0; i < n; i++)
      if (include[i]) System.out.print(data[i] + " ");
    System.out.printIn();
    return;
  }
  include[k] = false;
  // 왼쪽 노드 확인
  powerSet(k+1);
  include[k] = true;
  // 오른쪽 노드 확인
  powerSet(k+1);
}