문제: Nim 게임을 하는데, i번째 무더기에는 처음에 Ai개의 돌이 있고, i번째 무더기에 X개의 돌이 있을 때 최소 1개에서 최대 X/Ki (나머지는 버림)개 사이의 갯수에 돌만 가져갈 수 있습니다. 서로 번갈아가면서 하나의 무더기에서 몇개의 돌을 번갈아가면서 가져가고, 더 이상 돌을 가져갈 수 없는 사람이 진다고 했을 때, 첫째 사람이 이기는지, 둘째 사람이 이기는지를 구하시오.
풀이: 이 문제는 그냥 Nim 게임(모든 K가 1인 경우)를 푼다고 하면, Impartial Game에서의 Sprague-Grundy Theorem를 사용합니다. 이에 관한 정보는 위키피디아 게시글에서 얻을 수 있습니다.
요약하자면 Impartial Game이란 누가 플레이하냐에 관계 없이, 현재 상태로, 바꿀 수 있는 다음 상태가 정해지며 (첫째 사람 혹은 둘째 사람만 가져갈 수 있는 돌이라는건 존재하지 않습니다.) 유한한 게임입니다. (게임이 무한히 진행될 수 없습니다.) Normal play condition 이라는 것은, 더 이상 일을 계속할 수 없는 사람이 진다는 것을 의미합니다. 여기서 상태라고 함은, 이 게임에서는 각 무더기에 있는 돌의 갯수가 됩니다.
여기에서 우리는 Nimber(혹은 Grundy Number)라는 것을 정의합니다. 이 Nimber는 재귀적으로 정의 되는데, "갈 수 있는 상태의 Nimber를 모두 모았을 때, 등장하지 않는 가장 작은 0 이상의 정수." 라고 정의합니다.
예를 들어, K=4인 상황을 가정합시다. 이 경우에 X = 0, 1, 2, 3인 경우에는 돌을 더 이상 가져갈 수 없기 때문에 (X/K 가 1 미만이기 때문에) 갈 수 있는 상태가 존재하지 않기 때문에, 어떤 정수도 등장하지 않고 Nimber는 0이 됩니다. X = 4인 경우에는, X=3인 상태로만 갈 수 있습니다. 이 때의 Nimber는 0이고, 1은 존재하지 않기 때문에, X=4일때 Nimber는 1입니다. 마찬가지로 X=5일때는 0, X=6일때는 1, X=7일때는 0, X=8일 때는, 1개 이상 2개 이하의 돌을 가져갈 수 있고, X=6, 7인 상태로 갈 수 있고, Nimber를 모두 모으면 0, 1이기 때문에 2가 등장하지 않고, X=8일때의 Nimber는 2입니다. X=9일때의 Nimber는 X=7, 8일때의 Nimber를 모아 보면 0과 2인데, 1이 등장하지 않는 가장 작은 수 이므로 Nimber가 1입니다.
이렇게 K=4인 경우에 Nimber를 만들어 보면 다음과 같은 표가 나옵니다.
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0
0
0
0
1
0
1
0
2
1
0
2
3
1
0
2
4
3
이 Nimber에는 다음과 같은 성질이 있습니다. Nimber가 0이면 자신이 패배하고, Nimber가 0이 아니면 자신이 승리합니다. 이유는, Nimber가 0이 아니면, 0이 다음 상태라는 것을 의미하고, 다음 사람의 턴을 Nimber가 0인 상태로 만들 수 있습니다. 이렇게 다음 사람의 Nimber를 0으로 계속 만들 수 있고, Nimber가 0이 아니면 갈 수 있는 상태가 있다는 것을 의미하기 때문에 패배하지 않습니다. 그래서 4, 6, 8, 9, 11, 12, ... 이면 K=4일때 무더기가 하나일 때 처음 사람이 이깁니다.
그럼 여러개의 게임을 동시에 진행하는 경우에는 어떨까요? 우리는 Sprague-Grundy Theory를 쓰게 됩니다. 여러개의 게임을 동시에 진행하는 경우의 Nimber는 모든 subgame의 Nimber를 XOR한 값입니다. 증명은 위키백과 문서를 참고해 주시기 바랍니다.
이제 우리는 이 멋진 정리의 도움을 받아, 각 수의 Nimber를 구하는 것으로 일이 줄었습니다. 각 수의 Nimber를 구하는 방법은 어떻게 될까요? 위의 표에서 규칙성을 찾으면 다음과 같은 정리를 증명할 수 있습니다.
정리: nimber(X-X/K)... nimber(X)는 0부터 X/K까지의 X/K+1개의 수가 모두 존재한다.
증명: 강한 수학적 귀납법을 사용합니다.
base case: X<K, nimber(X)는 0이고, 자명합니다.
inductive case:
1. X가 K를 정확히 나눌 때, X-X/K는 (X-1) - (X-1)/K과 같으므로, X가 갈 수 있는 상태인 nimber(X-X/K)...nimber(X-1)를 모두 모으면 0부터 (X-1)/K = X/K-1 까지 이고, nimber(X)는 X/K 이므로, 성립합니다.
2. X가 K를 정확히 나누지 않을 때, X 가 갈 수 있는 상태인 nimber(X - X/K)...nimber(X-1)을 모두 모으면, 0부터 X/K = (X-1)/K 에서, nimber(X-1 - (X-1)/K)가 빠진 형태가 됩니다. 여기서 nimber의 정의에 의해서 nimber(X) = nimber(X-1 - (X-1)/K)가 되어서, 성립하게 됩니다.
우리는 이 증명을 하면서 유용한 정보를 얻었습니다. X가 K를 정확히 나누면 nimber(X) = X/K이고, 아니면 nimber(X) = nimber(X-X/K-1)이 됩니다.
이것을 그대로 구현하면 시간이 오래 걸리게 됩니다. 가령이면, K=A/2+1 정도의 경우를 생각 해 봅시다. X/K = 1이고, 계속 2씩 숫자를 빼게 되면, A/4번 정도 뺄셈을 하게 됩니다. 이렇게 되면 O(A)의 시간이 걸리게 되겠죠. 그래서 우리는 여기서 시간복잡도를 줄일 아이디어를 얻을 수 있습니다. 바로 계속 2씩 숫자를 뺀다는 부분은, X/K 가 작은 경우에, 같은 숫자를 빼는 경우가 많다는 것입니다. X/K 가 바뀌려면, X%K 만큼을 빼야하고, 이것을 빼는 동안에는 X/K의 배수로 한번에 빼 줄수가 있습니다.
이 경우에 숫자는 X/K가 적어도 1씩 줄고, K가 적어도 1씩 줄게 됩니다. (X/K - 1)(K-1) = XK - X/K - K + 1이므로, 숫자는 X/K + K 만큼은 적어도 줄게 되고, 산술기하평균 부등식에 의해서, 숫자가 적어도 sqrt(X)씩 줄게 되어서, 시간복잡도가 O(sqrt(A)) 가 나오게 됩니다.
우리는 어떤 무더기 하나의 Nimber를 sqrt(A)에 구할 수 있습니다! 이 Nimber를 모두 xor하면 답이 나오게 됩니다. 총 시간복잡도는 O(nsqrt(A)) 가 됩니다.
문제: 0과 1로 이루어진 문자열 S에서, 어떤 부분구간을 flip한다는 것은, 그 구간의 부분문자열의 문자를 0을 1로, 1을 0으로 바꾼 다는 것을 의미합니다. 길이 K이상의 구간만을 flip하여 모든 문자를 0으로 만들 수 있다고 할 때, 가능한 K의 최댓값을 구하시오. (동시에 K는 N이하)
풀이: [1, X] 구간을 flip하고 [1, X+1] 구간을 flip하면 X+1번째 문자만 flip할 수 있습니다. 즉, X가 K이상일 경우에는, 숫자를 하나씩 flip하는게 마찬가지라는 것 입니다. 그래서 왼쪽에서 세어서 K+1번째 이후 문자의 상태는 모두를 0으로 바꿀때 상관이 없습니다. 오른쪽에서도 마찬가지의 논리가 성립합니다.
그래서 왼쪽에서 세어도, 오른쪽에서 세어도 K+1번째 이내인 수들에 대하여, 이 문자들이 모두 같은지 (같으면 전체를 flip할 수 있습니다.)를 판단해 주면 됩니다. 다른 경우에는, 이 문자들은 길이 K의 구간을 어떻게 잡아도 모두를 한번에 뒤집을 수 밖에 없기 때문에, 전부를 0으로 만드는게 불가능 합니다.
이런 K를 문자열의 가운데 부터 시작해서 같은 구간이 어디까지를 세어 줄 경우에 O(|S|)의 시간복잡도로 풀 수 있습니다.
문제: 두 정수의 쌍 (a, b) 중에서, 1 ≤ a, b ≤ N 이면서, a를 b로 나눈 나머지가 K 이상인 것의 갯수를 세시오.
풀이: mod를 규칙적으로 보기 위해서는 b가 고정되어있으면 편하기 때문에, b를 고정시켜 놓습니다. b를 고정시킨 경우에는 어떤 수를 b로 나눈 나머지는 0, 1, 2, 3, ..., b-1 까지 갔다가, 다시 0부터 시작해서 1씩 증가하고, b-1 다음엔 0으로 돌아온다는 사실을 알 수 있습니다. 그래서 이렇게 돌아가는 수열 중에서 K 이상인 것의 갯수를 세는 것은, 반복되는 부분인 0, 1, 2, 3, ..., b-1이 몇번 반복 되는가와, 가장 마지막에 어떤식으로 수열이 끝나는지를 알면 알 수 있습니다. 자세한 수식은 코드의 수식을 보면 알 수 있습니다.
문제: 길이 N의 두 수열 a와 b가 주어진다. 모든 가능한 i와 j에 대해서 ai+bj의 합 N2개를 모두 XOR 한 값을 구하시오.
풀이: 모든 합을 구해서 XOR하는것은 구하기 때문에, XOR을 구할때 비트별로 볼 것 입니다. 우리가 어떤 특정한 2B 에 해당하는 비트를 볼 때, 그 위에 있는 비트는 영향을 주지 않습니다. 중요한 것은 우리가 구하려는 비트(2B)에 있는 값과, 그보다 하위 비트(작은 두 수를 더해서 2B의 비트에 영향을 줄 수 있음)에 있는 값 입니다. 한 비트에 대해서 XOR은 1이 홀수개 있는지 짝수개 있는지 묻는 연산이기 때문에, 우리는 결국에는 해당하는 비트의 1의 홀짝성을 알려주면 됩니다. 즉, 이 비트에 영향을 홀수번 주었나, 짝수번 주었나를 묻는 문제입니다.
일단 우리가 구하려는 비트 부터 살펴봅시다. 한 원소의 구하려는 비트가 1이라는 것은, 합 N개의 비트에 영향을 주었다는 의미입니다. 그래서 우리는 수열에 해당하는 비트의 원소가 몇개 있는지를 살펴보면 됩니다. 이제 우리가 구하려는 비트에 영향을 끼치는 갯수는 구했습니다.
이 후에는, 우리가 구하려는 비트보다 하위 비트에 영향을 주는 갯수만 세면 됩니다.
그래서 우리는 이 문제를 비트마다 구하는 문제로 바꾸면, 각 수열의 원소는 2B 보다 작고, 2B에 해당하는 비트가 0인지 1인지를 구하는 문제가 됩니다. 2B에 해당하는 비트가 0인지 1인지 구하는 문제이지만, 어떤 수열의 두 원소의 합은 2B의 두배를 넘지 못하기 때문에, 결국 합해서 2B보다 큰 합의 갯수를 세는 문제로 바뀝니다. 이 문제는 sliding window를 사용하거나, 아니면 binary search를 사용해서 간단하게 구할 수 있습니다. 어떤 한 원소가 x이면, N-x보다 크거나 같은 수의 갯수를 std::lower_bound 등을 이용해 간단하게 세어 줄 수 있습니다. 제 구현은 후자를 사용했고 코드는 다음과 같습니다.
문제: A와 B가 주어졌을 때, 흰색 connected component의 갯수가 A개, 검정색 connected component의 갯수가 B개인 격자를 아무거나 찾아서 출력하시오.
풀이: 이것은 저의 풀이고, 다양한 풀이가 존재할 수 있습니다.
A가 1인 경우를 풀려고 해 봅시다. 일단 B가 0이면, 흰색으로 전체를 덮으면 됩니다. 여기서 B가 매우 크다고 해 봅시다. 그러면 흰색 component에 검정색 component 여러개를 끼워넣는 방법이 되어야 합니다. 그래서 우리는 흰색 component에 검정색 component를 서로 영향을 주지 않게 흰색끼리 연결되어있는 상태로 추가할 수 있습니다. 마치 수박에 있는 수박씨 처럼 흰색에다가 검정색으로 점을 서로 띄어서 찍어주면 됩니다. 이럴 경우에는 A가 1인 경우를 풀 수 있습니다. B를 추가하는게 점을 찍는것 입니다.
마찬가지로, 하얀색을 한 칸 추가할 수 있는 환경을 만드려면, 검정색으로 전체를 덮으면 됩니다.
저는 하얀색과 검정색을 반 반 나눠서 칠한 후 (A = B = 1), 검정색과 하얀색으로 점을 B-1, A-1개 각각 찍는데 서로 최소 2씩 거리가 나게 찍었습니다. 이렇게 찍은 경우에는 하얀색도 검정색도 배경은 연결되어 있다는 것을 알 수 있습니다.
이 문제는 다양한 풀이가 존재하고, 우리가 만들어야하는 격자의 크기가 작을 경우에 만들 수 있는가? 를 물어보는 문제도 재밌을 것 같습니다.
티스토리 블로그에도 달고 싶은데 지원을 안하네요... SSL 인증서 좀 지원해주면 좋으려만
좀 더 자세한 얘기는 호스팅을 bluehost에서 linode로 옮겼습니다. 처음에 서버 세팅에 관한 지식이 없을때 php로 사이트를 뚝딱뚝딱 만든것도 있고, 가격이 월 10$라서 샀었는데, 지금은 linode하나 올려서 쓰는게 좀 더 마음이 편하네요. 가장 싼 플랜이 한 달 5$ 에요. 역시 루트권한 있으니까 마음이 편해요.
예전에는 블루호스트가 인증서 적용하는게 유료라서 못 달았는데, 지금은 letsencrypt로 인증서를 받아서 적용했습니다.
호스팅이 이것저것 난잡해지기도 해서, 아예 flask기반으로 웹을 재구성 하고 있는데 아직 옮기지 못한게 있네요.
하나는 Musicplayer인데, Youtube Red를 쓰니까 별로 쓸 일이 없어지는것 같기도 하네요.
다른건, 사람들에게 링크를 줄 때 잡다하게 파일들을 올리고 그걸 주는 방식으로 문제를 만들었는데, 이것들은 지금 살려둬야 할지 말아야 할지 모르겠는 링크도 많아서 일단은 안 살려 뒀습니다...
사실 최근에 트래픽 부담이 꽤나 있는데 linode도 한달정도 써보고 트래픽 부담이 있으면 여기만 따로 옮기는 방식을 쓰든가 여러가지 조치를 취해야 할 것 같네요.
Sandbox는 기본적으로 libsandbox를 사용했고, ptrace를 이용해서 시스템콜이 있을 때 마다 가로채서 올바른지 확인하는 방법을 사용했습니다. 이러면 퍼포먼스 저하가 있기 때문에 다른 방법을 사용할까 하고 고민중입니다.
하드디스크의 I/O로 인한 병목을 없애기 위해서, 모두다 메모리에 가져와서 memfd로 메모리 공간 위에 파일 디스크립터를 만드는 방법을 사용했습니다. 하드디스크의 상태와 관계 없이 꽤나 안정적인 테스팅 환경을 가집니다. 디테일이 궁금하신 분이 있다면 저한테 메일을 주시거나 아니면, 제가 언젠가 이런것들을 전부다 정리할 수 있는 여유가 될 때 까지 기다려 주시기 바랍니다.
프론트엔드는 사실 별 게 없습니다... 그냥 빡구현을 했습니다.
뭐 결론적으로 제가 만들고 싶은 Online Judge를 만들었고, 많은 사람이 써줬으면 좋겠다는 바람을 가지고 있습니다. 코딩 테스트 준비를 하든, 개인적으로 PS를 배우든, 알고리즘 커뮤니티에 기여할 수 있는 것은 언제든지 좋은 일이라고 생각하니까요.
General Maximum Matching Algorithm에 대해 삽질을 많이 했고, 다른 사람이 삽질을 하지 않기 위해서 이 글을 쓰려고 합니다.
이 글은 매칭에 대해 잘 이해를 하고 있다고 가정하고, 이분그래프의 매칭을 찾는 법을 아는 사람을 위한 글입니다.
이분그래프에 대해서는 O(EV^0.5) 풀이가 존재하고, 시간복잡도 증명이 어렵지만 그 부분을 제외하고는 이해할 만 합니다.
사실 일반그래프에 대해서도 마찬가지가 존재하나, 구현하기가 굉장히 어렵습니다. 그래서 O(V^3) 시간 알고리즘을 소개하려고 합니다.
먼저 General Graph의 Matching에 어려움의 대해 설명을 하겠습니다.
odd는 정점에서 간선을 홀수 번 타고 갈 수 있는 점, even은 정점에서 간선을 짝수번 타고 갈 수 있는 점이라고 말하겠습니다.
Graph 에서 Augment Path를 찾을 때, 이분그래프는 2가지 색으로 잘 칠해질 수 있기 때문에(odd, even) odd 정점이 비어있는지 아닌지만 확인하면 됩니다.
하지만 General graph에서는 어떤 정점이 odd이면서 동시에 even일 수 있기 때문에, 잘 처리를 해야 하고 처리를 한다고 해도 역추적이 어렵습니다.
우리는 이 Augment Odd cycle을 Blossom이라고 부르겠습니다.
이 cycle에는 특징이 하나 있습니다. 모든 Cycle위에 있는 점은 even입니다. 방향에 따라서 case가 2개가 나뉘게 되는데, 두 케이스 모두 blossom 위를 한 방향이나 반대방향으로 돌면서 Augment Path를 만들게 됩니다.
그래서 우리는, 이 blossom 위에 있는 점을 모두 even이라고 표시 해 줄것입니다. 이렇게 되면, augmenting path를 탐색하지 못하는 일이 없고 이것은 Edmond의 논문에서 증명이 되어 있습니다. 그러면 역추적을 할 때 굉장히 힘들것입니다. 우리는 이 역추적을 하는 방법을, blossom을 줄이지 않고 두개로 확장하는 방법을 사용할 것 입니다.
그림을 보면 10 - 3 - 4 - 6 - 5 의 blossom 이, 10 - 3 - 4 - 6 - 5와 10 - 5 - 6 - 4 - 3으로 나뉘어져 있다는 것을 확인할 수 있습니다. 우리는 탐색을 even인 점에서만 시작하기 때문에 (odd인 점은 단지 matching을 타고 가는것으로 끝납니다.) 탐색을 중복해서 하는일은 없게 되고, 10 - 3 - 4 - 6 - 5 가 모두 10에서 시작하는 blossom에 속해있다. 라는 사실을 저장해 두면, 다른 blossom이 생겼을 때 합치는 일도 어렵지 않습니다.
제가 과제로 작성했던 pdf 하나를 첨부하고 글을 마치도록 하겠습니다. 소스코드는 제가 첨부한 pdf안에 존재합니다.