给定 N 个点,第 i 点有一个点权 Xi,再给定一棵边带权的树,第 i 条 (Ai, Bi) 边权为 Ci。算法
构建一个彻底图,彻底图中边 (i, j) 的边权为 dist(i, j) + Xi + Xj,其中 dist(i, j) 是点 i 与点 j 在树上的距离。数据结构
求该彻底图的最小生成树。优化
Constraints
2≤N≤200000; 1≤Xi≤10^9; 1≤Ai,Bi≤N; 1≤Ci≤10^9spa
Input
输入形式以下:
N
X1 X2 … XN
A1 B1 C1
A2 B2 C2
:
AN−1 BN−1 CN−1code
Output
输出最小生成树的边权总和。队列
Sample Input 1
4
1 3 5 1
1 2 1
2 3 2
3 4 3
Sample Output 1
22ip
选择边 (1, 2), (1, 4), (3, 4),边权总和为 5 + 8 + 9 = 22get
点此跳转到题目it
彻底图并不能直接套 prim 或者 kruskal。
而且也不像最小曼哈顿生成树同样,有比较好用的性质能够去除大量无用边。
考虑这类题的一个通用解法:boruvka(不会念)。
实际上是基本无人问津的最小生成树算法(我也不知道为何),可是能够将其算法思想拓展,解决一些彻底图的最小生成树问题。
说是彻底图,边权也有性质,并且每每会和点权挂钩(好比这道题)。
扯了这么多,所谓的 boruvka 算法是什么呢?
咱们对于每个连通块去找与它相邻的最小边。能够证实这样的边必定是存在于最小生成树之中(破圈法之类的都能证)。
因而咱们把这些边加入最小生成树,并将连通块合并。
由于每次选最小边的时候,一条边最多会被选两次,因此最多执行 log 次寻找最小边的操做。
而这类题的特色是:每每寻找最小边的过程能够优化。
什么?你说这道题须要点分治来寻找路径最小值?而后复杂度就炸成 O(nlog^3n) 了?
NONONO。咱们其实能够 O(n) 一次性给全部点找到它对应的最小值。
考虑全部连通块都是一个点的时候,咱们能够作一个换根 dp 求出全部点的最小边(求距离一个点最近的点)。
因为是找最小值,重复通过一条边确定不优,咱们甚至不用去存 dp 的次小值,来避免从上往下传递 dp 值的时候走入同一棵子树。
这个过程是 O(n) 的。
那么连通块是一堆点的时候,咱们怎么去排除与它同一连通块的点呢?
其实。。。很简单嘛。
假如最小边是同一连通块的,咱们就去找与最小边不在同一连通块的次小边,不就解决了嘛。
dp 的时候存两维:最小边,与不和最小边在同一连通块的次小边。转移时讨论一下便可。
一次求最小边的复杂度为 O(n),因此总复杂度为 O(nlogn)。
#include<cstdio> #include<iostream> using namespace std; typedef long long ll; #define mp make_pair #define fi first #define se second typedef pair<ll, int> pli; typedef pair<pli, pli> st; const int MAXN = 200000; const ll INF = (1LL<<60); struct edge{ int to; ll dis; edge *nxt; }edges[2*MAXN + 5], *adj[MAXN + 5], *ecnt = edges; void addedge(int u, int v, int w) { edge *p = (++ecnt); p->to = v, p->dis = w, p->nxt = adj[u], adj[u] = p; p = (++ecnt); p->to = u, p->dis = w, p->nxt = adj[v], adj[v] = p; } pli lnk[MAXN + 5]; int fa[MAXN + 5], clr[MAXN + 5]; ll X[MAXN + 5]; int find(int x) { return fa[x] = (fa[x] == x ? x : find(fa[x])); } bool unite(int x, int y) { int fx = find(x), fy = find(y); if( fx != fy ) { fa[fx] = fy; return true; } else return false; } st dp[MAXN + 5]; void update(st &a, st b) { if( b.fi.fi < a.fi.fi ) { if( b.fi.se != a.fi.se ) a.se = a.fi; a.fi = b.fi; if( b.se.fi < a.se.fi ) a.se = b.se; } else { if( b.fi.se != a.fi.se ) { if( b.fi.fi < a.se.fi ) a.se = b.fi; } else if( b.se.fi < a.se.fi ) a.se = b.se; } } void dfs1(int x, int f) { dp[x] = mp(mp(X[x], clr[x]), mp(INF, -1)); for(edge *p=adj[x];p;p=p->nxt) { if( p->to == f ) continue; dfs1(p->to, x); st t = dp[p->to]; t.fi.fi += p->dis, t.se.fi += p->dis; update(dp[x], t); } } void dfs2(int x, int f, st k) { update(dp[x], k); for(edge *p=adj[x];p;p=p->nxt) { if( p->to == f ) continue; st t = dp[x]; t.fi.fi += p->dis, t.se.fi += p->dis; dfs2(p->to, x, t); } } int num[MAXN + 5]; int main() { int N; scanf("%d", &N); for(int i=1;i<=N;i++) scanf("%lld", &X[i]), fa[i] = i; for(int i=1;i<N;i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); addedge(u, v, w); } ll ans = 0; while( true ) { int cnt = 0; for(int i=1;i<=N;i++) if( fa[i] == i ) num[clr[i] = (++cnt)] = i; if( cnt == 1 ) break; for(int i=1;i<=N;i++) clr[i] = clr[find(i)]; dfs1(1, 0), dfs2(1, 0, mp(mp(INF, -1), mp(INF, -1))); for(int i=1;i<=cnt;i++) lnk[i] = mp(INF, -1); for(int i=1;i<=N;i++) { if( dp[i].fi.se == clr[i] ) lnk[clr[i]] = min(lnk[clr[i]], mp(dp[i].se.fi + X[i], dp[i].se.se)); else lnk[clr[i]] = min(lnk[clr[i]], mp(dp[i].fi.fi + X[i], dp[i].fi.se)); } for(int i=1;i<=cnt;i++) if( unite(num[i], num[lnk[i].se]) ) ans += lnk[i].fi; } printf("%lld\n", ans); }
被数据结构困住的我,用点分治 + 优先队列写了一个 prim。
而后它 MLE 了。
当时我就哭了。
咳咳。不过也算是增加了一点见识,同时还告诉我一个深入的道理:不要被套路所困。不必定非得要数据结构才能维护的啊。