DP 凸优化
wqs 二分
wqs 二分主要用来处理一类恰好选 k 个的问题,这类题目若不限制选的个数,那么很容易求出最优方案。
使用前提:原问题具有凸性。设 gi 表示选 i 个物品的答案,那么所有 (i,gi) 点组成一个凸包,满足 g′(k) 单调。
先不考虑恰好选 k 个的限制,考虑二分附加权值。判定是每选一个就减去附加权值,则选取的次数越多,附加权值影响越大,进而影响选的数量。根据选的数量来调整 mid ,最后调整到恰好选 k 个时减掉 k 倍的附加权值即为答案。
本质就是二分斜率,使得凸包切线切点在 x=k 处,检查函数返回的是截距。

可能出现一条切线切多个点的情况,此时就会出现 mid 时切到的 x 坐标小于 k ,而 mid+1 时切到的 x 坐标大于 k 。因此最后要减掉的是 k 倍的附加权值。
实现时每次判定时能选就选,这样就会切到最右边的点,此时为了让斜线的斜率逼近 g′(k) ,于是就要根据凸壳形状减小(下凸壳)或增大(上凸壳)切线斜率,并更新答案。
给定一张无向图,求 degs=k 的情况下的最小生成树,或报告无解。
n≤5×104 ,m≤5×105 ,k≤100
二分附加权值 mid ,与 s 连接的边的边权都减去这个 mid ,则最终MST中与 s 连接的边的数量受 mid 影响,于是可以调整使得最终 degs=k 。
时间复杂度 O(mlogmlogV) 。
P2619 [国家集训队] Tree I 做法也是类似的,把附加权值放在白色边上即可。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int inf = 0x3f3f3f3f; |
| const int N = 5e4 + 7, M = 5e5 + 7; |
| |
| struct Edge { |
| int u, v, w, c; |
| |
| inline bool operator < (const Edge &rhs) const { |
| return w == rhs.w ? c > rhs.c : w < rhs.w; |
| } |
| } e[M]; |
| |
| struct DSU { |
| int fa[N]; |
| |
| inline void prework(int n) { |
| iota(fa + 1, fa + 1 + n, 1); |
| } |
| |
| inline int find(int x) { |
| while (x != fa[x]) |
| fa[x] = fa[fa[x]], x = fa[x]; |
| |
| return x; |
| } |
| |
| inline void merge(int x, int y) { |
| fa[find(y)] = find(x); |
| } |
| } dsu; |
| |
| int n, m, s, k, tot; |
| |
| inline pair<ll, int> Kruskal() { |
| sort(e + 1, e + 1 + m), dsu.prework(n); |
| pair<ll, int> ans = make_pair(0ll, 0); |
| |
| for (int i = 1; i <= m; ++i) |
| if (dsu.find(e[i].u) != dsu.find(e[i].v)) |
| dsu.merge(e[i].u, e[i].v), ans.first += e[i].w, ans.second += e[i].c; |
| |
| return ans; |
| } |
| |
| inline pair<ll, int> check(int k) { |
| for (int i = 1; i <= m; ++i) |
| if (e[i].c) |
| e[i].w -= k; |
| |
| pair<ll, int> ans = Kruskal(); |
| |
| for (int i = 1; i <= m; ++i) |
| if (e[i].c) |
| e[i].w += k; |
| |
| return ans; |
| } |
| |
| signed main() { |
| scanf("%d%d%d%d", &n, &m, &s, &k); |
| dsu.prework(n); |
| |
| for (int i = 1; i <= m; ++i) { |
| scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w); |
| e[i].c = (e[i].u == s || e[i].v == s), dsu.merge(e[i].u, e[i].v); |
| } |
| |
| for (int i = 2; i <= n; ++i) |
| if (dsu.find(i) != dsu.find(1)) |
| return puts("Impossible"), 0; |
| |
| if (check(-inf).second > k || check(inf).second < k) |
| return puts("Impossible"), 0; |
| |
| int l = -inf, r = inf, ans = 0; |
| |
| while (l <= r) { |
| int mid = (l + r) >> 1; |
| |
| if (check(mid).second >= k) |
| ans = mid, r = mid - 1; |
| else |
| l = mid + 1; |
| } |
| |
| printf("%lld", check(ans).first + 1ll * k * ans); |
| return 0; |
| } |
定义一段序列 al∼r 的价值为:
((r∑i=lai)+1)2
将 a1∼n 分为 m 段,求每段价值和的最小值。
n≤105
首先由于 (a+b)2≥a2+b2 ,所以要分的段数要越多越好,答案关于 m 增加而减小。且因为先选减小值更小的地方分开更优,具有凸性。
于是可以 wqs 二分,判定部分可以斜率优化,注意斜率优化的判断要加入选取数量为第二关键字,尽量多选。
时间复杂度 O(nlogV) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 1e5 + 7; |
| |
| ll s[N], f[N]; |
| int a[N], q[N], g[N]; |
| |
| int n, m; |
| |
| inline double slope(int i, int j) { |
| return ((f[i] + s[i] * s[i]) - (f[j] + s[j] * s[j])) / (s[i] - s[j]); |
| } |
| |
| inline pair<ll, int> check(ll k) { |
| for (int i = 1, head = 0, tail = 0; i <= n; ++i) { |
| while (head < tail && (slope(q[head], q[head + 1]) < (s[i] + 1) * 2 || |
| (slope(q[head], q[head + 1]) == (s[i] + 1) * 2 && g[q[head + 1]] > g[q[head]]))) |
| ++head; |
| |
| f[i] = f[q[head]] + (s[i] - s[q[head]] + 1) * (s[i] - s[q[head]] + 1) - k; |
| g[i] = g[q[head]] + 1; |
| |
| while (head < tail && (slope(q[tail - 1], q[tail]) > slope(q[tail], i) || ( |
| slope(q[tail - 1], q[tail]) == slope(q[tail], i) && g[q[tail - 1]] > g[q[tail]]))) |
| --tail; |
| |
| q[++tail] = i; |
| } |
| |
| return make_pair(f[n], g[n]); |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &m); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i), s[i] = s[i - 1] + a[i]; |
| |
| ll l = -1e18, r = 1e18, ans = 0; |
| |
| while (l <= r) { |
| ll mid = (l + r) >> 1; |
| |
| if (check(mid).second >= m) |
| ans = mid, r = mid - 1; |
| else |
| l = mid + 1; |
| } |
| |
| printf("%lld", check(ans).first + ans * m); |
| return 0; |
| } |
有 n 道题,第 i 天可以:
- 花费 ai 准备一道题,上限一次。
- 花费 bi 打印一道题,上限一次。
二者可以同时选择,准备的题可以留到以后打印。
求准备并打印 k 道题的最小花费。
k≤n≤5×105
若不考虑 k 的限制,则是一道反悔贪心模板。
加入 k 的限制后,只要在外面套一个 wqs 二分即可。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 5e5 + 7; |
| |
| ll a[N], b[N]; |
| |
| int n, k; |
| |
| inline pair<ll, int> check(ll k) { |
| pair<ll, int> res = make_pair(0, 0); |
| priority_queue<pair<ll, int> > q; |
| |
| for (int i = 1; i <= n; ++i) |
| b[i] -= k; |
| |
| for (int i = 1; i <= n; ++i) { |
| q.emplace(-a[i], 1); |
| |
| if (-b[i] > -q.top().first) { |
| res.first += b[i] - q.top().first, res.second += q.top().second; |
| q.pop(), q.emplace(b[i], 0); |
| } |
| } |
| |
| for (int i = 1; i <= n; ++i) |
| b[i] += k; |
| |
| return res; |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &k); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%lld", a + i); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%lld", b + i); |
| |
| ll l = -1e18, r = 1e18, ans = r; |
| |
| while (l <= r) { |
| ll mid = (l + r) >> 1; |
| |
| if (check(mid).second >= k) |
| ans = mid, r = mid - 1; |
| else |
| l = mid + 1; |
| } |
| |
| printf("%lld", check(ans).first + ans * k); |
| return 0; |
| } |
现有 n 只神奇宝贝,你有 a 个宝贝球和 b 个超级球,宝贝球抓到第 i 只神奇宝贝的概率是 pi,超级球抓到的概率则是 ui 。
不能在同一只神奇宝贝上使用超过一个同种球,但是可以往同一只上既使用宝贝球又使用超级球(都抓到算一个)。
求合理分配下抓到神奇宝贝的总个数期望的最大值。
n≤2000
首先球全部用完一定最优。考虑 wqs 二分斜率 λ ,每用一个超级球答案就减去 λ ,DP 出超级球选 b 个的方案。
直接 DP 是 O(n2logV) 的,但是可以进一步优化,考虑每个位置的四种选择:
- 用宝贝球和超级球:p+u−pu−λ 。
- 用宝贝球:p 。
- 用超级球: u−λ 。
- 不用球:0 。
先钦定所有位置都不用宝贝球,考虑一个位置从不用宝贝球到用宝贝球,答案增加量就是:
max(p+u−pu−λ,p)−max(u−λ,0)
对这个增加量排序后贪心即可,时间复杂度 O(nlognlogV) 。
| #include <bits/stdc++.h> |
| using namespace std; |
| const double eps = 1e-9; |
| const int N = 2e3 + 7; |
| |
| struct Node { |
| double x; |
| int ka, kb; |
| |
| inline bool operator < (const Node &rhs) const { |
| return x > rhs.x; |
| } |
| } nd[N]; |
| |
| double p[N], u[N]; |
| |
| int n, a, b; |
| |
| inline pair<double, int> check(double k) { |
| pair<double, int> res = make_pair(0, 0); |
| |
| for (int i = 1; i <= n; ++i) { |
| if (p[i] + u[i] - p[i] * u[i] - k > p[i]) |
| nd[i].ka = 1, nd[i].x = p[i] + u[i] - p[i] * u[i] - k; |
| else |
| nd[i].ka = 0, nd[i].x = p[i]; |
| |
| if (u[i] - k > 0) |
| nd[i].kb = 1, nd[i].x -= u[i] - k, res.first += u[i] - k; |
| else |
| nd[i].kb = 0; |
| } |
| |
| sort(nd + 1, nd + 1 + n); |
| |
| for (int i = 1; i <= a; ++i) |
| res.first += nd[i].x, res.second += nd[i].ka; |
| |
| for (int i = a + 1; i <= n; ++i) |
| res.second += nd[i].kb; |
| |
| return res; |
| } |
| |
| signed main() { |
| scanf("%d%d%d", &n, &a, &b); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%lf", p + i); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%lf", u + i); |
| |
| double l = 0, r = 1, ans = r; |
| |
| while (r - l > eps) { |
| double mid = (l + r) / 2; |
| |
| if (check(mid).second >= b) |
| l = mid; |
| else |
| ans = r = mid; |
| } |
| |
| printf("%.6lf", check(ans).first + ans * b); |
| return 0; |
| } |
给定一棵 n 个点的树,边带权。在树上选出 k+1 条互不相交链,最大化权值和。
n≤3×105
根据 wqs 二分的经典套路,二分斜率 λ ,下面考虑没有 k 的限制怎么做。
设 fu,0/1/2 表示考虑到 u 且 degu=0/1/2 时的答案,具体的:
- deg=0 :这个点没有连边。
- deg=1 :这个点连着一条未完成的链,该链还未计入答案。
- deg=2 :这个点连着一条连接两个不同子树的链。
首先约定在每个节点的全部转移结束时,进行一次更新:
gu=max(fu,0,fu,1+λ,fu,2)
这样就把 u 的全部最优解统计了出来,答案即为 g1 。
则有转移方程:
fu,2=max⎧⎪⎨⎪⎩fu,2+gvfu,1+w(u,v)+fv,1fu,2⎫⎪⎬⎪⎭
第一行表示 u 不接到 v 上,直接继承 v 的最优解。
第二行表示把 u,v 两条未完成的链拼起来,得到一条完成的链。
fu,1=max⎧⎪⎨⎪⎩fu,1+gvfu,0+w(u,v)+fv,1fu,1⎫⎪⎬⎪⎭
第一行表示 u 不接到 v 上,直接继承 v 的最优解。
第二行表示 u 接到 v 上,继承 v 一条未完成的链,得到一条未完成的链
fu,0=max{fu,0+gvfu,0}
这里 u 必须不接 v ,只能取 v 的最优解。
时间复杂度 O(nlogV) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 3e5 + 7; |
| |
| struct Graph { |
| vector<pair<int, int> > e[N]; |
| |
| inline void insert(int u, int v, int w) { |
| e[u].emplace_back(v, w); |
| } |
| } G; |
| |
| pair<ll, int> f[N][3], g[N]; |
| |
| int n, k; |
| |
| template <class T = int> |
| inline T read() { |
| char c = getchar(); |
| bool sign = (c == '-'); |
| |
| while (c < '0' || c > '9') |
| c = getchar(), sign |= (c == '-'); |
| |
| T x = 0; |
| |
| while ('0' <= c && c <= '9') |
| x = (x << 1) + (x << 3) + (c & 15), c = getchar(); |
| |
| return sign ? (~x + 1) : x; |
| } |
| |
| inline pair<ll, int> operator + (const pair<ll, int> &a, const pair<ll, int> &b) { |
| return make_pair(a.first + b.first, a.second + b.second); |
| } |
| |
| inline pair<ll, int> operator + (const pair<ll, int> &a, const ll &b) { |
| return make_pair(a.first + b, a.second); |
| } |
| |
| void dfs(int u, int fa, const pair<ll, int> lambda) { |
| f[u][0] = f[u][1] = f[u][2] = make_pair(0, 0); |
| f[u][2] = max(f[u][2], lambda); |
| |
| for (auto it : G.e[u]) { |
| int v = it.first, w = it.second; |
| |
| if (v == fa) |
| continue; |
| |
| dfs(v, u, lambda); |
| f[u][2] = max(f[u][2], max(f[u][2] + g[v], f[u][1] + w + f[v][1] + lambda)); |
| f[u][1] = max(f[u][1], max(f[u][1] + g[v], f[u][0] + w + f[v][1])); |
| f[u][0] = max(f[u][0], f[u][0] + g[v]); |
| } |
| |
| g[u] = max(f[u][0], max(f[u][1] + lambda, f[u][2])); |
| } |
| |
| inline pair<ll, int> check(ll lambda) { |
| return dfs(1, 0, make_pair(-lambda, 1)), g[1]; |
| } |
| |
| signed main() { |
| n = read(), k = read() + 1; |
| |
| for (int i = 1; i < n; ++i) { |
| int u = read(), v = read(), w = read(); |
| G.insert(u, v, w), G.insert(v, u, w); |
| } |
| |
| ll l = -1e12, r = 1e12, ans = l; |
| |
| while (l <= r) { |
| ll mid = (l + r) >> 1; |
| |
| if (check(mid).second >= k) |
| ans = mid, l = mid + 1; |
| else |
| r = mid - 1; |
| } |
| |
| printf("%lld", check(ans).first + ans * k); |
| return 0; |
| } |
有 n 个村庄,需要放 m 个邮局,求每个村庄到最近邮局的距离和的最小值。
m≤n≤5×105
设 fi,j 表示前 i 个村庄放 j 个邮局的最小距离和,w(l,r) 表示在 [l,r] 范围村庄放一个邮局的最小距离和,则有:
fi,j=i−1mink=0{fk,j−1+w(k+1,i)}
决策单调性优化做到 O(n2) 。
考虑用 wqs 二分规避 j 的限制,于是得到一个1D/1D 的 DP,并且也有决策单调性,可以二分队列做到 O(nlognlogV) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 5e5 + 7; |
| |
| struct Node { |
| int j, l, r; |
| } q[N]; |
| |
| ll s[N], f[N]; |
| int a[N], g[N]; |
| |
| int n, m; |
| |
| inline ll w(int l, int r) { |
| int mid = (l + r) >> 1; |
| return (s[r] - s[mid]) - 1ll * a[mid] * (r - mid) + 1ll * a[mid] * (mid - l + 1) - (s[mid] - s[l - 1]); |
| } |
| |
| inline ll calc(int i, int j) { |
| return f[j] + w(j + 1, i); |
| } |
| |
| inline int BinarySearch(int l, int r, int i, int j) { |
| int ans = r + 1; |
| |
| while (l <= r) { |
| int mid = (l + r) >> 1; |
| |
| if (calc(mid, i) <= calc(mid, j)) |
| ans = mid, r = mid - 1; |
| else |
| l = mid + 1; |
| } |
| |
| return ans; |
| } |
| |
| inline pair<ll, int> check(ll lambda) { |
| int head = 1, tail = 0; |
| q[++tail] = (Node) {0, 1, n}; |
| |
| for (int i = 1; i <= n; ++i) { |
| if (q[head].r == i - 1) |
| ++head; |
| |
| f[i] = calc(i, q[head].j) + lambda, g[i] = g[q[head].j] + 1; |
| int pos = n + 1; |
| |
| while (head <= tail) { |
| if (calc(q[tail].l, i) <= calc(q[tail].l, q[tail].j)) |
| pos = q[tail--].l; |
| else { |
| pos = BinarySearch(q[tail].l, q[tail].r, i, q[tail].j); |
| q[tail].r = pos - 1; |
| break; |
| } |
| } |
| |
| if (pos != n + 1) |
| q[++tail] = (Node) {i, pos, n}; |
| } |
| |
| return make_pair(f[n], g[n]); |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &m); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i); |
| |
| sort(a + 1, a + 1 + n); |
| |
| for (int i = 1; i <= n; ++i) |
| s[i] = s[i - 1] + a[i]; |
| |
| ll l = 0, r = 1e12, ans = 0; |
| |
| while (l <= r) { |
| ll mid = (l + r) >> 1; |
| |
| if (check(mid).second >= m) |
| ans = mid, l = mid + 1; |
| else |
| r = mid - 1; |
| } |
| |
| printf("%lld", check(ans).first - ans * m); |
| return 0; |
| } |
你面临 n 名参赛者的挑战,最终要将他们全部战胜。
每轮可以选择淘汰一些选手,同时获得该轮淘汰人数除以该轮总人数的奖金。
需要举办 k 轮比赛使得最终只剩一个人,最大化奖金和。
n≤105
可以发现淘汰的顺序不影响答案,因此不妨每次将淘汰的人视为一段。
套路的,先用 wqs 二分规避 k 的限制。考虑倒序 DP,设 fi 表示从后往前数某轮还剩 i 个人的最大奖金,有:
fi=maxj<i(fj+i−ji)
假设对于 k<j 有 j 优于 k ,则:
fj+i−ji>fk+i−kifj−fkj−k>1i(1)(2)
直接斜率优化即可,时间复杂度 O(nlogV) 。
| #include <bits/stdc++.h> |
| typedef long double ld; |
| using namespace std; |
| const ld eps = 1e-12; |
| const int N = 1e5 + 7; |
| |
| ld f[N]; |
| int g[N], q[N]; |
| |
| int n, k; |
| |
| inline ld slope(int i, int j) { |
| return (f[i] - f[j]) / (i - j); |
| } |
| |
| inline int check(ld k) { |
| int head = 1, tail = 0; |
| q[++tail] = 0; |
| |
| for (int i = 1; i <= n; ++i) { |
| while (head < tail && (slope(q[head], q[head + 1]) > (ld)1 / i || |
| (slope(q[head], q[head + 1]) == 1.0 / i && g[q[head]] < g[q[head + 1]]))) |
| ++head; |
| |
| f[i] = f[q[head]] + (ld)(i - q[head]) / i - k, g[i] = g[q[head]] + 1; |
| |
| while (head < tail && (slope(q[tail - 1], q[tail]) < slope(q[tail], i) || |
| (slope(q[tail - 1], q[tail]) == slope(q[tail], i) && g[q[tail - 1]] < g[q[tail]]))) |
| --tail; |
| |
| q[++tail] = i; |
| } |
| |
| return g[n]; |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &k); |
| ld l = -1e9, r = 1e9, ans = r; |
| |
| while (r - l > eps) { |
| ld mid = (l + r) / 2; |
| |
| if (check(mid) >= k) |
| ans = l = mid; |
| else |
| r = mid; |
| } |
| |
| check(ans); |
| printf("%.9LF", f[n] + ans * k); |
| return 0; |
| } |
Slope Trick
Slope Trick 通常用于维护连续、分段、一次、凸函数,满足每段斜率较小且每段斜率为整数。
考虑维护某个点 x0 处的 f(x0) 与 kx0 ,以及函数每个转折点(斜率突变的点)的集合。具体的,若函数在 x 处斜率增加了 Δk ,就在集合中插入 Δk 个 x ,这样集合中相邻两个点的斜率差就为 1 。
事实上连续、分段、一次、凸函数的性质是很好的,可以支持很多操作,以下凸函数为例:
- 相加:将 f(x0) 与 kx0 直接相加,转折点集合直接合并即可。
- 取前缀 min 或后缀 min :去掉 k>0 或 k<0 的部分。
- 求全局 min :仅保留 k=0 的部分。
- 整体平移:维护 f(x0) 与 kx0 的变化,转折点打全局平移标记。
给定 a1∼n,每次可以将一个位置的数字加一或减一,求使得原序列不降的最少操作次数。
原题面还要求修改后的数列只能出现修改前的数,事实上归纳可以证明这个限制不会让答案变劣。
n≤5×105
设 fi,j 表示考虑前 i 个数,最后一个数为 j 的答案,则:
fi,j=|j−ai|+mink≤jfi−1,k
记 Fi(j)=fi,j ,显然 Fi 是 Fi−1 取前缀 min 后加绝对值函数 y=|x−ai| 得到,答案即为 minFn(i) 。
考虑用堆维护 F ,记当前最右侧函数为 y=kx+b ,则:
- 先取一遍前缀 min ,推平斜率 >0 的部分。
- 对于转折点的维护,就直接弹出右边 k 个转折点即可。
- 对于最右侧函数的维护,记当前要弹出的最右侧转折点为 x0 。由于 x0 同时满足左边和右边的一次函数,则 kx+b=(k−1)x+b′ ,因此 b′=b+x 。
- 再加上一个绝对值函数 y=|x−a| 。
- 先合并转折点,直接往堆中加入两个 a 即可。
- 再维护最右侧函数,新的最右侧函数为 y=(kx+b)+(x−p)=(k+1)x+(b−p) 。
时间复杂度 O(nlogn) 。
几道类似的题目:
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| |
| priority_queue<int> q; |
| |
| int n; |
| |
| signed main() { |
| scanf("%d", &n); |
| int k = 0; |
| ll b = 0; |
| |
| for (int i = 1; i <= n; ++i) { |
| int x; |
| scanf("%d", &x); |
| |
| for (; k; --k) |
| b += q.top(), q.pop(); |
| |
| q.emplace(x), q.emplace(x); |
| ++k, b -= x; |
| } |
| |
| for (; k; --k) |
| b += q.top(), q.pop(); |
| |
| printf("%lld", b); |
| return 0; |
| } |
有一个数轴上的游戏,0 时刻在 0 号节点。每时刻可以选择将坐标 ±1 ,或者不移动。
接下来按时间升序给出 n 个事件,每一个事件用 Ti,Di,Xi 描述。假设 Ti 时刻你在 p 点:
- 若 Di=0,则会受到 max{0,Xi−p} 的伤害。
- 若 Di=1,则会受到 max{0,p−Xi} 的伤害。
最小化 n 次事件所受伤害量。
n≤2×105
设 fi,j 表示 Ti 时刻在 j 可能的最小伤害,t 为与上一次的时间间隔,则:
fi,j=(j+tmink=j−tfi−1,k)+|j−Xi|[(j>Xi)=Di]
记 [l,r] 为斜率为 0 的段,可以发现一次更新就是把 <l 的部分和 >r 的部分向两边平移 t ,再加上一个只有半边的绝对值函数。
用一个大根堆和一个小根堆维护斜率为 0 的段两边的转折点,那么平移操作可以通过打标记实现。然后就是插入一个拐点 Xi ,根据 Xi 在斜率为 0 的段的左侧、右侧、中间,分类讨论贡献即可。
直接这么做会导致插入的拐点不在定义域范围内,一个做法是往两个堆中插入 n 个 0 。由于扩大定义域的操作是平移操作,那么即使加入了定义域外的转折点,也不会从堆顶取出。
时间复杂度 O(nlogn) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 2e5 + 7; |
| |
| priority_queue<int> ql; |
| priority_queue<int, vector<int>, greater<int> > qr; |
| |
| int n, tagl, tagr; |
| |
| signed main() { |
| scanf("%d", &n); |
| ll ans = 0; |
| |
| for (int i = 1; i <= n; ++i) |
| ql.emplace(0), qr.emplace(0); |
| |
| for (int i = 1, lst = 0; i <= n; ++i) { |
| int t, d, x; |
| scanf("%d%d%d", &t, &d, &x); |
| tagl -= t - lst, tagr += t - lst, lst = t; |
| |
| if (d) { |
| if (x < ql.top() + tagl) |
| ans += ql.top() + tagl - x, qr.emplace(ql.top() + tagl - tagr), ql.pop(), ql.emplace(x - tagl); |
| else |
| qr.emplace(x - tagr); |
| } else { |
| if (x > qr.top() + tagr) |
| ans += x - qr.top() - tagr, ql.emplace(qr.top() + tagr - tagl), qr.pop(), qr.emplace(x - tagr); |
| else |
| ql.emplace(x - tagl); |
| } |
| } |
| |
| printf("%lld", ans); |
| return 0; |
| } |
在一个二维平面上,你一开始在 (0,0) ,只能向上或向右移动。
给定 n 个关键点点,第 i 个点为 (xi,yi) 。在点 (X,Y) 对点 (x,y) 打标记,需要花费 max(|X−x|,|Y−y|) 的代价。
求标记所有点的最小代价。
n≤8×105 ,0≤xi,yi≤109 。
首先套路地把切比雪夫距离转化为曼哈顿距离,于是问题转化为每次向右上或右下走一格,代价变为曼哈顿距离。
将所有点按 x 坐标排序,设 fi,j 表示标记到第 i 个点时 y 坐标为 j 的答案,则:
fi,j=|j−yi|+j+(xi−xi−1)mink=j−(xi−xi−1)fi−1,k
直接开两个堆维护斜率为 0 的那段左右两边的转折点即可,时间复杂度 O(nlogn) 。
一道类似的题目:CF372C Watching Fireworks is Fun
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 8e5 + 7; |
| |
| struct Point { |
| int x, y; |
| |
| inline bool operator < (const Point &rhs) const { |
| return x < rhs.x; |
| } |
| } p[N]; |
| |
| priority_queue<int> ql; |
| priority_queue<int, vector<int>, greater<int> > qr; |
| |
| int n, tagl, tagr; |
| |
| signed main() { |
| scanf("%d", &n); |
| |
| for (int i = 1; i <= n; ++i) { |
| int x, y; |
| scanf("%d%d", &x, &y); |
| p[i] = (Point) {x + y, x - y}; |
| } |
| |
| sort(p + 1, p + 1 + n); |
| ll ans = 0; |
| ql.emplace(p[1].y), qr.emplace(p[1].y); |
| |
| for (int i = 2; i <= n; ++i) { |
| tagl -= p[i].x - p[i - 1].x, tagr += p[i].x - p[i - 1].x; |
| |
| if (p[i].y < ql.top() + tagl) { |
| ans += ql.top() + tagl - p[i].y; |
| qr.emplace(ql.top() + tagl - tagr); |
| ql.pop(), ql.emplace(p[i].y - tagl), ql.emplace(p[i].y - tagl); |
| } else if (p[i].y > qr.top() + tagr) { |
| ans += p[i].y - (qr.top() + tagr); |
| ql.emplace(qr.top() + tagr - tagl); |
| qr.pop(), qr.emplace(p[i].y - tagr), qr.emplace(p[i].y - tagr); |
| } else |
| ql.emplace(p[i].y - tagl), qr.emplace(p[i].y - tagr); |
| } |
| |
| printf("%lld", ans / 2); |
| return 0; |
| } |
给定一棵以 1 为根的 n+m 个节点的带边权树,其中叶子为 n+1∼n+m 。可以花费 |x−y| 的代价将一条边的边权从 x 修改至 y ,最小化代价使得所有叶子到根节点的距离相同。
1≤n+m≤3×105
设 fu,i 为以 u 为根的子树中到叶子的距离为 i 的最小代价,有:
fu,i=∑v∈son(u)minj≤i{fv,j+|w(u,v)−(i−j)|}
可以发现这是一个下凸函数,记 Fu(x)=mini≤x{fu,i+|w−x+i|} ,[l,r] 为 fu 中斜率为 0 的段,则:
Fu(x)=⎧⎪
⎪
⎪⎨⎪
⎪
⎪⎩fu,x+w(x<l)fu,l+w−x+l(l≤x<l+w)fu,l(l+w≤x≤r+w)fu,r+x−r−w(x>r+w)
具体的:
- x<l :因为改变边权的代价函数斜率为 1 ,而 f 函数 <l 的时候斜率一定 ≤−1 ,因此把边权改为 0 一定不劣。
- l≤x<l+w :只要保证 x=l 就能取到函数的最小值,而 f 函数 <l 的时候斜率一定 ≤−1 ,因此尽量从 fu 的最小值转移一定不劣。
- l+w≤x≤r+w :不用改变 w 就可以保证能取到最小值 fu,x−w=fu,l 。
- x>r+w :与第一条类似。
考虑一次更新对 F 的变化:
- x<l :向上平移 w 单位。
- l≤x<l+w :向上平移 w 后把斜率变为 −1 。
- l+w≤x≤r+w :将原先 [l,r] 的函数平移到 [l+w,r+w] 。
- x>r+w :斜率变为 1 。
接下来考虑维护转折点集合,可以发现只要把 >l 的转折点全部删除,再加入 l+w 和 r+w 两个转折点即可。
由于有求和操作,需要合并转折点集合,故考虑用可并堆维护转折点。
接下来考虑维护每个点的函数值,一个取巧的方法是发现 F1(0) 即为所有边权之和,因此最终可以将所有转折点拿出计算答案。
使用左偏树,时间复杂度 O((n+m)log(n+m)) ,注意左偏树要开两倍空间因为一个点会新增两个转折点。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 6e5 + 7; |
| |
| int fa[N], deg[N], val[N]; |
| |
| int n, m; |
| |
| template <class T = int> |
| inline T read() { |
| char c = getchar(); |
| bool sign = (c == '-'); |
| |
| while (c < '0' || c > '9') |
| c = getchar(), sign |= (c == '-'); |
| |
| T x = 0; |
| |
| while ('0' <= c && c <= '9') |
| x = (x << 1) + (x << 3) + (c & 15), c = getchar(); |
| |
| return sign ? (~x + 1) : x; |
| } |
| |
| namespace LT { |
| ll val[N]; |
| int fa[N], lc[N], rc[N], dist[N]; |
| |
| int tot; |
| |
| inline int newnode(ll k) { |
| return val[++tot] = k, tot; |
| } |
| |
| int merge(int a, int b) { |
| if (!a || !b) |
| return a | b; |
| |
| if (val[a] < val[b]) |
| swap(a, b); |
| |
| rc[a] = merge(rc[a], b); |
| |
| if (dist[lc[a]] < dist[rc[a]]) |
| swap(lc[a], rc[a]); |
| |
| dist[a] = dist[rc[a]] + 1; |
| return a; |
| } |
| |
| template<class ...Args> |
| inline int merge(int a, int b, Args ...args) { |
| return merge(a, merge(b, args...)); |
| } |
| |
| inline void pop(int &x) { |
| x = merge(lc[x], rc[x]); |
| } |
| } |
| |
| signed main() { |
| n = read(), m = read(); |
| ll ans = 0; |
| |
| for (int i = 2; i <= n + m; ++i) |
| ++deg[fa[i] = read()], ans += (val[i] = read()); |
| |
| for (int i = n + m; i > 1; --i) { |
| ll l = 0, r = 0; |
| |
| if (i <= n) { |
| for (int j = 1; j < deg[i]; ++j) |
| LT::pop(LT::fa[i]); |
| |
| r = LT::val[LT::fa[i]], LT::pop(LT::fa[i]); |
| l = LT::val[LT::fa[i]], LT::pop(LT::fa[i]); |
| } |
| |
| LT::fa[fa[i]] = LT::merge(LT::fa[fa[i]], LT::fa[i], LT::newnode(l + val[i]), LT::newnode(r + val[i])); |
| } |
| |
| while (deg[1]--) |
| LT::pop(LT::fa[1]); |
| |
| while (LT::fa[1]) |
| ans -= LT::val[LT::fa[1]], LT::pop(LT::fa[1]); |
| |
| printf("%lld", ans); |
| return 0; |
| } |
给定一个不降序列 x1∼n∈[1,V] 与两个整数 A,B 。
求一个实数序列 y1∼n∈[1,V] 满足 yi−yi−1∈[A,B] ,最小化代价 ∑ni=1(xi−yi)2 。
n≤6000
设 fi,j 表示前 i 个位置且 yi=j 的最小代价,则:
fi,j=(xi−j)2+j−Amink=j−Bfi−1,k
不难发现 Fi(j) 是一个凸函数,设 Fi−1 的最小值点为 u ,则:
Fi(j)=⎧⎪⎨⎪⎩Fi−1(j−A)+(xi−j)2(j<u+A)Fi−1(u)+(xi−j)2(u+A≤j≤u+B)Fi−1(j−B)+(xi−j)2(j>u+B)
但是二次函数的斜率不是整数,并不好直接用 Slope Trick 维护。
发现凸函数求导后是单调函数,考虑对其求导:
F′i(j)=⎧⎪⎨⎪⎩F′i−1(j−A)+2j−2xi(j<u+A)2j−2xi(u+A≤j≤u+B)F′i−1(j−B)+2j−2xi(j>u+B)
发现其等价于把 u 左边的部分向右平移 A 单位, u 右边的部分向右平移 B 单位,然后中间部分变为 0 ,再给整体加上 2j−2xi 。
观察上述转移,每次需要动态找到一个极值点。如果每次都暴力找,时间复杂度 O(n2) ,可以通过但不够优美。
由凸函数斜率单调的性质,考虑用平衡树维护所有斜率相同的段,每次找到跨过 0 点的段即可。然后,每次把左边的段打一个平移 A 的标记,右边的段打一个 B 标记,加入中间的 0 段,最后整体加一个一次函数就好了。
具体实现只要在 fhq-Treap 上维护平移量,和一个一次函数的标记即可,时间复杂度 O(nlogn) 。
| #include <bits/stdc++.h> |
| typedef unsigned int uint; |
| using namespace std; |
| const double eps = 1e-9; |
| const int N = 6e3 + 7; |
| |
| double x[N], mnp[N], y[N]; |
| |
| double V, A, B; |
| int n; |
| |
| template <class T = int> |
| inline T read() { |
| char c = getchar(); |
| bool sign = (c == '-'); |
| |
| while (c < '0' || c > '9') |
| c = getchar(), sign |= (c == '-'); |
| |
| T x = 0; |
| |
| while ('0' <= c && c <= '9') |
| x = (x << 1) + (x << 3) + (c & 15), c = getchar(); |
| |
| return sign ? (~x + 1) : x; |
| } |
| |
| namespace fhqTreap { |
| const int S = 1e5 + 7; |
| |
| struct Node { |
| double k, b, l, r; |
| } nd[S]; |
| |
| struct Tag { |
| double k, b, mv; |
| } tag[S]; |
| |
| uint dat[S]; |
| int lc[S], rc[S]; |
| |
| mt19937 myrand(time(0)); |
| int tot, root; |
| |
| inline int newnode(Node k) { |
| dat[++tot] = myrand(), nd[tot] = k; |
| return tot; |
| } |
| |
| inline void spread(int x, Tag k) { |
| tag[x].b += -k.mv * tag[x].k + k.b, nd[x].b += -k.mv * nd[x].k + k.b; |
| tag[x].k += k.k, nd[x].k += k.k; |
| tag[x].mv += k.mv, nd[x].l += k.mv, nd[x].r += k.mv; |
| } |
| |
| inline void pushdown(int x) { |
| if (lc[x]) |
| spread(lc[x], tag[x]); |
| |
| if (rc[x]) |
| spread(rc[x], tag[x]); |
| |
| tag[x].k = tag[x].b = tag[x].mv = 0; |
| } |
| |
| void splitl(int x, int &a, int &b) { |
| if (!x) { |
| a = b = 0; |
| return; |
| } |
| |
| pushdown(x); |
| |
| if (nd[x].k * nd[x].l + nd[x].b < 0) |
| a = x, splitl(rc[x], rc[a], b); |
| else |
| b = x, splitl(lc[x], a, lc[b]); |
| } |
| |
| void splitr(int x, int &a, int &b) { |
| if (!x) { |
| a = b = 0; |
| return; |
| } |
| |
| pushdown(x); |
| |
| if (nd[x].k * nd[x].r + nd[x].b > 0) |
| b = x, splitr(lc[x], a, lc[b]); |
| else |
| a = x, splitr(rc[x], rc[a], b); |
| } |
| |
| int merge(int a, int b) { |
| if (!a || !b) |
| return a | b; |
| |
| if (dat[a] > dat[b]) |
| return pushdown(a), rc[a] = merge(rc[a], b), a; |
| else |
| return pushdown(b), lc[b] = merge(a, lc[b]), b; |
| } |
| |
| template <class ...Args> |
| inline int merge(int a, int b, Args ...args) { |
| return merge(merge(a, b), args...); |
| } |
| |
| inline int findl(int x) { |
| while (lc[x]) |
| x = lc[x]; |
| |
| return x; |
| } |
| } |
| |
| using namespace fhqTreap; |
| |
| signed main() { |
| scanf("%d%lf%lf%lf", &n, &V, &A, &B); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%lf", x + i); |
| |
| root = newnode((Node){2, -2 * x[1], 1, V}); |
| |
| for (int i = 2; i <= n; ++i) { |
| int L, X, R; |
| splitr(root, L, R), splitl(R, X, R); |
| |
| if (X) { |
| mnp[i - 1] = -nd[X].b / nd[X].k; |
| |
| if (nd[X].l < mnp[i - 1]) |
| lc[X] = newnode((Node){nd[X].k, nd[X].b, nd[X].l, mnp[i - 1]}), spread(lc[X], (Tag) {0, 0, A}); |
| |
| if (mnp[i - 1] < nd[X].r) |
| rc[X] = newnode((Node){nd[X].k, nd[X].b, mnp[i - 1], nd[X].r}), spread(rc[X], (Tag) {0, 0, B}); |
| |
| nd[X] = (Node){0, 0, mnp[i - 1] + A, mnp[i - 1] + B}; |
| } else |
| mnp[i - 1] = nd[findl(R)].l, X = newnode((Node){0, 0, mnp[i - 1] + A, mnp[i - 1] + B}); |
| |
| spread(L, (Tag){0, 0, A}), spread(R, (Tag){0, 0, B}); |
| spread(root = merge(L, X, R), (Tag){2, -2 * x[i], 0}); |
| } |
| |
| int L, X, R; |
| splitr(root, L, R), splitl(R, X, R); |
| double res = min(X ? -nd[X].b / nd[X].k : nd[findl(R)].l, V); |
| |
| for (int i = n; i; --i) { |
| y[i] = res; |
| |
| if (res < mnp[i - 1] + A) |
| res -= A; |
| else if (res > mnp[i - 1] + B) |
| res -= B; |
| else |
| res = mnp[i - 1]; |
| } |
| |
| double ans = 0; |
| |
| for (int i = 1; i <= n; ++i) |
| printf("%lf ", y[i]), ans += (y[i] - x[i]) * (y[i] - x[i]); |
| |
| printf("\n%lf", ans); |
| return 0; |
| } |
闵可夫斯基和
定义:对于两个点集 A,B ,它们的闵可夫斯基和为点集 C={a+b∣a∈A,b∈B} 。
考虑当 A,B 是凸包/凸壳的情况:

可以发现此时 C 也是凸包,并且 A,B 的边向量在 C 中恰好出现一次,即 C 的边就是把 A 和 B 的边按斜率排序拼接而成的。
考虑 x∈{1,2,⋯,n} 的两个下凸壳 f,g ,其差分数组分别单调。于是可以用 O(|f|+|g|) 的时间归并差分数组,得到 f 与 g 的闵可夫斯基和。
考虑 (min,+) 卷积,记 hk=mini+j=kfi+gj 。由于 f,g 都是下凸壳,因此 h 实际上就是 f 与 g 做闵可夫斯基和后的下凸包。因此采用闵可夫斯基和可以在线性时间内求出上/下凸包 (min,+) 卷积的结果。
给定权值 a1∼n 和常数 k ,一次操作可以覆盖一个 1×k 的区域。
对于 i=1∼⌊nk⌋ ,求选 i 个长度为 k 的不交区间后选出区间内权值和的最大值。
n≤2×105 ,k≤min(n,5)
令 bi=∑i+k−1j=iaj ,问题转化为在 b 中选若干个数,相邻两个数之间至少间隔 k−1 个数,最大化所选数的和。
可以发现答案关于所选的数构成上凸壳。考虑分治,设 fi,j,t,gi,j,t 表示左/右区间选了 t 个数,其中左边空余至少 i 个,右边空余至少 j 个的最大和。转移就是对 fi,p 和 gk−1−p,j 做 (max,+) 卷积得到左右恰好空 i,j 个的答案,最后再更新一遍 f 即可。注意需要特殊转移左区间或右区间不选 b 的情况,直接做可以做到 O(nk3logn) ,难以通过。
注意到若区间长度不超过 k ,则区间最多只能选一个数,因此该部分区间无需分治,直接求即可。此时分治树上只有 ⌊nk⌋ 个叶子,而分治树的节点数与叶子数同阶,时间复杂度将为 O(nk2logn) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const ll inf = 0x3f3f3f3f3f3f3f3f; |
| const int N = 2e5 + 7, LOGN = 19, K = 5; |
| |
| vector<ll> f[LOGN][2][K][K]; |
| |
| ll b[N]; |
| int a[N]; |
| |
| int n, k, m; |
| |
| inline vector<ll> Minkowski(vector<ll> a, vector<ll> b) { |
| vector<ll> c(a.size() + b.size() - 1); |
| c[0] = a[0] + b[0]; |
| |
| for (int i = a.size() - 1; i > 1; --i) |
| a[i] -= a[i - 1]; |
| |
| for (int i = b.size() - 1; i > 1; --i) |
| b[i] -= b[i - 1]; |
| |
| merge(a.begin() + 1, a.end(), b.begin() + 1, b.end(), c.begin() + 1, greater<ll>()); |
| |
| for (int i = 1; i < c.size(); ++i) |
| c[i] += c[i - 1]; |
| |
| return c; |
| } |
| |
| inline void maintain(vector<ll> &a, vector<ll> b) { |
| while (a.size() < b.size()) |
| a.emplace_back(-inf); |
| |
| for (int i = 0; i < b.size(); ++i) |
| a[i] = max(a[i], b[i]); |
| } |
| |
| void solve(int l, int r, int d, int op) { |
| for (int i = 0; i < k; ++i) |
| for (int j = 0; j < k; ++j) |
| f[d][op][i][j] = {max(i, j) <= r - l + 1 ? 0 : -inf}; |
| |
| if (r - l + 1 <= k) { |
| for (int i = 0; i < k; ++i) |
| for (int j = 0; j < k; ++j) |
| if (l + i <= r - j) |
| f[d][op][i][j].emplace_back(*max_element(b + l + i, b + r - j + 1)); |
| |
| return; |
| } |
| |
| int mid = (l + r) >> 1; |
| solve(l, mid, d + 1, 0), solve(mid + 1, r, d + 1, 1); |
| |
| for (int i = 0; i < k; ++i) |
| for (int j = 0; j < k; ++j) { |
| maintain(f[d][op][i][min(j + r - mid, k - 1)], f[d + 1][0][i][j]); |
| maintain(f[d][op][min(i + mid - l + 1, k - 1)][j], f[d + 1][1][i][j]); |
| } |
| |
| for (int i = 0; i < k; ++i) |
| for (int j = 0; j < k; ++j) |
| for (int p = 0; p < k; ++p) |
| maintain(f[d][op][i][j], Minkowski(f[d + 1][0][i][p], f[d + 1][1][k - 1 - p][j])); |
| |
| for (int i = k - 1; ~i; --i) |
| for (int j = k - 1; ~j; --j) { |
| if (i) |
| maintain(f[d][op][i - 1][j], f[d][op][i][j]); |
| |
| if (j) |
| maintain(f[d][op][i][j - 1], f[d][op][i][j]); |
| } |
| } |
| |
| signed main() { |
| scanf("%d%d", &n, &k); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i); |
| |
| m = n - k + 1; |
| |
| for (int i = 1; i <= m; ++i) |
| for (int j = i; j <= i + k - 1; ++j) |
| b[i] += a[j]; |
| |
| solve(1, m, 0, 0); |
| vector<ll> ans; |
| |
| for (int i = 0; i < k; ++i) |
| for (int j = 0; j < k; ++j) |
| maintain(ans, f[0][0][i][j]); |
| |
| for (int i = 1; i <= n / k; ++i) |
| printf("%lld ", ans[i]); |
| |
| return 0; |
| } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】