洛谷题单指南-图论之树-P2680 [NOIP 2015 提高组] 运输计划
原题链接:https://www.luogu.com.cn/problem/P2680
题意解读:边带权的n节点树中,给定m条路径,问能否将任意一条边权置0,使得m条路径的最大值最小,求此最大路径的最小值。
解题思路:
最大值最小,第一想到二分,考虑一下能否二分。
设对这个最大路径的最小值进行二分,得到一个mid,分析一下路径的特性:所有路径长度要么<=mid,要么将一条边权置0后长度<=mid,则符合要求,mid可以继续缩小,否则mid要扩大。
1、对于<=mid的路径,显然都是满足
2、对于>mid的路径,要看将某一条边权置0,能否都<=mid,只需要看最长的路径,将所有>mid的路径重叠的边权中最大的减掉,能否<=mid
根据上面分析,有几个关键问题要解决:
1、如何计算一条路径的长度
可以通过dfs计算每个节点到根节点的距离dist[N],这样u->v路径长度就可以转化为dist[u] - dist[lca(u,v)] + dist[v] - dist[lca(u,v)]
2、如何找出多条路径重叠的边
首先,想到的是枚举,枚举所有>mid的路径,再枚举路径中的每一条边,出现的边进行标记+1,最后边的标记数等于路径数的是重叠边,总复杂度为O(n^2)。
如何优化?要将路径上每一条边的标记+1,可以借助树上差分https://www.cnblogs.com/jcwy/p/18754359
对u->v路径上边标记+1,设差分数组为a,操作为a[u] += 1, a[v] += 1, a[lca(u,v)] -= 2
再通过树上前缀和还原边的标记数量,得到数组s,枚举s,找到等于>mid的路径条数的边,记录权值最大的一条,最后用最长路径-最大重叠权值,如果能<=mid,说明符合条件,继续缩小范围二分,否则扩大范围。
注意1:计算路径长度也可以用树链剖分,但是这里加上二分,总的复杂度达到O(n*logn*logn),n=300000会超时。
注意2:在对树上差分还原前缀和时,如果采用如下递归的方式,将会有一个数据点无法通过
void sum(int u, int p)
{
s[u] = a[u];
for(auto item : g[u])
{
int v = item.v, w = item.w;
if(v == p) continue;
sum(v, u);
s[u] += s[v];
}
}
因此,可以改成迭代的方式,需要先记录每个节点的dfs序,然后逆着dfs序遍历每一个节点的差分,将其累加到父节点
for(int i = 1; i <= n; i++) s[i] = a[i];
for(int i = n; i >= 1; i--) //逆着dfs序将所有差分累加到父节点
{
s[fa[dfn[i]][0]] += s[dfn[i]];
}
100分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 300005;
struct Node
{
int v, w;
};
vector<Node> g[N];
struct Path
{
int u, v, l, d; //路径起点、终点、lca、长度
};
Path paths[N]; //所有路径信息
int dist[N]; //所有节点到根节点的距离
int depth[N], fa[N][20]; //lca相关
int dfn[N], idx; //dfs序
int a[N], s[N]; //差分数组,前缀和数组
int n, m;
//预处理
void dfs(int u, int p, int d)
{
dfn[++idx] = u;
dist[u] = d;
depth[u] = depth[p] + 1;
fa[u][0] = p;
for(int j = 1; j < 20; j++)
fa[u][j] = fa[fa[u][j - 1]][j - 1];
for(auto item : g[u])
{
int v = item.v, w = item.w;
if(v == p) continue;
dfs(v, u, d + w);
}
}
//计算LCA
int lca(int u, int v)
{
if(depth[u] < depth[v]) swap(u, v);
for(int j = 19; j >= 0; j--)
{
if(depth[fa[u][j]] >= depth[v])
u = fa[u][j];
}
if(u == v) return u;
for(int j = 19; j >= 0; j--)
{
if(fa[u][j] != fa[v][j])
{
u = fa[u][j];
v = fa[v][j];
}
}
return fa[u][0];
}
//树上前缀和,注意性能不如直接迭代
void sum(int u, int p)
{
s[u] = a[u];
for(auto item : g[u])
{
int v = item.v, w = item.w;
if(v == p) continue;
sum(v, u);
s[u] += s[v];
}
}
bool check(int x)
{
memset(a, 0, sizeof(a));
memset(s, 0, sizeof(s));
int cnt = 0; //记录有多少个路径长度>x
int maxx = 0; //记录>x的路径中最长的
for(int i = 1; i <= m; i++)
{
if(paths[i].d > x) //长度超过x的需要判断能否找到公共边置0后,长度<=x
{
cnt++;
//树上差分标记边
a[paths[i].u] += 1;
a[paths[i].v] += 1;
a[paths[i].l] -= 2;
maxx = max(maxx, paths[i].d);
}
}
if(cnt == 0) return true; //没有>x的路径,都满足要求
//sum(1, 0); //递归还原前缀和,用此方法会超时一个数据点
//迭代还原前缀和
for(int i = 1; i <= n; i++) s[i] = a[i];
for(int i = n; i >= 1; i--) //逆着dfs序将所有差分累加到父节点
{
s[fa[dfn[i]][0]] += s[dfn[i]];
}
int maxy = 0; //记录公共边中权值最大的
for(int i = 1; i <= n; i++)
{
//判断是公共边
if(s[i] == cnt) maxy = max(maxy, dist[i] - dist[fa[i][0]]); //i边权值是i到根的距离-i的父节点到根的距离
}
if(maxx - maxy <= x) return true;
return false;
}
int main()
{
cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
cin >> n >> m;
for(int i = 1; i < n; i++)
{
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
}
dfs(1, 0, 0);
//计算所有路径的长度
for(int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
int l = lca(u, v);
paths[i] = {u, v, l, dist[u] + dist[v] - 2 * dist[l]};
}
//开始二分
int l = 0, r = 3e8;
while(l < r)
{
int mid = l + r >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
cout << l;
return 0;
}
浙公网安备 33010602011771号