DP 凸优化
DP 凸优化
wqs 二分
wqs 二分主要用来处理一类恰好选 \(k\) 个的问题,这类题目若不限制选的个数,那么很容易求出最优方案。
使用前提:原问题具有凸性。设 \(g_i\) 表示选 \(i\) 个物品的答案,那么所有 \((i, g_i)\) 点组成一个凸包,满足 \(g'(k)\) 单调。
先不考虑恰好选 \(k\) 个的限制,考虑二分附加权值。判定是每选一个就减去附加权值,则选取的次数越多,附加权值影响越大,进而影响选的数量。根据选的数量来调整 \(mid\) ,最后调整到恰好选 \(k\) 个时减掉 \(k\) 倍的附加权值即为答案。
本质就是二分斜率,使得凸包切线切点在 \(x = k\) 处,检查函数返回的是截距。
可能出现一条切线切多个点的情况,因此最后要减掉的是 \(k\) 倍的附加权值。
P5633 最小度限制生成树
给定一张无向图,求 \(deg_s = k\) 的情况下的最小生成树,或报告无解。
\(n \leq 5 \times 10^4\) ,\(m \leq 5 \times 10^5\) ,\(k \leq 100\)
二分附加权值 \(mid\) ,与 \(s\) 连接的边的边权都减去这个 \(mid\) ,则最终MST中与 \(s\) 连接的边的数量受 \(mid\) 影响,于是可以调整使得最终 \(deg_s =k\) 。
时间复杂度 \(O(m \log m \log V)\) 。
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;
}
Maximum Deviation Spanning Tree
定义可重集 \(S\) 的权值为 \(f(S) = \min_x (\sum_{i \in S} |i - x|)\) 。
给定一张无向图,边带边权,求一棵生成树满足边集的权值最大。
\(n \leq 2 \times 10^5\) ,\(m \leq 5 \times 10^5\) ,\(w \in [1, 10^9]\)
先观察权值的性质,显然 \(x\) 会取到中位数。
考虑拆开绝对值,当 \(n - 1\) 为偶数时,考虑钦定一半的数为 \(+\) ,剩余一半的数为 \(-\) ,最大化带符号和。将边 \((u, v, w)\) 拆为一条正边 \((u, v, w)\) 和一条负边 \((u, v, -w)\) ,恰好一半的限制只要做 wqs 二分即可。
再考虑 \(n - 1\) 为奇数的情况,问题等价于钦定 \(\frac{n - 2}{2}\) 条正边和负边以及一条 \(0\) 边,只要在 Kruskal 过程中加入 \(n - 2\) 条边然后退出即可。
时间复杂度 \(O(m \alpha(n) \log V)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, M = 5e5 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 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;
struct Edge {
ll w;
int u, v, op;
inline bool operator < (const Edge &rhs) const {
return w == rhs.w ? op > rhs.op : w > rhs.w;
}
} e1[M], e2[M], e[M << 1];
int n, m;
inline pair<ll, int> calc(ll lambda) {
for (int i = 1; i <= m; ++i)
e1[i].w -= lambda;
merge(e1 + 1, e1 + m + 1, e2 + 1, e2 + m + 1, e + 1);
for (int i = 1; i <= m; ++i)
e1[i].w += lambda;
dsu.prework(n);
pair<ll, int> ans = make_pair(0ll, 0);
for (int i = 1, cnt = 0; i <= m * 2 && cnt < (n - 1) >> 1 << 1; ++i) {
int u = e[i].u, v = e[i].v;
if (dsu.find(u) != dsu.find(v))
++cnt, ans.first += e[i].w, ans.second += (e[i].op == 1), dsu.merge(u, v);
}
return ans;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d%d%lld", &e1[i].u, &e1[i].v, &e1[i].w);
e1[i].op = 1, e2[i] = e1[i], e2[i].w *= -1, e2[i].op *= -1;
}
sort(e1 + 1, e1 + m + 1), sort(e2 + 1, e2 + m + 1);
ll l = -2e9, r = 2e9, ans = r;
while (l <= r) {
ll mid = (l + r) >> 1;
if (calc(mid).second >= (n - 1) / 2)
ans = mid, l = mid + 1;
else
r = mid - 1;
}
printf("%lld", calc(ans).first + ans * ((n - 1) / 2));
return 0;
}
P4983 忘情
定义一段序列 \(a_{l \sim r}\) 的价值为:
\[((\sum_{i = l}^r a_i) + 1)^2 \]将 \(a_{1 \sim n}\) 分为 \(m\) 段,求每段价值和的最小值。
\(n \leq 10^5\)
首先由于 \((a + b)^2 \geq a^2 + b^2\) ,所以要分的段数要越多越好,答案关于 \(m\) 增加而减小。且因为先选减小值更小的地方分开更优,具有凸性。
于是可以 wqs 二分,判定部分可以斜率优化,注意斜率优化的判断要加入选取数量为第二关键字,尽量多选。
时间复杂度 \(O(n \log V)\) 。
#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;
}
CF802O April Fools' Problem (hard)
有 \(n\) 道题,第 \(i\) 天可以:
- 花费 \(a_i\) 准备一道题,上限一次。
- 花费 \(b_i\) 打印一道题,上限一次。
二者可以同时选择,准备的题可以留到以后打印。
求准备并打印 \(k\) 道题的最小花费。
\(k \leq n \leq 5 \times 10^5\)
若不考虑 \(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;
}
CF739E Gosha is hunting
现有 \(n\) 只神奇宝贝,你有 \(a\) 个宝贝球和 \(b\) 个超级球,宝贝球抓到第 \(i\) 只神奇宝贝的概率是 \(p_i\),超级球抓到的概率则是 \(u_i\) 。
不能在同一只神奇宝贝上使用超过一个同种球,但是可以往同一只上既使用宝贝球又使用超级球(都抓到算一个)。
求合理分配下抓到神奇宝贝的总个数期望的最大值。
\(n \leq 2000\)
首先球全部用完一定最优。考虑 wqs 二分斜率 \(\lambda\) ,每用一个超级球答案就减去 \(\lambda\) ,DP 出超级球选 \(b\) 个的方案。
直接 DP 是 \(O(n^2 \log V)\) 的,但是可以进一步优化,考虑每个位置的四种选择:
- 用宝贝球和超级球:\(p + u - pu - \lambda\) 。
- 用宝贝球:\(p\) 。
- 用超级球: \(u - \lambda\) 。
- 不用球:\(0\) 。
先钦定所有位置都不用宝贝球,考虑一个位置从不用宝贝球到用宝贝球,答案增加量就是:
对这个增加量排序后贪心即可,时间复杂度 \(O(n \log n \log V)\) 。
#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;
}
P4383 【八省联考2018】林克卡特树
给定一棵 \(n\) 个点的树,边带权。在树上选出 \(k + 1\) 条互不相交链,最大化权值和。
\(n \leq 3 \times 10^5\)
根据 wqs 二分的经典套路,二分斜率 \(\lambda\) ,下面考虑没有 \(k\) 的限制怎么做。
设 \(f_{u, 0/1/2}\) 表示考虑到 \(u\) 且 \(deg_u = 0/1/2\) 时的答案,具体的:
- \(deg = 0\) :这个点没有连边。
- \(deg = 1\) :这个点连着一条未完成的链,该链还未计入答案。
- \(deg = 2\) :这个点连着一条连接两个不同子树的链。
首先约定在每个节点的全部转移结束时,进行一次更新:
这样就把 \(u\) 的全部最优解统计了出来,答案即为 \(g_1\) 。
则有转移方程:
第一行表示 \(u\) 不接到 \(v\) 上,直接继承 \(v\) 的最优解。
第二行表示把 \(u, v\) 两条未完成的链拼起来,得到一条完成的链。
第一行表示 \(u\) 不接到 \(v\) 上,直接继承 \(v\) 的最优解。
第二行表示 \(u\) 接到 \(v\) 上,继承 \(v\) 一条未完成的链,得到一条未完成的链
这里 \(u\) 必须不接 \(v\) ,只能取 \(v\) 的最优解。
时间复杂度 \(O(n \log V)\) 。
#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;
}
P6246 [IOI2000] 邮局 加强版 加强版
有 \(n\) 个村庄,需要放 \(m\) 个邮局,求每个村庄到最近邮局的距离和的最小值。
\(m \leq n \leq 5 \times 10^5\)
设 \(f_{i, j}\) 表示前 \(i\) 个村庄放 \(j\) 个邮局的最小距离和,\(w(l, r)\) 表示在 \([l, r]\) 范围村庄放一个邮局的最小距离和,则有:
决策单调性优化做到 \(O(n^2)\) 。
考虑用 wqs 二分规避 \(j\) 的限制,于是得到一个1D/1D 的 DP,并且也有决策单调性,可以二分队列做到 \(O(n \log n \log V)\) 。
#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;
}
P5308 [COCI 2018/2019 #4] Akvizna
你面临 \(n\) 名参赛者的挑战,最终要将他们全部战胜。
每轮可以选择淘汰一些选手,同时获得该轮淘汰人数除以该轮总人数的奖金。
需要举办 \(k\) 轮比赛使得最终只剩一个人,最大化奖金和。
\(n \leq 10^5\)
可以发现淘汰的顺序不影响答案,因此不妨每次将淘汰的人视为一段。
套路的,先用 wqs 二分规避 \(k\) 的限制。考虑倒序 DP,设 \(f_i\) 表示从后往前数某轮还剩 \(i\) 个人的最大奖金,有:
假设对于 \(k < j\) 有 \(j\) 优于 \(k\) ,则:
直接斜率优化即可,时间复杂度 \(O(n \log V)\) 。
#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 通常用于维护连续、分段、一次、凸函数,满足每段斜率较小且每段斜率为整数。
考虑维护某个点 \(x_0\) 处的 \(f(x_0)\) 与 \(k_{x_0}\) ,以及函数每个转折点(斜率突变的点)的集合。具体的,若函数在 \(x\) 处斜率增加了 \(\Delta k\) ,就在集合中插入 \(\Delta k\) 个 \(x\) ,这样集合中相邻两个点的斜率差就为 \(1\) 。
事实上连续、分段、一次、凸函数的性质是很好的,可以支持很多操作,以下凸函数为例:
- 相加:将 \(f(x_0)\) 与 \(k_{x_0}\) 直接相加,转折点集合直接合并即可。
- 取前缀 \(\min\) 或后缀 \(\min\) :去掉 \(k > 0\) 或 \(k < 0\) 的部分。
- 求全局 \(\min\) :仅保留 \(k = 0\) 的部分。
- 整体平移:维护 \(f(x_0)\) 与 \(k_{x_0}\) 的变化,转折点打全局平移标记。
P4597 序列 sequence
给定 \(a_{1 \sim n}\),每次可以将一个位置的数字加一或减一,求使得原序列不降的最少操作次数。
原题面还要求修改后的数列只能出现修改前的数,事实上归纳可以证明这个限制不会让答案变劣。
\(n \leq 5 \times 10^5\)
设 \(f_{i, j}\) 表示考虑前 \(i\) 个数,最后一个数为 \(j\) 的答案,则:
记 \(F_i(j) = f_{i, j}\) ,显然 \(F_i\) 是 \(F_{i - 1}\) 取前缀 \(\min\) 后加绝对值函数 \(y = |x - a_i|\) 得到,答案即为 \(\min F_n(i)\) 。
考虑用堆维护 \(F\) ,记当前最右侧函数为 \(y = kx + b\) ,则:
- 先取一遍前缀 \(\min\) ,推平斜率 \(>0\) 的部分。
- 对于转折点的维护,就直接弹出右边 \(k\) 个转折点即可。
- 对于最右侧函数的维护,记当前要弹出的最右侧转折点为 \(x_0\) 。由于 \(x_0\) 同时满足左边和右边的一次函数,则 \(kx + b = (k - 1)x + b'\) ,因此 \(b' = b + x\) 。
- 再加上一个绝对值函数 \(y = |x - a|\) 。
- 先合并转折点,直接往堆中加入两个 \(a\) 即可。
- 再维护最右侧函数,新的最右侧函数为 \(y = (kx + b) + (x - a) = (k + 1)x + (b - a)\) 。
时间复杂度 \(O(n \log n)\) 。
几道类似的题目:
- CF713C Sonya and Problem Wihtout a Legend :这个 \(O(n^2)\) 能过。
- CF13C Sequence :这个 \(O(n^2)\) 也能过。
- P2893 [USACO08FEB] Making the Grade G :做不升、不降两遍即可。
- P4331 [BalticOI 2004] Sequence 数字序列 :令 \(a_i \gets a_i - i\) ,求完后令 \(b_i \gets b_i + i\) 即可。
- AT dwango2016qual E. 花火 :\(t_i > t_{i - 1}\) 时推平后面的即可。
#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;
}
[ABC217H] Snuketoon
有一个数轴上的游戏,\(0\) 时刻在 \(0\) 号节点。每个时刻可以选择将坐标 \(\pm 1\) ,或者不移动。
接下来按时间升序给出 \(n\) 个事件,每一个事件用 \(T_i, D_i, X_i\) 描述。假设 \(T_i\) 时刻在 \(p\) :
- 若 \(D_i=0\),则会受到 \(\max(0, X_i - p)\) 的伤害。
- 若 \(D_i=1\),则会受到 \(\max(0, p - X_i)\) 的伤害。
最小化 \(n\) 次事件所受伤害量。
\(n \leq 2 \times 10^5\)
设 \(f_{i, j}\) 表示 \(T_i\) 时刻在 \(j\) 可能的最小伤害,\(t\) 为与上一次的时间间隔,则:
记 \([l, r]\) 为斜率为 \(0\) 的段,可以发现一次更新就是把 \(< l\) 的部分和 \(>r\) 的部分向两边平移 \(t\) ,再加上一个只有半边的绝对值函数。
用一个大根堆和一个小根堆维护斜率为 \(0\) 的段两边的转折点,那么平移操作可以通过打标记实现。然后就是插入一个拐点 \(X_i\) ,根据 \(X_i\) 在斜率为 \(0\) 的段的左侧、右侧、中间,分类讨论贡献即可。
直接这么做会导致插入的拐点不在定义域范围内,一个做法是往两个堆中插入 \(n\) 个 \(0\) 。由于扩大定义域的操作是平移操作,那么即使加入了定义域外的转折点,也不会从堆顶取出。
时间复杂度 \(O(n \log n)\) 。
#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;
}
CF1534G A New Beginning
在一个二维平面上,你一开始在 \((0,0)\) ,只能向上或向右移动。
给定 \(n\) 个关键点点,第 \(i\) 个点为 \((x_i,y_i)\) 。在点 \((X,Y)\) 对点 \((x,y)\) 打标记,需要花费 \(\max(|X-x|,|Y-y|)\) 的代价。
求标记所有点的最小代价。
\(n \leq 8 \times 10^5\) ,\(0 \leq x_i, y_i \leq 10^9\) 。
首先套路地把切比雪夫距离转化为曼哈顿距离,于是问题转化为每次向右上或右下走一格,代价变为曼哈顿距离。
将所有点按 \(x\) 坐标排序,设 \(f_{i, j}\) 表示标记到第 \(i\) 个点时 \(y\) 坐标为 \(j\) 的答案,则:
直接开两个堆维护斜率为 \(0\) 的那段左右两边的转折点即可,时间复杂度 \(O(n \log n)\) 。
一道类似的题目: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;
}
P3642 [APIO2016] 烟火表演
给定一棵以 \(1\) 为根的 \(n + m\) 个节点的带边权树,其中叶子为 \(n + 1 \sim n + m\) 。可以花费 \(|x - y|\) 的代价将一条边的边权从 \(x\) 修改至 \(y\) ,最小化代价使得所有叶子到根节点的距离相同。
\(1 \leq n + m \leq 3 \times 10^5\)
设 \(f_{u, i}\) 为以 \(u\) 为根的子树中到叶子的距离为 \(i\) 的最小代价,有:
可以发现这是一个下凸函数,记 \(F_u(x) = \min_{i \leq x} \{ f_{u, i} + |w - x + i| \}\) ,\([l, r]\) 为 \(f_u\) 中斜率为 \(0\) 的段,则:
具体的:
- \(x < l\) :因为改变边权的代价函数斜率为 \(1\) ,而 \(f\) 函数 \(<l\) 的时候斜率一定 \(\leq -1\) ,因此把边权改为 \(0\) 一定不劣。
- \(l \leq x < l + w\) :只要保证 \(x = l\) 就能取到函数的最小值,而 \(f\) 函数 \(<l\) 的时候斜率一定 \(\leq -1\) ,因此尽量从 \(f_u\) 的最小值转移一定不劣。
- \(l + w \leq x \leq r + w\) :不用改变 \(w\) 就可以保证能取到最小值 \(f_{u, x - w} = f_{u, l}\) 。
- \(x > r + w\) :与第一条类似。
考虑一次更新对 \(F\) 的变化:
- \(x < l\) :向上平移 \(w\) 单位。
- \(l \leq x < l + w\) :向上平移 \(w\) 后把斜率变为 \(-1\) 。
- \(l + w \leq x \leq r + w\) :将原先 \([l, r]\) 的函数平移到 \([l + w, r + w]\) 。
- \(x > r + w\) :斜率变为 \(1\) 。
接下来考虑维护转折点集合,可以发现只要把 \(>l\) 的转折点全部删除,再加入 \(l + w\) 和 \(r + w\) 两个转折点即可。
由于有求和操作,需要合并转折点集合,故考虑用可并堆维护转折点。
接下来考虑维护每个点的函数值,一个取巧的方法是发现 \(F_1(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]);
}
} // namespace LT
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;
}
CF280E Sequence Transformation
给定一个不降序列 \(x_{1 \sim n} \in [1, V]\) 与两个整数 \(A, B\) 。
求一个实数序列 \(y_{1 \sim n} \in [1, V]\) 满足 \(y_i - y_{i - 1} \in [A, B]\) ,最小化代价 \(\sum_{i = 1}^n (x_i - y_i)^2\) 。
\(n \leq 6000\)
设 \(f_{i, j}\) 表示前 \(i\) 个位置且 \(y_i = j\) 的最小代价,则:
不难发现 \(F_i(j)\) 是一个凸函数,设 \(F_{i - 1}\) 的最小值点为 \(u\) ,则:
但是二次函数的斜率不是整数,并不好直接用 Slope Trick 维护,考虑对其求导:
发现其等价于把 \(u\) 左边的部分向右平移 \(A\) 单位, \(u\) 右边的部分向右平移 \(B\) 单位,然后中间部分变为 \(0\) ,再给整体加上 \(2j - 2x_i\) 。
观察上述转移,每次需要动态找到一个极值点。如果每次都暴力找,时间复杂度 \(O(n^2)\) ,可以通过但不够优美。
由凸函数斜率单调的性质,考虑用平衡树维护所有斜率相同的段,每次找到跨过 \(0\) 点的段即可。然后,每次把左边的段打一个平移 \(A\) 的标记,右边的段打一个 \(B\) 标记,加入中间的 \(0\) 段,最后整体加一个一次函数就好了。
具体实现只要在 fhq-Treap 上维护平移量,和一个一次函数的标记即可,时间复杂度 \(O(n \log n)\) 。
#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;
}
} // namespace fhqTreap
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 \mid a \in A, b \in B \}\) 。
考虑当 \(A, B\) 是凸包/凸壳的情况:
可以发现此时 \(C\) 也是凸包,并且 \(A, B\) 的边向量在 \(C\) 中恰好出现一次,即 \(C\) 的边就是把 \(A\) 和 \(B\) 的边按斜率排序拼接而成的。
考虑 \(x \in \{1, 2, \cdots, n \}\) 的两个下凸壳 \(f, g\) ,其差分数组分别单调。于是可以用 \(O(|f| + |g|)\) 的时间归并差分数组,得到 \(f\) 与 \(g\) 的闵可夫斯基和。
考虑 \((\min, +)\) 卷积,记 \(h_k = \min_{i + j = k} f_i + g_j\) 。由于 \(f, g\) 都是下凸壳,因此 \(h\) 实际上就是 \(f\) 与 \(g\) 做闵可夫斯基和后的下凸包。因此采用闵可夫斯基和可以在线性时间内求出上/下凸包 \((\min, +)\) 卷积的结果。
[ABC383G] Bar Cover
给定权值 \(a_{1 \sim n}\) 和常数 \(k\) ,对于 \(i = 1, 2, \cdots \lfloor \frac{n}{k} \rfloor\) ,求选 \(i\) 个长度为 \(k\) 的不交区间后选出区间内权值和的最大值。
\(n \leq 2 \times 10^5\) ,\(k \leq \min(n, 5)\)
令 \(b_i = \sum_{j = i}^{i + k - 1} a_j\) ,问题转化为在 \(b\) 中选若干个数,相邻两个数之间至少间隔 \(k - 1\) 个数,最大化所选数的和。
可以发现答案关于所选的数量构成上凸壳。考虑分治,设 \(f_{i, j, t}, g_{i, j, t}\) 表示左/右区间选了 \(t\) 个数,其中左边空余至少 \(i\) 个,右边空余至少 \(j\) 个的最大和。转移就是对 \(f_{i, p}\) 和 \(g_{k - 1 - p, j}\) 做 \((\max, +)\) 卷积得到左右恰好空 \(i, j\) 个的答案,最后再更新一遍 \(f\) 即可。注意需要特殊转移左区间或右区间不选 \(b\) 的情况,直接做可以做到 \(O(n k^3 \log n)\) ,难以通过。
注意到若区间长度不超过 \(k\) ,则区间最多只能选一个数,因此该部分区间无需分治,直接求即可。此时分治树上只有 \(\lfloor \frac{n}{k} \rfloor\) 个叶子,而分治树的节点数与叶子数同阶,时间复杂度将为 \(O(n k^2 \log n)\) 。
#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; --i)
a[i] -= a[i - 1];
for (int i = b.size() - 1; i; --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;
}
CF2021E3 Digital Village (Extreme Version)
给出一张连通无向图,边带边权。
有 \(p\) 个关键点 \(s_{1 \sim p}\) ,需要选 \(k\) 个点作为据点。定义两点距离为两点间所有路径上最大边的最小值,最小化所有关键点到其距离最小的据点的距离和。
对每个 \(k = 1, 2, \cdots, n\) 求答案。
\(n, m \leq2 \times 10^5\)
“两点间所有路径上最大边的最小值”不难想到 Kruskal 重构树,该值即为两点 LCA 的权值。
考虑选出的 \(k\) 个据点,将其到根路径上的所有点染黑,则一个关键点的最小距离即为其第一个被染黑的父亲的权值,称其为该关键点的对应权值。
设 \(f_{u, i}\) 表示仅考虑 \(u\) 子树内、放了 \(i\) 个据点的最小距离和,记 \(siz_u\) 表示 \(u\) 子树内的关键点数量,分类讨论:
- 左右子树都没据点:\(f_{u, 0} = 0\) 。
- 左子树有据点,而右子树没有据点:此时右子树内的关键点的对应权值即为 \(val_u\) ,有 \(f_{u, i} \gets f_{lc_u, i} + val_u \times siz_{rc_u}\) 。
- 右子树有据点,而左子树没有据点:此时左子树内的关键点的对应权值即为 \(val_u\) ,有 \(f_{u, i} \gets f_{lc_u, i} + val_u \times siz_{rc_u}\) 。
- 左右子树都有据点:\(f_{u, i} \gets \min_{j + k = i} f_{lc_u, j} + f_{rc_u, k}\) 。
不难发现这是个 \((min, +)\) 卷积的形式,考虑闵可夫斯基和,用可并堆维护差分数组,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e5 + 7;
struct Edge {
int u, v, w;
inline bool operator < (const Edge &rhs) const {
return w < rhs.w;
}
} e[N];
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);
}
inline bool check(int x, int y) {
return find(x) == find(y);
}
} dsu;
int val[N], lc[N], rc[N], siz[N];
int n, m, p, ext;
inline void Kruskal() {
memset(val + 1, 0, sizeof(int) * n);
sort(e + 1, e + m + 1), dsu.prework(n * 2 - 1), ext = n;
for (int i = 1; i <= m; ++i) {
int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v);
if (fx == fy)
continue;
val[++ext] = e[i].w;
dsu.merge(ext, lc[ext] = fx), dsu.merge(ext, rc[ext] = fy);
}
}
namespace LT {
const int S = 1e6 + 7;
ll val[S];
int rt[N], lc[S], rc[S], dist[S];
int tot;
inline void clear(int n) {
memset(rt + 1, 0, sizeof(int) * n), tot = 0;
}
inline int newnode(ll k) {
return val[++tot] = k, lc[tot] = rc[tot] = dist[tot] = 0, 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;
}
inline void insert(int x, ll k) {
rt[x] = merge(rt[x], newnode(k));
}
inline ll top(int x) {
return val[rt[x]];
}
inline void pop(int x) {
rt[x] = merge(lc[rt[x]], rc[rt[x]]);
}
} //namespace LT
void dfs(int u) {
if (u <= n) {
for (int i = 1; i <= siz[u]; ++i)
LT::insert(u, 0);
return;
}
dfs(lc[u]), dfs(rc[u]), siz[u] = siz[lc[u]] + siz[rc[u]];
if (LT::rt[lc[u]]) {
ll x = 1ll * val[lc[u]] * siz[lc[u]] + LT::top(lc[u]);
LT::pop(lc[u]), LT::insert(lc[u], x - 1ll * val[u] * siz[lc[u]]);
}
if (LT::rt[rc[u]]) {
ll x = 1ll * val[rc[u]] * siz[rc[u]] + LT::top(rc[u]);
LT::pop(rc[u]), LT::insert(rc[u], x - 1ll * val[u] * siz[rc[u]]);
}
LT::rt[u] = LT::merge(LT::rt[lc[u]], LT::rt[rc[u]]);
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &p);
memset(siz + 1, 0, sizeof(int) * n);
for (int i = 1; i <= p; ++i) {
int x;
scanf("%d", &x);
++siz[x];
}
for (int i = 1; i <= m; ++i)
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
Kruskal(), LT::clear(ext), dfs(ext);
ll ans = 1ll * val[ext] * siz[ext];
for (int i = 1; i <= p; ++i)
printf("%lld ", ans += LT::top(ext)), LT::pop(ext);
for (int i = p + 1; i <= n; ++i)
printf("0 ");
puts("");
}
return 0;
}