add example to slope trick, + DSU trick, merging

This commit is contained in:
Benjamin Qi 2020-06-28 15:59:33 -04:00
parent 6e9e667abb
commit b1f7c0a5c8
5 changed files with 301 additions and 48 deletions

View file

@ -18,7 +18,6 @@ export const metadata = {
general: [
new Problem("Gold", "Mootube", "789", "Easy", false, [], "same as [CSES Road Construction](https://cses.fi/problemset/task/1676)"),
new Problem("Gold", "Closing the Farm", "646", "Easy", false, [], "similar to [CSES Network Breakdown](https://cses.fi/problemset/task/1677)"),
new Problem("Gold", "Favorite Colors", "1042", "Hard", false, [], ""),
],
rollback: [
new Problem("YS", "Persistent Union Find", "persistent_unionfind", "Normal", false, [], ""),

View file

@ -17,6 +17,7 @@ export const metadata = {
],
probs2: [
new Problem("CSA", "One-Way Streets", "one-way-streets", "Easy", false, [], ""),
new Problem("Plat", "Disruption", "842", "Normal", false, ["Merging"]),
],
bccSam: [
new Problem("CSES", "Forbidden Cities", "1705", "Normal", false, [], ""),
@ -41,6 +42,58 @@ export const metadata = {
(implementation)
### With DSU
The analysis for USACO Platinum Disruption mentions a $O(m\alpha(n))$ solution. Although this is not a two-connected component problem, we can in fact use DSU to generate two-connected components.
(explanation?)
<spoiler title="Code">
The DSU operations take $O(\log n)$ rather than $O(\alpha(n))$ because the DSU does not use union by size, but it's easy to change this.
```cpp
struct TwoEdgeCC {
struct {
vi e; void init(int n) { e = vi(n,-1); }
int get(int x) { return e[x] < 0 ? x : e[x] = get(e[x]); }
bool unite(int x, int y) { // set par[y] = x
x = get(x), y = get(y); if (x == y) return 0;
e[x] += e[y]; e[y] = x; return 1;
}
} DSU;
int N; vector<vi> adj; vi depth, par;
vpi extra;
void init(int _N) {
N = _N; DSU.init(N);
adj.rsz(N), depth.rsz(N), par = vi(N,-1);
}
void dfs(int x) {
trav(t,adj[x]) if (t != par[x])
par[t] = x, depth[t] = depth[x]+1, dfs(t);
}
void ae(int a, int b) {
if (DSU.unite(a,b)) adj[a].pb(b), adj[b].pb(a); // tree edge
else extra.pb({a,b});
}
void ad(int a, int b) { // OK
while (1) {
a = DSU.get(a), b = DSU.get(b);
if (a == b) return;
if (depth[a] < depth[b]) swap(a,b);
assert(par[a] != -1 && DSU.unite(par[a],a));
}
}
void gen() {
F0R(i,N) if (par[i] == -1) dfs(i);
DSU.init(N); // again!
trav(t,extra) ad(t.f,t.s);
}
};
```
</spoiler>
### Problems
<problems-list problems={metadata.problems.probs2} />

View file

@ -15,19 +15,22 @@ export const metadata = {
problems: {
sam: [
new Problem("CSES", "Distinct Colors", "1139", "Intro", false, ["Merging"]),
],
],
general: [
new Problem("CF", "Lomsat gelral", "contest/600/problem/E", "Normal", false, ["Merging"]),
new Problem("Plat", "Promotion Counting", "696", "Normal", false, ["Merging", "Indexed Set"], ""),
new Problem("Gold", "Favorite Colors", "1042", "Hard", false, ["DSU"], "Small to large merging is mentioned in the editorial, but we were unable to break solutions that just merged naively."),
new Problem("Plat", "Disruption", "842", "Hard", false, ["Merging"]),
new Problem("Plat", "Disruption", "842", "Normal", false, ["Merging"]),
new Problem("POI", "Tree Rotations", "https://szkopul.edu.pl/problemset/problem/sUe3qzxBtasek-RAWmZaxY_p/site/?key=statement", "Normal", false, ["Merging", "Indexed Set"], ""),
new Problem("Gold", "Favorite Colors", "1042", "Hard", false, ["DSU"], "Small to large merging is mentioned in the editorial, but we were unable to break solutions that just merged naively. Alternatively, just merge linked lists in $O(1)$ time."),
],
treeRot: [
new Problem("POI", "Tree Rotations 2", "https://szkopul.edu.pl/problemset/problem/b0BM0al2crQBt6zovEtJfOc6/site/?key=statement", "Very Hard", false, [], ""),
]
}
};
## Additional Reading
<resources>
<resource source="CPH" title="18.4 - Merging Data Structures"></resource>
<resource source="CF" title="Arpa - Sack (DSU on Tree)" url="blog/entry/44351"></resource>
@ -50,7 +53,7 @@ if(a.size() < b.size()){ //for two sets a and b
}
```
By merging the smaller set into the larger one, the runtime complexity becomes $O(N\log N).$ ?? (Ben - this is false)
By merging the smaller set into the larger one, the runtime complexity becomes $O(N\log^2N)$.
### Proof
@ -118,3 +121,67 @@ Prove that if you instead merge sets that have size equal to the depths of the s
## Problems
<problems-list problems={metadata.problems.general} />
<spoiler title="Solution to Promotion Counting">
```cpp
#include <bits/stdc++.h>
#include <ext/pb_ds/tree_policy.hpp>
#include <ext/pb_ds/assoc_container.hpp>
using namespace std;
using namespace __gnu_pbds;
template<class T> using Tree = tree<T,null_type,less<T>,rb_tree_tag,tree_order_statistics_node_update>;
const int MX = 1e5+5;
#define sz(x) (int)(x).size()
int N, a[MX], ind[MX], ans[MX], ret;
vector<int> child[MX];
Tree<int> d[MX];
void comb(int a, int b) {
if (sz(d[a]) < sz(d[b])) d[a].swap(d[b]);
for (int i: d[b]) d[a].insert(i);
}
void dfs(int x) {
ind[x] = x;
for (int i: child[x]) {
dfs(i);
comb(x,i);
}
ans[x] = sz(d[x])-d[x].order_of_key(a[x]);
d[x].insert(a[x]);
}
int main() {
freopen("promote.in","r",stdin);
freopen("promote.out","w",stdout);
cin >> N; for (int i = 1; i <= N; ++i) cin >> a[i];
for (int i = 2; i <= N; ++i) {
int p; cin >> p;
child[p].push_back(i);
}
dfs(1);
for (int i = 1; i <= N; ++i) cout << ans[i] << "\n";
}
```
</spoiler>
(also: same solution w/o indexed set)
## Faster Merging (Optional)
It's easy to merge two sets of sizes $n\ge m$ in $O(n+m)$ or $(m\log n)$ time, but sometimes $O\left(m\log \frac{n}{m}\right)$ can be significantly better than both of these.
<resources>
<resource source="CF" title="Splitting & Merging Segment Trees" url="https://codeforces.com/blog/entry/49446"></resource>
<resource source="CF" title="Splitting & Merging BSTs" url="https://codeforces.com/blog/entry/67980"></resource>
</resources>
Requires knowledge of BSTs such as treaps or splay trees.
<problems-list problems={metadata.problems.treeRot} />

View file

@ -4,7 +4,7 @@ title: "Slope Trick"
author: Benjamin Qi
prerequisites:
- Platinum - Convex Hull
description: Ways to manipulate piecewise linear convex functions.
description: "Slope trick refers to a way to manipulate piecewise linear convex functions. Includes a simple solution to USACO Landscaping."
frequency: 1
---
@ -12,9 +12,18 @@ import { Problem } from "../models";
export const metadata = {
problems: {
buy: [
new Problem("CF", "Buy Low Sell High", "contest/866/problem/D", "Easy", false, ["Slope Trick"], ""),
],
potatoes: [
new Problem("ojuz", "LMIO - Potatoes", "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", "Easy", false, ["Slope Trick"], ""),
new Problem("CC", "CCDSAP Exam", "CCDSAP", "Easy", false, ["Slope Trick"], ""),
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("CF", "April Fools' Problem", "contest/802/problem/O", "Very Hard", false, ["Slope Trick"], "binary search on top of slope trick"),
@ -23,8 +32,6 @@ export const metadata = {
}
};
**Slope trick** refers to manipulating piecewise linear convex functions. Includes a simple solution to [Landscaping](http://www.usaco.org/index.php?page=viewproblem2&cpid=650).
## Tutorials
<resources>
@ -39,27 +46,140 @@ From the latter link (modified):
> - 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. Usually you can come up with a slower DP (ex. $O(N^2)$) first and then optimize it to $O(N\log N)$ with slope trick. The rest of this module assumes that you are somewhat familiar with at least one of the tutorials mentioned above.
It's generally applicable as a DP optimization. The rest of this module assumes that you are somewhat familiar with at least one of the tutorials mentioned above.
## [Buy Low Sell High](https://codeforces.com/contest/866/problem/D)
<info-block title="Pro Tip">
**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 easily leads to an $O(N^2)$ DP.
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.
Of course, we never used the fact that the DP is concave down! 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$ (ignoring the case when we get $dp$ values of $-\infty$).
</info-block>
We'll process the shares in order. Suppose that on the current day shares are worth $p$. We can replace (buy or sell a share) in the statement with (buy, then sell between 0 and 2 shares).
## Buy Low Sell High
* If we currently have $j$ shares and overall balance $b$, then after buying, $j$ increases by one and $b$ decreases by $p$. The differences between every two consecutive elements do not change.
* If we choose to buy a share, this is equivalent to setting $dp[i][j]=\max(dp[i][j],dp[i][j+1]+p)$ for all $j$. By the concavity condition, $dp[i][j]=dp[i][j+1]+p$ will hold for all $j$ less than a certain threshold while $dp[i][j+1]$ will hold for all others. So this is equivalent to inserting $p$ into the list of differences while maintaining the condition that the differences are in sorted order.
* So we add $p$ 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.
<problems-list problems={metadata.problems.buy} />
(insert diagram)
### Slow Solution
(insert example)
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.
The implementation is quite simple; maintain a priority queue that allows you to pop the minimum element.
<spoiler title="Slow Code">
<spoiler title="My Solution">
```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.$ So we set $dp[i][j]=dp[i-1][j-1]-p$ 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)$ for all $j$ at the same time. By the concavity condition, $dp[i][j]=dp[i][j+1]+p$ 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$ 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$ 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}
```
<br/>
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}
```
<br/>
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}
```
<br/>
Again, we can choose to sell one share at price $9$. The last three DP values remain the same while the others change.
```
dp[5] = { 7, 0, -7, -16, -25, -35}
dif[5] = { 7, 7, 9, 9, 10}
```
<br/>
(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>
@ -80,28 +200,39 @@ int main() {
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](https://oj.uz/problem/view/LMIO19_bulves)
## Potatoes
[Equivalent Problem](https://atcoder.jp/contests/kupc2016/tasks/kupc2016_h)
<problems-list problems={metadata.problems.potatoes} />
Let $dif_i=a_i-b_i$. Defining $d_j=\sum_{i=1}^jdif_i$, our goal is to move around the potatoes such that $d_0,d_1,\ldots,d_N$ is a non-decreasing sequence. Moving a potato is equivalent to changing exactly one of the $d_i$ (aside from $d_0,d_N$) by one.
### Simplifying the Problem
**Slow Solution:** Let $dp[i][j]$ be the minimum cost to determine $d_0,d_1,\ldots,d_i$ such that $d_i\le j$ for each $0\le j\le d_N$. This gives a $O(N\cdot d_N)$ solution.
Let $dif_i=a_i-b_i$. Defining $d_j=\sum_{i=1}^jdif_i$, our goal is to move around the potatoes such that $d_0,d_1,\ldots,d_N$ is a non-decreasing sequence. Moving a potato one position is equivalent to changing exactly one of the $d_i$ by one (although $d_0,d_N$ cannot be modified).
As before, this DP is concave up for a fixed $i$! Given a piecewise linear function $DP_x$, we need to support the following operations.
### Slow Solution
Let $dp[i][j]$ be the minimum cost to determine $d_0,d_1,\ldots,d_i$ such that $d_i\le j$ for each $0\le j\le d_N$. This runs in $O(N\cdot d_N)$ time. By definition, $dp[i][j]\ge dp[i][j+1]$.
### Full Solution
<spoiler title="Explanation">
Similar to before, this DP is concave up for a fixed $i$! Given a piecewise linear function $DP_x$, we need to support the following operations.
* Add $|x-k|$ to the function for some $k$
* Set $DP_x=\min(DP_x,DP_{x-1})$ for all $x$
Again, these can be done with a priority queue in $O(N\log N)$ time!
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 in $O(N\log N)$ time.
<spoiler title="My Solution">
</spoiler>
<spoiler title="My Code">
```cpp
#include <bits/stdc++.h>
@ -134,22 +265,29 @@ int main() {
cout << fst << "\n";
}
```
</spoiler>
## [Landscaping](http://www.usaco.org/index.php?page=viewproblem2&cpid=650)
## USACO Landscaping
Equivalent Problem: GP of Wroclaw 20 J
<problems-list problems={metadata.problems.landscaping} />
This is quite similar to the previous task, so it's easy to guess that slope trick is applicable.
This looks quite similar to the previous task, so it's not hard to guess that slope trick is applicable.
Again, let's first come up with a slow DP. 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]$.
### 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$.
- 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]-dif[j]$ instead. Then the last operation is equivalent to the following:
@ -158,7 +296,7 @@ As before, it helps to look at the differences $dif[j]=DP[j+1]-dif[j]$ instead.
If we maintain separate deques for $dif$ depending on whether $j\ge 0$ or $j<0$ and update all of the differences in the deques "lazily" then we can do this in $O(\sum A_i+\sum B_i)$ time.
Bonus: Solve this problem when $\sum A_i+\sum B_i$ is not so small.
</spoiler>
<spoiler title="My Solution">
@ -201,6 +339,10 @@ int main() {
```
</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} />

View file

@ -29,12 +29,4 @@ IOI 2013 Game - https://oj.uz/submission/242393
A2OJ: https://a2oj.com/category?ID=14
?
## Splitting & Merging Segment Trees
### Tutorial
- [Merging Segment Trees](https://codeforces.com/blog/entry/49446)
- [Tree Rotations 2](https://szkopul.edu.pl/problemset/problem/b0BM0al2crQBt6zovEtJfOc6/site/?key=statement) [](193)
?