# Concept - 특정 형태의 점화식을 사용하는 [[DP(Dynamic Programming)]] 문제를 빠르게 계산하는 알고리즘이다. > 사용 가능한 형태 > 수열 수열 A<sub>n</sub> B<sub>n</sub>이 존재하고 j < i 일 때 `DP[i] = min(DP[j] + A[i] * B[j])`을 만족한다. > 최솟값을 구하는 DP : 수열 B<sub>n</sub>에 대해 B<sub>1</sub> >= B<sub>2</sub> >= B<sub>3</sub> ... >= B<sub>n</sub>을 만족한다. > 최댓값을 구하는 DP : 수열 B<sub>n</sub>에 대해 B<sub>1</sub> <= B<sub>2</sub> <= B<sub>3</sub> ... <= B<sub>n</sub>을 만족한다. - 이러한 형태에 대해서 `O(nlogn)`의 시간복잡도로 문제를 해결 할 수 있고 수열 A<sub>n</sub>이 A<sub>1</sub> <= A<sub>2</sub> <= A<sub>3</sub> ... <= A<sub>n</sub>을 만족한다면 `O(n)`까지 줄일 수 있다(최솟값 기준). - 점화식의 형태가 **Convex Hull**을 닮아 Convex Hull Trick이라고 한다. # Convex Hull Trick 원리 - `DP[i] = min(DP[j] + A[i] * B[j])`에서 `A[i]`를 x로 놓고 `DP[i]`를 y로 놓게 되면 `y = B[j]x + DP[j]` 같이 1차 함수가 된다. - B<sub>1</sub> >= B<sub>2</sub> >= B<sub>3</sub> ... >= B<sub>n</sub>을 만족하기 때문에 DP의 모든 1차 함수의 기울기는 점점 감소하는 형태로 나오게 된다. - DP의 최종 그래프는 모든 1차 함수 중 최소값만 가지는 것이기 때문에 밑에 그림에서 볼 수 있듯이 Convex Hull처럼 나타낼 수 있다. ![[CHT 그래프 형태.svg]] - `DP[j]`를 알기 위해서는 1~j-1까지의 모든 DP값을 알아야 하기 때문에 `DP[0]`부터 시작해 Stack을 통해 직선을 순차적으로 만들어야 한다. - 직선 i번째를 추가할 때는 i-2와 i-1번째 직선의 교점 x(x<sub>q</sub>)과 i-1번째와 i 번째 직선의 교점 x(x<sub>p</sub>)을 비교해 기존 직선을 뺄지 말지 결정 후 추가한다. > x<sub>p</sub> > x<sub>q</sub> 일 때, > i-1번째 직선의 범위는 [x<sub>q</sub>,x<sub>p</sub>]이고 i번째 직선의 범위는[x<sub>p</sub>,∞]이다. > x<sub>p</sub> <= x<sub>q</sub> 일 때, > i-1번째 직선의 범위는 없기 때문에 i-1번째 직선은 삭제되고 i번째와 i-2번째 직선의 교점 x(x<sub>r</sub>)이라 할 때 > i번째 직선의 범위는 [x<sub>r</sub>,∞]이 된다. - 이러한 과정을 거쳐 1~n번째 직선을 만들고 삭제하고 이어서 DP 그래프를 만들 수 있다. - 최종 구하고자 하는 값(x)은 [[Binary Search]]을 통해 해당 위치에 있는 **DP 직선**을 찾고 그 직선에 x값에 대입해 값을 구하면 된다. #### 🖼️그림으로 이해하기 ![[CHT Graph.svg]] # Convex Hull Trick CODE 📑[13263 - 나무 자르기](https://www.acmicpc.net/problem/13263) - CHT를 수행하기 위해 일차 방정식 정보를 가진 Struct을 만들어야 한다. - top : `가장 마지막 직선 Index`을 선언하고 만약 기존 직선을 제거할 때는 기존 위치에 값을 바꾸는 방식으로 진행한다. - [[Binary Search]]을 이용하여 `A`값의 위치에 있는 직선을 찾고 해당 값을 대입하여 `DP`값을 구하면 된다. #### ⌨️ Code ```cpp #include <bits/stdc++.h> using namespace std; int n, tree_Len[100001], charge_Cost[100001]; long long dp[100001]; struct Linear{ long long a, b; double intersect; Linear(long long _a = 0, long long _b = 0, double _intersect = 0) : a(_a), b(_b), intersect(_intersect) {} }; double calCul_Intersect(Linear &L1, Linear &L2) { return (L2.b - L1.b) / (L1.a - L2.a); } Linear line[100001]; int main() { ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); memset(dp, 0, sizeof(dp)); cin >> n; for ( int i = 1; i <= n; i++ ) cin >> tree_Len[i]; for ( int i = 1; i <= n; i++ ) cin >> charge_Cost[i]; int top = 0; for ( int i = 2; i <= n; i++ ) { Linear nextLine = Linear(charge_Cost[i-1], dp[i-1]); while ( top > 0 ) { nextLine.intersect = calCul_Intersect(line[top-1], nextLine); if ( line[top-1].intersect < nextLine.intersect ) break; top--; } line[top] = nextLine; int target_idx = top; long long target_X = tree_Len[i]; if ( target_X < line[top].intersect ) { int low = 0, high = top; while ( low + 1 < high ) { int mid = (low + high) / 2; if ( target_X < line[mid].intersect ) high = mid; else low = mid; } target_idx = low; } top++; dp[i] = line[target_idx].a * target_X + line[target_idx].b; } cout << dp[n]; return 0; } ``` ##### ❓ 예제 Input 5 1 2 3 10 20 5 4 3 2 0 ##### ⭐ 예제 Output 75 # Convex Hull Trick 응용문제 ### 📑[4008 - 특공대](https://www.acmicpc.net/problem/4008) #### 🔓 KeyPoint - DP 식을 구현하는 과정에서 CHT가 떠오르는 것이 핵심이다. - `dp[i] = i번째 병사까지 조정했을 때의 최대 전투력`, `S[i] = 1~i까지의 누적합`로 표기할 때 밑처럼 표현 할 수 있다. > dp[i] = max(dp[j] + a(S[i] - S[j])<sup>2</sup> + b(S[i] - S[j]) + c), (j < i)이다. 이를 전개하면 > = max(dp[j] + S[i]<sup>2</sup>a - 2S[i]S[j]a + S[j]<sup>2</sup>a + S[i]b - S[j]b + c)로 나타낼 수 있고 S[i]<sup>2</sup>a, S[i]b, c는 상수이기 때문에 따로 표기할 수 있다. > = max(dp[j] - 2S[i]S[j]a + S[j]<sup>2</sup>a - S[j]b ) + S[i]<sup>2</sup>a+ S[i]b + c 에서 S[i]를 x로 치환한다면, > = max(-2xS[j]a + dp[j] S[j]<sup>2</sup>a - S[j]b ) + S[i]<sup>2</sup>a+ S[i]b + c 즉, max() 안을 1차 함수로 표현할 수 있다. > 1차 함수의 기울기인 -2xS[j]a에서 a는 음수이기 때문에 전체 함수 기울기는 양수이다. > a < b 일 때, S[a] < S[b]이기 때문에 j가 커지면서 기울기가 커지므로 CHT를 사용할 수 있다. - 전체 그래프의 모양은 위 예제(13263-나무자르기)에서 보았던 그래프에서 상하좌우 대칭한 모양이다. - 단 여기서 주의할 점은 `j = 0`인 경우, 다시 말해 0~i를 모두 한 그룹으로 넣은 경우도 포함해야 한다는 것이고 다시 말해 y = 0인 그래프를 추가해야 한다는 뜻이다. - 따라서 처음에는 `DP[1] = S[1]`로 놓고 그래프에 수를 대입할 때 그 값이 0보다 작다면 0으로 놓는 식으로 한다. #### ⌨️ Code ```cpp #include <bits/stdc++.h> using namespace std; int n, a, b, c; long long prefix_Sum[1000001], dp[1000001]; struct Linear { long long a, b; double intersect; Linear(long long _a = 0, long long _b = 0, double _intersect = 0) : a(_a), b(_b), intersect(_intersect) {} }; double calcul_Intersect(Linear &L1, Linear &L2) { return 1.0 * (L2.b - L1.b) / (L1.a - L2.a); } long long calcul_Power(long long s) { return 1LL * s * s * a + 1LL * s * b + 1LL * c; } Linear line[1000001]; int main() { ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); memset(prefix_Sum, 0, sizeof(prefix_Sum)); memset(dp, 0, sizeof(dp)); cin >> n >> a >> b >> c; for ( int i = 1; i <= n; i++ ) { long long num; cin >> num; prefix_Sum[i] = prefix_Sum[i-1] + num; } int top = 0; dp[1] = calcul_Power(prefix_Sum[1]); for ( int i = 2; i <= n; i++ ) { Linear next = Linear(-2 * prefix_Sum[i-1] * a, dp[i-1] + prefix_Sum[i-1] * prefix_Sum[i-1] * a - prefix_Sum[i-1] * b); while ( top > 0 ) { next.intersect = calcul_Intersect(line[top-1], next); if ( line[top-1].intersect < next.intersect ) break; top--; } line[top] = next; int target_Idx = top; long long target_X = prefix_Sum[i]; if ( target_X < line[top].intersect ) { int low = 0, high = top; while ( low + 1 < high ) { int mid = (low + high) / 2; if ( target_X < line[mid].intersect) high = mid; else low = mid; } target_Idx = low; } dp[i] = max(line[target_Idx].a * target_X + line[target_Idx].b, 0LL) + calcul_Power(target_X); top++; } cout << dp[n]; return 0; } ``` ### 📑[6171 - 땅따먹기](https://www.acmicpc.net/problem/6171) #### 🔓 KeyPoint - i번째 땅의 W<sub>i</sub>, H<sub>i</sub>와 j번째 땅의 W<sub>j</sub>, H<sub>j</sub>가 있을 때 W<sub>i</sub> <= W<sub>j</sub> && H<sub>i</sub> <= H<sub>j</sub>라면 i번째 땅은 가격을 계산할 때 영향을 주지 않는다. - 따라서 땅들의 W값을 기준으로 오름차순 정렬을 한 뒤, 만약 다음 값의 H가 기존 값의 H보다 값이 크거나 같으면 기존에 있던 값을 제거하는 방식으로 땅을 줄여나갈 수 있다. - 최종적으로 땅은 W기준으로 오름차순, H기준으로 내림차순으로 정렬된다. - `DP[i] : 1~i번째 땅까지 구매했을 때 최소 값`이라 정의하자. > dp[i] = max(dp[j] + W[i] * H[j]), (j < i)이다. > W[i]를 x로 치환한다면, dp[i] = max(H[j]x + dp[j])이다. > a < b 일 때, H[a] > H[b]이기 때문에 j가 커지면서 기울기가 작아지므로 CHT를 사용할 수 있다. - 이렇듯 데이터 주어졌을 때 조건을 고려하여 CHT가 가능한 형태의 전처리 과정을 수행하는 것이 중요하다. #### ⌨️ Code ```cpp #include <bits/stdc++.h> using namespace std; int n; pair<int,int> info[50001], ground[50001]; long long dp[50001]; struct Linear{ long long a, b; double intersect; Linear(long long _a = 0, long long _b = 0, double _intersect = 0) : a(_a), b(_b), intersect(_intersect) {} }; Linear line[50001]; double calcul_Intersect(Linear &L1, Linear &L2) { return 1.0 * (L2.b - L1.b) / (L1.a - L2.a); } int main() { ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); memset(dp, 0, sizeof(dp)); cin >> n; for ( int i = 1; i <= n; i++ ) { int w, h; cin >> w >> h; info[i] = {w, h}; } sort(info + 1, info + n + 1); int g_Idx = 1; for ( int i = 1; i <= n; i++ ) { while ( g_Idx > 1 && ground[g_Idx-1].second <= info[i].second ) g_Idx--; ground[g_Idx++] = info[i]; } g_Idx--; int top = 0; for ( int i = 1; i <= g_Idx; i++ ) { Linear next = Linear(ground[i].second,dp[i-1]); while ( top > 0 ) { next.intersect = calcul_Intersect(line[top-1], next); if ( line[top-1].intersect < next.intersect ) break; top--; } line[top] = next; long long target_X = ground[i].first; long long target_Idx = top; if ( target_X < line[top].intersect ) { int low = 0, high = top; while ( low + 1 < high ) { int mid = (low + high) / 2; if ( target_X < line[mid].intersect ) high = mid; else low = mid; } target_Idx = low; } dp[i] = target_X * line[target_Idx].a + line[target_Idx].b; top++; } cout << dp[g_Idx]; return 0; } ```