[Algorithm] 수학적 개념과 알고리즘
Table of Contents
비트 조작 #
비트 조작 트릭 #
0s
는 모든 비트가0
인 값이며,1s
는 모든 비트가1
인 값이다.
x ^ 0s = x x & 0s = 0 x | 0s = x
x ^ 1s = ~x x & 1s = x x | 1s = 1s
x ^ x = 0 x & x = x x | x = x
음수를 나타내기 위한 2의 보수법 #
-
보수(Complementary Number) 란?
- 보충을 해주는 수이다.
-
$R$진수에서, $N$에 대한 $R-1$의 보수란?
- $N$이 $R$진수에서 해당 자릿수의 최대값이 되기 위해 얼마를 보충해 주어야 하는 가이다.
-
$R$진수에서, $N$에 대한 $R$의 보수란?
- $N$이 $R$진수에서 자릿수를 한 자리 높이기 위헤 얼마를 보충해 주어야 하는 가이다.
- 이것은 $R-1$의 보수 $+ 1$ 과 같다.
-
예시
- $10$진수에서, $33$에 대한 $9$의 보수는?
- $99 - 33 = 66$이다.
- $10$진수에서, $33$에 대한 $10$의 보수는?
- $100 - 33 = 67$이다.
- $2$진수에서,
1010
에 대한 $1$의 보수는?1111 - 1010 = 0101
이다.- 이것은
0
과1
의 반전이다.
- $2$진수에서,
1010
에 대한 $2$의 보수는?10000 - 1010 = 0110
이다.- 이것은 $1$의 보수 $+ 1$이다.
- 즉,
0
과1
을 반전 시키고 $1$을 더하면 된다.
- $10$진수에서, $33$에 대한 $9$의 보수는?
-
컴퓨터에서 음수 양수를 구별 하기위해 최상위 비트(MSB; Most significant bit) 를 부호비트로 사용한다.
- $1$은
0001
이므로 $-1$는1001
가 되겠다. - 그 둘을 빼면?
- $0$이어야 하는데
1010
이 나오는 문제가 있다.
- $0$이어야 하는데
- $1$은
-
그러면 $1$의 보수법을 사용해서 음수를 표현하면 어떨까?
0
과1
를 반전시켜서 음수를 표현하면 되겠다.- $1$인
0001
과 $-1$인1110
을 더하면?1111
이 나온다.- $1$의 보수는 $+0$인
0000
과 $-0$인1111
의 두 가지 $0$이 존재한다는 문제점이 있다.
-
그래서 $2$의 보수법을 사용해서 음수를 표현한다.
0
과1
을 바꾼다음 $1$을 더해서 음수를 표현하면 되겠다.- $1$인
0001
과 $-1$인1111
을 더하면?10000
이 나온다.
- 여기서 MSB가
1
이지만, 범위를 벗어났기 때문에 결과적으로 $0$인0000
이 된다.
-
결과적으로, 음수를 나타내려면,
0
과1
을 반전시켜서 $1$을 더한 수에다가 MSB에1
을 붙이면 되겠다.
논리 우측 시프트와 산술 우측 시프트 #
-
논리 우측 시프트(Logical right shift)
- 비트를 오른쪽으로 옮긴 후, MSB에
0
을 넣는다. >>>
연산과 같다.
- 비트를 오른쪽으로 옮긴 후, MSB에
-
산술 우측 시프트(Arithmetic right shift)
- 비트를 오른쪽으로 옮기지만, MSB 비트는 변하지 않는다.
>>
연산과 같다.- 이것은 대략 값을 $2$로 나눈 것과 같은 결과이다.
기본 비트 조작 예시 #
#include <iostream>
#include <bitset>
using namespace std;
const int bitSize = 4;
void Printer(int num, int mask, int result)
{
cout << "\t원래 숫자 : \t" << bitset<bitSize>(num) << endl;
cout << "\t마스크 : \t" << bitset<bitSize>(mask) << endl;
cout << "\t결과 숫자 : \t" << bitset<bitSize>(result) << endl;
}
bool IsBitOne(int num, int i)
{
int mask = 1 << i;
cout << i << " 번째 자리 수가 1인가? : " << endl;
Printer(num, mask, num & mask);
return (num & mask) != 0;
}
int SetBitOne(int num, int i)
{
int mask = 1 << i;
cout << i << " 번째 자리 수를 1로 만들기 : " << endl;
Printer(num, mask, num | mask);
return num | mask;
}
int SetBitZero(int num, int i)
{
int mask = ~(1 << i);
cout << i << " 번째 자리 수를 0으로 만들기 : " << endl;
Printer(num, mask, num & mask);
return num & mask;
}
int SetBitZeroAfterIndex(int num, int i)
{
int mask = (1 << i + 1) - 1;
cout << i << " 번째 자리 이후(i 자리 제외)를 모두 0으로 만들기 : " << endl;
Printer(num, mask, num & mask);
return num & mask;
}
int SetBitZeroBeforeIndex(int num, int i)
{
int mask = ~((1 << i) - 1);
cout << i << " 번째 자리 이전(i 자리 제외)을 모두 0으로 만들기 : " << endl;
Printer(num, mask, num & mask);
return num & mask;
}
int UpdateBit(int num, int i, bool setOne)
{
int value = ((setOne) ? 1 : 0) << i;
int mask = ~(1 << i);
int remove = num & mask;
cout << i << " 번째 자리 비트를 " << setOne << "으로 만들기 : " << endl;
Printer(num, mask, remove);
cout << "\t숫자 입력 : \t" << bitset<bitSize>(remove | value) << endl;
return remove | value;
}
int main()
{
int num = 7; // 숫자 : 1 1 1
int i = 1; // 몇 번째 자리 : 2 1 0
IsBitOne(num, i);
SetBitOne(num, i);
SetBitZero(num, i);
SetBitZeroAfterIndex(num, i);
SetBitZeroBeforeIndex(num, i);
UpdateBit(num, i, 0);
UpdateBit(num, i, 1);
return 0;
}
1 번째 자리 수가 1인가? :
원래 숫자 : 0111
마스크 : 0010
결과 숫자 : 0010
1 번째 자리 수를 1로 만들기 :
원래 숫자 : 0111
마스크 : 0010
결과 숫자 : 0111
1 번째 자리 수를 0으로 만들기 :
원래 숫자 : 0111
마스크 : 1101
결과 숫자 : 0101
1 번째 자리 이후(i 자리 제외)를 모두 0으로 만들기 :
원래 숫자 : 0111
마스크 : 0011
결과 숫자 : 0011
1 번째 자리 이전(i 자리 제외)을 모두 0으로 만들기 :
원래 숫자 : 0111
마스크 : 1110
결과 숫자 : 0110
1 번째 자리 비트를 0으로 만들기 :
원래 숫자 : 0111
마스크 : 1101
결과 숫자 : 0101
숫자 입력 : 0101
1 번째 자리 비트를 1으로 만들기 :
원래 숫자 : 0111
마스크 : 1101
결과 숫자 : 0101
숫자 입력 : 0111
수학 및 논리 퍼즐 #
소수 #
- 모든 자연수는 소수의 곱으로 나타낼 수 있다는 법칙이 있다.
- 따라서 $x/y$로 나눌 수 있으려면,
- $x$에 대한 소수의 곱 $\supset$ $y$에 대한 소수의 곱이 되어야 한다.
- $x = 2^{a_0} \times 3^{a_1} \times 5^{a_2} \times 7^{a_3} …$
- $y = 2^{b_0} \times 3^{b_1} \times 5^{b_2} \times 7^{b_3}…$
- $a_i \geq b_i$
- 그렇다면 $x$와 $y$의 최대공약수(Greatest common divisor) 는?
- $gcd(x, y) = 2^{min(a_0, b_0)} \times 3^{min(a_1, b_1)} \times 5^{min(a_2, b_2)}$
- 그렇다면 $x$와 $y$의 최소공배수(Least Common multiple) 는?
- $lcm(x, y) = 2^{max(a_0, b_0)} \times 3^{max(a_1, b_1)} \times 5^{max(a_2, b_2)}$
- 어떤 수가 소수인지 판별하는 법
- 어떤 수가 소수이려면, $1$과 자기자신 말고는 약수가 없어야 한다.
- 소수가 아닌 경우, 가운데 위치한 약수를 중심으로 대칭적으로 약수가 형성된다.
- 예를 들어, $16$의 경우 약수가 $1$, $2$, $4$, $8$, $16$ 이므로 $4$를 중심으로 대칭적으로 약수가 형성됨을 알 수 있다.
- 따라서 어떤 수의 제곱근까지만 약수인지 판별하면 된다.
void IsPrimeNumber(int num)
{
for (int i = 2; i <= sqrt(num); i++) // num의 제곱근까지만 검사한다.
{
if (num % i != 0) continue;
return false; // 나누어 떨어지면 소수가 아니다.
}
return true;
}
- $1$부터 $N$까지의 모든 소수를 찾는 법 : 에라토스테네스의 체(Sieve of Eratosthenes)
vector<bool> GetPrimeNumbers(int num)
{
vector<bool> isPrime(num + 1, true);
for (int i = 2; i < sqrt(num); i++) // num의 제곱근까지만 검사한다.
{
if (isPrime[i] == false) continue; // 아직 처리하지 않은 가장 작은 수를 선택한다
int mul = 2;
while(i * mul <= num) // 그 수의 배수를 모두 제거한다 (그 수는 제거하면 안 됨)
{
isPrime[i * mul] = false;
mul++;
}
}
return isPrime;
}
확률 #
- 조건부 확률
- $P(A | B)$는 $B$ 사건이 발생했을 때, $A$가 일어날 확률이다.
- 즉, $B$를 사건 전체로 보고 $B$ 중에서 $A$인 부분이다.
- 이것을 사건 $B$에 대한 $A$의 조건부 확률이라고 한다.
- $$ P(A | B) = \frac{ P(A \cap B) }{ P(B) } $$
- $$ P(A \cap B) = P(A | B) P(B) $$
- 교집합의 확률
- $1$부터 $10$까지의 수 중에서 하나를 뽑을 때, 짝수이면서 $5$ 이하의 수를 뽑을 확률은 어떻게 될까?
- $$ P(짝수 \cap 5이하) = P(짝수 | 5이하) P(5이하) $$
- $$ P(짝수 \cap 5이하) = \frac{2}{5} * \frac{1}{2} = \frac{1}{5}$$
- 베이즈 정리(Bayes’ Theorem)
- 조건부 확률을 다음과 같이 정리할 수 있겠다.
- $$ P(A \cap B) = P(A | B) P(B) = P(B | A) P(A) $$
- $$ P(A | B) = \frac{ P(B \cap A)P(A) }{ P(B) } $$
- 합집합의 확률
- $A$ 사건 혹은 $B$ 사건이 벌어질 확률은?
- $$ P(A \cup B) = P(A) + P(B) - P(A \cap B) $$
- $1$부터 $10$까지의 수 중에서 하나를 뽑을 때, 짝수이거나 $5$ 이하의 수를 뽑을 확률은 어떻게 될까?
- $$ P(짝수 \cup 5이하) = P(짝수) + P(5이하) - P(짝수 \cap 5이하)$$
- $$ P(짝수 \cup 5이하) = \frac{1}{2} + \frac{1}{2} - \frac{1}{5} = \frac{4}{5}$$
- 독립 사건(independent event)
- $A$와 $B$가 서로 아무런 영향을 미치지 않는 사건을 말한다.
- $ P(A \cap B) = 0 $, $ A \cup B = U $
- 예를 들면, $A$ 라는 사람이 공부를 하는 것과 $B$ 라는 사람이 음식을 먹는 것은 독립적인 사건이다. 서로에게 아무런 영향이 없다.
- 따라서 다음과 같은 공식이 성립한다.
- $$ P(A | B) = P(A | B^c) = P(A) $$
- 이것을 조건부 확률 공식에 대입하면 다음과 같다.
- $$ P(A \cap B) = P(A) P(B) $$
- 상호 배타적 사건(mutually exclusive event)
- 어떤 한 사건이 일어나면 다른 사건은 일어날 수 없는 경우이다.
- $ P(A \cap B) = 0 $, $ (A, B) \subset C $
- $A$와 $B$는 $C$라는 집단 안에 소속되어 있지만 서로 공통적인 부분은 없다는 뜻이다.
- 예를 들면, 남성과 여성은 상호배타적이다. 남성과 여성은 둘 다 ‘인간’이라는 카테고리에 포함되지만 서로 다르기 때문이다.
- $P(A \cap B) = 0$이므로 다음과 같다.
- $$ P(A \cup B) = P(A) + P(B) $$
- 독립 사건과 상호 배타적 사건은 서로 다른 개념이며, 독립 사건이면서 상호 배타적 사건인 경우는 존재하지 않는다.
재귀와 동적 프로그래밍 #
재귀적 해법 #
- 해당 문제가 재귀 문제인지 아는 방법?
- 해당 문제를 작은 크기의 부분문제(Subproblem) 로 만들 수 있는지 본다.
- (1) 하향식 접근법(Top-down approach)
- 문제를 어떻게 하면 부분문제로 나눌 수 있는지 생각해본다. 부분문제끼리 겹치지 않아야 한다.
- (2) 상향식 접근법(Bottom-up approach)
- 간단한 것부터 풀이를 시작해서, 이전에 풀었던 사례를 확장하여 다음 풀이를 찾는다.
- 예를 들면, 원소 하나를 갖는 리스트에서 시작하여, 두 개, 세 개 늘려가며 풀이법을 찾는다.
- (3) 반반 접근법(Half-and-half approach)
- 데이터를 절반으로 나누는 방법이다.
- 예를 들면, 이진 탐색이나 병합 정렬이 있겠다.
- 재귀적 해법 vs. 순환적 해법
- 재귀 호출이 한 번 발생할 때마다 스택에 새로운 층(layer)을 추가해야 하므로, 공간 효율성이 나빠질 수 있다.
- 모든 재귀적(recursive) 알고리즘은 순환적(iterative) 으로 구현이 가능하다.
- 하지만 때로 순환적 알고리즘은 코드가 복잡해지므로, 잘 절충해서 선택해야겠다.
동적 프로그래밍 #
- 동적 프로그래밍이란?
- 부분문제를 찾아서, 재귀나 반복문을 사용해서 푸는데, 현재 결과를 캐시에 저장해서 연산 속도를 비약적으로 높인다.
- 예를 들면, 바로 앞 두 항의 합을 구하는 피보나치 수(Fibonacci number) 가 있다.
- 간단하게 구현한 아래 코드의 경우 재귀 트리를 참고하면, 계속 2개의 자식으로 분기되므로 $O(2^n)$의 시간복잡도가 걸린다.
int GetFibonacci(int x)
{
if (x == 1 || x == 2) return 1;
return GetFibonacci(x - 1) + GetFibonacci(x - 2);
}
- (1) 하향식 동적 프로그래밍(메모이제이션)
- 재귀 트리를 살펴보면, 중복되는 노드들이 있다.
- 이 값들을 저장했다가 사용하면 시간복잡도를 $O(n)$으로 만들 수 있겠다.
int GetFibonacci(int x, vector<int>& d)
{
if (x == 1 || x == 2) return 1;
if (d[x] != 0) return d[x]; // 이미 계산한 적 있는 문제라면 그대로 반환
d[x] = GetFibonacci(d, x - 1) + GetFibonacci(d, x - 2);
return d[x];
}
- (2) 상향식 동적 프로그래밍
- 초기 사례인
fib(0)
과fib(1)
을 구하고 이것을 이용해fib(2)
를 계산하며 점점 올라가는 방식이다.
- 초기 사례인
int GetFibonacci(int x, vector<int>& d)
{
d[1] = 1;
d[2] = 1;
for (int i = 3; i <= x; i++)
d[i] = d[i - 1] + d[i - 2];
return d[x];
}