最小生成树
最小生成树
常用结论
- 对任意最小生成树,仅保留权值 \(\le L\) 的边所得森林的连通性相同。
- 对于完全图 \((V, E)\) ,若 \(E = E_1 \cup E_2 \cup \cdots \cup E_k = E\) ,则 \(\mathrm{MST}(E) = \mathrm{MST}(\mathrm{MST}(E_1) \cup \mathrm{MST}(E_2) \cup \cdots \cup \mathrm{MST}(E_k))\) 。
- MST 的唯一性:对于 Kruskal 算法,只要计算出权值相同的边能放几条,实际放了几条,若两者不同则形成了环,此时 MST 不唯一。
- 对于所有 MST,每种权值的边出现次数相同。
- 若次小生成树存在,则总可以和最小生成树只差一条边。
实现
Kruskal 算法
按边权升序排序后依次选取边尝试加入 MST,用并查集维护连通性,时间复杂度 \(O(m \log m)\) 。
inline int Kruskal() {
sort(e + 1, e + 1 + m, [](const Edge &a, const Edge &b) {
return a.w < b.w;
});
dsu.prework(n);
int res = 0, cnt = n;
for (int i = 1; i <= m && cnt > 1; ++i)
if (dsu.find(e[i].u) != dsu.find(e[i].v))
dsu.merge(e[i].u, e[i].v), res += e[i].w, --cnt;
return cnt == 1 ? res : -1;
}
Prim 算法
维护一个生成树集合,不断向这个集合内加点。
具体的,每次要选择距离当前生成树集合最小的一个结点加入,并更新其他结点的距离。
暴力是 \(O(n^2 + m)\) 的,用堆维护可以做到 \(O((n + m) \log n)\) 。
inline int Prim() {
memset(dis + 1, inf, sizeof(int) * n);
memset(vis + 1, false, sizeof(bool) * n);
priority_queue<pair<int, int> > q;
dis[1] = 0, q.emplace(-dis[1], 1);
int res = 0, cnt = 0;
while (!q.empty() && cnt < n) {
int u = q.top().second;
q.pop();
if (vis[u])
continue;
vis[u] = true, res += dis[u], ++cnt;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > w)
dis[v] = w, q.emplace(-dis[v], v);
}
}
return cnt == n ? res : -1;
}
Boruvka 算法
定义一个连通块的最小边为它连向其它连通块的边中权值最小的那一条。
流程:
- 计算每个点分别属于哪个连通块。
- 遍历每条边,若两端点不连通,则用该边尝试更新两连通块的最小边。
- 如果所有连通块都没有最小边,则已经找到了 MST ,否则将各连通块最小边加入 MST。
边权相同时加入第二关键字编号区分边权相同的边。
由于每次迭代连通块数量至少减半,所以时间复杂度是 \(O(m \log n)\) 。
优势在于完全图的求解,每次合并的时候可以把所有点都扫一遍,找到每个点距离最短的连通块,更新最小边。
对于某些稠密图上的 MST,Boruvka 可以用 \(O(\log n)\) 的代价将问题转化为不同连通块间的最短边问题。
inline int Boruvka() {
dsu.prework(n);
int cnt = 0, res = 0;
for (;;) {
memset(outd + 1, -1, sizeof(int) * n);
for (int i = 1; i <= m; ++i) {
int u = dsu.find(e[i].u), v = dsu.find(e[i].v), w = e[i].w;
if (dsu.find(u) == dsu.find(v))
continue;
if (outd[u] == -1 || w < e[outd[u]].w)
outd[u] = i;
if (outd[v] == -1 || w < e[outd[v]].w)
outd[v] = i;
}
bool flag = false;
for (int i = 1; i <= n; ++i)
if (outd[i] != -1 && dsu.find(e[outd[i]].u) != dsu.find(e[outd[i]].v))
flag = true, ++cnt, res += e[outd[i]].w, dsu.merge(e[outd[i]].u, e[outd[i]].v);
if (!flag)
break;
}
return cnt == n - 1 ? res : -1;
}
次小生成树
对于非严格次小生成树,求出原图的 MST 后枚举所有未选的边 \((u, v, w)\) ,找到 MST 上 \(u, v\) 路径中边权最大的边,将其替换掉,记录最小值即可。
对于严格次小生成树,类似的,维护 \(u, v\) 路径上边权的次小值,若替换时边权不严格,则使用次小边替换,时间复杂度 \(O(m \log m + n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1e5 + 7, M = 3e5 + 7, LOGN = 17;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
struct Edge {
int u, v;
ll w;
inline bool operator < (const Edge &rhs) const {
return w < rhs.w;
}
} e[M];
bool used[M];
ll sum;
int n, m;
namespace Tree {
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
ll fir[N][LOGN], sec[N][LOGN];
int fa[N][LOGN], dep[N];
void dfs(int u, int f) {
dep[u] = dep[f] + 1, fa[u][0] = f, sec[u][0] = -inf;
for (int i = 1; i < LOGN; ++i) {
fa[u][i] = fa[fa[u][i - 1]][i - 1];
vector<ll> vec = {-inf, fir[u][i - 1], fir[fa[u][i - 1]][i - 1], sec[u][i - 1], sec[fa[u][i - 1]][i - 1]};
sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
fir[u][i] = vec.back(), sec[u][i] = vec[vec.size() - 2];
}
for (auto it : G.e[u]) {
int v = it.first;
if (v != f)
fir[v][0] = it.second, dfs(v, u);
}
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int h = dep[x] - dep[y]; h; h &= h - 1)
x = fa[x][__builtin_ctz(h)];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
inline ll query(int x, int h, ll k) {
ll res = -inf;
for (int i = 0; h; ++i, h >>= 1)
if (h & 1)
res = max(res, k == fir[x][i] ? sec[x][i] : fir[x][i]), x = fa[x][i];
return res;
}
} // namespace Tree
inline void Kruskal() {
sort(e + 1, e + 1 + m), dsu.prework(n);
int cnt = 1;
for (int i = 1; i <= m; ++i) {
int u = e[i].u, v = e[i].v, w = e[i].w;
if (dsu.find(u) == dsu.find(v))
continue;
Tree::G.insert(u, v, w), Tree::G.insert(v, u, e[i].w);
dsu.merge(u, v), used[i] = true, sum += e[i].w, ++cnt;
if (cnt == n)
break;
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d%lld", &e[i].u, &e[i].v, &e[i].w);
Kruskal(), Tree::dfs(1, 0);
ll ans = inf;
for (int i = 1; i <= m; ++i)
if (!used[i]) {
int u = e[i].u, v = e[i].v, w = e[i].w, lca = Tree::LCA(u, v);
ll res = max(Tree::query(u, Tree::dep[u] - Tree::dep[lca], w),
Tree::query(v, Tree::dep[v] - Tree::dep[lca], w));
if (res > -inf)
ans = min(ans, sum - res + e[i].w);
}
printf("%lld", ans == inf ? -1 : ans);
return 0;
}
Kruskal 重构树
在 Kruskal 的过程中建立一张新图,对于加入的每一条边都新建一个点, 点权设为这条边的边权,把这条边连接的两个集合设为其儿子,把它作为这两个集合的根。
不难发现这三者等价:
- 原图中两点简单路径最大边权最小值。
- MST 上两点简单路径边权最大值。
- Kruskal 重构树上两点 LCA 的点权。
inline void Kruskal() {
sort(e + 1, e + 1 + m);
T.clear(n * 2), dsu.clear(n * 2);
int ext = n;
for (int i = 1; i <= m; ++i) {
int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v);
if (fx == fy)
continue;
val[++ext] = e[i].h;
T.insert(ext, fx), T.insert(ext, fy);
dsu.merge(ext, fx), dsu.merge(ext, fy);
}
}
最小树形图
给定一张有向图,求以节点 \(r\) 为根的最小树形图的权值和(边权和最小值),即叶向生成树。
\(n \le 100\) ,\(m \le 10^4\)
最小树形图的求解通常采用朱刘算法(Edmonds 算法)。
首先观察到这棵树上除 \(r\) 外的每个点有且仅有一条入边。考虑直接贪心选出所有点边权最小的入边。
-
若除了根以外存在点没有入边则无解。
-
若这些边形成一棵树,这显然是最优解。
-
否则会出现环,考虑对其进行缩点。同时对于不在环内的边 \((u, v, w)\) ,将其边权减去 \(v\) 入边的边权,这样若选了这条边就可以视作同时去掉了非法边
重复上述操作直到无环,时间复杂度 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e2 + 7, M = 1e4 + 7;
struct Edge {
int u, v, w;
} e[M];
int fa[N], id[N], tag[N], mnw[N];
int n, m, r;
inline int Edmonds() {
int ans = 0;
for (;;) {
memset(mnw, inf, sizeof(mnw));
mnw[r] = 0;
for (int i = 1; i <= m; ++i)
if (e[i].u != e[i].v && e[i].w < mnw[e[i].v])
fa[e[i].v] = e[i].u, mnw[e[i].v] = e[i].w;
memset(id, 0, sizeof(id)), memset(tag, 0, sizeof(tag));
int cnt = 0;
for (int i = 1; i <= n; ++i) {
if (mnw[i] == inf)
return -1;
ans += mnw[i];
int u = i;
while (u != r && !id[u] && tag[u] != i)
tag[u] = i, u = fa[u];
if (u != r && !id[u]) {
id[u] = ++cnt;
for (int v = fa[u]; v != u; v = fa[v])
id[v] = cnt;
}
}
if (!cnt)
return ans;
for (int i = 1; i <= n; ++i)
if (!id[i])
id[i] = ++cnt;
for (int i = 1; i <= m; ++i) {
if (id[e[i].u] != id[e[i].v])
e[i].w -= mnw[e[i].v];
e[i].u = id[e[i].u], e[i].v = id[e[i].v];
}
n = cnt, r = id[r];
}
}
signed main() {
scanf("%d%d%d", &n, &m, &r);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
e[i] = (Edge){u, v, w};
}
printf("%d", Edmonds());
return 0;
}
应用
QOJ9904. 最小生成树
给定 \(a_{3 \sim 2n - 1}\) ,定义完全图 \(G\) ,其中 \(i, j\) 之间的边权为 \(a_{i + j}\) ,求 MST。
\(n \le 2 \times 10^5\)
显然将 \(a\) 排序后依次连边,下面考虑优化连边的复杂度。
由于连边形如回文,考虑先将并查集的大小开到 \(2n\) ,其中 \(1 \sim n\) 表示原来的点,\(n + 1 \sim 2n\) 表示翻转后的点。一开始先将 \(i, 2n - i + 1\) 连上边,表示位置相对应。
一个想法是用线段树维护区间哈希值,每次二分找到第一个哈希值不同(不连通)的对应位置连边,时间复杂度 \(O(n \log^2 n)\) 。
另一个想法是用 ST 表优化,具体做法和 P3295 [SCOI2016] 萌萌哒 是类似的。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e5 + 7, LOGN = 19;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu[LOGN];
int a[N];
ll ans;
int n;
void merge(int x, int y, int k, int val) {
if (dsu[k].find(x) == dsu[k].find(y))
return;
dsu[k].merge(x, y);
if (k)
merge(x, y, k - 1, val), merge(x + (1 << (k - 1)), y + (1 << (k - 1)), k - 1, val);
else
ans += val;
}
signed main() {
scanf("%d", &n);
for (int i = 3; i <= n * 2 - 1; ++i)
scanf("%d", a + i);
vector<int> id(n * 2 - 3);
iota(id.begin(), id.end(), 3);
sort(id.begin(), id.end(), [](const int &x, const int &y) {
return a[x] < a[y];
});
for (int i = 0; i <= __lg(n); ++i)
dsu[i].prework(n * 2);
for (int i = 1; i <= n; ++i)
dsu[0].merge(i, n * 2 - i + 1);
for (int it : id) {
int l = (it <= n ? 1 : it - n), r = (it <= n ? it - 1 : n), k = __lg(r - l + 1);
merge(l, n * 2 - r + 1, k, a[it]), merge(r - (1 << k) + 1, n * 2 - (l + (1 << k) - 1) + 1, k, a[it]);
}
printf("%lld", ans);
return 0;
}
CF1305G Kuroni and Antihype
给定 \(a_{1 \sim n}\) ,定义一张无向图 \(G\) 为:\(i, j\) 之间有边当且仅当 \(a_i \operatorname{and} a_j = 0\) 。
执行如下过程 \(n\) 次:
- 选择一个未染色的点 \(u\) ,将其染色。
- 选择一个与 \(u\) 相邻的已染色的点 \(v\) ,将 \(a_v\) 加入答案,若不存在则跳过该步。
最大化答案。
\(n, a_i \le 2 \times 10^5\)
考虑加入一个点权为 \(0\) 的虚点,定义边权为两端点点权和,答案即为最大生成树减去点权和。
考虑 Kruskal,降序枚举边权 \(w\) 。由于 \(a_i \operatorname{and} a_j = 0\) ,因此 \(a_i + a_j = a_i \operatorname{or} a_j\) ,因此只要枚举 \(w\) 的所有子集即可,时间复杂度 \(O(3^{\log V} \alpha(n))\) 。
也可以考虑 Boruvka,问题转化为:给定权值 \(a\) 和颜色 \(c\) ,求:
高维前缀和维护最大值和次大值即可,时间复杂度 \(O(V \log V \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int LOGN = 18, N = 1 << LOGN;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
pair<int, int> f[N], outd[N];
int val[N];
int n;
inline pair<int, int> operator + (pair<int, int> a, pair<int, int> b) {
int tmp[4] = {a.first, a.second, b.first, b.second};
sort(tmp, tmp + 4, [](const int &x, const int &y) {
return val[x] > val[y];
});
for (int i = 1; i <= 3; ++i)
if (!tmp[i] || dsu.find(tmp[i]) != dsu.find(tmp[0]))
return make_pair(tmp[0], tmp[i]);
}
signed main() {
scanf("%d", &n);
ll ans = 0;
for (int i = 1; i <= n; ++i)
scanf("%d", val + i), ans -= val[i];
val[++n] = 0, dsu.prework(n), val[0] = -1;
for (;;) {
fill(f, f + N, make_pair(0, 0)), fill(outd, outd + N, make_pair(0, 0));
for (int i = 1; i <= n; ++i)
f[val[i]] = f[val[i]] + make_pair(i, 0);
for (int j = 0; j < LOGN; ++j)
for (int i = 0; i < N; ++i)
if (i >> j & 1)
f[i] = f[i] + f[i ^ (1 << j)];
for (int i = 1; i <= n; ++i) {
int x = dsu.find(i), s = (N - 1) ^ val[i];
if (f[s].first && dsu.find(f[s].first) != x)
outd[x] = max(outd[x], make_pair(val[i] + val[f[s].first], f[s].first));
else if (f[s].second && dsu.find(f[s].second) != x)
outd[x] = max(outd[x], make_pair(val[i] + val[f[s].second], f[s].second));
}
bool flag = false;
for (int i = 1; i <= n; ++i)
if (dsu.find(i) == i && outd[i].second && dsu.find(outd[i].second) != dsu.find(i))
ans += outd[i].first, dsu.merge(i, outd[i].second), flag = true;
if (!flag)
break;
}
printf("%lld", ans);
return 0;
}
CF888G Xor-MST
给定 \(n\) 个点的无向完全图,每个点有一个点权为 \(a_i\) ,\(i, j\) 之间存在边权为 \(a_i \oplus a_j\) 的边,求 MST。
\(n \le 2 \times 10^5\)
考虑 Kruskal 算法,每次合并两个连通块,显然每次在 LCA 处合并是最优的(高的位都被异或掉了)。
把每个点的权值放在 Trie 树上后,实际就相当于每次选最小边合并左右两棵子树。
走到一个有两个儿子的点时,考虑枚举小子树的点,然后求大子树与其的异或最小值,时间复杂度 \(O(n \log n \log V)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7, B = 30;
int a[N];
int n;
namespace Trie {
const int S = N << 5;
int ch[S][2], L[S], R[S];
int tot = 1;
inline void insert(int k, int id) {
int u = 1;
for (int i = B; ~i; --i) {
L[u] = (L[u] ? L[u] : id), R[u] = id;
int idx = k >> i & 1;
if (!ch[u][idx])
ch[u][idx] = ++tot;
u = ch[u][idx];
}
L[u] = (L[u] ? L[u] : id), R[u] = id;
}
inline int query(int u, int d, int k) {
int res = 0;
for (int i = d; ~i; --i) {
if (ch[u][k >> i & 1])
u = ch[u][k >> i & 1];
else
u = ch[u][~k >> i & 1], res |= 1 << i;
}
return res;
}
ll dfs(int u, int d) {
if (d == -1)
return 0;
else if (ch[u][0] && ch[u][1]) {
int ans = inf;
if (R[ch[u][0]] - L[ch[u][0]] + 1 <= R[ch[u][1]] - L[ch[u][1]] + 1) {
for (int i = L[ch[u][0]]; i <= R[ch[u][0]]; ++i)
ans = min(ans, query(ch[u][1], d - 1, a[i]) | (1 << d));
} else {
for (int i = L[ch[u][1]]; i <= R[ch[u][1]]; ++i)
ans = min(ans, query(ch[u][0], d - 1, a[i]) | (1 << d));
}
return ans + dfs(ch[u][0], d - 1) + dfs(ch[u][1], d - 1);
} else if (ch[u][0])
return dfs(ch[u][0], d - 1);
else if (ch[u][1])
return dfs(ch[u][1], d - 1);
else
return 0;
}
} // namespace Trie
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; ++i)
Trie::insert(a[i], i);
printf("%lld", Trie::dfs(1, B));
return 0;
}
AT_cf17_final_j Tree MST
给出一棵树,现有一张无向完全图,\(x, y\) 之间的边权为 \(w_x + w_y + \mathrm{dist}(x, y)\) ,求该完全图的 MST。
\(n \le 2 \times 10^5\)
考虑点分治,对于路径过重心的点对 \((x, y)\) ,则其边权为 \((w_x + d_x) + (w_y + d_y)\) (\(d\) 为到重心的距离),因此该部分的 MST 即子树内所有点和 \(w + d\) 最小的点连边。可能会出现两个点在同一子树的情况边权会算大,但是不影响答案。
最后将所有 \(O(n \log n)\) 边再拿出来做一次 Kruskal 即可,时间复杂度 \(O(n \log^2 n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
vector<tuple<ll, int, int> > edg;
vector<int> vec;
ll dis[N];
int a[N], siz[N], mxsiz[N];
bool vis[N];
int n, root;
int getsiz(int u, int f) {
siz[u] = 1;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v] && v != f)
siz[u] += getsiz(v, u);
}
return siz[u];
}
void getroot(int u, int f, int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
}
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void dfs(int u, int f) {
vec.emplace_back(u);
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (!vis[v] && v != f)
dis[v] = dis[u] + w, dfs(v, u);
}
}
void solve(int u) {
vis[u] = true, dis[u] = 0, vec.clear(), dfs(u, 0);
sort(vec.begin(), vec.end(), [](const int &x, const int &y) {
return a[x] + dis[x] < a[y] + dis[y];
});
for (int i = 1; i < vec.size(); ++i)
edg.emplace_back(a[vec[0]] + dis[vec[0]] + a[vec[i]] + dis[vec[i]], vec[0], vec[i]);
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v])
root = 0, getroot(v, u, getsiz(v, u)), solve(root);
}
}
inline ll Kruskal() {
sort(edg.begin(), edg.end()), dsu.prework(n);
ll ans = 0;
for (auto it : edg) {
int u = get<1>(it), v = get<2>(it);
if (dsu.find(u) == dsu.find(v))
continue;
ans += get<0>(it), dsu.merge(u, v);
}
return ans;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
getroot(1, 0, n), solve(root);
printf("%lld", Kruskal());
return 0;
}
也可以考虑 Boruvka 算法,问题转化为每次找到一个与 \(x\) 不属于一个连通块的点 \(y\) 使得 \(w_x + w_y + \mathrm{dist}(x, y)\) 最小。
若没有连通块的限制,则可以做两遍 DP 求出 \(x\) 与子树内和子树外的 \(y\) 的边权最小值。
然后考虑连通块的限制,实际上只要再记录一个所属连通块不同的 \(y\) 的作为次小值即可。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
pair<ll, int> fir[N], sec[N], mnd[N];
int a[N], shortest[N];
int n, m;
void dfs1(int u, int f) {
fir[u] = make_pair(a[u] * 2, u), sec[u] = make_pair(inf, -1);
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v == f)
continue;
dfs1(v, u);
vector<pair<ll, int> > vec = {fir[u], sec[u]};
vec.emplace_back(fir[v].first + w - a[v] + a[u], fir[v].second);
vec.emplace_back(sec[v].first + w - a[v] + a[u], sec[v].second);
vec.emplace_back((ll)a[u] + a[v] + w, v);
sort(vec.begin(), vec.end()), fir[u] = vec[0];
for (int j = 1; j < vec.size(); ++j)
if (vec[j].second == -1 || dsu.find(vec[j].second) != dsu.find(fir[u].second)) {
sec[u] = vec[j];
break;
}
}
}
void dfs2(int u, int f) {
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v == f)
continue;
vector<pair<ll, int> > vec = {fir[v], sec[v]};
vec.emplace_back(fir[u].first + w - a[u] + a[v], fir[u].second);
vec.emplace_back(sec[u].first + w - a[u] + a[v], sec[u].second);
// 如果 fir[u], sec[u] 在 u 子树内则边权必然更大不会被选
vec.emplace_back((ll)a[u] + a[v] + w, u);
sort(vec.begin(), vec.end()), fir[v] = vec[0];
for (int j = 1; j < vec.size(); ++j)
if (vec[j].second == -1 || dsu.find(vec[j].second) != dsu.find(fir[v].second)) {
sec[v] = vec[j];
break;
}
dfs2(v, u);
}
}
inline ll Boruvka() {
dsu.prework(n);
ll res = 0;
for (;;) {
dfs1(1, 0), dfs2(1, 0);
memset(shortest + 1, -1, sizeof(int) * n);
for (int i = 1; i <= n; ++i) {
mnd[i] = (fir[i].second == -1 || dsu.find(fir[i].second) != dsu.find(i) ? fir[i] : sec[i]);
if (~mnd[i].second && (shortest[dsu.find(i)] == -1 || mnd[i].first < mnd[shortest[dsu.find(i)]].first))
shortest[dsu.find(i)] = i;
}
bool flag = false;
for (int i = 1; i <= n; ++i)
if (shortest[i] != -1 && dsu.find(shortest[i]) != dsu.find(mnd[shortest[i]].second))
flag = true, res += mnd[shortest[i]].first, dsu.merge(shortest[i], mnd[shortest[i]].second);
if (!flag)
break;
}
return res;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
printf("%lld", Boruvka());
return 0;
}
CF1550F Jumping Around
数轴上有 \(n\) 个点,坐标为 \(a_{1 \sim n}\) 。给定起点 \(S\) 和步长 \(d\) ,\(q\) 次询问,每次给出 \(k, T\) ,表示 \(x\) 能跳到 \(y\) 当且仅当 \(|y - x| \in [d - k, d + k]\) ,求 \(S\) 是否能跳到 \(T\) 。
\(n \le 2 \times 10^5\) ,\(q \le 10^6\)
首先可以发现可达性关于 \(k\) 是单调的,即 \(k < k_0\) 时不可达,\(k \ge k_0\) 时可达。
考虑建出完全图,附上合适的边权满足 \(k\) 的单调性。定义两个点 \(x, y\) 的边权为 \(||a_x - a_y| - d|\) ,则只要判断 MST 上两点路径的最大边权是否 \(\le k\) 即可。
完全图的 MST 考虑 Boruvka,每次合并维护每个数向左、向右第一个不在连通块的位置,然后双指针扫一遍即可。
时间复杂度 \(O(n \log n + q)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
pair<int, int> cur[N];
int a[N], L[N], R[N], pos[N], len[N], val[N], mxd[N];
int n, q, s, d;
inline void Boruvka() {
dsu.prework(n);
int cnt = 0;
while (cnt < n - 1) {
for (int i = 1; i <= n; ++i)
L[i] = (dsu.find(i - 1) == dsu.find(i) ? L[i - 1] : i - 1);
for (int i = n; i; --i)
R[i] = (dsu.find(i + 1) == dsu.find(i) ? R[i + 1] : i + 1);
memset(pos + 1, -1, sizeof(int) * n), memset(len + 1, inf, sizeof(int) * n);
auto update = [](int x, int y) {
if (x == y || !y || y == n + 1)
return;
if (pos[x] == -1 || abs(abs(a[x] - a[y]) - d) < len[x])
pos[x] = y, len[x] = abs(abs(a[x] - a[y]) - d);
};
for (int i = 1, j = 1; i <= n; ++i) {
while (j < n && a[j] <= a[i] + d)
++j;
if (dsu.find(j) != dsu.find(i))
update(i, j);
else
update(i, L[j]), update(i, R[j]);
if (dsu.find(j - 1) != dsu.find(i))
update(i, j - 1);
else
update(i, L[j - 1]), update(i, R[j - 1]);
}
for (int i = n, j = n; i; --i) {
while (j > 1 && a[j] >= a[i] - d)
--j;
if (dsu.find(j) != dsu.find(i))
update(i, j);
else
update(i, L[j]), update(i, R[j]);
if (dsu.find(j + 1) != dsu.find(i))
update(i, j + 1);
else
update(i, L[j + 1]), update(i, R[j + 1]);
}
memset(val + 1, inf, sizeof(int) * n);
for (int i = 1; i <= n; ++i)
if (len[i] < val[dsu.find(i)])
val[dsu.find(i)] = len[i], cur[dsu.find(i)] = make_pair(pos[i], i);
for (int i = 1; i <= n; ++i)
if (val[i] != inf && dsu.find(cur[i].first) != dsu.find(cur[i].second)) {
G.insert(cur[i].first, cur[i].second, val[i]), G.insert(cur[i].second, cur[i].first, val[i]);
dsu.merge(cur[i].first, cur[i].second), ++cnt;
}
}
}
void dfs(int u, int f) {
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v != f)
mxd[v] = max(mxd[u], w), dfs(v, u);
}
}
signed main() {
scanf("%d%d%d%d", &n, &q, &s, &d);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
Boruvka(), dfs(s, 0);
while (q--) {
int t, k;
scanf("%d%d", &t, &k);
puts(mxd[t] <= k ? "Yes" : "No");
}
return 0;
}
QOJ6520. Classic Problem
有一张 \(n\) 个点的完全图,\((i, j)\) 之间的边权为 \(|i - j|\) 。现在修改 \(m\) 条边的边权,求 MST 边权和。
\(n \le 10^9\) ,\(m \le 10^5\)
不难发现 \(m\) 条边出现的点将 \(1 \sim n\) 分为至多 \(2m + 1\) 段,而一定存在最优解使得每一段内部都连成一条递增链,因此可以将图缩成至多 \(2m + 1\) 个连续点和至多 \(2m\) 个关键点。
考虑 Boruvka 算法,则需要快速维护某连通块连出去的最小边权。
维护 \(L_i, R_i\) 表示 \(i\) 左右离它最近的且和它不在同一连通块的点,然后从左到右枚举每个点:
- 若当前点为连续点,则选择 \(L_i, R_i\) 中更近者即可。
- 若当前点为关键点,则需要考虑其所连的所有关键边,以及它左右最近的非关键边所连的点,后者可以暴力跳。
分析一下一轮的复杂度。首先 \(m\) 条边都会被遍历到,下面考虑暴力跳的复杂度,会遇到三种情况:
- 碰到有关键边相连的点:直接继续往前考虑一个点。
- 碰到同一连通块内的点:直接跳 \(L, R\) 。
- 找到了:直接退出。
由于第一种情况只会出现最多 \(m\) 次,而情况二只会被情况一遍历到,因此该部分复杂度也是 \(O(m)\) 。
时间复杂度 \(O(m \log m)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 4e5 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
struct Edge {
int u, v, w;
} e[N];
struct Node {
unordered_map<int, int> e;
int l, r;
bool op;
} nd[N];
pair<int, int> outd[N];
int L[N], R[N];
int n, m;
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
map<int, vector<pair<int, int> > > e;
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
e[u].emplace_back(v, w), e[v].emplace_back(u, w);
}
map<int, int> id;
int tot = 0, lst = 0;
for (auto it : e) {
int x = it.first;
if (lst + 1 != x)
nd[++tot] = (Node){(unordered_map<int, int>){}, lst + 1, x - 1, false};
nd[id[x] = ++tot] = (Node){(unordered_map<int, int>){}, x, x, true}, lst = x;
}
if (lst != n)
nd[++tot] = (Node){(unordered_map<int, int>){}, lst + 1, n, false};
ll ans = 0;
for (int i = 1; i <= tot; ++i) {
if (nd[i].op) {
for (auto &it : e[nd[i].l])
nd[i].e[id[it.first]] = it.second;
} else
ans += nd[i].r - nd[i].l;
}
dsu.prework(tot);
for (;;) {
L[0] = 0;
for (int i = 1; i <= tot; ++i)
L[i] = (dsu.find(i - 1) == dsu.find(i) ? L[i - 1] : i - 1);
R[tot] = tot + 1;
for (int i = tot; i; --i)
R[i] = (dsu.find(i + 1) == dsu.find(i) ? R[i + 1] : i + 1);
fill(outd + 1, outd + tot + 1, make_pair(inf, 0));
for (int i = 1; i <= tot; ++i) {
int x = dsu.find(i);
if (nd[i].op) {
for (auto it : nd[i].e)
if (dsu.find(it.first) != x)
outd[x] = min(outd[x], make_pair(it.second, it.first));
for (int j = L[i]; j;) {
if (dsu.find(j) == x)
j = L[j];
else if (nd[i].e.find(j) != nd[i].e.end())
--j;
else {
outd[x] = min(outd[x], make_pair(nd[i].l - nd[j].r, j));
break;
}
}
for (int j = R[i]; j <= tot;) {
if (dsu.find(j) == x)
j = R[j];
else if (nd[i].e.find(j) != nd[i].e.end())
++j;
else {
outd[x] = min(outd[x], make_pair(nd[j].l - nd[i].r, j));
break;
}
}
} else {
if (L[i] >= 1)
outd[x] = min(outd[x], make_pair(nd[i].l - nd[L[i]].r, L[i]));
if (R[i] <= tot)
outd[x] = min(outd[x], make_pair(nd[R[i]].l - nd[i].r, R[i]));
}
}
bool flag = false;
for (int i = 1; i <= tot; ++i)
if (outd[i] != make_pair(inf, 0) && dsu.find(i) != dsu.find(outd[i].second))
flag = true, dsu.merge(i, outd[i].second), ans += outd[i].first;
if (!flag)
break;
}
printf("%lld\n", ans);
}
return 0;
}
P8519 [IOI 2021] 钥匙
有 \(n\) 个房间和 \(m\) 条双向边,第 \(i\) 个房间里有种类为 \(r_i\) 的钥匙(只要到达该房间就可以收集),第 \(i\) 条通道需要种类为 \(c_i\) 的钥匙才能通过(通过好钥匙不会消失)。
定义 \(p_i\) 表示从 \(i\) 出发能到达的房间数,设 \(x\) 为最小的 \(p_i\) ,求所有 \(p_i = x\) 的 \(i\) 。
\(n, m \le 3 \times 10^5\)
首先对于一个点不难 \(O(n + m)\) bfs 求出答案,但是直接对所有点求 \(p_i\) 复杂度为平方级别,无法接受。
考虑一个性质,若 \(u\) 出发能到达 \(v\) ,则 \(p_u \ge p_v\) ,因此答案即为 SCC 最小的缩点后无出度的点集。
考虑 Boruvka 算法的思想,一开始将每个点染不同的颜色,用并查集维护若干连通块,其中每个连通块只保留一个所有点能到达它的点,其一定是 \(p\) 最小的点。
每次从一个保留的点开始 bfs,若遍历到不同的连通块,则将其合并到另一个连通块上;否则说明这个点能到达的位置已经确定,这些点的 \(p\) 都与保留点相等,是该连通块内最小的,尝试更新答案。
每一轮连通块的数量都会折半,时间复杂度 \(O((n + m) \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
struct Graph {
vector<pair<int, int> > e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
vector<pair<int, int> > linkedge;
vector<int> buc[N], res;
int a[N];
bool vis[N], got[N];
int n, ans;
inline void bfs(int S) {
vector<int> arrive, need, key;
queue<int> q;
q.emplace(S);
auto clear = [&]() {
for (int it : need)
buc[it].clear();
for (int it : key)
got[it] = false;
};
while (!q.empty()) {
int u = q.front();
q.pop();
if (dsu.find(u) != S) {
linkedge.emplace_back(u, S), clear();
return;
}
if (vis[u])
continue;
vis[u] = true, arrive.emplace_back(u);
if (!got[a[u]]) {
got[a[u]] = true, key.emplace_back(a[u]);
for (int v : buc[a[u]])
q.emplace(v);
}
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (got[w])
q.emplace(v);
else
buc[w].emplace_back(v), need.emplace_back(w);
}
}
if (arrive.size() < ans)
res = arrive, ans = arrive.size();
else if (arrive.size() == ans)
res.insert(res.end(), arrive.begin(), arrive.end());
clear();
}
vector<int> find_reachable(vector<int> r, vector<int> u, vector<int> v, vector<int> c) {
n = r.size(), dsu.prework(n), G.clear(n);
for (int i = 1; i <= n; ++i)
a[i] = r[i - 1] + 1;
for (int i = 0; i < u.size(); ++i)
G.insert(u[i] + 1, v[i] + 1, c[i] + 1), G.insert(v[i] + 1, u[i] + 1, c[i] + 1);
for (;;) {
memset(vis + 1, false, sizeof(bool) * n);
res.clear(), ans = n + 1;
for (int i = 1; i <= n; ++i)
if (dsu.find(i) == i && !vis[i])
bfs(i);
if (linkedge.empty())
break;
for (auto it : linkedge)
dsu.merge(it.first, it.second);
linkedge.clear();
}
vector<int> s(n);
for (auto it : res)
s[it - 1] = 1;
return s;
}
P11394 [JOI Open 2019] 病毒实验 / Virus Experiment
给定一个 \(n \times m\) 的棋盘和长度为 \(k\) 的字符串 \(S\) ,其中每个格子初始为白色,\(S\) 的字符集为上下左右四个方向。
在第 \(t\) 个时刻选取方向 \(S^{\infty}_t\) ,然后取出每个格子在该方向的邻居,如果某个格子 \((i, j)\) 在连续 \(a_{i, j}\) 个时刻都取到了一个黑色的邻居,那么这个格子也染黑。
选择一个格子染黑,最小化充分长时间后的黑色格子数量,并且求出有多少个取到最小值的起点。
\(n, m \le 800\) ,\(k \le 10^5\)
先考虑如何判定一个格子是否被染黑,取出其所有黑色邻居所在的方向集合 \(s\) ,则要求 \(S^{\infty}\) 中存在长度 \(\ge a_{i, j}\) 的连续段满足字符集为 \(s\) 的子集。
由于可能的 \(s\) 只有 \(16\) 种,因此可以考虑对每个 \(s\) 求出 \(S^{\infty}\) 中满足条件的最长子段长度,即可 bfs 判定。
考虑建图,若从 \(u\) 能染黑 \(v\) ,则连边 \(u \to v\) ,每个点的答案即为后继数量。显然答案即为缩点后无出度的 SCC,考虑取出一个内向生成树,答案即为每个根所在的 SCC。
考虑 Boruvka,然后就和 P8519 [IOI 2021] 钥匙 一样了,时间复杂度 \(O(k + nm \log nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int dx[] = {-1, 1, 0, 0};
const int dy[] = {0, 0, -1, 1};
const int inf = 0x3f3f3f3f;
const int N = 8e2 + 7, M = 2e5 + 7;
struct DSU {
int fa[N * N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
vector<pair<int, int> > linkedge;
int a[N][N], mxlen[1 << 4];
char str[M];
bool vis[N][N];
pair<int, int> ans = make_pair(inf, 0);
int n, m, len;
inline int getid(int x, int y) {
return (x - 1) * m + y;
}
inline int trans(char c) {
return c == 'N' ? 0 : (c == 'S' ? 1 : (c == 'W' ? 2 : 3));
}
inline void bfs(int Sx, int Sy) {
queue<pair<int, int> > q;
q.emplace(Sx, Sy), vis[Sx][Sy] = true;
int cnt = 0;
auto check = [&](int x, int y) {
return vis[x][y] && dsu.find(getid(x, y)) == getid(Sx, Sy);
};
while (!q.empty()) {
int x = q.front().first, y = q.front().second;
q.pop(), ++cnt;
for (int i = 0; i < 4; ++i) {
int nx = x + dx[i], ny = y + dy[i];
if (!a[nx][ny] || check(nx, ny))
continue;
int s = 0;
for (int j = 0; j < 4; ++j)
s |= check(nx + dx[j], ny + dy[j]) << j;
if (a[nx][ny] <= mxlen[s]) {
if (dsu.find(getid(nx, ny)) == getid(Sx, Sy))
q.emplace(nx, ny), vis[nx][ny] = true;
else {
linkedge.emplace_back(getid(nx, ny), getid(Sx, Sy));
return;
}
}
}
}
if (cnt < ans.first)
ans = make_pair(cnt, cnt);
else if (cnt == ans.first)
ans.second += cnt;
}
signed main() {
scanf("%d%d%d%s", &len, &n, &m, str);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
scanf("%d", a[i] + j);
for (int s = 1; s < (1 << 4); ++s) {
mxlen[s] = -1;
for (int i = 0, lst = -1; i < len * 2; ++i)
if (~s >> trans(str[i % len]) & 1) {
if (~lst)
mxlen[s] = max(mxlen[s], i - lst - 1);
lst = i;
}
if (mxlen[s] == -1)
mxlen[s] = inf;
}
dsu.prework(n * m);
for (;;) {
for (int i = 1; i <= n; ++i)
memset(vis[i] + 1, false, sizeof(bool) * m);
ans = make_pair(inf, 0), linkedge.clear();
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (a[i][j] && dsu.find(getid(i, j)) == getid(i, j) && !vis[i][j])
bfs(i, j);
if (linkedge.empty())
break;
for (auto it : linkedge)
dsu.merge(it.first, it.second);
}
printf("%d\n%d", ans.first, ans.second);
return 0;
}
P4768 [NOI2018] 归程
给出一张无向图,每条边有一个长度和海拔。\(q\) 次询问,每次给出 \(v, p\) ,称 \(u\) 是可达的当且仅当存在一条 \(v \to u\) 的路径满足所有边海拔 \(> p\) ,求所有可达的 \(u\) 中 \(1 \to u\) 最短路的最小值。
\(n \le 2 \times 10^5\) ,\(m, q \le 4 \times 10^5\) ,强制在线
路径上所有边海拔 \(> p\) 等价于路径上最小海拔 \(> p\) ,由此不难想到建立 Kruskal 重构树,则可达的 \(u\) 在一棵子树内。
预处理 \(1 \to u\) 的最短路 \(dis_u\) 与子树内 \(dis\) 的最小值,倍增找到对应的可达子树即可做到 \(O(m \log m + q \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 4e5 + 7, LOGN = 19;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + 1 + n, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
inline bool check(int x, int y) {
return find(x) == find(y);
}
} dsu;
struct OriginalGraph {
struct Edge {
int v, w, h;
};
vector<Edge> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v, int w, int h) {
e[u].emplace_back((Edge) {v, w, h});
}
} G;
struct Edge {
int u, v, h;
inline bool operator < (const Edge &rhs) const {
return h > rhs.h;
}
} e[N];
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} T;
int fa[N][LOGN];
ll dis[N], mndis[N];
int val[N];
int n, m, q;
inline void Dijkstra(int S) {
memset(dis + 1, inf, sizeof(ll) * n);
priority_queue<pair<ll, int> > q;
dis[S] = 0, q.emplace(-dis[S], S);
while (!q.empty()) {
auto c = q.top();
q.pop();
if (-c.first != dis[c.second])
continue;
int u = c.second;
for (auto it : G.e[u]) {
int v = it.v, w = it.w;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, q.emplace(-dis[v], v);
}
}
}
void dfs(int u, int f) {
fa[u][0] = f, mndis[u] = (u <= n ? dis[u] : inf);
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : T.e[u])
dfs(v, u), mndis[u] = min(mndis[u], mndis[v]);
}
inline void Kruskal() {
sort(e + 1, e + 1 + m);
T.clear(n * 2), dsu.prework(n * 2);
int ext = n;
for (int i = 1; i <= m; ++i) {
int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v);
if (fx == fy)
continue;
val[++ext] = e[i].h;
T.insert(ext, fx), T.insert(ext, fy);
dsu.merge(ext, fx), dsu.merge(ext, fy);
}
dfs(ext, 0);
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
G.clear(n);
for (int i = 1; i <= m; ++i) {
int u, v, w, h;
scanf("%d%d%d%d", &u, &v, &w, &h);
G.insert(u, v, w, h), G.insert(v, u, w, h);
e[i] = (Edge) {u, v, h};
}
Dijkstra(1), Kruskal();
int k, s;
scanf("%d%d%d", &q, &k, &s);
ll lstans = 0;
while (q--) {
int x, p;
scanf("%d%d", &x, &p);
x = (x + lstans * k - 1) % n + 1, p = (p + lstans * k) % (s + 1);
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] && val[fa[x][i]] > p)
x = fa[x][i];
printf("%lld\n", lstans = mndis[x]);
}
}
return 0;
}