This repository has been archived on 2022-06-22. You can view files and clone it, but cannot push or open issues or pull requests.
usaco-guide/content/6_Plat/Slope.mdx
Benjamin Qi b9fbac192b + Gold
2020-07-03 16:29:32 -04:00

369 lines
No EOL
14 KiB
Text

---
id: slope
title: "Slope Trick"
author: Benjamin Qi
prerequisites:
- Platinum - Convex Hull
description: "Slope trick refers to a way to manipulate piecewise linear convex functions. Includes a simple solution to USACO Landscaping."
frequency: 1
---
import { Problem } from "../models";
export const metadata = {
problems: {
ex: [
new Problem("CF", "Problem Without a Legend", "contest/713/problem/C", "Easy", false, ["Slope Trick"], ""),
],
buy: [
new Problem("CF", "Buy Low Sell High", "contest/866/problem/D", "Easy", false, ["Slope Trick"], ""),
],
potatoes: [
new Problem("ojuz", "LMIO - Potatoes & Fertilizers", "LMIO19_bulves", "Normal", false, ["Slope Trick"], "[Equivalent Problem](https://atcoder.jp/contests/kupc2016/tasks/kupc2016_h)"),
],
landscaping: [
new Problem("Plat", "Landscaping", "650", "Hard", false, ["Slope Trick"], "Equivalent Problem: GP of Wroclaw 20 J"),
],
general: [
new Problem("CF", "Bookface", "gym/102576/problem/C", "Normal", false, ["Slope Trick"], ""),
new Problem("CC", "CCDSAP Exam", "CCDSAP", "Normal", false, ["Slope Trick"], ""),
new Problem("CF", "Farm of Monsters", "gym/102538/problem/F", "Hard", false, ["Slope Trick"], ""),
new Problem("CF", "Moving Walkways", "contest/1209/problem/H", "Hard", false, ["Slope Trick"], ""),
new Problem("ojuz", "APIO - Fireworks", "APIO16_fireworks", "Hard", false, ["Slope Trick", "Small to Large"], ""),
new Problem("CF", "April Fools' Problem", "contest/802/problem/O", "Very Hard", false, ["Slope Trick"], "binary search on top of slope trick"),
new Problem("ICPC World Finals", "Conquer the World", "https://icpc.kattis.com/problems/conquertheworld", "Very Hard", false, ["Slope Trick", "Small to Large"], "ICPC world finals, 0 solves in contest - \"Potatoes\" on tree!!"),
],
}
};
## Tutorials
<resources>
<resource source="CF" title="zscoder - Slope Trick" url="blog/entry/47821">3 problems using this trick</resource>
<resource source="CF" title="Kuroni - Slope Trick Explained" url="blog/entry/77298">clarifying the above and another example problem</resource>
</resources>
From the latter link (modified):
> Slope trick is a way to represent a function that satisfies the following conditions:
>
> - It can be divided into multiple sections, where each section is a linear function (usually) with an integer slope.
> - It is a convex/concave function. In other words, the slope of each section is non-decreasing or non-increasing when scanning the function from left to right.
It's generally applicable as a DP optimization.
<info-block title="Pro Tip">
Usually you can come up with a slower (usually $O(N^2)$) DP first and then optimize it to $O(N\log N)$ with slope trick.
</info-block>
The rest of this module assumes that you know the basic idea of this trick. In particular, you should be at least somewhat familiar with the $O(N\log N)$ time solution to the first problem in zscoder's tutorial:
<problems-list problems={metadata.problems.ex} />
It's ok if you found the explanations confusing; the example below should help clarify.
## Buy Low Sell High
<problems-list problems={metadata.problems.buy} />
### Slow Solution
Let $dp[i][j]$ denote the maximum amount of money you can have on day $i$ if you have exactly $j$ shares of stock on that day. The final answer will be $dp[N][0]$. This solution runs in $O(N^2)$ time.
<spoiler title="Slow Code">
```cpp
vector<vl> dp = {{0}};
int N;
int main() {
re(N);
F0R(i,N) {
int x; re(x);
dp.pb(vl(i+2,-INF));
F0R(j,i+1) {
ckmax(dp.bk[j+1],dp[sz(dp)-2][j]-x);
ckmax(dp.bk[j],dp[sz(dp)-2][j]);
if (j) ckmax(dp.bk[j-1],dp[sz(dp)-2][j]+x);
}
}
int cnt = 0;
trav(t,dp) {
pr("dp[",cnt++,"] = ");
pr('{');
F0R(i,sz(t)) {
if (i) cout << ", ";
cout << setw(3) << t[i];
}
ps('}');
}
}
```
</spoiler>
If we run this on the first sample case, then we get the following table:
```
Input:
9
10 5 4 7 9 12 6 2 10
Output:
dp[0] = { 0}
dp[1] = { 0, -10}
dp[2] = { 0, -5, -15}
dp[3] = { 0, -4, -9, -19}
dp[4] = { 3, -2, -9, -16, -26}
dp[5] = { 7, 0, -7, -16, -25, -35}
dp[6] = { 12, 5, -4, -13, -23, -35, -47}
dp[7] = { 12, 6, -1, -10, -19, -29, -41, -53}
dp[8] = { 12, 10, 4, -3, -12, -21, -31, -43, -55}
dp[9] = { 20, 14, 7, -2, -11, -21, -31, -41, -53, -65}
```
However, the DP values look quite special! Specifically, let
$$
dif[i][j]=dp[i][j]-dp[i][j+1]\ge 0.
$$
Then $dif[i][j]\le dif[i][j+1]$ for all $j\ge 0$. In other words, $dp[i][j]$ as a function of $j$ is **concave down**.
### Full Solution
<spoiler title="Explanation">
We'll process the shares in order. Suppose that we are currently considering the $i$-th day, where shares are worth $p_i$. We can replace (buy or sell a share) in the statement with (buy, then sell somewhere between 0 and 2 shares).
- If we currently have $j$ shares and overall balance $b$, then after buying, $j$ increases by one and $b$ decreases by $p_i$. So we set $dp[i][j]=dp[i-1][j-1]-p_i$ for all $j$. Note that the differences between every two consecutive elements of $dp[i]$ have not changed.
- If we choose to sell a share, this is equivalent to setting $dp[i][j]=\max(dp[i][j],dp[i][j+1]+p_i)$ for all $j$ at the same time. By the concavity condition, $dp[i][j]=dp[i][j+1]+p_i$ will hold for all $j$ less than a certain threshold while $dp[i][j]$ will remain unchanged for all others. So this is equivalent to inserting $p_i$ into the list of differences while maintaining the condition that the differences are in sorted order.
- So choosing to sell between 0 and 2 shares is represented by adding $p_i$ to the list of differences two times. After that, we should pop the smallest difference in the list because we can't end up with a negative amount of shares.
**Example:** consider the transition from `dp[4]` to `dp[5]`. Note that $p_5=9$.
Start with:
```
dp[4] = { 3, -2, -9, -16, -26}
dif[4] = { 5, 7, 7, 10}
```
After buying one share, $9$ is subtracted from each value and they are shifted one index to the right.
```
dp[5] = { x, -6, -11, -18, -25, -35}
dif[5] = { x, 5, 7, 7, 10}
```
Then we can choose to sell one share at price $9$. The last two DP values remain the same while the others change.
```
dp[5] = { 3, -2, -9, -16, -25, -35}
dif[5] = { 5, 7, 7, 9, 10}
```
Again, we can choose to sell one share at price $9$. The last three DP values remain the same while the others change. $dif$ is still in increasing order!
```
dp[5] = { 7, 0, -7, -16, -25, -35}
dif[5] = { 7, 7, 9, 9, 10}
```
(insert diagrams)
</spoiler>
<spoiler title="My Code">
The implementation is quite simple; maintain a priority queue representing $dif[i]$ that allows you to pop the minimum element. After adding $i$ elements, $ans$ stores the current value of $dp[i][i]$. At the end, you add all the differences in $dif[N]$ to go from $dp[N][N]$ to $dp[N][0]$.
```cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
int N; cin >> N;
priority_queue<int,vector<int>,greater<int>> pq;
long long ans = 0;
for (int i = 0; i < N; ++i) {
int p; cin >> p; ans -= p;
pq.push(p); pq.push(p); pq.pop();
}
for (int i = 0; i < N; ++i) {
ans += pq.top();
pq.pop();
}
cout << ans << "\n";
}
```
</spoiler>
### Extension
*Stock Trading (USACO Camp)*: What if your amount of shares can go negative, but you can never have more than $L$ shares or less than $-L$?
## Potatoes & Fertilizers
<problems-list problems={metadata.problems.potatoes} />
### Simplifying the Problem
Instead of saying that moving fertilizer from segment $i$ to segment $j$ costs $|i-j|$, we'll say that it costs $1$ to move fertilizer from a segment to an adjacent segment.
Let the values of $a_1,a_2,\ldots,a_N$ after all the transfers be $a_1',a_2',\ldots,a_N'$. If we know this final sequence, how much did the transfers cost (in the best case scenario)? It turns out that this is just
$$
C=\sum_{i=1}^{N-1}\left|\sum_{j=1}^i(a_j-a_j')\right|.
$$
We can show that this is a lower bound and that it's attainable. The term $D=\sum_{j=1}^i(a_j-a_j')$ denotes the number of units of fertilizer that move from segment $i$ to segment $i+1$. Namely, if $D$ is positive then $D$ units of fertilizer moved from segment $i$ to segment $i+1$; otherwise, $-D$ units of fertilizer moved in the opposite direction. Note that it is never optimal to have fertilizer moving in both directions.
Let $dif_i=a_i-b_i$ and define $d_j=\sum_{i=1}^jdif_i$ for each $0\le j\le N$. Similarly, define $dif_i'=a_i'-b_i$ and $d_j'=\sum_{i=1}^jdif_i'$. Since we want $dif_i'\ge 0$ for all $i$, we should have $d_0=d_0'\le d_1'\le \cdots\le d_N'=d_N.$ Conversely, every sequence $(d_0',d_1',\ldots,d_N')$ that satisfies this property corresponds to a valid way to assign values of $(a_1',a_2',\ldots,a_N')$.
Now you can verify that $C=\sum_{i=1}^{N-1}|d_i-d_i'|$. This makes sense since moving one unit of fertilizer one position is equivalent to changing one of the $d_i$ by one (although $d_0,d_N$ always remain the same).
### Slow Solution
For each $0\le i\le N$ and $0\le j\le d_N$, let $dp[i][j]$ be the minimum cost to determine $d_0',d_1',\ldots,d_i'$ such that $d_i'\le j$. Note that by definition, $dp[i][j]\ge dp[i][j+1]$. We can easily calculate these values in $O(N\cdot d_N)$ time.
### Full Solution
<spoiler title="Explanation">
Similar to before, this DP is concave up for a fixed $i$! Given a piecewise linear function $f_i(x)$ that takes as input $x$ and outputs $dp[i][x]$, we need to support the following two operations to transform this function into $f_{i+1}$.
- Add $|x-k|$ to the function for some $k$
- Set $f(x)=\min(f(x),f(x-1))$ for all $x$
Again, these can be done with a priority queue. Instead of storing the consecutive differences, we store the points where the slope of the piecewise linear function changes by one.
- The first operation corresponds to inserting $k$ into the priority queue two times because the slope increases by two at $x=k$.
- The latter operation just corresponds to removing the greatest element of the priority queue.
This solution runs in $O(N\log N)$ time.
</spoiler>
<spoiler title="My Code">
```cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int N;
ll fst = 0; // value of DP function at 0
priority_queue<ll> points; // points where DP function changes slope
int main() {
cin >> N;
vector<ll> dif(N+1);
for (int i = 1; i <= N; ++i) {
int a,b; cin >> a >> b;
dif[i] = a-b+dif[i-1];
}
assert(dif[N] >= 0); // assume solution exists
for (int i = 1; i < N; ++i) {
if (dif[i] < 0) fst -= dif[i], dif[i] = 0;
fst += dif[i];
points.push(dif[i]); points.push(dif[i]);
points.pop();
}
while (points.size()) {
ll a = points.top(); points.pop();
fst -= min(a,dif[N]);
}
cout << fst << "\n";
}
```
</spoiler>
## USACO Landscaping
<problems-list problems={metadata.problems.landscaping} />
This looks similar to the previous task (we're moving dirt instead of fertilizer), so it's not too hard to guess that slope trick is applicable.
### Slow Solution
Let $dp[i][j]$ equal the number of ways to move dirt around the first $i$ flowerbeds such that the first $i-1$ flowerbeds all have the correct amount of dirt while the $i$-th flowerbed has $j$ extra units of dirt (or lacks $-j$ units of dirt if $j$ is negative). The answer will be $dp[N][0]$.
### Full Solution
<spoiler title="Explanation">
This DP is concave up for any fixed $i$. To get $dp[i+1]$ from $dp[i]$ we must be able to support the following operations.
- Shift the DP curve $A_i$ units to the right.
- Shift the DP curve $B_i$ units to the left.
- Add $Z\cdot |j|$ to $DP[j]$ for all $j$.
- Set $DP[j] = \min(DP[j],DP[j-1]+X)$ and $DP[j] = \min(DP[j],DP[j+1]+Y)$ for all $j$.
As before, it helps to look at the differences $dif[j]=DP[j+1]-DP[j]$ instead. We'll maintain separate deques for $dif$ depending on whether $j < 0$ or $j\ge 0$. We'll call these the left and right deques, respectively.
- The first two operations correspond to repeatedly popping the last element off of the left deque and adding it to the front of the right deque (or vice versa, depending on the direction of the shift).
- The third operation corresponds to subtracting $Z$ from all elements of the left deque and adding $Z$ to all elements of the right deque.
- The last operation corresponds to setting $dif[j]=\max(dif[j],-Y)$ for all $j < 0$
and $dif[j] = \min(dif[j],X)$ for all $j\ge 0$.
We can implement the last operation by updating all of the differences in the deques "lazily." This solution runs in $O(\sum A_i+\sum B_i)$ time.
</spoiler>
<spoiler title="My Solution">
```cpp
#include <bits/stdc++.h>
using namespace std;
int N,X,Y,Z;
int difl, difr; // "lazy" update
deque<int> L, R;
long long ans;
void rig() { // shift right A, so origin moves left
if (L.size() == 0) L.push_back(-Y-difl);
int t = L.back()+difl; L.pop_back();
t = max(t,-Y); ans -= t;
R.push_front(t-difr);
}
void lef() { // shift left B, so origin moves right
if (R.size() == 0) R.push_front(X-difr);
int t = R.front()+difr; R.pop_front();
t = min(t,X); ans += t;
L.push_back(t-difl);
}
int main() {
freopen("landscape.in","r",stdin);
freopen("landscape.out","w",stdout);
cin >> N >> X >> Y >> Z;
for (int i = 0; i < N; ++i) {
int A,B; cin >> A >> B;
for (int j = 0; j < A; ++j) rig(); // or we can just do |A-B| shifts in one direction
for (int j = 0; j < B; ++j) lef();
difl -= Z, difr += Z; // adjust slopes differently for left and right of j=0
}
cout << ans << "\n";
}
```
</spoiler>
### Extension
We can solve this problem when $\sum A_i+\sum B_i$ is not so small with lazy balanced binary search trees.
## Problems
<problems-list problems={metadata.problems.general} />