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\) 为起点的路径,则有:

\[\text{dp}(u, j) = \min_{v\in\text{son}(u)}\left\{ \text{dp}(v, j) + \sum_{w\in\text{son}(u), w\neq v}f(w)\right\} \]

也就是枚举一个儿子 \(v\),让它里面的路径,最小深度达到了 \(j\)。然后其他儿子 \(w\) 就可以随便选了。细心的读者或许发现,\(f(w)\) 中可能包含,覆盖到的最小深度比 \(j\) 更小的情况,但这显然是不影响答案的。

上述转移要枚举 \(v\)\(w\),是一个双重循环,比较丑。把它改写一下:

\[\text{dp}(u, j) = \sum_{v \in\text{son}(u)} f(v) + \min_{v\in\text{son}(u)}\{\text{dp}(v,j) - f(v)\} \]

接下来考虑以 \(u\) 为起点的路径,设终点的深度为 \(j\),价格为 \(c\),则转移也是类似的:

\[\text{dp}(u,j) = \sum_{v \in\text{son}(u)} f(v) + 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;
}
posted @ 2021-01-03 18:13  duyiblue  阅读(355)  评论(0编辑  收藏  举报