imeimi's Blog

Convex hull trick과 Li-Chao tree


Convex hull trick은 여러 개의 일차함수들이 있을 때, 어떤 x좌표에서의 최댓값 혹은 최솟값을 빠르게 구하기 위해 사용할 수 있는 방법이다. CHT라고 부르기도 한다.

일차함수는 다양한 특징을 가져서 비교적 쉽게 관리할 수 있고, 그래서 문제에도 많이 나온다. 일차함수의 최댓값 문제에서 중요하게 사용되는 성질들은 다음과 같다.

1. 여러 일차함수들이 있을 때, 이들의 최댓값을 m(x)라고 하면, 각 일차함수 f(x)에 대해 m(x)=f(x)x값은 닫힌 구간(\pm\infty을 포함)으로 표현된다.

증명 생략.

2. 일차함수는 볼록하다.

\frac{f(x)+f(y)}{2}=f(\frac{x+y}{2})이므로 볼록하면서 오목하다.

3. 일차함수들의 최댓값은 볼록하다. 일반적으로 볼록함수들의 최댓값은 볼록하다.

\frac{f(x)+f(y)}{2}\geq f(\frac{x+y}{2}), \frac{g(x)+g(y)}{2}\geq g(\frac{x+y}{2})를 만족하는 두 볼록함수가 있다고 하자.

\frac{max(f(x), g(x))+max(f(y), g(y))}{2}\geq \frac{f(x)+f(y)}{2}\geq f(\frac{x+y}{2})

\frac{max(f(x), g(x))+max(f(y), g(y))}{2}\geq \frac{g(x)+g(y)}{2}\geq g(\frac{x+y}{2})

위의 두 식에 의해 

\frac{max(f(x), g(x))+max(f(y), g(y))}{2}\geq max(f(\frac{x+y}{2}), g(\frac{x+y}{2}))이므로 볼록하다.

 볼록하다는 말은 엄밀하지 않게는 기울기가 증가한다고 말할 수도 있다. 최댓값에 관여되는 일차함수들을 기울기 순서대로 늘어놓으면 각 일차함수들이 관여하는 구간들이 증가하게 되고, 일차함수들을 기울기 순서대로 일렬로 늘어놓고 관리하면 이분탐색을 통해 특정 x값에서의 최댓값을 찾을 수 있다.

 이 성질들을 이용하여 다음과 같은 Convex hull trick을 생각할 수 있다. 뒤에서 나오는 N은 삽입 연산의 횟수, Q는 최댓값 쿼리의 개수이다.

A. Incremental convex hull trick

 일차함수의 삽입 연산과 x에서의 최댓값 쿼리가 있다고 하자.

 일차함수들을 기울기 순서대로 배열에 넣어서 관리한다고 생각하자. 단, m(x)=f(x)x가 없는 f(x)는 무효하기 때문에 없는 것 취급한다.

 새로운 일차함수를 삽입한다면, 삽입되는 일차함수의 기울기를 기준으로 삽입될 위치를 결정할 수 있다. 삽입 후의 일차함수 배열을 l_i라고 하자. 다음과 같은 경우가 생길 수 있다.

1. l_i가 무효하다.

 l_imax(l_{i-1}, l_{i+1})보다 작다면 l_i는 무효하다. 이 때는 삽입을 하지 않아야 한다.

2. l_{i-1}이나 l_{i+1}이 무효하다.

 l_i의 삽입으로 인해 바로 옆의 일차함수가 무효하게 된 경우이다. 이 경우에는 그 일차함수를 지운 후 l_i를 다시 삽입한다. 이 경우에 무조건 일차함수가 1개씩 사라지므로 amortized O(N)번의 연산만이 사용된다.

3. l_{i-1}, l_i, l_{i+1}이 모두 무효하지 않다.

 세 일차함수가 모두 유효하다면, l_i가 다른 일차함수들에는 영향을 미치지 않는다. 따라서 그 자리에 그대로 삽입할 수 있다.

 이 배열을 BBST로 관리한다면 2번 연산을 amortized O(N \log N), 나머지 연산을 O(\log N)에 처리할 수 있다. x에서의 최댓값 쿼리도 O(\log N)에 처리할 수 있다.

 이 방법은 어떤 일차함수가 들어오던 온라인으로 삽입하고 쿼리하는 것이 가능하다. 그러나 BBST의 특성상 O(\log N)이지만 느리고 복잡하다. 또 제거가 불가능하다는 문제점이 있다. 구현을 단순하게 하기 위해서 삽입되는 일차함수나 쿼리로 들어오는 x좌표에 제한 조건을 둘 수 있다.

B. Incremental convex hull trick (increasing query)

 쿼리로 들어오는 x좌표가 단조 증가 또는 감소한다면, 쿼리를 좀 더 간단하게 구현할 수 있다. x좌표가 증가한다는 의미는 x보다 작은 쿼리는 더 이상 들어오지 않는다는 것을 의미한다. 가장 왼쪽의 두 일차함수 l_1, l_2를 보자. l_1이 관리하는 구간은 (-\infty, X_1], l_2가 관리하는 구간은 [X_1, X_2]일 것이다. 만약 x\leq X_1이라면 l_1의 값이 최댓값이 된다. x>X_1이라면 앞으로의 모든 쿼리에서 x>X_1이 되고, l_1은 더 이상 사용되지 않게 된다. 따라서 l_1을 지워버리고 위의 과정을 반복할 수 있다.

 이 때의 쿼리당 시간복잡도는 x\leq X_1일 때 O(\log N)이고, x>X_1일 때 일차함수가 무조건 하나 사라지므로 amortized O(N \log N)이다. 총 시간복잡도 O(N \log N+Q)로 처리할 수 있다.

C. Incremental convex hull tick (increasing slope)

 삽입되는 일차함수의 기울기가 단조 증가 또는 감소한다면, BBST 없이도 구현이 가능하다. f(x)의 삽입 연산이 들어왔다면, 이미 삽입된 일차함수들의 기울기는 f(x)의 기울기 이하일 것이다. 가장 기울기가 큰 일차함수가 관리하는 구간은 [X, \infty)가 될 것이고, 따라서 새로 추가된 일차함수에 의해 제거되는 일차함수들은 오른쪽의 몇 개일 것이다. 가장 오른쪽에 있는 일차함수 두 개와 새로 삽입될 일차함수를 비교하여 가장 오른쪽에 있는 일차함수가 무효하다면 그 일차함수를 제거하고 반복하면 되고, 무효하지 않다면 가장 오른쪽에 일차함수를 삽입하면 된다. 이것은 스택을 이용하여 관리할 수 있고, 일차함수 삽입의 시간복잡도는 amortized O(1)이 되고, 쿼리의 시간복잡도는 O(\log N)이 된다.

 이분탐색을 통해 오른쪽에서 제거될 일차함수의 개수를 O(\log N)에 찾아서 한 번에 제거할 수도 있다. 이것을 배열을 이용하여 구현하면 amortized가 붙지 않는 대신 일차함수 삽입의 시간복잡도가 O(\log N)이 된다.

D. Persistent incremental convex hull trick

 A, B, C의 BBST나 스택을 persistent하게 구현하여 사용할 경우, CHT를 persistent하게 사용할 수 있다.

E. Offline dynamic convex hull trick

 일차함수의 제거를 처리하기 위해서 오프라인으로 segment tree를 이용할 수 있다.

 segment tree를 시간 기준으로 만들면 각 노드들은 [s_i, e_i]를 관리하는 노드가 될 것이다. 각각의 노드들을 A, B, C와 같은 자료구조로 만들어 각각이 CHT를 처리할 수 있도록 만든다.

 오프라인 알고리즘이기 때문에 각각의 일차함수들이 관여하는 시간을 구간 [u_i, v_i]로 표현할 수 있다. [u_i, v_i]에 포함되는 가장 높은 O(\log N)개의 노드에 해당 일차함수를 삽입하면 각 노드의 시간복잡도\times O(\log N)에 삽입을 처리할 수 있다.

 쿼리 또한 쿼리가 들어온 시간 t를 포함하는 O(\log N)개의 노드들에 쿼리를 해서 모두 합쳐주면 각 노드의 시간복잡도\times O(\log N)에 쿼리를 처리할 수 있다.

 segment tree를 시간 기준으로 만드는 대신 x좌표 기준으로 만들 경우 일차함수(직선)이 아닌 선분의 삽입을 처리하는 것도 가능한 등, segment tree를 통해 다양한 문제를 해결할 수 있다.

F. Li-Chao tree

 Convex hull trick 글에 썼지만 Convex랑 관련이 없다…

 Li-Chao segment tree라고도 부르는 것 같다. 이름에서 볼 수 있듯 segment tree의 일종인데, 각 노드들은 x좌표 [s_i, e_i]를 관리하며, 하나의 일차함수를 가지고 있다.

 Li-Chao tree가 사용될 수 있는 상황은 쿼리로 들어오는 x좌표가 유한한 경우로, 보통 쿼리들을 모두 알고 있거나, 쿼리가 범위가 정해진 정수일 때 사용할 수 있다.

 최댓값을 구하고자 할 때, Li-Chao tree의 초기 상태는 모든 노드가 z(x)=0x-\infty를 가지고 있는 상태이다. 이 트리에서 쿼리 x에서의 최댓값을 구하기 위해서는 x를 포함하는 O(\log X) 모든 노드에서의 f(x) 값의 최댓값을 가져오면 된다.

 이제 Li-Chao tree가 위의 성질을 유지하도록 새로운 일차함수를 삽입하자.

 [x_s, x_e]를 관리하는 노드에 f(x)가 삽입된다고 가정하자. 일차함수가 관리하는 x가 구간으로 나타나기 때문에 노드에 있던 일차함수와 새로운 일차함수 중에서 단 하나의 일차함수가 관리하는 구간만이 x_m (m=\frac{s+e}{2})을 포함한다.

 x_m가 관리하는 구간에 포함된다는 뜻은, 나머지 하나의 일차함수는 x_m의 왼쪽이나 오른쪽 중 하나만을 관리한다는 의미가 된다. 따라서 나머지 하나의 일차함수가 관리하는 구간은 L=[x_s, x_m) 또는 R=(x_m, x_e]에 포함된다. L에 포함될 경우에는 왼쪽 자식 노드, R에 포함될 경우에는 오른쪽 자식 노드에 재귀적으로 삽입해줄 수 있다.

 계속해서 재귀적으로 삽입하면 쿼리로 가능한 x좌표의 유한성으로 인해 x_s=x_e인 노드가 나타나고, 원래 있던 일차함수와 새로 삽입된 일차함수 중 하나가 무효해지는 순간이 발생하게 된다. 이 때 재귀를 중지할 수 있다.

 x좌표로 가능한 경우의 수를 X라고 할 때, Li-Chao tree의 시간복잡도는 삽입, 쿼리가 각각 O(\log X)가 된다. Li-Chao tree도 E의 segment tree를 이용한 것과 똑같이 선분을 삽입하는 것이 가능한데, 이 때 시간복잡도는 O(\log^2X)가 된다.

  f(x)=ax+b일 때 A부터 E까지의 방법은 두 일차함수의 교점을 비교하는 과정이 필요하다. 교점을 정확히 비교하기 위해 정수 자료형을 사용할 경우 자료형의 범위가 a\times b를 포함해야 하는데, 많은 DP 문제에서 a, x\leq 10^9, b>10^{10}의 범위가 나와서 어쩔수 없이 실수 자료형을 사용하게 된다. 그러나 Li-Chao tree는 함수의 함수값만을 비교하기 때문에 실수 자료형을 사용하게 되는 경우가 잘 발생하지 않는다.

 Li-Chao tree도 segment tree의 일종이기 때문에 동적으로 구성할 수 있다. Li-Chao tree의 특이한 점은 동적으로 트리 압축 없이 구성해도 공간복잡도가 O(Q)라는 것이다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다