CF671D Roads in Yusland
CF671D Roads in Yusland
题目大意
给定一棵 \(n\) 个节点的、以 \(1\) 为根的有根树。
有 \(m\) 条路径 \((u_i, v_i)\),保证 \(v_i\) 是 \(u_i\) 的祖先(祖先包括这个点自己)。每条路径有一个价格。
请选择若干条路径,将树上所有边覆盖(一条边可以被多次覆盖)。求所选路径价格之和的最小值。
数据范围:\(1\leq n,m\leq 3\times 10^5\)。
本题题解
定义根节点的深度为 \(1\),其他每个点的深度是它父亲的深度 \(+1\)。记点 \(u\) 的深度为 \(\text{dep}(u)\)。
对于给出的路径 \((u_i, v_i)\)(\(v_i\) 是 \(u_i\) 的祖先),我们在节点 \(u_i\) 处考虑是否选择它。以下称 \(u_i\) 为“起点”,\(v_i\) 为“终点”。
朴素的树形 DP。设 \(\text{dp}(u,j)\) 表示选择了若干条起点在 \(u\) 的子树内的路径,使得这些路径覆盖了 \(u\) 子树内的所有边,并且它们的终点的深度的最小值为 \(j\)(\(j\leq\text{dep}(u)\)),达到这种情况所需的最小花费。感性理解,就是所选路径,在 \(u\) 的子树外,最远覆盖到深度为 \(j\) 的祖先。
同时设 \(f(u) = \min_{j = 1}^{\text{dep}(u) - 1} \text{dp}(u, j)\),也就是至少覆盖到 \(u\) 和父亲之间的边,所需的花费。
转移。先不考虑以 \(u\) 为起点的路径,则有:
也就是枚举一个儿子 \(v\),让它里面的路径,最小深度达到了 \(j\)。然后其他儿子 \(w\) 就可以随便选了。细心的读者或许发现,\(f(w)\) 中可能包含,覆盖到的最小深度比 \(j\) 更小的情况,但这显然是不影响答案的。
上述转移要枚举 \(v\) 和 \(w\),是一个双重循环,比较丑。把它改写一下:
接下来考虑以 \(u\) 为起点的路径,设终点的深度为 \(j\),价格为 \(c\),则转移也是类似的:
答案就是 \(\text{dp}(1, 1)\)。这个朴素 DP 的时间复杂度为 \(\mathcal{O}(n^2)\)。
考虑优化。容易想到,用线段树来维护 DP 的第二维。需要支持:
- 区间加:转移前把所有 \(\text{dp}(v,j)\) 加上 \(-f(v)\);以及最后把所有 \(\text{dp}(u,j)\) 加上 \(\sum_{v \in\text{son}(u)} f(v)\)。
- 查询区间最小值:也就是求出 \(f(v)\)。
- 线段树合并。合并时,对应位置取 \(\min\)。发现这相当于要支持区间取 \(\min\)。
- 单点对一个数取 \(\min\):在考虑以 \(u\) 为起点的路径时,单点对 \(c\) 取 \(\min\)。发现这已经被上一条(区间取 \(\min\))包含。
可以用线段树维护两个懒标记:区间加法,和区间对一个数取 \(\min\),同时记录区间最小值。就能支持上述的操作了。关于双标记的顺序问题:下放懒标记时,先下放区间加法,再下放区间取 \(\min\)。做区间加时,要同时更新区间取 \(\min\) 的懒标记。
时间、空间复杂度 \(\mathcal{O}((n + m) \log n)\)。因为本题空间限制较小,难以通过。
继续优化。考虑对每个点,维护一个 \(\texttt{std::set}\)。里面存二元组:\((j, \text{dp}(v, j))\)。以 \(j\) 为关键字排序。这个 \(\texttt{set}\) 里,其实就是原来的 DP 数组中所有不为 \(\infty\) 的元素。
考虑转移时的操作。
- 区间加法。发现只要我们把 \(j > \text{dep}(u)\) 的元素及时弹出,区间加就变为了全局加。因此只要维护一个全局的标记即可。
- 合并可以采用启发式合并。也就是把小的一个一个“插入”到大的里面。
- 单点取 \(\min\) 也相当于“插入”一个新元素。
注意,这里的“插入”不是直接插入,如果 \(\texttt{set}\) 里已经存在相同的 \(j\),则需要把第二元拿出来比大小。最终每个 \(j\) 只会在 \(\texttt{set}\) 里出现一次。
最后一个问题是查询最小值(和区间加类似,在支持弹出后,区间最小值查询就变成了全局最小值查询)。因为我们的二元组,在 \(\texttt{set}\) 里是按第一元 \(j\) 排序的,故难以同时维护出第二元的最小值。数据结构的能力已经用到极限了,我们就必须回过头来继续挖掘题目的性质。发现对于 \(j_1 < j_2\),若 \(\text{dp}(u, j_1) < \text{dp}(u, j_2)\),则 \(\text{dp}(u, j_2)\) 不会对答案有任何贡献。换句话说,此时我们把 \(\text{dp}(u, j_2)\) 视为 \(\infty\),也不会影响答案。因此可以直接把 \(\text{dp}(u, j_2)\) 从 \(\texttt{set}\) 里删掉。于是这样维护出的 \(\texttt{set}\),所有元素第二元一定单调递减。直接取最后一个元素,就是第二元最小的。
时间复杂度 \(\mathcal{O}((n + m)\log^2 n)\),空间复杂度 \(\mathcal{O}(n + m)\)。可以通过本题。
参考代码
实际提交时建议使用读入优化,详见本博客公告。
// problem: CF671D
#include <bits/stdc++.h>
using namespace std;
#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 3e5;
const ll LL_INF = 1e18;
int n, m;
struct EDGE { int nxt, to; } edge[MAXN * 2 + 5];
int head[MAXN + 5], tot;
inline void add_edge(int u, int v) { edge[++tot].nxt = head[u]; edge[tot].to = v; head[u] = tot; }
vector<pii> plan[MAXN + 5];
int dep[MAXN + 5];
int id[MAXN + 5];
set<pair<int, ll> > dps[MAXN + 5];
ll tag_add[MAXN + 5];
void ins(int u, int p, ll val) {
set<pair<int, ll> > :: iterator it = dps[id[u]].lower_bound(make_pair(p, -LL_INF));
if (it != dps[id[u]].end() && (it -> fi) == p) {
if ((it -> se) + tag_add[id[u]] > val) {
dps[id[u]].erase(it);
dps[id[u]].insert(make_pair(p, val - tag_add[id[u]]));
}
} else {
if (it != dps[id[u]].begin() && ((--it) -> se) + tag_add[id[u]] <= val) {
return;
}
dps[id[u]].insert(make_pair(p, val - tag_add[id[u]]));
}
set<pair<int, ll> > :: iterator st = dps[id[u]].lower_bound(make_pair(p + 1, -LL_INF));
set<pair<int, ll> > :: iterator ed = st;
while (ed != dps[id[u]].end() && (ed -> se) + tag_add[id[u]] >= val)
++ed;
if (ed != it)
dps[id[u]].erase(st, ed); // 维持单调性
}
void del(int u, int lim) {
while (SZ(dps[id[u]])) {
set<pair<int, ll> > :: iterator it = dps[id[u]].end();
--it;
if ((it -> fi) <= lim)
break;
dps[id[u]].erase(it);
}
}
ll get_min(int u) {
assert(SZ(dps[id[u]]) > 0);
return (dps[id[u]].rbegin() -> se) + tag_add[id[u]];
}
void dfs(int u, int fa) {
dep[u] = dep[fa] + 1;
for (int i = 0; i < SZ(plan[u]); ++i) {
int v = plan[u][i].fi;
int c = plan[u][i].se;
ins(u, dep[v], c);
}
ins(u, dep[u], 0);
ll sum = 0;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (v == fa)
continue;
dfs(v, u);
del(v, dep[u]);
if (!SZ(dps[id[v]])) {
cout << -1 << endl;
exit(0);
}
ll f = get_min(v);
sum += f;
tag_add[id[v]] -= f;
if (SZ(dps[id[u]]) < SZ(dps[id[v]]))
swap(id[u], id[v]);
for (set<pair<int, ll> > :: iterator it = dps[id[v]].begin();
it != dps[id[v]].end(); ++it) {
ins(u, it -> fi, (it -> se) + tag_add[id[v]]);
}
dps[id[v]].clear();
}
tag_add[id[u]] += sum;
// cerr << "************ " << u << endl;
// for (set<pair<int, ll> > :: iterator it = dps[id[u]].begin();
// it != dps[id[u]].end(); ++it) {
// cerr << (it -> fi) << " " << (it -> se) + tag_add[id[u]] << endl;
// }
}
int main() {
cin >> n >> m;
for (int i = 1; i < n; ++i) {
int u, v;
cin >> u >> v;
add_edge(u, v);
add_edge(v, u);
}
for (int i = 1; i <= m; ++i) {
int u, v, c;
cin >> u >> v >> c;
if (u != v) {
plan[u].push_back(mk(v, c));
}
}
for (int i = 1; i <= n; ++i)
id[i] = i;
dfs(1, 1);
del(1, 1);
ll ans = get_min(1);
cout << ans << endl;
return 0;
}