임의의 Directed Graph 가 주어졌을 때, 시작점 S에서 끝점 E까지 흐를 수 있는 최대 유량을 구하는 Algorithm.

Network Flow(Maximum Flow) 를 해결하는 방법 중 하나로 Ford-Fulkerson 알고리즘이 있다.

Ford-Fulkerson 알고리즘 :
시작점 -> 끝점까지 하나의 경로를 찾는다. (이 때 BFS로 찾아 간선수를 최소로 해야 효율적임 : Edmond-Karp Algorithm)
경로 중 가중치가 최소인 간선을 찾아 아까 찾은 경로의 정방향에서 빼주고 역방향에서 더해준다.
(이것은 정방향으로 일정한 유량을 흘리고 흘릴 수 있는 가중치를 다시 계산해주는 과정이다.)
유량 그래프 역시 갱신해준다.
더 이상 경로를 찾을 수 없을 때까지 위의 과정을 반복한다.

=> Network Flow 알고리즘의 응용으로 Bipartite Matching(이분 매칭) 문제를 해결 할 수 있다.

임의의 시작점 s를 만들어 이분 매칭의 한쪽 집합을 모두 연결한다. (모든 가중치 1)
Matching 가능한 두 원소를 가중치 1인 Edge로 연결한다.
임의의 끝점 e를 만들어 다른쪽 집합을 모두 연결한다. (가중치 1)

이 그래프에 대하여 Network Flow를 실행한다.
이 때의 Flow가 최대 이분 매칭 쌍의 수이다.

=> 제거되는 그래프 간선의 가중치의 합을 최소로 하여 그래프를 두 부분으로 분리하는 Minimum Cut 문제 역시 전체 간선 가중치의 합에서 Maximum Flow를 제거하면 된다는 것이 증명되어 있다.

(문제) R명의 사람이 있다. 사람들 사이에는 서로 친구이거나 서로 적인 관계가 성립하고 있다. 그렇다면 만약 R명의 사람이 존재하기만 한다면 이중에서 3명은 서로 친구이거나 3명이 서로 적인 경우가 반드시 존재한다고 한다. 이 경우 R의 최소 값은 얼마이겠는가?

위와 같은 문제가 주어졌다. 위 문제는 Ramsey Number를 제기하는 가장 기초적인 문제로써 비둘기 집의 원리로써 쉽게 해결 할 수 있다. 한편 여기서 3명이 서로 친구(mutual friends)란 말은 (대칭적으로 서로 적이란 말에도 성립한다.) 이 3명중에서 아무나 두 명을 선택했을 때 그 두 사람은 반드시 친구인 경우인 경우를 말한다.

(풀이)

사람이 총 n명이 있다고 가정하자. 그리고 각 사람들을 점(point)으로 두고 그 사람사이의 관계를 선(edge)으로 연결하자. 이 n 개의 점을 모두 다 이은 모양의 그래프(Graph)를 Complete graph of order n (Kn)이라고 한다. 이제 이 그래프의 선분들은 2가지색으로 Coloring을 할 수 있고, 그 색들은 각각, 친구, 적이라는 의미를 가진다고 생각하면 위 문제는 변의 색이 모두 같은 삼각형을 찾는 문제로 변형이 된다.

먼저 R>5 임을 보이자. 이것은 반례를 보이면 쉽게 증명된다.

R=5 ; Counter Example.


R=6 : 한 사람(A)에 대해서 생각하자. Pigeonhall Principle 에 의해서 나머지 다섯 사람 중에는 적어도 세 사람 이상이 A와 친구이거나 적이고, 대칭적으로 세 사람이 친구라고 한다면 이 사람들끼리 친구 사이가 존재하면 그 두 사람과 A는 서로 친구 사이. 만일 이 세 사람이 모두 적이라면 서로 적인 경우가 되므로 적어도 R이 6이상 이라면 반드시 세 명이 서로 친구이거나 서로 적인 경우가 존재한다. 따라서 R의 최소 값은 6이다. □

(Definition) R명의 사람이 있을 때 m명이 서로 친구이거나 (mutual friends), n명이 서로 적인 경우(mutual enemies)인 경우가 반드시 생긴다고 할 때, R의 최소값을 Ramsey Number R(m,n) 이라고 정의한다.

일반적으로 Ramsey Number 의 구체적인 값에 대해서는 알려져 있지 않다. 대신에 몇몇 특수한 값들에 대해서는 이미 밝혀지고 증명이 되어 있다. 필자는 이번 원고에서 R(4,4)=18 임을 증명하려 한다.

Lemma 1) R(3,3) = 6

위에 증명되어 있다.

Lemma 2) R(4,3) = 9

(풀이)

먼저 R(4,3) > 8 임을 보이자. 이것은 R=8 일 때 4명이 서로 친구이거나 3명이 서로 적일 경우가 꼭 생기지 않는 반례를 보이면 증명이 된다. 왜냐하면 R=8 일 때 반례가 존재하면 R<8 일 때 역시 R=8 일 때의 예에서 사람을 한 명씩만 줄이면 그것이 각각의 반례가 되기 때문이다. 친구를 실선, 적을 점선으로 연결한다고 하면 R=8 일 때 다음과 같은 반례가 존재한다.


이제 R(4,3)<= 9 임을 보이자.

어떤 한 사람 A를 기준으로 친구가 6명 있다고 가정하자. 그러면 Lemma 1에 의해 이 6명 중에는 반드시 서로 친구인 세 사람이 있거나 적인 세 사람이 존재한다. 서로 적인 세 사람이 존재한다면 R(4,3)의 정의에 만족하고, 서로 친구인 세 사람이 존재한다면 A와 함께 서로 친구인 네 사람이 존재하는 것이 된다. 따라서 증명 끝.

또한 A와 적관계인 4 사람이 존재한다고 가정하자. 그러면 이 사람들 중에 서로 적인 두 사람이 있다면 A와 함께 서로 적인 세 사람이 존재하는 것이다. 그런 관계가 없다면 정의에 의해 이 네 사람은 서로 친구가 되는 것이다. 따라서 9명의 사람이 있을 때, 어떤 한 사람도 친구가 6명 이상이거나 적이 4명 이상이면 반드시 네 명이 서로 친구이거나 세 명이 서로 적인 경우가 반드시 생기게 된다.

9가 R(4,3)을 만족하지 않을 반례가 존재하기 위해선 9명의 사람 모두가 5명의 친구, 3명의 적을 가지고 있어야 한다. 즉, 위와 같은 그래프에서 각 점에 실선이 모두 5개여야 한다는 말이다. 점과 실선(edge)만으로 이루어진 Simple Graph 를 생각하면,

2e = Σdeg(v) (e는 edge 의 개수, deg(v) 는 각 꼭지점에 연결된 edge의 개수)

가 성립하는 데, deg(v)=5 이고 |v|=9 이므로, 2e = Σdeg(v) = 9×5 = 45 이다.

하지만 45는 홀수 이므로 모순. 따라서 그러한 예는 존재하지 않는다. 따라서 반드시 누군가 한 명은 6명 이상의 친구를 가지고 있거나 4명 이상의 적을 가지고 있어야 한다. 따라서 9명의 사람이 있으면 반드시 네 명이 서로 친구이거나 세 명이 서로 적인 경우를 찾을 수 있고 R(4,3)>8 에서, R(4,3)=9 이다.□

Lemma 3)

R(m,n) = R(n,m)

Lemma 4)

R(n,2) = n = R(2,n)

(증명) Lemma 3와 Lemma 4는 자명하다.

Theorem) Ramsey's Theorem.

Claim. R(m,n) ≤ R(m-1,n) + R(m,n-1) for m, n(>2) ∈ N

pf) n명의 사람이 있다고 하자. 그 중에서 임의의 고정된 한 사람 A를 잡고 각 사람들 사이의 관계를 선으로 표시하자. 즉, 친구일 때는 붉은 색을, 적일 때는 푸른색의 선분을 칠한다고 생각하자. 만일 붉은 색이 선분이 R(m-1,n) 개 이상 있다고 하면 그 사람들이랑 A 와 함께 존재해서 R(m,n) 이 만들어진다. 비슷하게 푸른색이 R(m,n-1) 개 이상 있다고 하면 역시나 A와 함께 R(m,n) 이 만들어진다. 따라서 어떤 고정된 사람 A에 대하여 총 (R(m-1,n)-1)+(R(m,n-1)-1)+1 개의 관계를 가진 사람들이 존재하면 그 Pigeonhall Principle에 의해서 R(m-1,n) 개의 붉은 색 선분이나 R(m,n-1) 개의 푸른색 선분을 가진 사람들이 존재하고 따라서 이 수는 R(m,n) 보다 크다고 할 수 있다. . □

이제 R(4,4) 가 18임을 증명하자.

Ramsey Theorem 에 의해 R(4,4) ≤ R(4,3) + R(3,4) 이 성립하고 Lemma 2와 Lemma 3에 의해 R(4,4) ≤ R(4,3)+R(3,4) = 2R(4,3) = 18 이 성립한다. R(4,3)>17 임을 보이기 위해선 R=17 일 때, 반례가 있음을 보이면 된다. 이것은 그다지 어렵지 않기 때문에 숙제로 남긴다. 여러분들이 한번 찾아보길 바란다.

이상 우리는 간단하게나마 Ramsey Number 에 대해서 알고 몇 가지 경우에 대해서 Ramsey Number 를 직접 찾고 증명해 보았다. 또한 우리는 R(4,4)=18 임을 증명할 수 있었다. 마지막으로 현재까지 밝혀진 Ramsey Number 에 대한 표는 다음과 같다.

l

k

3

4

5

6

7

8

3

6

9

14

18

23

27/30

4


18

25/28

34/44



5



42/55

51/94



6



69/102



References

1. 「Ramsey Theory」, Ronald L. Graham, Bruce L. Rothschild, Joel H. Spencer (1980 ; John Wiley & Sons, Inc)

2. 「Discrete Mathematics and Its Applications」, Kenneth H. Rosen
  (4th ed ; VAGA, New York)

출처 : http://math.kaist.ac.kr/%7Edeneb/study/Ramsey_Numbers.htm


//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓
//  [ 해 쉬 (Hash) ]
//
//    1. 해쉬
//    2. 해쉬의 전략
//    3. 해쉬 함수의 작성
//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓

/*
//─────────────────────────────────
   1. 해쉬
//─────────────────────────────────
 
  자료수에 상관없이 상수의 검색 길이를 가진다.
  내부검색(메모리)과 외부검색(디스크)을 빠른 속도로 검색해주는 알고리즘
//─────────────────────────────────
   2. 해쉬의 전략
//─────────────────────────────────

  1) 해쉬 용어
     ex) 우체국에서 편지를 다루는 방법

 - 편지 : 주소, 우편번호
   우체국 --> 우편번호에 따라 통(bucket)에 정리

    - key    : 주소      // 데이터
   hash value  : 우편번호     // 실제로 저장되는 장소
   hash table  : 편지를 넣는 통들의 집합
   bucket      : 각 통들
   slot        : 통들에 들어가는 편지의 갯수

      hash function : key(주소)를 hash value(우편번호)로 바꾸어 주는
                   함수
     - 해쉬 검색법의 성능 좌우
       key 값을 해쉬 함수에 넣어 얻어진 hash value의 위치에
      레코드를 저장하는 방법

          => 이상적인 경우 단 한번의 계산으로 삽입과 삭제, 그리고
       검색이 가능하다.

      ex) 간단한 해쉬 함수 구현
       - key     : 세 글자의 문자
       - hash value : 세 글자 중의 첫 글자

 // 소스 작성 ------------------------------------------------
 #define TABLE_SIZE 256
 char hash_table[TABLE_SIZE][3]

 // 해쉬 함수
 int hash1(char str[])
 {
  int h;
  h = str[0];
  return h;
 }
 key   : 저장하고자 하는 값 char str[](3개의 문자)
 hash Value  : 3개의 문자 첫번째 문자
 hash table  : hash_table --> 편지를 넣는 통들의 집합, 크기?? 256
 bucket  : hash_table[0].... 각 통들...
 slot  : 통들에 들어가는 편지의 갯수, 자료의 갯수
 hash function : hash1

 ==> 3글자의 키는 hash_table[hash1(key)]에 저장된다.

 만약)
 ABC, BCA, CBA : 모두 다른 해쉬값을 갖는다... 서로 다른 곳에 저장된다.

 ABC, ACD, AXY : 같은 곳에 저장된다.(충돌)

 --> 이 충돌해결 방법 : 선형탐사법, 이중 해쉬법, 연결법등으로 세분화된다.
  
 ---------------------------------------------------------------


//─────────────────────────────────
   3. 해쉬 함수의 작성
//─────────────────────────────────

  (1) 가장 좋은 함수는?
   - 평균적인 입력에 대해 해쉬값 분포를 고르게 작성...
   - 특정 해쉬값이 많이 나올 경우 : 부가적인 작업이 필요해진다.

   => 따라서 해쉬 함수는 고른 분포여야 하는데 입력 자료에 따라
   적절한 해쉬 함수도 다르게 된다.(그때 그때...)


     ex)
     ABC, ACD, AXY등의 문자가 대부분의 경우일 경우?
  첫번째 문자가 아니라 2번째 문자를 hash값으로 하는 등 방법적인 부분 고려...
 


  해쉬 함수가 사용할 수 있는 형태
     나머지, 제곱, 쪼개기, 합치기

  1) 가장 많이 사용하는 해쉬 함수의 형태는 나머지 연산
    - % 연산자로 쉽게 할 수 있으며 속도가 빠르다.
    - 키값을 수치화시킨 후에 M으로 나머지 연산을 하면 해쉬값은
      0 ~ (M-1)의 분표를 갖는다.

   => M : 소수를 사용 or
          20이하의 인수를 가지지 않는 수
    => M이 인수 2를 가진다면(2의 배수라면)
       //답변)두곳의 버킷에 모든 수가 들어간다.

  2) 그 다음 형태는 거듭 제곱
     - 키값을 제곱 또는 세제곱하면 구성하는 비트의 형태가 확연히
    달라지게 된다.

     3) 키를 쪼개고 합치는 방법
 
   ex) 만약 입력 자료들의 첫 글자가 한쪽에 편중되어 있다면
       해쉬값 역시 편중될 우려가 있다.
       -> 거듭 제곱, 쪼개어 합치기, 너머지 이용등의 방법들을 사용

  int hash2(char str[])
  {
  int h;
  h = str[0]^((str[1]*str[1] + str[2]))%TABLE_SIZE);
  return h;
  }

     - 고른 분포
  - 해쉬 테이블의 크기를 넘어서는 안된다.
  - 해쉬 테이블 크기 또한 해쉬 함수의 범위에 합당하게 설정
  ==> 즉 상황에 따라 해쉬 함수는 다르게 된다.
 
*/

 

 

//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓
//  [ 해 쉬 (Hash) ]
//
//    4. 충돌의 해결 방법
//       4.1 선형 탐사법(Linear Probing)
//           4.1.1 선형 탐사법(Linear Probing) 구현
//           4.1.2 선형 탐사법의 개선
//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓

/*

//─────────────────────────────────
   4. 충돌의 해결 방법
//─────────────────────────────────
   해쉬 테이블의 한곳에 여러개의 key들이 들어 갈 경우


//─────────────────────────────────
   4.1 선형 탐사법(Linear Probing)
//─────────────────────────────────

     정적인 해쉬 검색법 규칙
  정적인 해쉬 테이블 사용 :
  배열과 같이 소스 코드상 크기가 정해져 있슴.
  입력자료의 수는 해쉬 테이블의 버킷을 초과 할 수 없슴

     사용 시점
     --> 자료의 변동이 별로 없는 경우 적합, 구현 간단    
  --> 자료 검색은 해쉬 함수에 의해 구해진 해쉬값으로 해쉬 테이블을 참조
  --> 충돌시 가장 널리 사용하는 방법임
    


    선형 탐사법
 1) 현재 삽입하려는 키의 해쉬값에 의한 버킷이 이미 다른 레코드가
       들어있다면 단순히 다음의 버킷에 새로운 레코드를 기입하는 방법
 2) 만일 다음 버킷에도 다른 레코드가 들어있다면 그 다음 버킷을
    찾고 하는 과정 반복

    < 삽입 알고리즘 >
 1. 삽입할 레코드의 해쉬값을 해쉬 함수를 이용하여 구하여 try에 저장
 2. while(해쉬 테이블[try]가 비어있지 않을 동안...)
    2.1 try = (try+1) % TABLE_SIZE
 3. 해쉬 테이블[try]에 레코드 기입


    [ex] 해쉬 함수 작성
     레코드는 단순한 정수형
  해쉬 테이블의 크기 10
  해쉬 함수는 키값을 TABLE_SIZE로 나눈 나머지를 리턴

 #define TABLE_SIZE 10
 int hash_func(int key)
 {
  int h;
  h = key % TABLE_SIZE;
  return h;
 }

    10의 크기를 해쉬 테이블에 A C E O F P I K U 차례로 삽입
 
 1. A(아스키 : 65)%10 = 5  => 5번 버킷에 A를 기입
 2. C(아스키 : 67)%10 = 7  => 7번 버킷에 B를 기입
 3. E(아스키 : 69)%10 = 9  => 9번 버킷에 E를 기입
 4. O(아스키 : 79)%10 = 9  => 9번 버킷에 O를 기입 ??

 5. F(아스키 : 80)%10 = 0  => 0번 버킷에 F를 기입 ??

 6. P(아스키 : 90)%10 = 0  => 0번 버킷에 P를 기입 ??

 7. I--> 3, K-->6, U--> 4


 <검색 알고리즘>
 1. 검색할 키의 해쉬값을 구하여 try에 저장
 2. while(해쉬 테이블[try]가비었지 않을 동안)
     해쉬 테이블[try]가 찾고자 하는 레코드이면 리턴
  아니면 try = (try+1)%TABLE_SIZE
 3. 여기까지 오면 찾지 못한 것이므로 실패


    [ 위의 저장 예에서의 검색 ]

    1. A나 C나 E를 찾는 것은 한번의 해쉬값으로 충분
 2. O, F, P를 찾는 것은 선형 탐사 필요
    
 [ 용어 ]
    cluster(덩어리)
   해쉬 테이블에서 연속된 공간을 차지하는 것
   덩어리가 크느냐 작느냐에 따라 삽입과 검색을 위해 선형 탐사를
   하는 횟수가 결정됨
 Loading density(적재밀도)
   전체자료수에다 전체 버킷수를 나눈 수
   적재 밀도는 1보다 작다.
   적재 밀도가 커질수록 큰 덩어리가 생길 확률이 높다.

    ex) D까지 삽입한 상태에서 U를 삽입할 경우
     최고 9번의 선형 탐사 후에야 삽입할 곳을 찾게 된다.


 <삭제 알고리즘>

    --> 만약 10번 상태에서 p를 삭제하여 빈 곳으로 만들면
     U를 찾을 수 있을까?
  U를 찾기 위해 선형 탐사를 하던 도중 P가 있던 곳이
  빈 곳이 되므로 탐사를 멈추게 된다.

 --> 삭제 표시와 빈 곳이라는 표시를 따로 해줄 필요 존재함
  검색시 삭제된 버킷이 존재해도 계속 검색
  삽입시 삭제된 버킷에 삽입 가능...
   

    [ 선형 탐사법의 단점 ]
 1. 동적 자료의 검색시 매우 취약(최초 테이블의 크기를 크게 잡아야 함)
    - 적재 밀도(Loading density)가 매우 작아져 효율적이다.

 2. 해쉬 테이블이 꽉 찬상태에서 존재 안하는 키를 검색시 ??
    - 무한 루프
   해결) 시작위치(최초 해쉬값) 기록 후 try값이 원래의 값이 되었을 경우 종료


 3. 해쉬 테이블이 꽉찬 상태면 최대 N의 검색길이를 갖는다.
    - 해결방법???
      120% 정도 크게 잡아주는 것이 효율적...

 

//─────────────────────────────────
  4.1.1 선형 탐사법(Linear Probing) 구현
//─────────────────────────────────
*/
// 구현 : 초기화, 삽입, 삭제, 검색 함수


//─────────────────────────────────
//  1) 빈 곳과 삭제된 곳을 표시할 상수 정의
//─────────────────────────────────
#define EMPTY  -1  // 빈곳을 표시
#define DELETED  -2  // 삭제된 곳을 표시
 

//─────────────────────────────────
//  2) 선형 탐사를 하기 위해서 해쉬 테이블 초기화
//─────────────────────────────────
// 테이블을 모드 EMPTY로 채운다.

// 인자값 : 테이블... 채워진 갯수
int HlpInit(int a[], int *np)
{
 int i;
 for (i=0; i < TABLE_SIZE; i++)
  a[i] = EMPTY;
 *np = 0;
 return 1;
}


//─────────────────────────────────
//  3) 해쉬 함수 구현
//─────────────────────────────────
#define TABLE_SIZE 10
int Hash_Func(int key)
{
 int h;
 h = key % TABLE_SIZE;
 return h;
}


//─────────────────────────────────
//  4) 해쉬 테이블에 자료 삽입
//─────────────────────────────────
// 초기의 해쉬값... try1와 try2 저장
// try2 : 해쉬 테이블이 꽉 찼을 경우 무한 루프에 빠지는 것을 방지...
// - try1 가 해쉬 테이블을 한바퀴 돌고 제자리로 돌아온 것을 확인하기 위해
//  마련한 변수

// [ 자료 삽입 장소는???]
// - EMPTY로 표시된 곳... DELETED로 표시된 곳

// 인자값 : 저장할 키, 테이블, 실제 저장 갯수
int Hlp_Insert(int key, int a[], int *np)
{
 int try1, try2;

 // 초기 무한 루프 방지...
 try1 = try2 = HashFunc(key);

 // 빈 곳이나 삭제된 곳을 찾음
 while ( a[try1] != EMPTY || a[try1] != DELETED) {
  try1 = (try1 + 1) % TABLE_SIZE;  // 선형 탐사
  if ( try1 == try2)     // 꽉 차 있는 상태
   return -1;
 }
 // 찾은 곳에 key를 수록
 a[try1] = key;
 // 자료수 증가
 (*np)++;
 // 삽입한 곳 리턴
 return try1;
}

 

//─────────────────────────────────
//  5) 해쉬 테이블에서 자료 삭제
//─────────────────────────────────
// 자료 삭제 후에 삭제된 곳이라고 표시
int Hlp_Delete(int key, int a[], int *np)
{
 int try1;
 // 자료수가 0이면 리턴
 if ( *np == 0 )
  return -1;
 // 삭제할 키를 찾음
 if (( try1 = Hlp_Search(key, a, np)) < 0 )
  return -1;
 // 찾았을 경우
 a[try1] = DELETED;
 // 자료수 감소
 (*np)--;
}


//─────────────────────────────────
//  6) 해쉬 테이블 검색 함수(핵심 함수)
//─────────────────────────────────
// 빈 곳을 만나면 실패를 리턴
// 키를 찾으면 그 위치를 리턴
// 빈 곳은 없지만 찾는 키도 없는 경우는 실패를 리턴
int Hlp_Search(int key, int a[], int *np)
{
 int try1, try2;
 try1 = try2 = Hash_Func(key);

 while ( a[try1] != EMPTY ) {
  // 키를 찾았으면 성공
  if ( a[try1] == key )
   return try1;
  // 선형 검색
  try1 = (try1 + 1) % TABLE_SIZE;

  // 찾고자 하는 키가 없음
  if ( try1 == try2 )
   return -1;
 }
 return -1;
}


//  ==> 선행 탐사법은 기본적으로 배열을 이용 => 프로그램이 쉽다.
//      해쉬 테이블을 충분히 크게 잡으면 => 검색 효율이 좋다.(자료수에 관계없이 상수 검색 길이)
//     해쉬 테이블을 작게 잡으면 => 평균 검색 길이가 길어진다.
//              적재 밀도가 높아짐 : 큰 덩어리가 생김..
//

 


/*
//─────────────────────────────────
    4.1.2 선형 탐사법의 개선
//─────────────────────────────────

   한 버킷에 들어올 값을 인접한 곳에 두기 때문에 큰 덩어리가 생길 확율이
   많다.         

   앞의 예는 해쉬 테이블의 크기가 10이라고 가정하고,
     충돌이 일어난 경우 아래와 같이 작성하여 다음 버킷을 찾음

   try = (try+1) % TABLE_SIZE;

   // 어떻게 하면 좋을까??
   // [ 답변 ]
   꼭 1을 더하라는 법은 없다.
   try = (try + i) % TABLE_SIZE;
   i를 적절히 선택
   만약 3이라면??
   ==> 덩어리가 적어진다.
*/

 

 

http://blog.naver.com/ymhooky/120007525175

//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓
//  [ 해 쉬 (Hash) ]
//
//    4. 충돌의 해결 방법
//       4.2 이중 해쉬법(Double Hash)
//           4.2.1 이중 해쉬법(Double Hash) 구현
//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓

/*

//─────────────────────────────────
   4. 충돌의 해결 방법
//─────────────────────────────────
   해쉬 테이블의 한곳에 여러개의 key들이 들어 갈 경우


//─────────────────────────────────
   4.2 이중 해쉬법(Double Hash)
//─────────────────────────────────

    - 선형 탐사법보다 큰 덩어리를 만들 확률이 적다.
 - 해쉬를 중복해서 사용

    - 2개의 해쉬 함수 사용
  - Primary Hash Function
    : 최초 검색, 삽입시 사용하는 함수

  - Secondary Hash Function
    : 1차 검색으로 해쉬값을 찾았을 경우 이미 다른 자료가 그
      버킷을 사용함 --> 충돌
   또다른 함수로 새로운 버킷을 찾는다.

    - 이중 해쉬의 효율은 2차 함수에 달려 있다.

  => 이중 해쉬법은 선형 탐사법의 변형이다.
     사용하는 자료의 구조는 동일
  삽입, 삭제, 검색 방법도 동일
  다만 충돌의 문제를 어떻게 해결하느냐에 달려있다.


    [ 예 ]
 // 선형 에서의 충돌 방지
 try = (try+1)%TABLE_SIZE;

 // 이중 해쉬의 2차 함수
    int hash2_func(int key)
 {
  // 키의 값이 128보다 작으면 3을 아니면 7을 리턴
  if(key < 128)
   return 3;
  else
   return 7; // 3과 7은 서로소
 }

 // 충돌 방지를 위한 문장
 try = (try + hash2_func))%TABLE_SIZE;

 

 

〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓
//  [ 해 쉬 (Hash) ]
//
//    4. 충돌의 해결 방법
//       4.3 연결법(Separate chaining)
//           4.3.1 연결법(Separate chaining) 구현
//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓

/*
//─────────────────────────────────
   4. 충돌의 해결 방법
//─────────────────────────────────
   해쉬 테이블의 한곳에 여러개의 key들이 들어 갈 경우


//─────────────────────────────────
   4.3 연결법(Separate chaining)
//─────────────────────────────────

   연결법(동적인 해쉬법)
    - 동적 할당을 통하여 해쉬 테이블의 크기보다는 큰 자료를 저장할 수 있다.
 - 기본적인 해쉬 테이블과 단순 연결리스트를 이용하여 동적인 자료를 빠르게 검색
 - 매우 큰 자료 집합에 대해서 유용


   ( ex ) 양의 정수 자료 검색

   해쉬 테이블의 크기 TABLE_SIZE 10
   배열로 10 크기의 해쉬 테이블 생성
     - 단순히 레코드를 저장하는 것이 아니라 버킷에 저장될 레코드를
    가리킬 포인터를 가지고 있어야 한다.

   // 노드 구성
   typedef struct _node
   {
  int key;
  struct _node *next;
   }node;

   -> 이 노드 구조를 TABLE_SIZE크기의 배열로 잡고
      내용을 초기화(next = NULL)

   -> 이후 모든 삽입과 삭제, 검색은 연결 리스트에서 순차 검색을
      하던 것과 매우 유사하다.
  
   -> 연결법은 간단히 해쉬법과 연결 리스트의 순차 검색을 혼용한 방법


   ex) A C E O F P I K D U 자료 연속 삽입

  < 연결법에서의 삽입 알고리즘 >
  1. 키에 해당하는 해쉬값을 해쉬 함수를 이용하여 찾아서 try에 저장
  2. 삽입할 레코드를 동적 저장
  3. 해쉬 테이블[try]의 연결 리스트의 앞에 레코드를 끼워 넣는다.
 
  ------------ 
  버 킷
  ------------
  0 --> F --> p
  1
  2
  3 --> I
  4
  5 --> A --> k --> U
  6
  7 --> C
  8 --> D
  9 --> E --> O

 

   < 연결법에서의 검색 알고리즘 >
   1. 찾을 키의 해쉬값을 해쉬 함수를 이용하여 구하고 try에 저장
   2. 해쉬 테이블[try]에 연결된 리스트에서 순차 검색

 

   < 연결법에서의 삭제 알고리즘 >
   1. 찾을 키의 해쉬값을 해쉬 함수를 이용하여 구하고 try에 저장
   2. 해쉬 테이블[try]에 연결된 리스트에서 삭제할 레코드를 검색
   3. 포인터 조작으로 삭제할 레코드를 연결 리스트에서 제외
   4. 삭제할 레코드를 메모리에서 해제


  => 연결법을 이용할 경우 적재 밀도 1보다 커질 수 있다.
  => 레코드 검색시 시간이 더 걸릴 수도 있다.

  => 동적인 해쉬 방법이기 때문에 융통성이 있고 많은 자료 저장 가능

 


//─────────────────────────────────
   4.3.1 연결법(Separate chaining) 구현
//─────────────────────────────────
 

// 구현 : 초기화, 삽입, 삭제, 검색 함수
*/

//─────────────────────────────────
//  1) 해쉬 함수 구현
//─────────────────────────────────
#define TABLE_SIZE 10

int Hash_Func(int key)
{
 int h;
 h = key % TABLE_SIZE;
 return h;
}

 

//─────────────────────────────────
//  2) 선형 탐사를 하기 위해서 해쉬 테이블 초기화
//─────────────────────────────────

// 모든 해쉬 테이블의 next를 NULL로 설정
// 인자 : 테이블, 갯수, 전체 크기
int Hsc_Int( node a[], int *np, int N)
{
 // N은 전체 해쉬 테이블 크기
 int i;
 // NULL 로 초기화
 for (i=0; i < N; i++)
  a[i].next = NULL;
 *np = 0;
 return 1;
}


//─────────────────────────────────
//  3) 자료 삽입 함수 
//─────────────────────────────────
int *Hsc_Insert(int key, node a[], int *np)
{
 int try1;
 node *t;
 // 새로운 노드 할당
 t = (node *)malloc(sizeof(node));

 // 해쉬값을 찾음
 try1 = HashFunc(key);

 // 연결리스트의 선두에 t를 삽입
 t->next = a[try1].next;
 t->key = key;
 a[try1].next = t;

 // 자료수 증가
 (*np)++;
 return 1;
}


//─────────────────────────────────
//  4) 자료 삭제 함수 
//─────────────────────────────────
node *HscDelete(int key, node a[], int *np)
{
 // 삭제할 노드의 앞 노드를 가리킴
 node *p;

 // 삭제할 노드
 node *t;

 if ( *np > 0 ) {
  // p와 t의 초기화
  p = &a[Hash_Func(key)];
  t = p->next;

  // 삭제할 노드를 찾음
  while ( t != NULL && t->key != key ) {
   p = t;
   t = t->next;
  }
  // 찾지 못하면 실패
  if ( t == NULL )
   return NULL;
  // 연결리스트에서 t를 제외
  p->next = t->next;
  // t를 메모리에서 해제
  free(t);
  // 자료수 감소
  (*np)--;
  return p;
 }
 return NULL;
}


//─────────────────────────────────
//  5) 자료 검색 함수 
//─────────────────────────────────
node *HscSearch(int key, node a[], int *np)
{
 node *t;
 // 연결리스트 처음
 t = a[HashFunc(key)].next;

 // 순차 검색
 while ( t != NULL && t->key != key)
  t = t->next;
 // t 리턴...
 // 검색을 실패했을 경우 NULL...
 return t;
}


//─────────────────────────────────
//  6) 전체를 삭제하는 함수
//─────────────────────────────────
// 해쉬 테이블의 각 버킷에 연결되어 있는 연결 리스트 순회
// 하면서 메모리 삭제
int HscDeleteAll(node a[], int *np, int N)
{
 node *t, *p;  // p 는 임시 저장용
 int  i;

 for ( i = 0; i < N; i++) {
  // 버킷에 연결된 리스트의 선두
  t = a[i].next;
  while ( t != NULL ) {
   // 삭제를 위해 물려둠
   p = t;
   // t 는 다음 노드를 가리킴
   t = t->next;

   // p를 삭제
   free(p);
  }
 }
 // 자료수 =0
 *np = 0;
 return 1;
}
}

 

 

//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓
//  [ 해 쉬 (Hash) ]
//
//    5. 해쉬 검색의 분석
//〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓


/*
//─────────────────────────────────
   5. 해쉬
//─────────────────────────────────

  1) 종류
     정적 해쉬법 : 선형 탐사법, 이중 해쉬법
    - 해쉬 테이블의 고정적 크기
      동적 자료 처리에 부적절
    - 큰 덩어리가 생길 확률이 연결법에 비해서 높다. (비효율적)
    - 구현 처리는 간단하다.

  동적 해쉬법 : 연결법
    - 연결 리스트와 동적 할당 이용
      고정적인 해쉬 테이블의 크기보다 큰 자료를 저장 검색 가능
    - 해쉬 함수를 잘만 설정하면 큰 덩어리가 생길 확률은 거의 없어
      효율적이다.

  2) 성능
     - 두 이론 모두 O(1)의 성능을 가진다.
     즉 자료수에 관계없이 일정한 검색과 삽입과 삭제 속도를 나타낸다.
  순차 검색, 이진나무검색의 실행시간이 N의 증가 함수인 것에 비하면
  매우 높은 효율이다.
  하지만 해쉬 테이블이 충분히 커야만 O(1)의 성능을 가진다.

  3) 메모리 내부 검색 방법
     - 두 이론 모두 방법이 국한되지 않는다.
    대량의 자료를 검색할 경우 가장 널리 사용되는 검색방법이다.

  4) 해쉬 테이블의 단점
     - 만일 검색과 동시에 정렬된 리스트를 얻어야 한다면 해쉬 검색법은
    재고의 대상이 된다.

*/

[출처] [펌] hash|작성자 라이온킹



...쌕이라고 하면 학생 분들이 매고 다니는 가방 같은것을 말하죠.

...Greedy(욕심쟁이) 알고리즘을 배우신 분들이시라면 다시 한번

...회상에 잠기시는 기회가 되시길 바랍니다.

http://www.koiz.net/algo/greedy-2.htm


Knapsack Problem


안녕하세요?

그리디를 이용하여 배낭(Knapsack)문제를 풀어보려고 합니다.
이 배낭문제는 Dynamic Programming으로 최적해를 100% 보장하는 솔루션이 있긴하지만
그리디 강좌 이므로!! (-_- ;) 그리디를 이용한 배낭문제를 풀어보도록 하겠습니다.
배낭문제는 다 아시죠? 모르시는 분들을 위해 간단히 설명하겠습니다.

------------------------------------------------------------------------------------
배낭에 물건들을 채우려고 하는데, 배낭이 담을 수 있는 무게는 한정되어 있고

각각의 물건들은 자기만의 값어치와 무게를 가진다. 어떻게 하면 배낭이 담을 수

있는 무게를 넘지 않고 최대의 값어치를 얻을 수 있도록 물건들을 배낭에 넣을 수

있을 것인가? (물건은 안쪼개진다)
(Knapsack Problem : 배낭문제)
------------------------------------------------------------------------------------

앞 강좌를 보신분이라면 그리디를 이용한 해법이 딱~ 떠오르실겁니다.
어떤 기준을 정하여 우선순위를 정해 넣는거죠. 여기서 기준이라는 것은 무게가 될 수도

있고 값어치가 될 수 도 있습니다. 그리고 무게는 가장 무거운부터라던가 가장 작은

거부터라던가..

====빰~빠~빠밤!=====> 예제맨~ 예제를 들어서 해보도록 하죠 (음..너무설치는군. )

물건 번호 -     1  2  3   4   5
무게 (weight)  3  4  7   8   9
값어치(value)  4  5  10 11  13
가방의 무게 :   17kg

이렇게 구성 되어 있다면 우린 3가지로 나누어서 생각해 볼수 있습니다.
/* 문제에서는 쪼갬이 불가능 하다고 하였지만 그리디 방법 중 최선의 경우를 모색하기
위하여 물체가 쪼개지는것으로 간주하여 값을 정리합니다. 나중에 문제 풀때에는 아무
상관이 없습니다 */
i ) 가장 무거운거 부터 넣어보면,
5번 과 4번 : 17kg : 24원( 편의상 원으로 하죠... ㅡㅡ )
ii ) 가장 가벼운거 부터 넣어보면,
1번과 2번과 3번 과 4번: 14+3/8*11kg : 23.12원
iii) 가장 값어치 있는거 부터
5번 과 4번 : 17kg : 24원

이렇게 대충 비교해 보면 1번과 3번이 좀 더 높은 값을 가진다는 것을 알 수 있습니다. 그런데
이 데이터에서는 특이하게 값어치와 무게가 양의 상관관계를 가지는군요. 그래서 1번과 3번
방법이 비슷하게 나왔습니다.

우선 기본적으로 이 세가지 방법을 생각 할 수 있고, 또 한가지 생각할 수 있는 것은
무게에 대한 가치를 각각 구해 그중 가장 큰 것부터 고르는 방법이 있습니다.
그렇게 되면,


iv) 1번 : 4/3

     2번 : 5/4

     3번 : 10/7

     4번 : 11/8

     5번 : 13/9


 이 중 값이 젤 큰 것부터,

 5번 + 3번 + 4번 : 17kg : 13 + 10 + 1/8*11 = 24.38원

 이 방법이 가장 좋죠? 이렇게 해서 이때까지 네가지의 그리디 방법 중 4번째 것이 가장
좋다고 판정되었습니다. 그러면 이걸 쓰는거죠. 그리디라 해도 너무 욕심쟁이 바보 멍청이
그리디 방법을 쓰면 안되니까 이렇게 조목조목 따져가면서 하는 것입니다.

Dijkstra의 알고리즘은 V에서부터 다른 모든 정점들까지 증가하는 거리의 순으로 최단 경로를 찾을 수 있다. 이 알고리즘은 팡의 최소 스패닝 트리 알고리즘에서처럼 정점 V에서 시작하여 새로운 정점들로 도달하는 특정한 에지들을 선택하면서 분기해 나간다. 또한 항상 가장 가까운 거리를 갖는 정점으로의 에지를 선택한다. 그리고, 각 인접 정점에 대해 오직 하나의 후보 에지만을 추적한다.

예를 들어, 어떤 회사에서 1000만원짜리 기계와 100만원짜리 기계중에 하나를 꼭 구입해야 하는 상황에 빠져 있을 때, Greedy는 말그대로 가장 싼것을 택하는 것이다. 1000만원짜리 기계를 써서 1억원이 넘게 벌수 있고, 100만원짜리를 써서, 10만원도 벌지 못한다 하더라도, 우선 뒤를 생각하지 않고 무조건 지금 상황에서의 최적만을 찾는 것이다. 그런 의미에서 Greedy라는 알고리즘은 항상 최적해를 뽑아내지 못하는 경우가 많다. 따라서, Greedy방법으로 최적해를 해결하고자 할 때 사용되는 선택규칙이 궁극적으로 최적해를 구해낸다는 것에 대한 증명이 반드시 뒷받침 되어야 한다.

Dynamic이라는 것은 Divide and Conquer의 반대말이다. Divide and Conquer는 큰 문제를 먼저 작은 문제들로 분할하고 각각 해결해나가는 것인데 반해, Dynamic은 작은 문제를 먼저 해결하고, 그 결과를 저장한 다음, 후에 똑같은 계산을 반복하는 대신, 그 저장한 결과를 이용하는 방식이다. 따라서 메모리가 많이 필요하다는 것이 Dynamic의 단점이다.


-----------------------------------------------------------------------------------------------------

다익스트라 알고리즘(Dijkstra algorithm)은 네덜란드컴퓨터과학자 에츠허르 다익스트라의 이름을 딴, 어떤 간선도 음수 값을 갖지 않는 유향 그래프에서 주어진 출발점과 도착점 사이의 최단 경로 문제를 푸는 알고리즘이다.

예를 들어, 그래프의 점들이 각각 도시를 나타내고, 연결선들이 도시 사이를 연결하는 도로의 길이를 나타낸다면, 다익스트라 알고리즘은 임의의 두 도시 사이의 최단 경로를 찾는다.

다익스트라 알고리즘은 방향이 주어진 가중 그래프(weighted graph) G와 출발점 s를 입력으로 받는다. 그래프 G의 모든 점들의 집합을 V라 하고, 그래프의 간선을 간선의 출발점 u와 도착점 v의 쌍 (u, v)로 표현한다. G의 모든 간선들의 집합을 E라 하고, 간선들의 가중치는 함수 w: E → [0, ∞]로 표현한다. 이때 가중치 w(u, v)는 점 u에서 점 v로 이동하는 데 드는 비용(시간, 거리 등)이 된다. 경로의 비용은 경로 사이의 모든 간선들의 가중치의 합이 된다. 다익스트라 알고리즘은 V의 임의의 점의 쌍 st가 있을 때 s에서 t로 가는 가장 적은 비용이 드는 경로(최단 경로)를 찾는다. 이 알고리즘은 주어진 출발점 s로부터 다른 모든 점까지의 최단 경로를 계산하는 데도 사용할 수 있다.

목차

[숨기기]

[편집] 알고리즘의 개요

다익스트라 알고리즘은 각각의 점 v에 대해 s에서 v까지의 최단 거리 d[v]를 저장하면서 작동한다. 알고리즘의 시작 시에 이 값은 s에 대해서는 0이고, (d[s]=0) 다른 모든 점에 대해서는 무한대(∞) 값으로 놓아 다른 점에 대해서는 아직 최단 경로를 모른다는 사실을 표시한다. 알고리즘이 종료되었을 때 d[v]는 s에서 v까지의 최단 경로의 거리를 나타내게 되고, 만약 경로가 존재하지 않으면 거리는 여전히 무한대로 남는다.

다익스트라 알고리즘은 이완(edge relaxation)이라고 불리는 기본 연산을 바탕으로 한다. s에서 u까지의 최단 경로(d[u])를 이미 알고 있고, u에서 v까지의 링크 (u, v)가 존재할 때, s에서 v까지의 최단경로는 u까지의 최단경로에 에 링크 (u, v)를 연장함으로써 얻을 수 있다. 이 경로의 비용은 d[u]+w(u, v)가 되며, 이 비용이 현재의 d[v] 값보다 낮으면 d[v]를 새로운 값으로 바꾼다.

이완 연산은 모든 간선 (u, v)에 대해 한번씩 이완이 적용되어 모든 d[v]가 최단 경로의 비용을 나타내게 되었을 때 끝난다.

알고리즘은 과정이 끝날 때까지 점의 집합 SQ를 저장한다. Sd[v]가 최단 경로의 비용임이 이미 알려진 점 v의 집합이고 Q는 나머지 점들의 집합을 가리킨다. S는 공집합에서 시작하여 매 단계마다 Q에서 S로 점들이 하나씩 옮겨 오며, 이때 옮겨 오는 점들은 d[u]가 가장 낮은 값을 갖는 점 u로 정해진다. uS로 옮겨 오면, 알고리즘은 u에서 시작하는 모든 간선에 대해 이완 연산을 행한다.

[편집] 수도코드

아래 알고리즘에서 u := Extract_Min(Q)는 점의 집합 Q에서 가장 작은 d[u]값을 찾은 다음 그 점 uQ에서 제거한 후 반환하는 함수를 가리킨다.

1   function Dijkstra(G, w, s)
2      for each vertex v in V[G]                        // 초기화
3            d[v] := infinity
4            previous[v] := undefined
5      d[s] := 0
6      S := empty set
7      Q := set of all vertices
8      while Q is not an empty set                      // 알고리즘의 실행
9            u := Extract_Min(Q)
10           S := S union {u}
11           for each edge (u,v) outgoing from u
12                  if d[v] > d[u] + w(u,v)             // (u,v)의 이완
13                        d[v] := d[u] + w(u,v)
14                        previous[v] := u

만약 모든 v에 대해서가 아니라 특정한 s에서 v까지의 최단 거리만을 알고 싶다면 9번째 행에서 u = t 일때 종료하면 된다.

s에서 t까지의 최단 경로는 다음과 같이 얻을 수 있다.

1 S := empty sequence 
2 u := t
3 while defined u                                        
4       insert u to the beginning of S
5       u := previous[u]

이제 Ss에서 t까지의 최단경로 상에 있는 점들의 목록이 된다.

[편집] 실행 시간

m개의 간선과 n개의 점을 가진 그래프에 대해 대문자 O 표기법으로 다익스트라 알고리즘의 실행시간을 나타낼 수 있다.

가장 간단한 구현으로, Q의 집합을 연결 리스트배열 구조로 구현하고 Extract-Min(Q) 함수를 단순한 선형 탐색으로 구현했을 때 실행 시간은 O(n2) 시간이 된다.

만약 희소 그래프(sparse graph), 즉 n2보다 훨씬 작은 개수의 간선만을 갖는 그래프에 대해서는, 인접 리스트(adjacency list)와 바이너리 또는 피보나치 힙으로 구현한 우선순위 큐(priority queue)를 이용해 다익스트라 알고리즘을 더 효율적으로 구현할 수 있다. 바이너리 힙을 이용하면 실행 시간은 O((m+n)log n) 시간이 되고, 피보나치 힙을 통해 O(m + n log n) 시간까지 개선할 수 있다.

[편집] 관련된 문제와 알고리즘

인터넷 라우팅에서 사용되는 OSPF(Open Shortest Path First) 방식의 프로토콜은 다익스트라 알고리즘이 실제 현장에서 사용되는 좋은 사례이다.

그래프의 가중치가 음수가 아니라고 가정하는 다익스트라 알고리즘과는 달리, 벨만-포드 알고리즘은 음수 가중치를 갖는 그래프에 대해서도 정확하게 동작한다. (단, 그래프에 음수 값을 갖는 순환 경로가 존재하지 않을 때)

목적지까지의 '거리'를 추정할 수 있는 방법이 있을 때, 다익스트라 알고리즘의 변형인 A* 알고리즘은 탐색해야 할 그래프의 크기를 줄이는 방법으로 더 빠르게 경로를 찾을 수 있다.



알고리즘에 발가락 담그기
[한빛미디어] 2005-12-02 11:42  
저자: 김대곤

* 알고리즘의 Complexity 또는 계산복잡도
* 탐욕 알고리즘(Greedy Algorithm)
* 동적 프로그래밍(Dynamic Programming) – 고급 설계 기법인가?
* Induction과 병합 정렬(Merge Sort) 알고리즘
* 거짓말 같은 Induction


다음과 같은 신탁이 주어졌다고 합시다. “1에서 n까지 출력하는 알고리즘을 개발하라”

너무 단순해 보여서 한가지 방법만 있는 듯 보입니다. 보기와는 다르게 아주 다양한 방법이 있습니다. 프로그래밍 책을 한 번이라도 본 사람들은 쉽게 다음과 같은 프로그램을 작성할 수 있을 것입니다.

public static void printUsingLoop(int N) {
    for ( int i=1 ; i <= N ; i++ ) {
        System.out.println(i);
    }
}

하지만 재귀 프로그램으로도 쉽게 구현할 수 있습니다.

public static void printUsingRecursion(int N) {
    if ( N == 1 ) {
        System.out.println(N);
    } else {
        printUsingRecursion(N-1);
        System.out.println(N);
    }
}

실행속도는 어떻게 될까요? 반복문을 사용한 경우에는 N에 비례하는 것이 분명하게 보입니다. 그러면 위의 재귀 프로그램의 속도는 어떻게 될까요? 이것도 N에 비례합니다. 재귀 프로그램의 실행속도는 재귀방정식으로 주어집니다. 즉 위의 프로그램의 수행속도는 다음과 같이 정의됩니다.



T(N)를 계속적으로 전개해 보면, N개의 1이 나옵니다. 이것들을 모두 더하면 N이므로 실행속도는 N이 됩니다. 조금 더 복잡해 보이는 프로그램을 만들어 봅시다. 다음과 재귀 프로그램도 1부터 N까지 출력합니다. 초기값은 1과 N이 되겠지요.

public static void printUsingRecursion(int i, int j) {
    if ( i == j ) {
        System.out.println(i);
    } else {
        int k = (int)((i+j)/2);
        printUsingRecursion(i,k);
        printUsingRecursion(k+1,j);
    }
}

수행속도는 어떻게 될까요? 먼저 실행속도는 재귀함수로 표현해 봅시다. 그러면 실행속도는 다음과 같이 표현됩니다.



간단히 말하기 힘들어 보입니다. T(1,5)를 전개해 봅시다.
T(1,4)=T(1,2)+T(3,4)+1={T(1,1)+T(2,2)+1}+{T(3,3)+T(4,4)+1}+1
다시 정리하면 T(1,4)=7이 됩니다. 동일하게 방법으로 T(1,8)=15가 됩니다. N에 비례한다고 말하기가 조금 주저되는 상황입니다. 결론부터 말하면 실행속도는 N에 비례합니다. 그리고 N(=j-i+1)이 2의 제곱승일 때 T(1,N)은 2N-1이 됩니다. 이것은 고등학교 수학으로 간단하게 계산이 가능합니다. 그 전에 재귀 트리(Recursion Tree)를 생각해 봅니다.



실제적인 출력은 leaf에서만 수행됩니다. 만약 leaf의 갯수가 N이고 leaf가 아닌 모든 노드들이 2개의 하위 노드를 가지고 있는 경우, 이러한 노드의 갯수는 N-1이 됩니다. 이것은 고등학교 수학으로 증명이 가능합니다. 최상위 노드는 하나이고 그 다음은 2개가 되고 그 다음은 4개 됩니다. 최상위 노드에서 Leaf 노드까지의 거리는 일정합니다. 그리고 이 거리를 h라고 합시다. Leaf가 아닌 노드의 갯수는 다음과 같이 계산됩니다. 등비수열의 합을 구하는 공식 생각나시죠.



N이 이므로 내부 노드의 갯수는 N-1이 됩니다. 전체 합은 2N-1입니다. 여기서 비례한다는 것은 알고리즘의 계산복잡도를 말하는 것입니다. 이것에 대한 간략한 설명은 “알고리즘 Complexity 또는 계산복잡도” 기사에서 찾을 수 있습니다.

알고리즘을 시작하는 사람들이 가지는 가장 일반적인 고정관념 중에 하나는 여기서 알고리즘이 끝났다고 생각하는데 있습니다. 하지만 알고리즘은 반드시 증명을 따라옵니다. 즉, 알고리즘이 목표한 결과를 가져온다는 것을 증명하지 않으면 무용지물입니다. 어떻게 그 알고리즘을 사용해서 프로그램을 할 수 있습니까 어떤 값을 나올지 모르는 상황에서. 일반적으로 알고리즘의 핵심을 알고리즘을 증명하는 과정에 있다고 할 수 있습니다. 이 과정을 연습해서 얻는 결과물은 자신의 문제에 대한 알고리즘 개발할 수 있는 것입니다. 알고리즘의 개발과 알고리즘의 증명은 동전의 양면과 같습니다.

여기서 사설은 중단하고 위에 있는 각 알고리즘을 증명해 봅시다. 먼저 첫번째 재귀 프로그램(R1이라고 합시다)을 증명해 봅시다. 만약 N=1이면, R1은 1(=N)를 출력합니다. 1에서 N(=1)까지 출력하였습니다. 만약 이 R1이 N=k일 때, 1에서 k까지 출력한다고 가정합시다. N=k+1일 때, R1(k)이 1부터 k까지 출력합니다. 그리고 나서 k+1를 출력합니다. 그러므로 1부터 k+1까지 출력합니다. 즉, 어떠한 자연수 N에 대하여 R1은 1부터 N까지 출력합니다. 이것으로 증명은 끝났습니다. 재귀 프로그램은 Induction를 이용하여 증명합니다.

그러면 처음에 있는 반복문은 어떻게 증명할까요? Loop Invariant로 이용하여 반복문을 증명합니다. Loop Invariant은 간단하게 말하면, 반복문이 시작되는 위치에서 항상 만족하는 성질입니다. 즉

각 루프의 시작점(for 과 출력문 사이), 이 때의 i의 값은 k라고 합시다. 1부터 k-1까지 출력되었다.

이것에 대하여 반복문 이전, 반복문이 수행되는 동안, 반복문이 끝난 시점에 위의 성질이 만족하는 것을 보이는 것이 반복문 증명의 방법입니다. 자 시작해 봅시다.

반복문이 시작되는 전에는 i=1입니다. 아직 아무것도 출력되지 않았습니다. 1에서 0까지는 아무것도 없으므로 Loop invariant를 만족합니다.

반복문이 i=k라고 합시다. 이 때에는 1부터 k-1까지 출력되었습니다. 그리고 k까 출력됩니다. 그리고 반복문은 i=k+1인 시작점을 통과하게 됩니다. 이 때에 1부터 k까지 출력되었습니다. 즉 1부터 (k+1)-1까지 출력되었습니다. 그러므로 모든 루프의 시작점에서 위의 성질은 만족합니다.

반복문이 끝나면, i는 N+1이 됩니다. i=N인 루프의 시작점에서 1에서 N-1까지 출력했고, 반복문을 빠져나오기 전에 N를 출력하였습니다. i=N+1일 때 1부터 N까지 출력하였습니다.

즉 반복문을 빠져 나온 시점에 우리가 예상할 수 있는 결과는 1부터 N까지 출력되었다는 것입니다. 이것으로 반복문을 사용한 알고리즘의 증명은 끝났습니다.

무슨 말 장난이냐고 생각하실 분이 계실지 모릅니다. 하지만 이 과정 없이는 스스로 알고리즘을 개발하기는 아주 힘듭니다. 이제 위의 과정을 잘 이해되었다면 쉽게 풀리는 몇가지 문제를 제시함으로써 기사를 마무리 하려고 합니다.

1. 주어진 N에 대하여 N부터 1까지 출력하는 프로그램.
2. 주어진 N(= )에 대하여 1에서 N까지의 짝수를 순서대로 출력하는 프로그램.
3. 주어진 N(= )에 대하여 1에서 N까지의 수 중에서 4로 나누었을 때 2가 남는 수를 순서대로 출력하는 프로그램.
4. 정렬된 정수의 배열 A[1…N]에서 k가 있는지 없는지 확인하는 프로그램. 예를 들어 A[1…5]가 다음과 같이 주어졌다고 하자.



k=8이면 2, k=15이면 -1를 출력해야 한다. 즉 있으면 그 위치를 출력하고 없으면 -1를 출력하는 프로그램.

+ Recent posts