【UR #5】怎样更有力气
Problem#
Description#
大力水手问禅师:“大师,很多事情都需要用很大力气才能完成,而我在吃了菠菜之后力气很大,于是就导致我现在非常依赖菠菜。我很讨厌我的现状,有没有办法少吃点菠菜甚至不吃菠菜却仍很有力气?”
禅师浅笑,答:“方法很简单,不过若想我教你,你需先下山徒手修路。”
山下是 nn 座村庄从 11 到 nn 编号,之间没有路相连。禅师给了大力水手一张草图,这张草图里 nn 座村庄被 n—1n—1 条双向道路连接,任意一个村庄都可以通过双向道路到达其它所有村庄。
现在大力水手要根据禅师的意思在村庄间修路。禅师规定大力水手需要在 mm 天内完成任务,其中大力水手的修路方式如下:
第 ii 天,禅师指定了两个村庄 vivi 和 uiui,在草图上 vivi 号村庄到 uiui 号村庄的最短路径上的所有村庄(包括 vivi 和 uiui)中,大力水手需要选出若干对村庄(一个村庄可以被重复选多次,当然大力水手在这天也可以一对村庄都不选),然后在选出的每一对村庄间修建双向道路。
在实地考察中大力水手发现,有 pp 个限制关系 (ti,ai,bi)(ti,ai,bi),表示在第 titi 天无法在 aiai 号村庄到 bibi 号村庄间修路(路是双向的,所以自然也无法在 bibi 号村庄到 aiai 号村庄间修路)。
每一天都有个修理所需力气值 wiwi,表示在第 ii 天每修建一条道路都要耗费 wiwi 点力气值。
大力水手开始蛮力干了起来,一罐又一罐地吞食菠菜,结果经常修建一些无用的道路,每天都累得筋疲力尽。
作为一个旁观者,请你帮大力水手求出要想让 mm 天后任意一对村庄之间都可以互相到达,所需要的总力气值最少是多少。注意最后修出来的道路不必和草图一致。
Input Format#
第一行三个非负整数 n,m,pn,m,p。保证 n≥1n≥1。
接下来一行 n—1n—1 个整数,其中第 ii 个整数 fi+1fi+1 (1≤fi+1≤i1≤fi+1≤i)表示草图中 i+1i+1 号村庄与 fi+1fi+1 号村庄间有一条双向道路。
接下来 mm 行,第 ii 行包含三个整数 vi,ui,wivi,ui,wi (1≤vi,ui≤n,vi≠ui,1≤wi≤1091≤vi,ui≤n,vi≠ui,1≤wi≤109)表示第 ii 天禅师指定了 vivi 号村庄和 uiui 号村庄,大力水手修一条路耗费 wiwi 点力气值。
接下来 pp 行,每行包含三个整数 ti,ai,biti,ai,bi 表示一个限制关系。保证 1≤ti≤m1≤ti≤m,1≤ai,bi≤n1≤ai,bi≤n,ai≠biai≠bi,且 草图上 aiai 号村庄和 bibi 号村庄都在 vtivti 号村庄到 utiuti 号村庄的最短路径上。另外,保证输入中不会出现重复的限制关系,即不会有两个限制关系 i,ji,j 满足 ti=tj,ai=aj,bi=bjti=tj,ai=aj,bi=bj 或 ti=tj,ai=bj,bi=ajti=tj,ai=bj,bi=aj。
Output Format#
输出一行一个整数,表示所需要的最小总力气值。保证至少存在一种修路的方法使得任意一对村庄之间都可以互相到达。
C/C++ 输入输出 long long 时请用 %lld
。C++ 可以直接使用 cin/cout 输入输出。
Sample#
Input#
5 2 3
1 1 3 3
2 4 1
5 4 2
1 3 2
1 3 1
1 3 4
Output#
6
Explanation#
Explanation for Input#
第一天大力水手本来可以在 (1,2),(1,3),(1,4),(2,3),(2,4),(3,4)(1,2),(1,3),(1,4),(2,3),(2,4),(3,4) 间修路,但是由于第一天不能在 (3,2),(3,1)(3,2),(3,1) 和 (3,4)(3,4) 间修路,所以可能的选择只有 (1,2),(1,4),(2,4)(1,2),(1,4),(2,4)。对于第二天,大力水手可能的选择有 (3,4),(3,5),(4,5)(3,4),(3,5),(4,5)。
一种可能的最优方案是,第一天大力水手在 (1,2),(1,4)(1,2),(1,4) 间修路,第二天在 (3,4),(4,5)(3,4),(4,5) 间修路,总共耗费 1+1+2+2=61+1+2+2=6 点力气值。
Range#
对于 10%10%,n≤100,m≤100,p≤100n≤100,m≤100,p≤100
对于 30%30%,n≤300000,m≤300000,p=0n≤300000,m≤300000,p=0
对于 20%20%,n≤300000,m≤300000,p≤300000n≤300000,m≤300000,p≤300000,保证对于 1<i≤n1<i≤n, fi+1=ifi+1=i
对于 40%40%,n≤300000n≤300000 m≤300000m≤300000 p≤300000p≤300000
Postscript#
由于你的帮助,大力水手顺利修完了道路而且使用的力气值是原定计划的 0.01%0.01%。
大力水手对禅师说:“我明白了!我以前都是在使用蛮力,从今往后我要多思索,多使用巧力解决问题。”
禅师摆摆手,嘿嘿一笑:“对不起,我只是想请你帮忙修路而已。”
大力水手吃了一罐菠菜,把禅师打死了。
Algorithm#
并查集
Mentality#
不难想到,过程应该是像 kruskalkruskal 那样,先把每天按照 wiwi 排序,然后在满足当天要求的情况下,尽可能多地将联通块们合并起来。
接下来的问题就是具体怎么合并了。
有一个很显然的优化,能使这一题的复杂度降低很多:对于第 ii 天,设 pipi 为当天的限制个数。如果 dis(ui,vi)>pidis(ui,vi)>pi ,那么 (u,v)(u,v) 这条路径上的所有点之间都能联通。
那么对于这种情况,维护并查集,每次在链上跳到所在的并查集根部,然后合并自己和父亲即可。同时注意并查集根部应当为联通块内深度最小的点。总复杂度 O(n)O(n) 。
而且,这种情况和另一种情况的并查集应当分开维护,否则会出现错误。
而对于另一种情况,由于 dis(ui,vi)<pidis(ui,vi)<pi ,我们直接对路径上的每个点都考虑就行,反正总数不超过 pp 。
对于每一天,将所有限制 (a,b)(a,b) ,将 (a,b)(a,b) 之间连上一条限制边。我们找到限制边的度数最小的点 xx,考虑先将 xx 与路径上所有与 xx 之间没有限制边的点都合并在一起。
一个很显然的结论:度数最小的点的度数最多为 √m√m 级别(mm 为边数)。
所以对与 xx 连边的点构成的集合(我们称为 SS),我们可以直接 |S|2|S|2 判断两两之间是否有限制边来合并。然后考虑对每个 SS 内的点,判断它是否能够与 SS 外的任意一点合并。由于那些点都会与 xx 合并,所以直接将这个点与 xx 合并即可。这部分复杂度只有 O(p)O(p) 。
Code#
#include <algorithm>
#include <cmath>
#include <complex>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <vector>
using namespace std;
long long read() {
long long x = 0, w = 1;
char ch = getchar();
while (!isdigit(ch)) w = ch == '-' ? -1 : 1, ch = getchar();
while (isdigit(ch)) {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar();
}
return x * w;
}
const int Max_n = 3e5 + 5;
int n, m, p, nd;
int fa[Max_n], f[Max_n], fl[Max_n], dep[Max_n], jp[20][Max_n];
int cntr, hd[Max_n], nx[Max_n << 1], to[Max_n];
int top, t, stk[Max_n], b2[Max_n], s[Max_n], S[Max_n], du[Max_n];
bool bk[Max_n], rd[600][600];
long long ans;
vector<int> ul[Max_n], vl[Max_n];
struct day {
int u, v, w, id;
} k[Max_n];
void addr(int u, int v) {
cntr++;
nx[cntr] = hd[u], to[cntr] = v;
hd[u] = cntr;
}
bool cmp(day a, day b) { return a.w < b.w; }
void build(int x) {
jp[0][x] = fa[x], f[x] = fl[x] = x, dep[x] = dep[fa[x]] + 1;
for (int i = hd[x]; i; i = nx[i]) build(to[i]);
}
int find(int *f, int x) { return f[x] == x ? x : f[x] = find(f, f[x]); }
int get_lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
for (int i = 19; ~i; i--)
if (dep[jp[i][u]] >= dep[v]) u = jp[i][u];
for (int i = 19; ~i; i--)
if (jp[i][u] != jp[i][v]) u = jp[i][u], v = jp[i][v];
return u == v ? u : jp[0][u];
}
void merge(int u, int v) {
u = find(f, u), v = find(f, v);
if (u == v) return;
ans += k[nd].w;
f[u] = v;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("61.in", "r", stdin);
freopen("61.out", "w", stdout);
#endif
n = read(), m = read(), p = read();
for (int i = 2; i <= n; i++) addr(fa[i] = read(), i);
for (int i = 1; i <= m; i++)
k[i].u = read(), k[i].v = read(), k[i].w = read(), k[i].id = i;
sort(k + 1, k + m + 1, cmp);
while (p--) {
int d = read();
ul[d].push_back(read());
vl[d].push_back(read());
}
du[0] = 1e9, build(1);
for (int j = 1; j <= 19; j++)
for (int i = 1; i <= n; i++) jp[j][i] = jp[j - 1][jp[j - 1][i]];
for (nd = 1; nd <= m; nd++) {
int x = k[nd].id, U = k[nd].u, V = k[nd].v;
int lca = get_lca(U, V);
if (dep[U] + dep[V] - 2 * dep[lca] > ul[x].size()) {
for (int i = find(fl, U); dep[i] > dep[lca]; i = fa[i])
fl[i] = fa[i], merge(fa[i], i);
for (int i = find(fl, V); dep[i] > dep[lca]; i = find(fl, fa[i]))
fl[i] = fa[i], merge(fa[i], i);
} else {
stk[top = 1] = lca, bk[lca] = 1;
for (int i = U; i != lca; i = fa[i]) stk[++top] = i, bk[i] = 1;
for (int i = V; i != lca; i = fa[i]) stk[++top] = i, bk[i] = 1;
for (int i = ul[x].size() - 1; ~i; i--) {
int u = ul[x][i], v = vl[x][i];
if (bk[u] && bk[v]) du[u]++, du[v]++;
}
int Min = 0;
for (int i = 1; i <= top; i++)
if (du[Min] > du[stk[i]]) Min = stk[i];
for (int i = ul[x].size() - 1; ~i; i--) {
int u = ul[x][i], v = vl[x][i];
if (bk[u] && bk[v]) du[u]--, du[v]--;
if (u == Min) b2[v] = ++t, s[t] = v;
if (v == Min) b2[u] = ++t, s[t] = u;
}
for (int i = 1; i <= top; i++)
if (!b2[stk[i]]) merge(Min, stk[i]);
for (int i = ul[x].size() - 1; ~i; i--) {
int u = ul[x][i], v = vl[x][i];
if (b2[u] && b2[v]) rd[b2[u]][b2[v]] = rd[b2[v]][b2[u]] = 1;
if (b2[u] && !b2[v]) S[u]++;
if (b2[v] && !b2[u]) S[v]++;
}
for (int i = 1; i < t; i++)
for (int j = i + 1; j <= t; j++)
if (!rd[i][j]) merge(s[i], s[j]);
for (int i = 1; i <= t; i++)
if (S[s[i]] < top - t) merge(s[i], Min);
for (int i = ul[x].size() - 1; ~i; i--) {
int u = ul[x][i], v = vl[x][i];
if (b2[u] && b2[v]) rd[b2[u]][b2[v]] = rd[b2[v]][b2[u]] = 0;
if (b2[u] && !b2[v]) S[u]--;
if (b2[v] && !b2[u]) S[v]--;
}
while (t) b2[s[t--]] = 0;
while (top) bk[stk[top--]] = 0;
}
}
cout << ans << endl;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 用 C# 插值字符串处理器写一个 sscanf
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!