洛谷P2680 运输计划 树剖+差分+二分+边权化点权


洛谷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;
}


双倍经验

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值