02. 수학적 귀납법과 invariant를 통한 증명 [1 2주차]

Posted by aliontory on January 15, 2024 · 44 mins read

2. 수학적 귀납법과 Invariant를 통한 증명 [1-2주차]

2024 1 9일 화요일

오후 10:48

<수학적 귀납법>

수학적 귀납법의 기본형 : P(1)[Base]이 참이고, P(n-1)->P(n)[Step]이 참이면, P(n)은 모든 자연수 n에 대해서 참이다.                           

       

P(n-1)->P(n)을 증명할 때 P(n-1)이 참이라고 ‘가정’하고 P(n)임을 증명한다.

수학적 귀납법의 강한 형태 : P(1)이 참이고, P(1)P(2)∧…∧P(n-1)->P(n)이 참이면 P(n)모든 자연수 n에 대해서 참이다.

( and, &&를 의미하는 수학 기호)

 

 

<명제 P->Q의 의미>

P(n) : n이 등장하는 문장

명제 : 객관적으로 참, 거짓을 판단할 수 있는 문장. 아래의 4개 문장은 명제이다.

조건 : 그 자체로는 명제가 아니지만, 변수값에 따라 참, 거짓을 판단할 수 있는 문장. 아래에서 P Q는 조건이다.

 

- P가 참, Q가 참 -> 전체는 참

- P가 참, Q가 거짓 -> 전체는 거짓

- P가 거짓, Q가 참 -> 전체는 참

- P가 거짓, Q가 거짓 -> 전체는 참

 

예전에는 3, 4번째 경우를 vacuous(공허한)이라고 했다.

그러나 이 경우를 true라고 하는 것이 더 편리하기에 지금은 참이라고 한다.

 

ex1) 자연수 n에 대해 3<n -> 10<n2

- 위 명제는 참인가?

- n = 1일 때, 위 명제는 참. (거짓->거짓)

 

ex2) 자연수 n에 대해 1<n -> 10<n2

**n=1, n=4일 때는 참이 되지만, n=2일 때는 거짓이 된다.**

 

 

<간단한 재귀함수증명>

unsigned int sum(unsigned int x)

{

    if(x <= 0) return 0;

    return x + sum(x 1);

}

위 함수가 항상 0+1+2++x 를 리턴한다는 것을 증명해 보자.

S(3)->S(2)->S(1)->S(0)->0->1->3->6 처럼 따라 들어가는 식으로 생각하면 복잡한 함수에서 증명하기 어렵다. 수학적 귀납법을 이용해 증명하자.

 

- P(1)이 참이다 : sum(1)이 리턴하는 값은 1이다”를 증명하면 됨. 실제 코드에 1을 대입하면 1을 리턴함을 알 수 있음.

- P(x-1) -> P(x) 가 참이다 : sum(x-1) 1+2++(x-1)을 리턴하면 sum(x) 1+2++x를 리턴한다”를 증명하면 됨.

- 코드를 보면 sum(x) x+sum(x-1)의 값을 리턴함. sum(x-1)의 리턴 값은 1+2++(x-1)과 같다고 가정했으므로 sum(x) 1+2++(x-1)+x를 리턴함을 확인할 수 있음

 

 

<루프 불변성(Invariant)>

루프 불변성 : 반복문이 실행되는 동안 변하지 않는 성질

ex) 삽입 정렬에서 인덱스j 원소를 뽑아 정렬하는 경우

for (j=1; j<array.length; j++)

"array[0..j-1] for문이 반복을 시작할 때마다 정렬된 상태이다." -> 루프 불변성

 

루프 불변성을 보이려면 다음 세 가지를 만족해야 한다.

1. 초기조건 : 루프가 첫 번째 반복을 시작하기 전에 루프 불변성이 이어야 한다.

2. 유지조건 : 루프의 반복이 시작되기 전에 루프 불변성이 참이었다고 가정하면, 다음 반복이 시작되기 전까지도 계속 참이여야 한다.

3. 종료조건 : 루프가 종료될 때 그 불변식이 알고리즘의 타당성을 보이는 데 유용한 특성을 가져야 한다.

 

종료조건 예) 위 삽입 정렬에서 for문이 종료될 때 j = length 이다. 이를 루프 불변성에 대입하면 "array[0..length-1]은 정렬된 상태이다"가 된다. 배열 전체가 정렬되었으며 이는 알고리즘이 타당함을 의미한다.

 

 

<Binary Search 증명>

int search(int a[], int n, int x)

{

    int l, r, m;

    l = 0; r = n-1;

    while(l <= r) {

        m = (l+r) / 2;  // l<=m<=r 이다.

        if(a[m] == x) return m;

        else if (a[m] > x) r = m-1;

        else l = m+1;

    }

    return -1;

}

 

(a는 정렬된 배열, n은 배열 크기, x는 찾을 값, m은 인덱스)

 

 

* Proof by Invariant

Invariant : 만약 어떤 i에 대해서 a[i] = x라면, l<=i<=r항상성립한다.

(1) 초기조건 : l은 인덱스의 시작, r은 인덱스의 끝이므로 l<=i<=r가 성립.

(2) 유지조건 : a[m]>x라면 m>i, r=m-1>i-1, r>=i.

a[m]<x라면 m<i, l=m+1<i+1, l<=i.

invariant가 깨질 수 있는 코드가 없다.

(3) 종료조건 : l r의 차이가 계속 줄어들기 때문에, 적어도 l=i=r이 될 때 i를 리턴하게 된다.

 

* a[i] = x i가 없다면, l r의 차이가 계속 줄어들기 때문에, 루프는 반드시 끝나고 -1이 리턴 됨

 

 

<Binary Search 증명(재귀)>

int search(int a[], int n, int x)

{

    int m;

    if(n<=0) return -1;

    m = n/2;

    if(a[m] == x) return m;

    else if(a[m] > x) return search(a, m, x);

 

else {

        r = search(a+m+1, n-m-1, x);

        if(r = -1) return -1;

        else return r+m+1;

    }

}

 

재귀 호출이 반복될수록 감소하는 값 n을 기준으로, 수학적 귀납법을 적용한다.

 

주장 : 만약 어떤 i에 대해서 a[i] = x라면, 위 함수는 i를 리턴한다.

Base : n = 0인 경우 “어떤 i에 대해서 a[i] = x”가 성립할 방법이 없고, 함수는 항상 -1을 리턴한다.

Step : n=0, n=1, n=2, , n = k-1인 경우 주장이 참(a[i] = x를 만족하는 i가 없거나, 있다면 i를 리턴)이라 가정하면, n = k인 경우 주장이 참이 됨을 보이면 된다.

n = k일 때,

- Case 1 : a[m] = x인 경우 m을 리턴하므로 주장이 성립

- Case 2 : a[m] > x인 경우 a[m], a[m+1], , a[n] 중에서는 x와 같은 값이 없다. 따라서 a[i] = x인 경우가 있다면 i 0, 1, , m-1중 하나이다. 위에서, n=0, n=1, , n=k-1인 경우 주장이 참이 된다고 가정하였다. 따라서 n=m일 경우, 주장이 참이 된다. , search(a, m, x)는 정확하게 작동하여 i를 리턴한다. search(a, k, x) i를 그대로 리턴하게 되므로, n=k일 때 주장은 참이 된다.

- Case 3 : a[m] < x인 경우, a[i] = x인 경우가 있다면 i m+1, m+2, , k 중 하나이다. search(a+m+1, k-m-1, x) n=0, , n=k-1인 경우 중 하나이므로, 위 가정에 따르면 정확하게 작동하여 i를 리턴한다. 그러나 a a+m+1을 입력했기 때문에, 재귀 호출된 함수에서의 i는 호출한 함수 기준으로는 i-m-1가 된다. 이를 고려하여 m+1을 더해 리턴하므로, n=k일 때 주장은 참이 된다.

* Case1, 2, 3에서 a[i] = x를 만족하는 i가 있다고 가정하였다. a[i] = x를 만족하는 i가 없는 경우, 주장은 무조건 성립하므로, 그러한 가정을 해도 문제가 없다. (만족하는 i가 없는 경우는 고려하지 않아도 된다.)

 

 

<Selection Sort의 증명>

int sort(int a[], int n)

{

int i, j, m, t;

for(i=0; i<n; i++) {

    // find minimum

    m = i;

    for (j=i; j<n; j++)

        if (a[m] > a[j]) m = j;

    // swap

    t = a[i]; a[i] = a[m]; a[m] = t;

}

return;

}

 

Sorting이 됐다는 증명을 어떻게 할 것인가?

입력 : a[0], a[1], , a[n-1] <- (정수) 집합

Sorting이 완료된 후 다음이 만족되어야 함

    - 구분을 위해, Sorting이 끝난 후 배열에 저장된 값들을 b[0], b[1], ,b[n-1]라고 부르자

    - 조건 1: {a[0], a[1], , a[n-1]} = {b[0], b[1], , b[n-1]} 집합으로 같음

    - 조건 2: b[0] < b[1] < < b[n-1] (편의상 같은 값은 없다고 가정)

 

Proof by Invariant

- 배열 요소를 교환만 하므로, 집합 조건을 깰 수 있는 코드는 없음

- Invariant: k번째 루프가 끝난 후에

    (1) a[0] < a[1] < < a[k-1]

-> k=n인 경우 a[0] < a[1]< < a[n-1]

    (2) a[k-1] < a[x] if x > k-1

- 이제 수학적 귀납법을 통해 invariant를 증명하자

Base : k = 0

(1): a[0]부터 a[-1]까지? 아무것도 없음. 이는 아무말도 하지 않은 것(null condition). 아무말도 하지 않으면 true.

(2): a[-1]은 말이 안 됨. null condition 이므로 true.

Step : k번째 루프가 끝났을 때 Invariant가 성립한다면, k+1번째 루프가 끝났을 때도 Invariant가 성립하는가?

    (1): a[0] < a[1] < < a[k-1] < a[k]

       a[0] < < a[k-1] k번째 루프의 (1)에 의해 성립

       a[k-1] < a[k] k번째 루프의 (2)에 의해 성립. a[k] a[x]중 무엇이였든간에 a[k-1] 보다 크다.

    (2): a[k] < a[x] if x > k

       a[k], , a[n-1]중 최소값을 a[k]로 옮겼으므로 성립

 

 

<Selection Sort 재귀호출 증명>

int sort(int a[], int n)

{

    if (n<=1) return;

int j, m, t;

m = 0;

for(j=0; j<n; j++) {

    if (a[m] > a[j]) m = j;

}

// swap

t = a[0]; a[0] = a[m]; a[m] = t;

sort(a+1, n-1);

return;

}

 

조건 1: {a[0], a[1], , a[n-1]} = {b[0], b[1], , b[n-1]} 집합으로 같음

조건 2: b[0] < b[1] < < b[n-1] (편의상 같은 값은 없다고 가정)

 

이번에도 조건1을 깰 수 있는 코드는 없음.

 

Base : n = 1일 때, 할 일이 없음.

Step : n-1일 때 sort() 함수가 성공한다면 (, 재귀 호출이 끝난 후 a[1]<a[2]<<a[n-1] 이라면)

n일 때 sort() 함수가 성공하는가? (함수가 끝날 때 a[0]<a[1]<<a[n-1]이 성립하는가?)

- 재귀호출 전 : 최소값을 a[0]에 저장하므로 a[0]<a[1], a[0]<a[2], , a[0]<a[n-1]이 성립한다.

- 재귀호출 이후 : 가정에 따라, a[1]<a[2]<<a[n-1]이 성립한다. 최소값을 a[0]에 저장했으므로, a[0]<a[1]이다. 따라서 n일 때 sort()함수가 성공한다.

 

 

OneNote에서 작성되었습니다.