1. 程式人生 > 實用技巧 >CF671D Roads in Yusland

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;
}