kruskal 及其应用
kruskal
最小生成树
kruskal 是一种常见且好理解的最小生成树(MST)算法。
前置知识
看到路径压缩就可以了。
生成树
在有 \(n\) 的顶点的无向图中,取其中 \(n-1\) 条边相连,所得到的树即为生成树。
最小生成树就是生成树边权和最小。
kruskal
kruskal 基于贪心。
如果让你的选择之和最小,该怎么选?
显然啊,每次选择的边权都是没选过的最小的,直到选了 \(n-1\) 条边。
但这样选有时会出问题。

如上图,选最小的边应该是:

但显然,这不是一个树。
所以在连边之前,还要判断一下两个点是否在同一个连通块内。
判连通用什么?
Link-Cut Tree 并查集!
那么整个 kruskal 的过程就是:
排序 -> 判连通 -> 加边 -> 判连通 -> 加边 -> 判连通 -> 加边……。
int n, m;
cin >> n >> m;
vector<int> fa(n + 7);
struct kru
{
int s, t, k;
kru() {}
kru(int u, int v, int w) : s(u), t(v), k(w) {}
bool operator<(const kru &b) const
{
return k < b.k;
}
};
vector<kru> k;
for (int i = 1; i <= n; i++)
fa[i] = i;
for (int i = 1; i <= m; i++)
{
int u, v, w;
cin >> u >> v >> w;
k.push_back(kru(u, v, w));
}
sort(k.begin(), k.end()); // 排序
auto find = [&](auto &self, int x) -> int
{
if (fa[x] == x)
return x;
return fa[x] = self(self, fa[x]);
};
int cnt = 0, mst = 0;
for (auto [u, v, w] : k)
{
int ru = find(find, u), rv = find(find, v);
if (ru == rv)
continue; // 判连通
fa[ru] = rv;
cnt++, mst += w; // 加边
if (cnt == n - 1)
break;
}
if (cnt == n - 1)
cout << mst << '\n';
else
cout << "orz";
练习
次小生成树
前置知识
为方便叙述,最小生成树中的 \(n-1\) 边叫做树边,剩余的 \(m-n+1\) 条边叫非树边。
非严格次小生成树
显然,对于已经生成的最小生成树来说,每一条非树边的加入,都会形成一个环。那么再将环上的树边中最大的边删除,就能得到次小生成树的一颗候选树。
令最小生成树大小为 \(minn\),新加入的非树边权值为 \(new\),环上的最大树边为 \(max\),那么候选树的大小就是 \(minn-max+new\),我们所求则是 \(min\{minn-max+new\}\)。
严格次小生成树
当 \(new=max\) 时,若按上述方法进行维护,得到的 \((minn-max+new)=minn\)。
此时,就应该选择环上树边的次大值 \(nexm\),\((minn-nexm+new)>minn\)。
解法
现在的问题就在于,如何快速求出两点间树边的最大值和次大值。
若直接用两个二维数组将两点间的最大值,次大值存下来是不现实的,\(O(n^2)\) 的空间复杂度不允许。
考虑倍增,储存下来每个节点到其 \(2^j\) 级祖先的最大值(严格时还需要统计次大值),然后在跳 lca 的过程中维护路径上可以加入候选树的最大值。
时间复杂度 \(O(m\log n)\)。
const int N = 3e5 + 7;
int n, m, mst, ans = 1e18;
int fa[N];
struct kru
{
int s, t, v;
kru() {}
kru(int s, int t, int v) : s(s), t(t), v(v) {}
bool operator<(const kru &b) const
{
return v < b.v;
}
} h[N];
int fir[N], nex[N << 1], poi[N << 1], val[N << 1], sum;
void ins(int x, int y, int z)
{
nex[++sum] = fir[x];
poi[sum] = y;
val[sum] = z;
fir[x] = sum;
}
bool vis[N];
int cnt;
int find(int s)
{
if (s == fa[s])
return s;
return fa[s] = find(fa[s]);
}
int dep[N], fat[N][20];
int maxn[N][20], nexm[N][20];
void dfs(int now, int from)
{
fat[now][0] = from;
dep[now] = dep[from] + 1;
for (int i = fir[now]; i; i = nex[i])
{
int p = poi[i];
if (p == from)
continue;
maxn[p][0] = val[i];
dfs(p, now);
}
}
int pd(int x, int i, int z)
{ // 判断是否与新加入的非树边相等
return (maxn[x][i] != z) ? maxn[x][i] : nexm[x][i];
}
int ask(int x, int y, int z)
{ // 倍增 lca 维护最大、次大值
int max_ = -2147483647;
if (dep[x] < dep[y])
swap(x, y);
for (int i = 19; i >= 0; i--)
{
if (dep[fat[x][i]] >= dep[y])
{
max_ = max(max_, pd(x, i, z));
x = fat[x][i];
}
}
if (x == y)
return max_;
for (int i = 19; i >= 0; i--)
{
if (fat[x][i] != fat[y][i])
{
max_ = max(max_, max(pd(x, i, z), pd(y, i, z)));
x = fat[x][i], y = fat[y][i];
}
}
return max(max_, max(pd(x, 0, z), pd(y, 0, z)));
}
signed main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
fa[i] = i;
for (int i = 1; i <= m; i++)
cin >> h[i].s >> h[i].t >> h[i].v;
sort(h + 1, h + m + 1);
for (int i = 1; i <= m; i++)
{ // 最小生成树
int r1 = find(h[i].s), r2 = find(h[i].t);
if (r1 == r2)
continue;
cnt++;
fa[r1] = r2, mst += h[i].v;
vis[i] = 1;
ins(h[i].s, h[i].t, h[i].v);
ins(h[i].t, h[i].s, h[i].v);
if (cnt == n - 1)
break;
}
for (int i = 1; i <= n; i++) // 赋初值
for (int j = 0; j < 20; j++)
maxn[i][j] = nexm[i][j] = -2147483647;
dfs(1, 1);
for (int j = 1; j < 20; j++)
{ // 倍增的预处理
for (int i = 1; i <= n; i++)
{
int f = fat[i][j - 1];
fat[i][j] = fat[f][j - 1];
maxn[i][j] = max(maxn[i][j - 1], maxn[f][j - 1]);
if (maxn[i][j - 1] == maxn[f][j - 1]) // 分类讨论次大值
nexm[i][j] = max(nexm[i][j - 1], nexm[f][j - 1]);
else if (maxn[i][j - 1] < maxn[f][j - 1])
nexm[i][j] = max(maxn[i][j - 1], nexm[f][j - 1]);
else
nexm[i][j] = max(nexm[i][j - 1], maxn[f][j - 1]);
}
}
for (int i = 1; i <= m; i++)
{
if (vis[i])
continue;
int max_ = ask(h[i].s, h[i].t, h[i].v);
if (max_ != h[i].v)
ans = min(ans, mst + h[i].v - max_);
}
cout << ans << '\n';
return 0;
}
kruskal 重构树
用途
巧妙地求解询问连接两点的所有路径中最大边的最小值或者最小边的最大值问题。
思路
kruskal 求 MST 的时候是逐步加边,而 kruskal 重构树则是将要加的边转化成点,并连接原来的两个点,边权为点权。
就像这样:

实例(图片来自 OI-Wiki)
原无向图:

重构树:

性质
kruskal重构树是一棵二叉堆- 最小生成树的重构树是大根堆,最大生成树的重构树是小根堆
- 图上两点最小路径的最大值或最大路径的最小值为重构树上两点的
lca的点权。 - 重构树上共 \(2n-1\) 的点,其中,\(n\) 个叶节点为原来图中的节点。
例题讲解
这个题可以用可持久化并查集切掉。
原谅我不会
海拔比水位高的路车可以通过,剩下的路只能步行涉水。
显然,需要优先走海拔比较高的路径,这样才能尽可能多的行车,也就是先预处理最大生成树。
那么就先预处理出整张图的 kruskal 重构树,然后找到树上的一个节点 \(v\),满足 val[v] > p && val[fa[v]] <= p,这样,以 \(v\) 为根的子树中的叶节点就是能通过车连通的。
提前预处理出图上每个节点到 \(1\) 号节点的最短路,然后在重构树中维护每个节点为根时子树中距离 \(1\) 号节点的最小值。
至于怎么找到重构树上的节点 \(v\),那就要用到倍增了。
坑点
- 最短路用 \(dijkstra\),因为 \(SPFA\) 死了。
- 多测记得清空,尤其是
last_ans容易忘。
Code
int n, m;
cin >> n >> m;
struct kru
{
int u, v, h;
kru() {}
kru(int u, int v, int h) : u(u), v(v), h(h) {}
bool operator<(const kru &b) const
{
return h > b.h;
}
};
struct node
{
int pos, val;
node() {}
node(int pos, int val) : pos(pos), val(val) {}
bool operator<(const node &b) const
{
return val > b.val;
}
};
vector<kru> k;
vector<vector<node>> e(n + 1);
for (int i = 0; i < m; i++)
{
int u, v, l, a;
cin >> u >> v >> l >> a;
k.push_back(kru(u, v, a));
e[u].push_back(node(v, l));
e[v].push_back(node(u, l));
}
vector<int> dis(n + 7, 1e9);
auto dij = [&](int sta) -> void
{
priority_queue<node> h;
vector<int> vis(n + 7);
dis[sta] = 0, h.push(node(sta, 0));
while (h.size())
{
auto [now, val] = h.top();
h.pop();
if (vis[now])
continue;
vis[now] = 1;
for (auto [p, val] : e[now])
{
if (dis[p] > dis[now] + val)
{
dis[p] = dis[now] + val;
h.push(node(p, dis[p]));
}
}
}
};
dij(1);
sort(k.begin(), k.end());
vector<int> rot(n << 1);
for (int i = 1; i < (n << 1); i++)
rot[i] = i;
auto find = [&](auto &find, int x) -> int
{
if (rot[x] == x)
return x;
return rot[x] = find(find, rot[x]);
};
int cntn = n;
struct kru_Tree
{
int lc, rc;
int high, dis;
};
vector<kru_Tree> T(n << 1);
vector<vector<int>> fa(n << 1, vector<int>(20));
for (int i = 1; i <= n; i++)
T[i].dis = dis[i], T[i].high = 1e9;
for (auto [u, v, w] : k)
{
int ru = find(find, u), rv = find(find, v);
if (ru == rv)
continue;
rot[ru] = rot[rv] = ++cntn;
fa[ru][0] = cntn, fa[rv][0] = cntn;
T[cntn].lc = ru, T[cntn].rc = rv;
T[cntn].dis = min(T[ru].dis, T[rv].dis);
T[cntn].high = w;
if (cntn + 1 == n << 1)
break;
}
for (int j = 1; j < 20; j++)
for (int i = 1; i <= cntn; i++)
fa[i][j] = fa[fa[i][j - 1]][j - 1];
int Q, K, S;
cin >> Q >> K >> S;
int last_ans = 0;
while (Q--)
{
int v_0, p_0;
cin >> v_0 >> p_0;
int v = (v_0 + K * last_ans - 1) % n + 1;
int p = (p_0 + K * last_ans) % (S + 1);
for (int j = 19; j >= 0; j--)
if (T[fa[v][j]].high > p)
v = fa[v][j];
cout << (last_ans = T[v].dis) << '\n';
}

浙公网安备 33010602011771号