引入
简单来讲,就是在Kruskal算法进行的过程中,我们把最小生成树的边权改为点权;
可以看成是Kruskal算法的扩展使用;
假设原树有 n n n个结点,因为最小生成树是 n − 1 n-1 n−1条边;
那么重构树有 2 n − 1 2n-1 2n−1个结点;
举个例子,比如
x
x
x到
y
y
y连了一条边;
那么重构以后变成这样;
性质
- 是一个小/大根堆(由建树时边权的排序方式决定)
- 所有原来的点是叶子节点
- L C A ( u , v ) LCA(u,v) LCA(u,v)的点权是 原图 u u u到 v v v路径上最大边权的最小值或者最小边权的最大值(由建树时边权的排序方式决定)
重构流程
其实就是在
K
r
u
s
k
a
l
Kruskal
Kruskal基础上加点代码;
void EX_Kru(){
sort(input+1,input+1+m);
for(int i=1;i<=2*n-1;++i) p[i] = i;
for(int i=1;i<=m;++i){
int u = input[i].p1,v = input[i].p2,val = input[i].val;
int fu = _find(u),fv = _find(v);
//如果不形成环
if(fu != fv){
//化边权为点权
p[fu] = p[fv] = ++idx;
pval[idx] = val;
add(idx,fu),add(idx,fv);
}
}
}
例题
货车运输
题面
思路
因为想让运送的货物尽可能重;
不难想到,货物重量的下限是取决于称重最少的边;
比如当前从 u u u到 v v v,我们需要求 u u u到 v v v中最短路径中权值最小的,而我们希望这条路径的边权是尽可能大的,也就是求最大边权的最小值;
之所以是最短路径,是因为现在 u u u已经能到 v v v了,那么多加其他的边,只可能让最小边权变小,因此我们可以贪心的搞;
Code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 2e4 + 10,M = 5e4 + 10;
int p[N],n,m,q;
struct Edge{
int next,to;
}e[M << 1];
struct Node{
int p1,p2,val;
bool operator<(const Node &o){
return val > o.val;
}
}input[M];
int head[N],tot;
int _find(int x){
if(x == p[x]) return x;
return p[x] = _find(p[x]);
}
int pval[N],idx;//点权、点的编号
void add(int u,int v){
e[++tot].to = v;
e[tot].next = head[u];
head[u] = tot;
}
//Kru重构树
void EX_Kru(){
sort(input+1,input+1+m);
for(int i=1;i<=2*n-1;++i) p[i] = i;
for(int i=1;i<=m;++i){
int u = input[i].p1,v = input[i].p2,val = input[i].val;
int fu = _find(u),fv = _find(v);
//如果不形成环
if(fu != fv){
//化边权为点权
p[fu] = p[fv] = ++idx;
pval[idx] = val;
add(idx,fu),add(idx,fv);
}
}
}
int dep[N],fa[N][30];
void init_LCA(int root){
queue<int> que;
dep[0] = 0,dep[root] = 1;
que.push(root);
while(!que.empty()){
int u = que.front();
que.pop();
for(int i=head[u];i;i=e[i].next){
int to = e[i].to;
if(dep[to] > dep[u] + 1){
dep[to] = dep[u] + 1;
que.push(to);
fa[to][0] = u;
for(int k=1;k<=25;++k){
fa[to][k] = fa[fa[to][k-1]][k-1];
}
}
}
}
}
int LCA(int u,int v){
if(dep[u] < dep[v]) swap(u,v);
for(int k=25;k>=0;--k){
if(dep[fa[u][k]] >= dep[v]){
u = fa[u][k];
}
}
if(u == v) return u;
for(int k=25;k>=0;--k){
if(fa[u][k] != fa[v][k]){
u = fa[u][k];
v = fa[v][k];
}
}
return fa[u][0];
}
void solve(){
cin >> n >> m;
idx = n;
for(int i=1;i<=m;++i){
cin >> input[i].p1 >> input[i].p2 >> input[i].val;
}
EX_Kru();
//因为这道题可能是一个森林 因此我们需要初始化每颗树
memset(dep,0x3f,sizeof dep);
for(int i=n+1;i<=idx;++i){
//说明是根结点
if(p[i] == i){
init_LCA(i);
}
}
cin >> q;
while(q--){
int u,v;
cin >> u >> v;
//不连通肯定不在同一个集合中
if(_find(u) != _find(v)) cout << -1 << '\n';
else{
int anc = LCA(u,v);
cout << pval[anc] << '\n';
}
}
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}
归程
题面
思路
由题意我们知道,从 v v v到 1 1 1,我们分为一半坐车另一半走路;
假设在点 u u u下车,那么答案要求 v v v到 u u u可以开车, u u u到 1 1 1走路距离最短;
要使得 v v v到 u u u可以开车,那么就要求这些路径的水位线高于当天的水位线;
这些路径一定在原图的最大生成树上,这就启发我们可以使用Kruskal重构树
;
而从任意一点到点 1 1 1走路的最短距离,我们只需要以点 1 1 1为源点,跑一个迪杰斯特拉最短路即可;
现在我们将每条边以海拔为关键字降序排序;
比如说重构树上的某个子树的根root
的水位线是大于当天的水位线的;
那么这个子树内部的其他路径也必然是满足大于当天水位线的(小根堆性质);
也就是这棵子树的叶子结点是开车可达的;
那么我们枚举从这些点到点 1 1 1走路的最小值,即是答案;
由于是树形,我们只需要用树形DP
来处理出dist(u)
,这样就不需要枚举了;
dist(u)
表示以某点为根,走路到点
1
1
1的最小长度;
而如何快速的找到重构树上的某一点的深度是最浅的,且大于水位线,只需要使用树上倍增即可;
Code
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
#include <utility>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int N = 400000 + 10,M = 800000 + 10;
int p[N],idx;
struct Node{
int u,v,height;
bool operator<(const Node &o){
return height > o.height;
}
}ip[M];
struct Edge{
int next,to,val;
}e[M];
int head[N],dist[N],tot;
bool vis[N];
void add(int u,int v,int w){
e[++tot].to = v;
e[tot].val = w;
e[tot].next = head[u];
head[u] = tot;
}
void add(int u,int v){
e[++tot].to = v;
e[tot].next = head[u];
head[u] = tot;
}
int n,m,pval[N],fa[N][30];
int _find(int x){
if(x == p[x]) return x;
return p[x] = _find(p[x]);
}
void dij(int start){
priority_queue<pii,vector<pii>,greater<pii>> pq;
memset(dist,0x3f,sizeof dist);
memset(vis,0,sizeof vis);
dist[start] = 0;
pq.push({dist[start],start});
while(!pq.empty()){
int u = pq.top().second;
pq.pop();
if(vis[u]) continue;
vis[u] = 1;
for(int i=head[u];i;i=e[i].next){
int to = e[i].to;
if(dist[to] > dist[u] + e[i].val){
dist[to] = dist[u] + e[i].val;
pq.push({dist[to],to});
}
}
}
}
void EX_Kru(){
idx = n;
for(int i=1;i<=2*n-1;++i) p[i] = i;
sort(ip+1,ip+1+m);
int cnt = 0;
for(int i=1;i<=m;++i){
int u = ip[i].u, v = ip[i].v, h = ip[i].height;
int fu = _find(u), fv = _find(v);
if(fu != fv){
if(++cnt == n) break;
//化边为点
p[fu] = p[fv] = ++idx;
pval[idx] = h;
add(idx,fu),add(idx,fv);
}
}
}
void dfs(int u){
//先处理深度较浅的点
for(int k=1;k<=25;++k)
fa[u][k] = fa[fa[u][k-1]][k-1];
for(int i=head[u];i;i=e[i].next){
int to = e[i].to;
fa[to][0] = u;
dfs(to);
dist[u] = min(dist[u],dist[to]);
}
}
int query(int u,int p){
for(int k=25;k>=0;--k)
//水位线要高于p
if(fa[u][k] && pval[fa[u][k]] > p)
u = fa[u][k];
return dist[u];
}
void solve(){
memset(head,0,sizeof head);
memset(fa,0,sizeof fa);
tot = 0;
cin >> n >> m;
for(int i=1,u,v,w,h;i<=m;++i){
cin >> u >> v >> w >> h;
ip[i] = {u,v,h};
add(u,v,w),add(v,u,w);
}
dij(1);//处理出dist数组
tot = 0;
memset(head,0,sizeof head);
EX_Kru();
for(int i=n+1;i<=2*n-1;++i){
if(p[i] == i){ //处理每颗树
dfs(i);
}
}
int q,k,s;
cin >> q >> k >> s;
int lastans = 0;
while(q--){
int v0,p0;
cin >> v0 >> p0;
v0 = (v0+k*lastans-1)%n+1;
p0 = (p0+k*lastans)%(s+1);
lastans = query(v0,p0);
cout << lastans << '\n';
}
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int t;
cin >> t;
while(t--)
solve();
return 0;
}
Life is a Game
题面
题意
思路
不难想到,因为我们要获取尽可能多的声望;
因此从当前点到另一个点,肯定要走所需声望少的边;
那么实际上,我们如果要走完所有的点,那么走的必然是最小生成树;
因此我们可以考虑使用Kruskal重构树
;
Code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
#include <utility>
#include <set>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
int n,m,q,pval[N];
struct Edge{
int next,to;
}e[N];
int head[N],tot;
void add(int u,int v){
e[++tot].to = v;
e[tot].next = head[u];
head[u] = tot;
}
struct Node{
int u,v,w;
bool operator<(const Node &o){
return w < o.w; //大根堆
}
}ip[N];
int p[N],fa[N][30],g[N][30],tr_sum[N];
int _find(int x){
if(x == p[x]) return x;
return p[x] = _find(p[x]);
}
void EX_Kru(){
int idx = n;
int cnt = 0;
for(int i=1;i<=2*n-1;++i) p[i] = i;
sort(ip+1,ip+1+m);
for(int i=1;i<=m;++i){
int u = ip[i].u,v = ip[i].v,w = ip[i].w;
int fu = _find(u),fv = _find(v);
if(fu != fv){
//最小生成树,n-1条边
if(++cnt == n) break;
p[fu] = p[fv] = ++idx;
pval[idx] = w;
add(idx,fu),add(idx,fv);
}
}
}
//当前结点
void dfs(int u){
for(int k=1;k<=25;++k){
fa[u][k] = fa[fa[u][k-1]][k-1];
}
for(int i=head[u];i;i=e[i].next){
//这里是单向边
int to = e[i].to;
fa[to][0] = u;
dfs(to);
tr_sum[u] += tr_sum[to];
g[to][0] = pval[u] - tr_sum[to];
}
}
typedef pair<int,int> pii;
void solve(){
cin >> n >> m >> q;
for(int i=1;i<=n;++i) cin >> pval[i],tr_sum[i] = pval[i];
for(int i=1;i<=m;++i){
cin >> ip[i].u >> ip[i].v >> ip[i].w;
}
EX_Kru();
for(int i=n+1;i<=2*n-1;++i){
if(p[i] == i){
fa[i][0] = 0;
dfs(i);
}
}
for(int k=1;k<=25;++k)
for(int i=1;i<=2*n-1;++i)
//防止父亲满足但是儿子不满足
g[i][k] = max(g[i][k-1],g[fa[i][k-1]][k-1]);
int x,power;
while(q--){
cin >> x >> power;
for(int k=25;k>=0;--k){
if(fa[x][k] == 0) continue;
if(g[x][k] <= power){
x = fa[x][k];
}
}
cout << power + tr_sum[x] << '\n';
}
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
solve();
return 0;
}