洛谷P2680 运输计划
标签
- 树链剖分
- 二分
- 边权化点权
简明题意
- 给一棵树,给边权,再给m条路径。你可以将一条边权置为0,使得m条路径中最短最长的路径最短。
思路
- 最短的路径最长,妥妥的二分…
然而我一开始太浮躁,连题目都没看清楚,以为他问的是使得m点路径的权值和最小… - 树上的边权不好处理,可以把边权转化成点权。转化后,如果查u-v,就是ask(u, v) - LCA(u, v),这里可以自己验证一下。下面的思路介绍中,我把点权和边权混用了,大家知道指的是一个东西就行了。
- 先说二分,二分的显然是答案,也就是去二分题目问的那个最短长度。重点在于check函数怎么写。对于一个最短距离x,如果发现所有的路径的长度都<=这个x,显然这个x是可行的。“所有的路径长度”该怎么求呢?这里先讲二分,下面会详细说怎么求。然后,如果有一部分路径的长度<=x,有一部分>=x,这个时候我们就需要判断,能否通过去掉一条边使得所有>x的路径都<=x这里去掉的边就相当于题目中,边权置0的那条边。判断方法:找到所有>x的路径,然后找到他们的边的交集。如果没有交集,那么绝对不可能去掉一个条边使得所有的>x的路径全部小于x。现在问题就是如何找交集了,我们可以树剖,剖完后,对这这些>x的路径,每一条路径使得路径上的点++,如果存在点的值== >x的路径的条数,那么这个点就是交集了,然后再进一步判断去掉这个点能否使得所有的>x的路径都<x,方法是遍历一遍>x的点,判断:路径长度 - w[去掉的点] <= x,如果去掉这个点后,所有不满足的点都变得满足了,就return1
- 然后就没了…是不是很简单?
注意事项
- 无
总结
- 记得树上边权不好处理,可以考虑边权化点权,再减去LCA即可
AC代码
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 3e5 + 10;
struct Edge
{
int v, w;
Edge(int v, int w) : v(v), w(w) {}
};
int n, m, a[maxn];
vector<Edge> g[maxn];
int dep[maxn], siz[maxn], son[maxn], fa[maxn];
void dfs1(int u, int f, int deep, int val)
{
dep[u] = deep;
fa[u] = f;
siz[u] = 1;
a[u] = val;
int max_son = -1;
for (auto& v : g[u])
if (v.v != f)
{
dfs1(v.v, u, deep + 1, v.w);
siz[u] += siz[v.v];
if (siz[v.v] > max_son)
max_son = siz[v.v], son[u] = v.v;
}
}
int id[maxn], cnt, top[maxn], w[maxn];
void dfs2(int u, int topf)
{
id[u] = ++cnt;
w[cnt] = a[u];
top[u] = topf;
if (son[u])
{
dfs2(son[u], topf);
for (auto& v : g[u])
if (v.v != fa[u] && v.v != son[u])
dfs2(v.v, v.v);
}
}
int LCA(int u, int v)
{
while (top[u] != top[v])
{
if (dep[top[v]] < dep[top[u]]) swap(u, v);
v = fa[top[v]];
}
return dep[u] < dep[v] ? u : v;
}
int pre[maxn];
int ask_path(int u, int v)
{
int k = LCA(u, v);
int ans = 0;
while (top[u] != top[v])
{
if (dep[top[v]] < dep[top[u]]) swap(u, v);
ans += pre[id[v]] - pre[id[top[v]] - 1];
v = fa[top[v]];
}
ans += pre[max(id[u], id[v])] - pre[min(id[u], id[v]) - 1];
return ans - (pre[id[k]] - pre[id[k] - 1]);
}
int uu[maxn], vv[maxn], path_length[maxn];
int cf[maxn], yu[maxn], book[maxn], res[maxn];
bool check(int x)
{
memset(cf, 0, sizeof cf);
memset(yu, 0, sizeof yu);
memset(book, 0, sizeof book);
memset(res, 0, sizeof res);
int cnt = 0;
for (int i = 1; i <= m; i++)
if ((res[i] = path_length[i]) > x)
{
book[i] = 1;
cnt++;
int u = uu[i], v = vv[i];
int k = LCA(u, v);
while (top[u] != top[v])
{
if (dep[top[v]] < dep[top[u]]) swap(u, v);
cf[id[top[v]]]++, cf[id[v] + 1]--;
v = fa[top[v]];
}
cf[min(id[u], id[v])]++, cf[max(id[u], id[v]) + 1]--;
cf[id[k]]--, cf[id[k] + 1]++;
}
if (cnt == 0)
return 1;
for (int i = 1; i <= n; i++)
yu[i] = yu[i - 1] + cf[i];
for (int i = 1; i <= n; i++)//遍历新点
if (yu[i] == cnt)//每个不符合要求的路径都将它路径上所有点标记了一次,如果标记数等于不符合的路径数,说明这一点是多有不符合要求的路径上的公共点,我们尝试去掉这个点
{
bool ok = 1;
for (int j = 1; j <= m; j++)//这里先遍历所有的路径
if (book[j])//book记录了路径是否符合,这里就找到了不符合的那条路径
{
if (res[j] - w[i] > x)//如果去掉这个条边后,这条路径仍不符合,那么去点这条边就是不可行的
ok = 0;
}
if (ok == 0)//如果去点这条边,仍然不可行,我们就考虑去掉别的边
continue;
else
return 1;//去掉后可行了,就返回1
}
return 0;//不符合的路径连公共点都没有,直接返回0
}
void solve()
{
scanf("%d%d", &n, &m);
for (int i = 1; i < n; i++)
{
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
g[u].push_back(Edge(v, w)), g[v].push_back(Edge(u, w));
}
dfs1(1, 1, 1, 0);
dfs2(1, 1);
for (int i = 1; i <= n; i++)
pre[i] = pre[i - 1] + w[i];
for (int i = 1; i <= m; i++)
scanf("%d%d", &uu[i], &vv[i]), path_length[i] = ask_path(uu[i], vv[i]);
int l = 0, r = maxn * 1000, ans;
while (l <= r)
{
int mid = (l + r) / 2;
if (check(mid))
r = mid - 1, ans = mid;
else
l = mid + 1;
}
printf("%d", ans);
}
int main()
{
freopen("Testin.txt", "r", stdin);
solve();
return 0;
}
双倍经验
- 无