kruskal重构树(以 2021 ICPC上海 H.Life is a Game 为例)
所需知识:
kruskal重构树 <- kruskal(克鲁斯卡尔)算法 <- 最小生成树
最小生成树:
最小生成树是一个图论的概念,它指的是一个连通图的极小连通子图,包含原图中的所有顶点,并且有保持图连通的最少的边。最小生成树的权值之和是最小的,也就是说它用最少的代价连接了所有的顶点。
为什么不叫最小生成图呢?因为树本身就是一种特殊的图,它没有环路,也就是说任意两个顶点之间只有一条路径。所以最小生成树其实就是一种特殊的最小生成图,不需要再另外命名。
详细请点这里
kruskal算法
克鲁斯卡尔算法是一种求解加权连通图的最小生成树的算法。它的基本思想是按照边的权值从小到大的顺序选择n-1条边,保证这些边不构成回路。为了判断是否构成回路,可以为每个顶点配置一个标记,如果两个顶点的标记不同,就可以加入这条边。
详细请点这里
kruskal重构树:
建立方法
- 对原图中的所有边按照权值从小到大(或从大到小)排序。
- 依次取出边,如果这条边连接了两个不同的连通分量(查并集不在一个并集),就新建一个节点,把这两个连通分量作为它的儿子节点,并把这条边的权值赋给这个节点。同时用并查集维护连通性。
- 重复上一步,直到所有的原图中的节点都成为了重构树中的叶子节点。
有了kruskal重构树之后,我们就可以方便地求解一些路径上最大(小)边权的问题。例如,如果要求解任意两点之间路径上最大边权的最小值 ,我们只需要在重构树上找到两点之间唯一路径上点权最大(或最小)的那个点即可。具体实现可以用LCA(最近公共祖先)算法
他的性质有:
-
一棵二叉树
-
叶子结点都是原图中的点,没有点权;非叶子结点都是新加的点,有点权
-
旧点 u,v两点间(不包括 u,v)的所有节点(都是新点)的点权最大值为原图中 u,v 两点间所有路径的最长边的最小值
-
新点构成一个堆(不一定是二叉堆),在最小Kruskal重构树中是大根堆(最大Kruskal重构树中是小根堆)
-
结合性质3和4,旧点 u,v两点的LCA(是新点)权值为原图中 u,v两点间所有路径的最长边的最小值
对于该题:
题目
人生就是一场游戏。(life is a game)
世界可以看作是一个无向的连接图n
城市和m
城市之间的无方向道路。现在,您,生活游戏玩家,将在世界图上玩生活游戏。
最初,您在第x
号市和有k
社交能力点。你可以通过生活和工作获得社交能力点。具体来说,你可以在第 i
个城市获得 aᵢ
个社交能力点 。但是在这个问题上,你不能在一个城市重复获得社交能力积分,所以你想环游世界,赚取更多的社交能力积分。然而,道路并不容易。具体来说,有在第i
条路上有一个能力阈值wᵢ
,所以当你通过第 i
条路时,你至少应该有wᵢ
点社交能力。 此外,你的社交能力点在过马路时不会降低,而只需要至少wᵢ
点社交能力。
所以正如你所看到的,生活游戏只是反复生活、工作和旅行。有q
游戏存档。对于每个游戏存档,都会给出初始城市和社交能力点,并且玩家没有在任何城市生活或工作过。现在,您,现实生活中的游戏玩家,需要确定游戏结束时可以拥有的最大可能社交能力点数,并在每次给定的游戏保存时输出它。
输入
第一行包含三个整数 n
,m
,q
(1≤n,m,q≤105)
,分别表示城市、道路和游戏保存的数量。
第二行包含n
个整数一个a1 , a2 ,⋯,an
(1≤an≤ 104 )
,表示城市的加成社交能力点数。
之后m行每行包含三个整数u,v,w
(1≤u<v≤n,1≤w≤109)
,表示城市u
,v
由能力阈值w
链接。
之后q行每行包含两个整数x,k
(1≤x≤n,1≤k≤109)
,表示 每个游戏存档。
输出
对于每个游戏存档,输出一行包含一个整数,表示您可以拥有的最大社交能力点数。
题意
给定一个 n 个节点和 m 条边的图,有 q 次询问
每次询问为以下含义:
你最开始在 x 节点,有 k 点能力值,每次第一次到一个节点 i ,就能获得a[i] 点能力值(最开始当然也会获得 x 节点的能力值)
而通过每一条边需要有最少有 wi 的能力值,通过边不会减少能力值,
可以多次经过同一个点和边,求 你最后的能力值最多为多少。
用Kruskal重构树怎么写呢?
先来看看这张图的最小生成树
建立一颗kruskal重构树
然后建立完成后让每颗树的叶子节点为原图中最小生成树的节点
最后回溯的时候让重构树上的非叶子节点点权为非叶子节点后代的和
为什么要让重构树上的非叶子节点点权为非叶子节点后代的和?
因为如果能到该节点说明能力值足够到后代所有的节点(就是当你到了某一个节点,说明你的能力值足够去他后代的所有城市),这样也可以很方便的更新能量值。
这样一来,查询的时候只需要不断向上查询,走到能力值能去到最远的点后,更新能力值,直道不能再更新后,输出答案。
彩蛋…(原本clang的还挖了…问了问AI还是WA了,换了个GNU就好了)
AC代码 原题点这里
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e5 + 5;
int cnt, n, m, q, p[N], f[N][20]; // p为查并集所用数组, f[][]为倍增法lca的fa数组,
ll nw[N], st[N]; // nw[]记录实体图的点权,st[]记录实体图的边权
vector<int> h[N]; // 邻接链表用于储存重构树
struct Edge
{
ll u, v, w;
} e[N];
bool cmp(Edge a, Edge b) { return a.w < b.w; }
int find(int x)
{
if (p[x] != x)
p[x] = find(p[x]);
return p[x];
}
void kruskal(int n, int m) //求kruskal重构树
{
cnt = n;
sort(e + 1, e + m + 1, cmp);
for (int i = 1; i <= m; i++) //枚举边
{
int u = find(e[i].u), v = find(e[i].v), w = e[i].w;
if (u != v) //如果u-v不连通,则连一条边
{
st[++cnt] = w; //新建一个点,点权为u-v边权w
p[cnt] = p[u] = p[v] = cnt; //将根节点置为新点(保证所以实点都为叶节点)
h[u].emplace_back(cnt);
h[v].emplace_back(cnt);
h[cnt].emplace_back(u);
h[cnt].emplace_back(v);
}
}
}
void dfs(int u, int fa) //为lca初始化
{
f[u][0] = fa;
for (int i = 1; i < 19; i++)
f[u][i] = f[f[u][i - 1]][i - 1];
for (auto v : h[u])
{
if (v == fa)continue;
if (v > n)dfs(v, u);
else
{
dfs(v, u);
nw[u] += nw[v]; // 回溯时更新nw[u],nw[u]表示以u为根的子树内所以实点的点权和
}
}
}
signed main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m >> q;
for (int i = 1; i <= n; i++)
p[i] = i;
for (int i = 1; i <= n; i++)
cin >> nw[i];
for (int i = 1; i <= m; i++)
cin >> e[i].u >> e[i].v >> e[i].w;
kruskal(n, m); //建立重构树
n = cnt; //结构体e包括值为边权的新建的边和原本的边
dfs(n, 0); //预处理需要的数组
st[0] = 1e18; //防止u节点跳过根节点
while (q--)
{
int u, s;
cin >> u >> s;
ll ans = nw[u] + s; //一开始的社交能力点数
while (u != n)
{
int t = u;
for (int i = 19; i >= 0; i--) //只要能往上跳就往上跳(需要能量数>这部分的最大边权)
if (st[f[u][i]] <= ans)
u = f[u][i];
if (t == u) break; //如果这一轮没有往上跳,说明不能再往上跳了(退出)
ans = nw[u] + s; //更新答案
}
cout << ans << "\n";
}
return 0;
}
测试集1
input
8 10 2
3 1 4 1 5 9 2 6
1 2 7
1 3 11
2 3 13
3 4 1
3 6 31415926
4 5 27182818
5 6 1
5 7 23333
5 8 55555
7 8 37
1 7
8 30
output
16
36