연분수¶
연분수는 실수를 유리수로 이루어진 특정한 수렴 수열로 나타낸 표현입니다. 연분수는 계산하기 쉽고, (분모가 주어진 값을 넘지 않는 모든 수 중에서) 원래 실수에 대한 최선의 유리 근사를 효율적으로 찾는 데 사용할 수 있으므로 경쟁 프로그래밍에서 유용합니다.
그 밖에도 연분수는 유클리드 알고리즘과 밀접한 관련이 있어 여러 정수론 문제에서 유용합니다.
연분수 표현¶
정의
$a_0, a_1, \dots, a_k \in \mathbb Z$이고 $a_1, a_2, \dots, a_k \geq 1$이라고 합시다. 그러면 다음 식을
유리수 $r$의 연분수 표현이라고 하며, 간단히 $r=[a_0;a_1,a_2,\dots,a_k]$로 나타냅니다.
Example
$r = \frac{5}{3}$이라고 합시다. 이를 연분수로 나타내는 방법은 두 가지입니다.
모든 유리수는 정확히 $2$가지 방법으로 연분수로 나타낼 수 있음을 증명할 수 있습니다.
또한 $r=\frac{p}{q}$에 대해 이러한 연분수의 길이 $k$는 $k = O(\log \min(p, q))$로 추정됩니다.
그 이유는 연분수 구성의 세부 사항을 자세히 살펴보면 명확해질 것입니다.
정의
$a_0,a_1,a_2, \dots$가 $a_1, a_2, \dots \geq 1$을 만족하는 정수 수열이라고 합시다. $r_k = [a_0; a_1, \dots, a_k]$라고 합시다. 그러면 다음 식을
무리수 $r$의 연분수 표현이라고 하며, 간단히 $r = [a_0;a_1,a_2,\dots]$로 나타냅니다.
$r=[a_0;a_1,\dots]$와 정수 $k$에 대해 $r+k = [a_0+k; a_1, \dots]$가 성립함에 유의하십시오.
또 다른 중요한 관찰은 $a_0 > 0$일 때 $\frac{1}{r}=[0;a_0, a_1, \dots]$이고, $a_0 = 0$일 때 $\frac{1}{r} = [a_1; a_2, \dots]$라는 것입니다.
정의
위의 정의에서 유리수 $r_0, r_1, r_2, \dots$를 $r$의 근사분수라고 합니다.
이에 따라 개별 $r_k = [a_0; a_1, \dots, a_k] = \frac{p_k}{q_k}$를 $r$의 $k$번째 근사분수라고 합니다.
Example
$r = [1; 1, 1, 1, \dots]$을 생각해 봅시다. 귀납법으로 $r_k = \frac{F_{k+2}}{F_{k+1}}$임을 증명할 수 있으며, 여기서 $F_k$는 $F_0 = 0$, $F_1 = 1$, $F_{k} = F_{k-1} + F_{k-2}$로 정의되는 피보나치 수열입니다. 비네 공식으로부터 다음이 알려져 있습니다.
여기서 $\phi = \frac{1+\sqrt{5}}{2} \approx 1.618$은 황금비이고 $\psi = \frac{1-\sqrt{5}}{2} = -\frac{1}{\phi} \approx -0.618$입니다. 따라서
이 특정한 경우에는 $r$을 구하는 또 다른 방법으로 다음 방정식을 풀 수도 있음에 유의하십시오.
정의
$r_k = [a_0; a_1, \dots, a_{k-1}, a_k]$라고 합시다. $1 \leq t \leq a_k$에 대하여 수 $[a_0; a_1, \dots, a_{k-1}, t]$를 반수렴자라고 합니다.
일반적으로 $r$보다 큰 (준)수렴자를 위쪽 (준)수렴자라고 하고, $r$보다 작은 (준)수렴자를 아래쪽 (준)수렴자라고 부르겠습니다.
정의
수렴자와 짝을 이루는 개념으로, $s_k = [a_k; a_{k+1}, a_{k+2}, \dots]$를 완전몫(complete quotient)으로 정의합니다.
이에 따라 개별 $s_k$를 $r$의 $k$번째 완전몫이라고 부르겠습니다.
위의 정의로부터 $k \geq 1$에 대하여 $s_k \geq 1$임을 알 수 있습니다.
$[a_0; a_1, \dots, a_k]$를 형식적인 대수식으로 취급하고 $a_i$ 대신 임의의 실수를 허용하면 다음을 얻습니다.
특히 $r = [s_0] = s_0$입니다. 한편, $s_k$는 다음과 같이 나타낼 수 있습니다.
즉, $s_k$로부터 $a_k = \lfloor s_k \rfloor$와 $s_{k+1} = (s_k - a_k)^{-1}$을 계산할 수 있습니다.
수열 $a_0, a_1, \dots$은 $s_k=a_k$인 경우를 제외하면 잘 정의되며, 이 경우는 $r$이 유리수일 때만 발생합니다.
따라서 연분수 표현은 모든 무리수 $r$에 대하여 유일하게 정의됩니다.
구현¶
코드 예시에서는 주로 유한 연분수를 가정하겠습니다.
$s_k$에서 $s_{k+1}$로의 전이는 다음과 같습니다.
이 식으로부터 다음 완전몫 $s_{k+1}$은 다음과 같이 얻어집니다.
$s_k=\frac{p}{q}$인 경우, 이는 다음을 의미합니다.
따라서 $r=\frac{p}{q}$의 연분수 표현을 계산하는 과정은 $p$와 $q$에 대한 유클리드 알고리즘의 단계를 그대로 따릅니다.
이로부터 $\frac{p_k}{q_k} = [a_0; a_1, \dots, a_k]$에 대하여 $\gcd(p_k, q_k) = 1$이라는 사실도 도출됩니다. 그러므로 수렴자는 항상 기약분수입니다.
auto fraction(int p, int q) {
vector<int> a;
while(q) {
a.push_back(p / q);
tie(p, q) = make_pair(q, p % q);
}
return a;
}
def fraction(p, q):
a = []
while q:
a.append(p // q)
p, q = q, p % q
return a
주요 결과¶
연분수를 더 깊이 공부할 동기를 부여하기 위해, 이제 몇 가지 핵심적인 사실을 소개합니다.
점화식
수렴자 $r_k = \frac{p_k}{q_k}$에 대하여 다음 점화식이 성립하며, 이를 통해 수렴자를 빠르게 계산할 수 있습니다.
여기서 $\frac{p_{-1}}{q_{-1}}=\frac{1}{0}$이고 $\frac{p_{-2}}{q_{-2}}=\frac{0}{1}$입니다.
편차
$r_k = \frac{p_k}{q_k}$와 $r$의 편차는 일반적으로 다음과 같이 추정할 수 있습니다.
양변에 $q_k$를 곱하면 다른 형태의 추정을 얻습니다.
위의 점화식으로부터 $q_k$가 적어도 피보나치 수만큼 빠르게 증가함을 알 수 있습니다.
아래 그림에서 수렴자 $r_k$가 $r=\frac{1+\sqrt 5}{2}$에 가까워지는 과정의 시각화를 볼 수 있습니다:
$r=\frac{1+\sqrt 5}{2}$는 파란 점선으로 표시되어 있습니다. 홀수 번째 수렴자는 위쪽에서, 짝수 번째 수렴자는 아래쪽에서 이 값에 접근합니다.
격자 껍질
직선 $y=rx$의 위쪽과 아래쪽에 있는 점들의 볼록 껍질을 생각해 봅시다.
홀수 번째 수렴자 $(q_k;p_k)$는 위쪽 껍질의 정점이고, 짝수 번째 수렴자 $(q_k;p_k)$는 아래쪽 껍질의 정점입니다.
껍질 위의 모든 정수 정점은 다음을 만족하는 $(q;p)$로 얻어집니다.
단, 이는 $0 \leq t \leq a_k$를 만족하는 정수에 대해서입니다. 다시 말해, 껍질 위의 격자점 집합은 반수렴자의 집합과 일치합니다.
아래 그림에서 $r=\frac{9}{7}$의 수렴자와 반수렴자(중간의 회색 점)를 볼 수 있습니다.
최선 근사
어떤 $x$에 대해 $q \leq x$라는 조건 아래에서 $\left|r-\frac{p}{q}\right|$를 최소화하는 분수를 $\frac{p}{q}$라고 합시다.
그러면 $\frac{p}{q}$는 $r$의 반수렴자입니다.
이 마지막 사실을 이용하면 $r$의 반수렴자를 확인하여 $r$의 최선 유리 근사를 찾을 수 있습니다.
아래에서는 이러한 사실들에 대한 추가 설명과 함께 약간의 직관과 해석을 제공합니다.
수렴자¶
앞에서 정의한 수렴자를 좀 더 자세히 살펴봅시다. $r=[a_0, a_1, a_2, \dots]$에 대해, 그 수렴자는 다음과 같습니다.
수렴자는 연분수의 핵심 개념이므로, 그 성질을 공부하는 것이 중요합니다.
수 $r$에 대해, $k$번째 수렴자 $r_k = \frac{p_k}{q_k}$는 다음과 같이 계산할 수 있습니다.
여기서 $P_k(a_0,\dots,a_k)$는 연속항(continuant)으로, 다음과 같이 정의되는 다변수 다항식입니다.
따라서 $r_k$는 $r_{k-1}$과 $r_{k-2}$의 가중 중간수(mediant)입니다.
일관성을 위해 두 개의 추가 수렴자 $r_{-1} = \frac{1}{0}$과 $r_{-2} = \frac{0}{1}$을 정의합니다.
자세한 설명
$r_k$의 분자와 분모는 $a_0, a_1, \dots, a_k$의 다변수 다항식으로 볼 수 있습니다:
수렴자의 정의로부터,
이로부터 $Q_k(a_0, \dots, a_k) = P_{k-1}(a_1, \dots, a_k)$가 성립합니다. 이로부터 다음 관계식을 얻습니다.
처음에는 $r_0 = \frac{a_0}{1}$이고 $r_1 = \frac{a_0 a_1 + 1}{a_1}$이므로,
일관성을 위해 $P_{-1} = 1$과 $P_{-2}=0$으로 정의하고, 형식적으로 $r_{-1} = \frac{1}{0}$과 $r_{-2}=\frac{0}{1}$이라고 하는 것이 편리합니다.
수치해석학에서 알려진 바에 따르면, 임의의 삼중대각 행렬의 행렬식은
$T_k = a_k T_{k-1} - b_{k-1} c_{k-1} T_{k-2}$와 같이 재귀적으로 계산할 수 있습니다. 이를 $P_k$와 비교하면 다음과 같은 직접적인 식을 얻습니다.
이 다항식은 연분수와 밀접한 관련이 있어 continuant라고도 불립니다. 주대각선의 수열을 뒤집어도 continuant는 변하지 않습니다. 이로부터 이를 계산하는 또 다른 공식을 얻습니다.
구현¶
수렴 분수는 두 수열 $p_{-2}, p_{-1}, p_0, p_1, \dots, p_k$와 $q_{-2}, q_{-1}, q_0, q_1, \dots, q_k$의 쌍으로 계산합니다.
auto convergents(vector<int> a) {
vector<int> p = {0, 1};
vector<int> q = {1, 0};
for(auto it: a) {
p.push_back(p[p.size() - 1] * it + p[p.size() - 2]);
q.push_back(q[q.size() - 1] * it + q[q.size() - 2]);
}
return make_pair(p, q);
}
def convergents(a):
p = [0, 1]
q = [1, 0]
for it in a:
p.append(p[-1]*it + p[-2])
q.append(q[-1]*it + q[-2])
return p, q
연분수의 트리¶
가능한 모든 연분수를 유용한 트리 구조로 묶는 주요 방법은 두 가지입니다.
Stern-Brocot 트리¶
Stern-Brocot 트리는 서로 다른 모든 양의 유리수를 포함하는 이진 탐색 트리입니다.
이 트리는 일반적으로 다음과 같은 모습입니다.
분수 $\frac{0}{1}$과 $\frac{1}{0}$은 각각 트리의 왼쪽과 오른쪽에 "가상으로" 놓여 있습니다.
그러면 노드에 있는 분수는 그 위에 있는 두 분수 $\frac{a}{b}$와 $\frac{c}{d}$의 중간수 $\frac{a+c}{b+d}$입니다.
점화식 $\frac{p_k}{q_k}=\frac{a_k p_{k-1} + p_{k-2}}{a_k q_{k-1} + q_{k-2}}$은 연분수 표현이 트리에서 $\frac{p_k}{q_k}$까지의 경로를 인코딩한다는 뜻입니다. $[a_0; a_1, \dots, a_{k}, 1]$을 찾으려면 오른쪽으로 $a_0$번, 왼쪽으로 $a_1$번, 오른쪽으로 $a_2$번 이동하는 식으로 $a_k$까지 이동해야 합니다.
그러면 $[a_0; a_1, \dots, a_k,1]$의 부모는 마지막으로 이동한 방향으로 한 단계 되돌아가서 얻는 분수입니다.
다시 말해, $a_k > 1$일 때는 $[a_0; a_1, \dots, a_k-1,1]$이고 $a_k = 1$일 때는 $[a_0; a_1, \dots, a_{k-1}, 1]$입니다.
따라서 $[a_0; a_1, \dots, a_k, 1]$의 자식은 $[a_0; a_1, \dots, a_k+1, 1]$과 $[a_0; a_1, \dots, a_k, 1, 1]$입니다.
Stern-Brocot 트리에 인덱스를 매겨 봅시다. 루트 정점에는 인덱스 $1$을 부여합니다. 그런 다음 정점 $v$에 대해, 왼쪽 자식의 인덱스는 $v$의 최상위 비트를 $1$에서 $10$으로 바꾸어 부여하고, 오른쪽 자식의 인덱스는 최상위 비트를 $1$에서 $11$로 바꾸어 부여합니다.
이 인덱싱에서 유리수의 연분수 표현은 그 이진 인덱스의 런 렝스 인코딩을 나타냅니다.
$\frac{5}{2} = [2;2] = [2;1,1]$의 경우 인덱스는 $1011_2$이고, 비트를 오름차순으로 볼 때 런 렝스 인코딩은 $[2;1,1]$입니다.
또 다른 예로 $\frac{2}{5} = [0;2,2]=[0;2,1,1]$이 있습니다. 이 수의 인덱스는 $1100_2$이고, 실제로 이 인덱스의 런 렝스 인코딩은 $[0;2,2]$입니다.
Stern-Brocot 트리는 사실 트립이라는 점에 주목할 만합니다. 즉, $\frac{p}{q}$를 기준으로는 이진 탐색 트리이지만, $p$와 $q$ 모두를 기준으로는 힙입니다.
연분수 비교하기
$A=[a_0; a_1, \dots, a_n]$와 $B=[b_0; b_1, \dots, b_m]$가 주어집니다. 어느 분수가 더 작습니까?
풀이
우선 $A$와 $B$가 무리수이고, 이들의 연분수 표현이 Stern-Brocot 트리에서의 무한한 하강을 나타낸다고 가정합시다.
앞서 언급했듯이, 이 표현에서 $a_0$은 하강 과정에서 오른쪽으로 꺾는 횟수를, $a_1$은 그 뒤에 이어서 왼쪽으로 꺾는 횟수를 나타내며, 이후도 마찬가지입니다. 따라서 $a_k$와 $b_k$를 비교할 때 $a_k = b_k$이면 그냥 $a_{k+1}$과 $b_{k+1}$의 비교로 넘어가면 됩니다. 그렇지 않은 경우, $A < B$인지 판단하려면 오른쪽 하강 중이라면 $a_k < b_k$인지를, 왼쪽 하강 중이라면 $a_k > b_k$인지를 확인해야 합니다.
다시 말해, 무리수 $A$와 $B$에 대해 $A < B$일 필요충분조건은 사전순 비교로 $(a_0, -a_1, a_2, -a_3, \dots) < (b_0, -b_1, b_2, -b_3, \dots)$인 것입니다.
이제 형식적으로 $\infty$를 연분수 표현의 원소로 사용하면 무리수 $A-\varepsilon$과 $A+\varepsilon$, 즉 $A$보다 작지만(크지만) 다른 어떤 실수보다도 큰(작은) 원소를 흉내 낼 수 있습니다. 구체적으로, $A=[a_0; a_1, \dots, a_n]$에 대해 이 두 원소 중 하나는 $[a_0; a_1, \dots, a_n, \infty]$로, 다른 하나는 $[a_0; a_1, \dots, a_n - 1, 1, \infty]$로 흉내 낼 수 있습니다.
어느 쪽이 $A-\varepsilon$에 해당하고 어느 쪽이 $A+\varepsilon$에 해당하는지는 $n$의 홀짝성으로 판단하거나, 두 수를 무리수로서 비교하여 판단할 수 있습니다.
# check if a < b assuming that a[-1] = b[-1] = infty and a != b
def less(a, b):
a = [(-1)**i*a[i] for i in range(len(a))]
b = [(-1)**i*b[i] for i in range(len(b))]
return a < b
# [a0; a1, ..., ak] -> [a0, a1, ..., ak-1, 1]
def expand(a):
if a: # empty a = inf
a[-1] -= 1
a.append(1)
return a
# return a-eps, a+eps
def pm_eps(a):
b = expand(a.copy())
a.append(float('inf'))
b.append(float('inf'))
return (a, b) if less(a, b) else (b, a)
최적의 내부 점
$\frac{0}{1} \leq \frac{p_0}{q_0} < \frac{p_1}{q_1} \leq \frac{1}{0}$이 주어집니다. $(q; p)$가 사전순으로 가장 작으면서 $\frac{p_0}{q_0} < \frac{p}{q} < \frac{p_1}{q_1}$을 만족하는 유리수 $\frac{p}{q}$를 구하십시오.
풀이
Stern-Brocot 트리의 관점에서 보면, 이는 $\frac{p_0}{q_0}$와 $\frac{p_1}{q_1}$의 LCA를 구해야 한다는 뜻입니다. Stern-Brocot 트리와 연분수 사이의 관계 때문에, 이 LCA는 대략 $\frac{p_0}{q_0}$와 $\frac{p_1}{q_1}$의 연분수 표현의 최장 공통 접두사에 대응합니다.
따라서 $\frac{p_0}{q_0} = [a_0; a_1, \dots, a_{k-1}, a_k, \dots]$와 $\frac{p_1}{q_1} = [a_0; a_1, \dots, a_{k-1}, b_k, \dots]$가 무리수라면, LCA는 $[a_0; a_1, \dots, \min(a_k, b_k)+1]$입니다.
유리수 $r_0$과 $r_1$의 경우, 둘 중 하나가 LCA 자체일 수 있으므로 경우를 나누어 처리해야 합니다. 유리수 $r_0$과 $r_1$에 대한 풀이를 단순화하기 위해, 앞의 문제에서 유도한 $r_0 + \varepsilon$과 $r_1 - \varepsilon$의 연분수 표현을 사용할 수 있습니다.
# finds lexicographically smallest (q, p)
# such that p0/q0 < p/q < p1/q1
def middle(p0, q0, p1, q1):
a0 = pm_eps(fraction(p0, q0))[1]
a1 = pm_eps(fraction(p1, q1))[0]
a = []
for i in range(min(len(a0), len(a1))):
a.append(min(a0[i], a1[i]))
if a0[i] != a1[i]:
break
a[-1] += 1
p, q = convergents(a)
return p[-1], q[-1]
GCJ 2019, Round 2 - New Elements: Part 2
양의 정수 쌍 $(C_i, J_i)$가 $N$개 주어집니다. $C_i x + J_i y$가 순증가 수열이 되도록 하는 양의 정수 쌍 $(x, y)$를 구해야 합니다.
그러한 쌍 중에서 사전순으로 가장 작은 것을 구하십시오.
풀이
문제를 바꾸어 말하면, 모든 $i$에 대해 $A_i x + B_i y$가 양수여야 합니다. 여기서 $A_i = C_i - C_{i-1}$이고 $B_i = J_i - J_{i-1}$입니다.
$A_i x + B_i y > 0$ 꼴의 이러한 방정식들은 네 가지 중요한 그룹으로 나눌 수 있습니다:
- $x, y > 0$인 해를 찾고 있으므로 $A_i, B_i > 0$인 경우는 무시할 수 있습니다.
- $A_i, B_i \leq 0$인 경우에는 답이 "IMPOSSIBLE"이 됩니다.
- $A_i > 0$, $B_i \leq 0$인 경우입니다. 이러한 제약 조건은 $\frac{y}{x} < \frac{A_i}{-B_i}$와 동치입니다.
- $A_i \leq 0$, $B_i > 0$인 경우입니다. 이러한 제약 조건은 $\frac{y}{x} > \frac{-A_i}{B_i}$와 동치입니다.
네 번째 그룹의 $\frac{-A_i}{B_i}$ 중 가장 큰 값을 $\frac{p_0}{q_0}$, 세 번째 그룹의 $\frac{A_i}{-B_i}$ 중 가장 작은 값을 $\frac{p_1}{q_1}$이라고 합시다.
이제 문제는 $\frac{p_0}{q_0} < \frac{p_1}{q_1}$이 주어졌을 때, $(q;p)$가 사전순으로 가장 작으면서 $\frac{p_0}{q_0} < \frac{p}{q} < \frac{p_1}{q_1}$을 만족하는 분수 $\frac{p}{q}$를 찾는 것입니다.
def solve():
n = int(input())
C = [0] * n
J = [0] * n
# p0/q0 < y/x < p1/q1
p0, q0 = 0, 1
p1, q1 = 1, 0
fail = False
for i in range(n):
C[i], J[i] = map(int, input().split())
if i > 0:
A = C[i] - C[i-1]
B = J[i] - J[i-1]
if A <= 0 and B <= 0:
fail = True
elif B > 0 and A < 0: # y/x > (-A)/B if B > 0
if (-A)*q0 > p0*B:
p0, q0 = -A, B
elif B < 0 and A > 0: # y/x < A/(-B) if B < 0
if A*q1 < p1*(-B):
p1, q1 = A, -B
if p0*q1 >= p1*q0 or fail:
return 'IMPOSSIBLE'
p, q = middle(p0, q0, p1, q1)
return str(q) + ' ' + str(p)
Calkin-Wilf 트리¶
연분수를 이진 트리로 구성하는 조금 더 간단한 방법으로 Calkin-Wilf 트리가 있습니다.
이 트리는 대체로 다음과 같은 모습입니다:
트리의 루트에는 수 $\frac{1}{1}$이 위치합니다. 그리고 수 $\frac{p}{q}$를 가진 정점의 자식은 $\frac{p}{p+q}$와 $\frac{p+q}{q}$입니다.
Stern-Brocot 트리와 달리 Calkin-Wilf 트리는 이진 탐색 트리가 아니므로, 유리수 이진 탐색을 수행하는 데 사용할 수 없습니다.
Calkin-Wilf 트리에서 분수 $\frac{p}{q}$의 직접 부모는 $p>q$일 때 $\frac{p-q}{q}$이고, 그렇지 않으면 $\frac{p}{q-p}$입니다.
Stern-Brocot 트리에서는 수렴 분수에 대한 점화식을 사용했습니다. 연분수와 Calkin-Wilf 트리 사이의 관계를 파악하려면 완전몫에 대한 점화식을 떠올려야 합니다. $s_k = \frac{p}{q}$이면 $s_{k+1} = \frac{q}{p \mod q} = \frac{q}{p-\lfloor p/q \rfloor \cdot q}$입니다.
한편, $p > q$인 동안 $s_k = \frac{p}{q}$에서 Calkin-Wilf 트리의 부모로 반복해서 이동하면 결국 $\frac{p \mod q}{q} = \frac{1}{s_{k+1}}$에 도달합니다. 이 과정을 계속하면 $s_{k+2}$에 도달하고, 이어서 $\frac{1}{s_{k+3}}$에 도달하는 식으로 진행됩니다. 이로부터 다음을 유도할 수 있습니다:
- $a_0> 0$일 때, Calkin-Wilf 트리에서 $[a_0; a_1, \dots, a_k]$의 직접 부모는 $\frac{p-q}{q}=[a_0 - 1; a_1, \dots, a_k]$입니다.
- $a_0 = 0$이고 $a_1 > 1$일 때, 그 직접 부모는 $\frac{p}{q-p} = [0; a_1 - 1, a_2, \dots, a_k]$입니다.
- 그리고 $a_0 = 0$이고 $a_1 = 1$일 때, 직접적인 부모는 $\frac{p}{q-p} = [a_2; a_3, \dots, a_k]$입니다.
이에 대응하여, $\frac{p}{q} = [a_0; a_1, \dots, a_k]$의 자식은 다음과 같습니다.
- $\frac{p+q}{q}=1+\frac{p}{q}$, 즉 $[a_0+1; a_1, \dots, a_k]$,
- $\frac{p}{p+q} = \frac{1}{1+\frac{q}{p}}$, 즉 $a_0 > 0$일 때는 $[0, 1, a_0, a_1, \dots, a_k]$이고 $a_0=0$일 때는 $[0, a_1+1, a_2, \dots, a_k]$입니다.
주목할 만한 점은, Calkin-Wilf 트리의 정점에 너비 우선 탐색 순서로 번호를 매기면(즉, 루트의 번호가 $1$이고, 정점 $v$의 자식들의 인덱스가 각각 $2v$와 $2v+1$이면) Calkin-Wilf 트리에서 유리수의 인덱스가 Stern-Brocot 트리에서의 인덱스와 같아진다는 것입니다.
따라서 Stern-Brocot 트리와 Calkin-Wilf 트리의 같은 레벨에 있는 수들은 동일하지만, 그 순서는 비트 반전 순열에 의해 달라집니다.
수렴¶
수 $r$과 그 $k$번째 근사분수 $r_k=\frac{p_k}{q_k}$에 대해 다음 공식이 성립합니다.
특히, 이는 다음을 의미합니다.
그리고
이로부터 다음과 같이 결론지을 수 있습니다.
뒤의 부등식은 $r_k$와 $r_{k+1}$이 일반적으로 $r$의 서로 다른 쪽에 위치한다는 사실에서 비롯되며, 따라서
자세한 설명
$|r-r_k|$를 추정하기 위해, 먼저 인접한 근사분수 사이의 차이를 추정하는 것부터 시작합니다. 정의에 의해,
분자의 $p_k$와 $q_k$를 각각의 점화식으로 치환하면 다음을 얻습니다.
따라서 $r_k - r_{k-1}$의 분자는 항상 $r_{k-1} - r_{k-2}$의 분자의 부호를 반전한 것입니다. 이 값은 다시 다음의 경우 $1$과 같습니다.
따라서
이로부터 $r_k$를 무한급수의 부분합으로 나타내는 또 다른 표현을 얻습니다.
점화 관계로부터 $q_k$가 적어도 피보나치 수만큼 빠르게 단조 증가함을 알 수 있으며, 따라서
는 기저의 급수가 항상 수렴하므로 항상 잘 정의됩니다. 주목할 만한 점은, 나머지 급수
는 $q_i q_{i-1}$이 빠르게 감소하기 때문에 $(-1)^k$와 같은 부호를 가진다는 것입니다. 따라서 짝수 번째 $r_k$는 아래쪽에서 $r$에 접근하고, 홀수 번째 $r_k$는 위쪽에서 접근합니다.
이 그림으로부터 다음을 알 수 있습니다.
따라서 $r$과 $r_k$ 사이의 거리는 $r_k$와 $r_{k+1}$ 사이의 거리보다 결코 크지 않습니다.
확장 유클리드?
$A, B, C \in \mathbb Z$가 주어집니다. $Ax + By = C$를 만족하는 $x, y \in \mathbb Z$를 찾으십시오.
풀이
이 문제는 보통 확장 유클리드 알고리즘으로 풀지만, 연분수를 이용한 간단하고 직관적인 풀이도 있습니다.
$\frac{A}{B}=[a_0; a_1, \dots, a_k]$라고 합시다. 앞에서 $p_k q_{k-1} - p_{k-1} q_k = (-1)^{k-1}$임을 증명했습니다. $p_k$와 $q_k$를 각각 $A$와 $B$로 치환하면 다음을 얻습니다.
여기서 $g = \gcd(A, B)$입니다. $C$가 $g$로 나누어떨어지면, 해는 $x = (-1)^{k-1}\frac{C}{g} q_{k-1}$이고 $y = (-1)^{k}\frac{C}{g} p_{k-1}$입니다.
# return (x, y) such that Ax+By=C
# assumes that such (x, y) exists
def dio(A, B, C):
p, q = convergents(fraction(A, B))
C //= A // p[-1] # divide by gcd(A, B)
t = (-1) if len(p) % 2 else 1
return t*C*q[-2], -t*C*p[-2]
일차 분수 변환¶
연분수와 관련된 또 다른 중요한 개념은 이른바 일차 분수 변환입니다.
정의
일차 분수 변환은 어떤 $a,b,c,d \in \mathbb R$에 대해 $f(x) = \frac{ax+b}{cx+d}$를 만족하는 함수 $f : \mathbb R \to \mathbb R$입니다.
일차 분수 변환 $L_0(x)=\frac{a_0 x + b_0}{c_0 x + d_0}$와 $L_1(x)=\frac{a_1 x + b_1}{c_1 x + d_1}$의 합성 $(L_0 \circ L_1)(x) = L_0(L_1(x))$ 역시 그 자체로 일차 분수 변환입니다:
일차 분수 변환의 역변환 또한 일차 분수 변환입니다:
DMOPC '19 Contest 7 P4 - Bob and Continued Fractions
양의 정수로 이루어진 배열 $a_1, \dots, a_n$이 주어집니다. $m$개의 쿼리에 답해야 합니다. 각 쿼리는 $[a_l; a_{l+1}, \dots, a_r]$을 계산하는 것입니다.
풀이
연분수를 이어 붙일 수 있다면 이 문제를 세그먼트 트리로 풀 수 있습니다.
일반적으로 $[a_0; a_1, \dots, a_k, b_0, b_1, \dots, b_k] = [a_0; a_1, \dots, a_k, [b_1; b_2, \dots, b_k]]$가 성립합니다.
$L_{k}(x) = [a_k; x] = a_k + \frac{1}{x} = \frac{a_k\cdot x+1}{1\cdot x + 0}$이라고 표기합시다. $L_k(\infty) = a_k$임에 유의하십시오. 이 표기법에서 다음이 성립합니다.
따라서 문제는 다음을 계산하는 것으로 귀결됩니다.
변환의 합성은 결합법칙을 만족하므로, 세그먼트 트리의 각 노드에서 그 서브트리에 속한 변환들의 합성을 계산할 수 있습니다.
연분수의 일차 분수 변환
$L(x) = \frac{ax+b}{cx+d}$라고 합시다. $A=[a_0; a_1, \dots, a_n]$에 대해 $L(A)$의 연분수 표현 $[b_0; b_1, \dots, b_m]$을 계산하십시오.
이를 이용하면 임의의 $\frac{p}{q}$에 대해 $A + \frac{p}{q} = \frac{qA + p}{q}$와 $A \cdot \frac{p}{q} = \frac{p A}{q}$를 계산할 수 있습니다.
풀이
앞에서 언급했듯이 $[a_0; a_1, \dots, a_k] = (L_{a_0} \circ L_{a_1} \circ \dots \circ L_{a_k})(\infty)$이므로, $L([a_0; a_1, \dots, a_k]) = (L \circ L_{a_0} \circ L_{a_1} \circ \dots L_{a_k})(\infty)$입니다.
따라서 $L_{a_0}$, $L_{a_1}$ 등을 차례로 덧붙여 나가면 다음을 계산할 수 있습니다.
$L(x)$는 가역이므로 $x$에 대해 단조이기도 합니다. 따라서 임의의 $x \geq 0$에 대해 $L(\frac{p_k x + p_{k-1}}{q_k x + q_{k-1}})$은 $L(\frac{p_k}{q_k}) = \frac{a_k}{c_k}$와 $L(\frac{p_{k-1}}{q_{k-1}}) = \frac{b_k}{d_k}$ 사이에 있습니다.
또한 $x=[a_{k+1}; \dots, a_n]$일 때 이 값은 $L(A)$와 같습니다. 그러므로 $b_0 = \lfloor L(A) \rfloor$는 $\lfloor L(\frac{p_k}{q_k}) \rfloor$와 $\lfloor L(\frac{p_{k-1}}{q_{k-1}}) \rfloor$ 사이에 있습니다. 이 두 값이 같으면 둘 다 $b_0$와도 같습니다.
$L(A) = (L_{b_0} \circ L_{b_1} \circ \dots \circ L_{b_m})(\infty)$임에 유의하십시오. $b_0$를 알면 현재 변환에 $L_{b_0}^{-1}$을 합성하고 $L_{a_{k+1}}$, $L_{a_{k+2}}$ 등을 계속 추가하면서 새로운 바닥값들이 일치하는 때를 찾을 수 있으며, 이를 통해 $b_1$ 등을 차례로 알아내어 $[b_0; b_1, \dots, b_m]$의 모든 값을 복원할 수 있습니다.
연분수 산술
$A=[a_0; a_1, \dots, a_n]$이고 $B=[b_0; b_1, \dots, b_m]$이라고 합시다. $A+B$와 $A \cdot B$의 연분수 표현을 계산하십시오.
풀이
기본 아이디어는 앞의 문제와 비슷하지만, $L(x) = \frac{ax+b}{cx+d}$ 대신 쌍선형 분수 변환 $L(x, y) = \frac{axy+bx+cy+d}{exy+fx+gy+h}$를 고려해야 합니다.
$L(x) \mapsto L(L_{a_k}(x))$ 대신 현재 변환을 $L(x, y) \mapsto L(L_{a_k}(x), y)$ 또는 $L(x, y) \mapsto L(x, L_{b_k}(y))$로 바꿉니다.
그런 다음 $\lfloor \frac{a}{e} \rfloor = \lfloor \frac{b}{f} \rfloor = \lfloor \frac{c}{g} \rfloor = \lfloor \frac{d}{h} \rfloor$인지 확인하고, 모두 일치하면 이 값을 결과 분수의 $c_k$로 사용하며 변환을 다음과 같이 바꿉니다.
정의
어떤 $k$에 대해 $x = [a_0; a_1, \dots, a_k, x]$이면 연분수 $x = [a_0; a_1, \dots]$를 주기적이라고 합니다.
$y$가 주기적일 때 $x = [a_0; a_1, \dots, a_k, y]$이면 연분수 $x = [a_0; a_1, \dots]$를 결국 주기적이라고 합니다.
$x = [1; 1, 1, \dots]$에 대해 $x = 1 + \frac{1}{x}$가 성립하므로 $x^2 = x + 1$입니다. 주기적 연분수와 이차방정식 사이에는 일반적인 연관성이 있습니다. 다음 방정식을 생각해 봅시다:
한편으로 이 방정식은 $x$의 연분수 표현이 주기 $k+1$로 주기적이라는 것을 의미합니다.
다른 한편으로 근사분수 공식을 이용하면 이 방정식은 다음을 의미합니다.
즉, $x$는 자기 자신의 일차 분수 변환입니다. 이 방정식으로부터 $x$가 다음 이차방정식의 근임을 알 수 있습니다:
결국 주기적인 연분수, 즉 $y=[b_0; b_1, \dots, b_k, y]$에 대해 $x = [a_0; a_1, \dots, a_k, y]$인 경우에도 비슷한 논리가 성립합니다. 실제로 첫 번째 방정식에서 $x = L_0(y)$를, 두 번째 방정식에서 $y = L_1(y)$를 유도할 수 있으며, 여기서 $L_0$와 $L_1$은 일차 분수 변환입니다. 따라서,
더 나아가 정수 계수를 갖는 임의의 이차방정식 $ax^2+bx+c=0$에 대해 그 해 $x$가 결국 주기적인 연분수라는 것도 증명할 수 있습니다(이는 Lagrange가 처음 증명했습니다).
이차 무리수
$x, y, z, n \in \mathbb Z$이고 $n > 0$이 완전제곱수가 아닐 때 $\alpha = \frac{x+y\sqrt{n}}{z}$의 연분수를 구하십시오.
풀이
이 수의 $k$번째 완전몫 $s_k$에 대해 일반적으로 다음이 성립합니다.
따라서,
분자와 분모에 $(xq_k - zp_k) - q_k y \sqrt n$을 곱하면 분모에서 $\sqrt n$을 없앨 수 있으므로, 완전몫은 다음과 같은 꼴이 됩니다.
$s_k$를 알고 있다고 가정하고 $s_{k+1}$을 구해 봅시다.
우선 $a_k = \lfloor s_k \rfloor = \left\lfloor \frac{x_k + y_k \lfloor \sqrt n \rfloor}{z_k} \right\rfloor$입니다. 그러면,
따라서 $t_k = x_k - y_k a_k$로 나타내면 다음이 성립합니다.
이러한 표현의 좋은 점은 $x_{k+1}, y_{k+1}, z_{k+1}$을 이들의 최대공약수로 나누어 약분하면 그 결과가 유일하다는 것입니다. 따라서 이를 이용하여 현재 상태가 이미 반복되었는지 확인할 수 있고, 이 상태를 가졌던 이전 인덱스가 어디였는지도 확인할 수 있습니다.
아래는 $\alpha = \sqrt n$의 연분수 표현을 계산하는 코드입니다.
# compute the continued fraction of sqrt(n)
def sqrt(n):
n0 = math.floor(math.sqrt(n))
x, y, z = 1, 0, 1
a = []
def step(x, y, z):
a.append((x * n0 + y) // z)
t = y - a[-1]*z
x, y, z = -z*x, z*t, t**2 - n*x**2
g = math.gcd(x, math.gcd(y, z))
return x // g, y // g, z // g
used = dict()
for i in range(n):
used[x, y, z] = i
x, y, z = step(x, y, z)
if (x, y, z) in used:
return a
같은 step 함수를 사용하되 초기 $x$, $y$, $z$를 다르게 설정하면 임의의 $\frac{x+y \sqrt{n}}{z}$에 대해서도 이를 계산할 수 있습니다.
Tavrida NU Akai Contest - Continued Fraction
$x$와 $k$가 주어지며, $x$는 완전제곱수가 아닙니다. $\sqrt x = [a_0; a_1, \dots]$라 할 때, $0 \leq k \leq 10^9$에 대해 $\frac{p_k}{q_k}=[a_0; a_1, \dots, a_k]$를 구하십시오.
풀이
$\sqrt x$의 주기를 계산한 후에는, 연분수 표현으로부터 유도되는 일차 분수 변환에 이진 거듭제곱을 적용하여 $a_k$를 계산할 수 있습니다. 최종 변환을 구하려면 크기가 $T$인 주기를 하나의 변환으로 압축하여 $\lfloor \frac{k-1}{T}\rfloor$번 반복한 다음, 나머지 변환들과 직접 결합하면 됩니다.
x, k = map(int, input().split())
mod = 10**9+7
# compose (A[0]*x + A[1]) / (A[2]*x + A[3]) and (B[0]*x + B[1]) / (B[2]*x + B[3])
def combine(A, B):
return [t % mod for t in [A[0]*B[0]+A[1]*B[2], A[0]*B[1]+A[1]*B[3], A[2]*B[0]+A[3]*B[2], A[2]*B[1]+A[3]*B[3]]]
A = [1, 0, 0, 1] # (x + 0) / (0*x + 1) = x
a = sqrt(x)
T = len(a) - 1 # period of a
# apply ak + 1/x = (ak*x+1)/(1x+0) to (Ax + B) / (Cx + D)
for i in reversed(range(1, len(a))):
A = combine([a[i], 1, 1, 0], A)
def bpow(A, n):
return [1, 0, 0, 1] if not n else combine(A, bpow(A, n-1)) if n % 2 else bpow(combine(A, A), n // 2)
C = (0, 1, 0, 0) # = 1 / 0
while k % T:
i = k % T
C = combine([a[i], 1, 1, 0], C)
k -= 1
C = combine(bpow(A, k // T), C)
C = combine([a[0], 1, 1, 0], C)
print(str(C[1]) + '/' + str(C[3]))
기하학적 해석¶
수렴값 $r_k = \frac{p_k}{q_k}$에 대해 $\vec r_k = (q_k;p_k)$라 합시다. 그러면 다음 점화식이 성립합니다.
$\vec r = (1;r)$이라 합시다. 그러면 각 벡터 $(x;y)$는 그 기울기 계수 $\frac{y}{x}$와 같은 수에 대응합니다.
의사 스칼라곱 $(x_1;y_1) \times (x_2;y_2) = x_1 y_2 - x_2 y_1$의 개념을 이용하면 다음을 보일 수 있습니다(아래 설명 참조).
마지막 방정식이 성립하는 것은 $r_{k-1}$과 $r_{k-2}$가 $r$을 기준으로 서로 반대편에 놓여 있어서, $\vec r_{k-1}$과 $\vec r$의 의사 스칼라곱과 $\vec r_{k-2}$와 $\vec r$의 의사 스칼라곱의 부호가 서로 다르기 때문입니다. $a_k = \lfloor s_k \rfloor$임을 고려하면, 이제 $\vec r_k$에 대한 식은 다음과 같습니다.
$\vec r_k \times r = (q;p) \times (1;r) = qr - p$임에 유의하십시오. 따라서
설명
이미 언급했듯이 $a_k = \lfloor s_k \rfloor$이며, 여기서 $s_k = [a_k; a_{k+1}, a_{k+2}, \dots]$입니다. 한편, 수렴값의 점화식으로부터 다음을 유도할 수 있습니다.
벡터 형태로 다시 쓰면 다음과 같습니다.
이는 $\vec r$과 $s_k \vec r_{k-1} + \vec r_{k-2}$가 한 직선 위에 있음(즉, 같은 기울기 계수를 가짐)을 의미합니다. 양변에 $\vec r$과의 유사 스칼라곱을 취하면 다음을 얻습니다.
이로부터 최종 공식을 얻습니다.
코 늘이기 알고리즘
벡터 $\vec p$에 $\vec r_{k-1}$을 더할 때마다 $\vec p \times \vec r$의 값은 $\vec r_{k-1} \times \vec r$만큼 증가합니다.
따라서 $a_k=\lfloor s_k \rfloor$는 $\vec r$과의 외적의 부호를 바꾸지 않으면서 $\vec r_{k-2}$에 더할 수 있는 $\vec r_{k-1}$ 벡터의 최대 정수 개수입니다.
다시 말해, $a_k$는 $\vec r$이 정의하는 직선을 넘지 않으면서 $\vec r_{k-2}$에 $\vec r_{k-1}$을 더할 수 있는 최대 정수 횟수입니다:
위 그림에서 $\vec r_2 = (4;3)$은 $\vec r_0 = (1;0)$에 $\vec r_1 = (1;1)$을 반복해서 더하여 얻습니다.
$y=rx$ 직선을 넘지 않고는 더 이상 $\vec r_0$에 $\vec r_1$을 더할 수 없게 되면, 반대편으로 넘어가 $\vec r_1$에 $\vec r_2$를 반복해서 더하여 $\vec r_3 = (9;7)$을 얻습니다.
이 과정은 직선에 가까워지면서 지수적으로 길어지는 벡터들을 생성합니다.
이러한 성질 때문에 Boris Delaunay는 연속된 근사분수 벡터를 생성하는 이 과정을 코 늘이기 알고리즘이라고 불렀습니다.
점 $\vec r_{k-2}$, $\vec r_{k}$, $\vec 0$ 위에 그린 삼각형을 살펴보면, 그 넓이의 두 배가 다음과 같음을 알 수 있습니다.
픽의 정리와 결합하면, 이는 삼각형의 내부(경계 제외)에는 격자점이 없으며 경계 위의 격자점은 $\vec 0$과 $0 \leq t \leq a_k$를 만족하는 모든 정수 $t$에 대한 $\vec r_{k-2} + t \cdot \vec r_{k-1}$뿐임을 의미합니다. 가능한 모든 $k$에 대해 이를 합치면, 짝수 번째 근사분수 벡터들과 홀수 번째 근사분수 벡터들이 이루는 다각형 사이의 공간에는 정수 점이 없음을 의미합니다.
이는 다시, 홀수 계수를 가진 $\vec r_k$들은 직선 $y=rx$ 위쪽에 있는 $x \geq 0$인 격자점들의 볼록 껍질을 이루고, 짝수 계수를 가진 $\vec r_k$들은 직선 $y=rx$ 아래쪽에 있는 $x > 0$인 격자점들의 볼록 껍질을 이룬다는 것을 의미합니다.
정의
이 다각형들은 클라인 다각형이라고도 하며, 연분수에 대한 이 기하학적 해석을 처음 제안한 Felix Klein의 이름을 딴 것입니다.
문제 예시¶
가장 중요한 사실과 개념을 소개했으니, 이제 구체적인 문제 예시를 살펴볼 차례입니다.
직선 아래의 볼록 껍질
$r=[a_0;a_1,\dots,a_k]=\frac{p_k}{q_k}$에 대해 $0 \leq x \leq N$이고 $0 \leq y \leq rx$인 격자점 $(x;y)$들의 볼록 껍질을 구하십시오.
풀이
무한 집합 $0 \leq x$를 고려한다면, 위쪽 볼록 껍질은 직선 $y=rx$ 자체로 주어질 것입니다.
하지만 $x \leq N$이라는 추가 제약이 있으면, 올바른 볼록 껍질을 유지하기 위해 결국 직선에서 벗어나야 합니다.
$t = \lfloor \frac{N}{q_k}\rfloor$라 하면, 껍질 위에서 $(0;0)$ 다음에 오는 처음 $t$개의 격자점은 정수 $1 \leq \alpha \leq t$에 대한 $\alpha \cdot (q_k; p_k)$입니다.
하지만 $(t+1)q_k$가 $N$보다 크므로 $(t+1)(q_k; p_k)$는 다음 격자점이 될 수 없습니다.
껍질의 다음 격자점으로 가려면, $x \leq N$을 유지하면서 $y=rx$에서 가장 작은 차이로 벗어나는 점 $(x;y)$로 가야 합니다.
볼록 껍질에서 현재 마지막 점을 $(x; y)$라고 합시다. 그러면 다음 점 $(x'; y')$는 $x' \leq N$을 만족하면서 $(x'; y') - (x; y) = (\Delta x; \Delta y)$가 직선 $y=rx$에 최대한 가까워지도록 하는 점입니다. 다시 말해, $(\Delta x; \Delta y)$는 $\Delta x \leq N - x$와 $\Delta y \leq r \Delta x$라는 조건 아래에서 $r \Delta x - \Delta y$를 최대화합니다.
이러한 점들은 $y=rx$ 아래에 있는 격자점들의 볼록 껍질 위에 놓입니다. 다시 말해, $(\Delta x; \Delta y)$는 $r$의 아래쪽 반수렴자여야 합니다.
따라서 $(\Delta x; \Delta y)$는 어떤 홀수 $i$와 $0 \leq t < a_i$에 대해 $(q_{i-1}; p_{i-1}) + t \cdot (q_i; p_i)$ 꼴입니다.
이러한 $i$를 찾기 위해, 가능한 모든 $i$를 가장 큰 것부터 차례로 살펴보면서 $N-x-q_{i-1} \geq 0$을 만족하는 $i$에 대해 $t = \lfloor \frac{N-x-q_{i-1}}{q_i} \rfloor$를 사용할 수 있습니다.
$(\Delta x; \Delta y) = (q_{i-1}; p_{i-1}) + t \cdot (q_i; p_i)$이면, 반수렴자의 성질에 의해 조건 $\Delta y \leq r \Delta x$가 유지됩니다.
또한 $i+2$에서 얻은 반수렴자를 이미 모두 사용했으므로 $x + q_{i-1} + a_i q_i = x+q_{i+1}$이 $N$보다 크고, 따라서 $t < a_i$가 성립합니다.
이제 $N$을 넘기 전까지 $(x;y)$에 $(\Delta x; \Delta y)$를 $k = \lfloor \frac{N-x}{\Delta x} \rfloor$번 더할 수 있으며, 그 후에는 다음 반수렴자를 시도합니다.
// returns [ah, ph, qh] such that points r[i]=(ph[i], qh[i]) constitute upper convex hull
// of lattice points on 0 <= x <= N and 0 <= y <= r * x, where r = [a0; a1, a2, ...]
// and there are ah[i]-1 integer points on the segment between r[i] and r[i+1]
auto hull(auto a, int N) {
auto [p, q] = convergents(a);
int t = N / q.back();
vector ah = {t};
vector ph = {0, t*p.back()};
vector qh = {0, t*q.back()};
for(int i = q.size() - 1; i >= 0; i--) {
if(i % 2) {
while(qh.back() + q[i - 1] <= N) {
t = (N - qh.back() - q[i - 1]) / q[i];
int dp = p[i - 1] + t * p[i];
int dq = q[i - 1] + t * q[i];
int k = (N - qh.back()) / dq;
ah.push_back(k);
ph.push_back(ph.back() + k * dp);
qh.push_back(qh.back() + k * dq);
}
}
}
return make_tuple(ah, ph, qh);
}
# returns [ah, ph, qh] such that points r[i]=(ph[i], qh[i]) constitute upper convex hull
# of lattice points on 0 <= x <= N and 0 <= y <= r * x, where r = [a0; a1, a2, ...]
# and there are ah[i]-1 integer points on the segment between r[i] and r[i+1]
def hull(a, N):
p, q = convergents(a)
t = N // q[-1]
ah = [t]
ph = [0, t*p[-1]]
qh = [0, t*q[-1]]
for i in reversed(range(len(q))):
if i % 2 == 1:
while qh[-1] + q[i-1] <= N:
t = (N - qh[-1] - q[i-1]) // q[i]
dp = p[i-1] + t*p[i]
dq = q[i-1] + t*q[i]
k = (N - qh[-1]) // dq
ah.append(k)
ph.append(ph[-1] + k * dp)
qh.append(qh[-1] + k * dq)
return ah, ph, qh
정수 $A$, $B$, $N$이 주어집니다. $Ax + By \leq N$을 만족하면서 $Ax + By$가 최대가 되는 $x \geq 0$과 $y \geq 0$을 찾으십시오.
풀이
이 문제에서는 $1 \leq A, B, N \leq 2 \cdot 10^9$이 성립하므로 $O(\sqrt N)$에 풀 수 있습니다. 하지만 연분수를 이용한 $O(\log N)$ 풀이가 존재합니다.
편의를 위해 $x \mapsto \lfloor \frac{N}{A}\rfloor - x$로 치환하여 $x$의 방향을 뒤집겠습니다. 그러면 이제 $0 \leq x \leq \lfloor \frac{N}{A} \rfloor$, $By - Ax \leq N \;\bmod\; A$를 만족하면서 $By - Ax$가 최대가 되는 점 $(x; y)$를 찾아야 합니다. 각 $x$에 대한 최적의 $y$는 $\lfloor \frac{Ax + (N \bmod A)}{B} \rfloor$의 값을 가집니다.
이를 더 일반적으로 다루기 위해, $0 \leq x \leq N$이고 $y = \lfloor \frac{Ax+B}{C} \rfloor$인 점 중 최적의 점을 찾는 함수를 작성하겠습니다.
이 문제의 핵심 풀이 아이디어는 본질적으로 앞의 문제를 그대로 반복하지만, 직선에서 멀어지기 위해 아래쪽 반수렴자를 사용하는 대신, 직선을 넘지 않고 $x \leq N$도 위반하지 않으면서 직선에 가까워지기 위해 위쪽 반수렴자를 사용합니다. 안타깝게도 앞의 문제와 달리, 직선 $y=\frac{Ax+B}{C}$에 가까워지는 동안 그 직선을 넘지 않도록 해야 하므로, 반수렴자의 계수 $t$를 계산할 때 이 점을 염두에 두어야 합니다.
# (x, y) such that y = (A*x+B) // C,
# Cy - Ax is max and 0 <= x <= N.
def closest(A, B, C, N):
# y <= (A*x + B)/C <=> diff(x, y) <= B
def diff(x, y):
return C*y-A*x
a = fraction(A, C)
p, q = convergents(a)
ph = [B // C]
qh = [0]
for i in range(2, len(q) - 1):
if i % 2 == 0:
while diff(qh[-1] + q[i+1], ph[-1] + p[i+1]) <= B:
t = 1 + (diff(qh[-1] + q[i-1], ph[-1] + p[i-1]) - B - 1) // abs(diff(q[i], p[i]))
dp = p[i-1] + t*p[i]
dq = q[i-1] + t*q[i]
k = (N - qh[-1]) // dq
if k == 0:
return qh[-1], ph[-1]
if diff(dq, dp) != 0:
k = min(k, (B - diff(qh[-1], ph[-1])) // diff(dq, dp))
qh.append(qh[-1] + k*dq)
ph.append(ph[-1] + k*dp)
return qh[-1], ph[-1]
def solve(A, B, N):
x, y = closest(A, N % A, B, N // A)
return N // A - x, y
June Challenge 2017 - Euler Sum
$\sum\limits_{x=1}^N \lfloor ex \rfloor$를 계산하십시오. 여기서 $e = [2; 1, 2, 1, 1, 4, 1, 1, 6, 1, \dots, 1, 2n, 1, \dots]$는 오일러 수이고 $N \leq 10^{4000}$입니다.
풀이
이 합은 $1 \leq x \leq N$과 $1 \leq y \leq ex$를 만족하는 격자점 $(x;y)$의 개수와 같습니다.
$y=ex$ 아래에 있는 점들의 볼록 껍질을 구성한 후, 이 개수는 픽의 정리를 이용해 계산할 수 있습니다:
// sum floor(k * x) for k in [1, N] and x = [a0; a1, a2, ...]
int sum_floor(auto a, int N) {
N++;
auto [ah, ph, qh] = hull(a, N);
// The number of lattice points within a vertical right trapezoid
// on points (0; 0) - (0; y1) - (dx; y2) - (dx; 0) that has
// a+1 integer points on the segment (0; y1) - (dx; y2).
auto picks = [](int y1, int y2, int dx, int a) {
int b = y1 + y2 + a + dx;
int A = (y1 + y2) * dx;
return (A - b + 2) / 2 + b - (y2 + 1);
};
int ans = 0;
for(size_t i = 1; i < qh.size(); i++) {
ans += picks(ph[i - 1], ph[i], qh[i] - qh[i - 1], ah[i - 1]);
}
return ans - N;
}
# sum floor(k * x) for k in [1, N] and x = [a0; a1, a2, ...]
def sum_floor(a, N):
N += 1
ah, ph, qh = hull(a, N)
# The number of lattice points within a vertical right trapezoid
# on points (0; 0) - (0; y1) - (dx; y2) - (dx; 0) that has
# a+1 integer points on the segment (0; y1) - (dx; y2).
def picks(y1, y2, dx, a):
b = y1 + y2 + a + dx
A = (y1 + y2) * dx
return (A - b + 2) // 2 + b - (y2 + 1)
ans = 0
for i in range(1, len(qh)):
ans += picks(ph[i-1], ph[i], qh[i]-qh[i-1], ah[i-1])
return ans - N
NAIPC 2019 - It's a Mod, Mod, Mod, Mod World
$p$, $q$, $n$이 주어졌을 때 $\sum\limits_{i=1}^n [p \cdot i \bmod q]$를 계산하십시오.
풀이
$a \bmod b = a - \lfloor \frac{a}{b} \rfloor b$라는 점에 주목하면 이 문제는 이전 문제로 환원됩니다. 이 사실을 이용하면 합은 다음과 같이 환원됩니다.
그런데 $x$가 $1$부터 $N$까지일 때 $\lfloor rx \rfloor$를 모두 더하는 것은 이전 문제에서 이미 할 수 있게 된 일입니다.
void solve(int p, int q, int N) {
cout << p * N * (N + 1) / 2 - q * sum_floor(fraction(p, q), N) << "\n";
}
def solve(p, q, N):
return p * N * (N + 1) // 2 - q * sum_floor(fraction(p, q), N)
Library Checker - Sum of Floor of Linear
$N$, $M$, $A$, $B$가 주어졌을 때 $\sum\limits_{i=0}^{N-1} \lfloor \frac{A \cdot i + B}{M} \rfloor$를 계산하십시오.
풀이
이 문제는 지금까지 다룬 문제 중 기술적으로 가장 까다로운 문제입니다.
같은 접근법을 사용하여 직선 $y = \frac{Ax+B}{M}$ 아래에 있는 점들의 볼록 껍질 전체를 구성할 수 있습니다.
$B = 0$인 경우를 푸는 방법은 이미 알고 있습니다. 게다가 $[0, N-1]$ 구간에서 이 직선에 가장 가까운 격자점까지 이 볼록 껍질을 구성하는 방법도 이미 알고 있습니다(이는 위의 "Crime and Punishment" 문제에서 다루었습니다.
이제 직선에 가장 가까운 점에 도달하고 나면 직선이 실제로 그 가장 가까운 점을 지난다고 가정해도 된다는 점에 주목해야 합니다. 실제 직선과, 가장 가까운 점을 지나도록 약간 아래로 옮긴 직선 사이에는 $[0, N-1]$ 위의 다른 격자점이 없기 때문입니다.
따라서 $[0, N-1]$에서 직선 $y=\frac{Ax+B}{M}$ 아래의 볼록 껍질 전체를 구성하려면, $[0, N-1]$에서 직선에 가장 가까운 점까지 볼록 껍질을 구성한 다음 $B=0$일 때 볼록 껍질을 구성하는 알고리즘을 재사용하여 직선이 이 점을 지나는 것처럼 계속 진행하면 됩니다.
# hull of lattice (x, y) such that C*y <= A*x+B
def hull(A, B, C, N):
def diff(x, y):
return C*y-A*x
a = fraction(A, C)
p, q = convergents(a)
ah = []
ph = [B // C]
qh = [0]
def insert(dq, dp):
k = (N - qh[-1]) // dq
if diff(dq, dp) > 0:
k = min(k, (B - diff(qh[-1], ph[-1])) // diff(dq, dp))
ah.append(k)
qh.append(qh[-1] + k*dq)
ph.append(ph[-1] + k*dp)
for i in range(1, len(q) - 1):
if i % 2 == 0:
while diff(qh[-1] + q[i+1], ph[-1] + p[i+1]) <= B:
t = (B - diff(qh[-1] + q[i+1], ph[-1] + p[i+1])) // abs(diff(q[i], p[i]))
dp = p[i+1] - t*p[i]
dq = q[i+1] - t*q[i]
if dq < 0 or qh[-1] + dq > N:
break
insert(dq, dp)
insert(q[-1], p[-1])
for i in reversed(range(len(q))):
if i % 2 == 1:
while qh[-1] + q[i-1] <= N:
t = (N - qh[-1] - q[i-1]) // q[i]
dp = p[i-1] + t*p[i]
dq = q[i-1] + t*q[i]
insert(dq, dp)
return ah, ph, qh
OKC 2 - From Modular to Rational
$1 \leq p, q \leq 10^9$를 만족하는 유리수 $\frac{p}{q}$가 있습니다. 여러 소수 $m$에 대해 $m \sim 10^9$을 법으로 하는 $p q^{-1}$의 값을 질의할 수 있습니다. $\frac{p}{q}$를 복원하십시오.
동등한 표현: $1 \leq x \leq N$에서 $Ax \;\bmod\; M$의 최솟값을 주는 $x$를 찾으십시오.
풀이
중국인의 나머지 정리에 의해, 여러 소수를 법으로 하는 결과를 질의하는 것은 그 소수들의 곱을 법으로 하는 결과를 질의하는 것과 같습니다. 이 때문에 일반성을 잃지 않고 충분히 큰 수 $m$을 법으로 하는 나머지를 알고 있다고 가정하겠습니다.
주어진 나머지 $r$에 대해 $p \equiv qr \pmod m$의 해 $(p, q)$는 여러 개 있을 수 있습니다. 하지만 $(p_1, q_1)$과 $(p_2, q_2)$가 모두 해라면 $p_1 q_2 \equiv p_2 q_1 \pmod m$도 성립합니다. $\frac{p_1}{q_1} \neq \frac{p_2}{q_2}$라고 가정하면, 이는 $|p_1 q_2 - p_2 q_1|$이 적어도 $m$이라는 뜻입니다.
문제에서 $1 \leq p, q \leq 10^9$라고 주어졌으므로, $p_1, q_1$과 $p_2, q_2$가 모두 $10^9$ 이하라면 그 차는 최대 $10^{18}$입니다. 즉 $m > 10^{18}$이면 $1 \leq p, q \leq 10^9$인 해 $\frac{p}{q}$는 유리수로서 유일합니다.
따라서 문제는 $m$을 법으로 하는 $r$이 주어졌을 때 $1 \leq q \leq 10^9$이고 $qr \;\bmod\; m \leq 10^9$를 만족하는 임의의 $q$를 찾는 것으로 귀결됩니다.
이는 사실상 $1 \leq q \leq 10^9$에서 가능한 가장 작은 $qr \bmod m$을 주는 $q$를 찾는 것과 같습니다.
$qr = km + b$일 때, 이는 $1 \leq q \leq 10^9$이면서 $qr - km \geq 0$이 가능한 한 최소가 되는 쌍 $(q, m)$을 찾아야 한다는 뜻입니다.
$m$은 상수이므로 $m$으로 나눌 수 있으며, 문제를 $1 \leq q \leq 10^9$이면서 $\frac{r}{m} q - k \geq 0$이 가능한 한 최소가 되는 $q$를 찾는 문제로 다시 쓸 수 있습니다.
연분수의 관점에서 이는 $\frac{k}{q}$가 $\frac{r}{m}$에 대한 최량 디오판토스 근사라는 뜻이며, $\frac{r}{m}$의 아래쪽 반수렴자만 확인하면 충분합니다.
# find Q that minimizes Q*r mod m for 1 <= k <= n < m
def mod_min(r, n, m):
a = fraction(r, m)
p, q = convergents(a)
for i in range(2, len(q)):
if i % 2 == 1 and (i + 1 == len(q) or q[i+1] > n):
t = (n - q[i-1]) // q[i]
return q[i-1] + t*q[i]
연습 문제¶
- UVa OJ - Continued Fractions
- ProjectEuler+ #64: Odd period square roots
- Codeforces Round #184 (Div. 2) - Continued Fractions
- Codeforces Round #201 (Div. 1) - Doodle Jump
- Codeforces Round #325 (Div. 1) - Alice, Bob, Oranges and Apples
- POJ Founder Monthly Contest 2008.03.16 - A Modular Arithmetic Challenge
- 2019 Multi-University Training Contest 5 - fraction
- SnackDown 2019 Elimination Round - Election Bait
- Code Jam 2019 round 2 - Continued Fraction