【图论 LCA BFS DFS 倍增】P9487 「LAOI-1」小熊游景点|普及+

本文涉及的基础知识点

C++图论
C++DFS
C++BFS算法
本题繁难,实际难度比官网标记的高一个(甚至两个)等级

P9487 「LAOI-1」小熊游景点

题目描述

小熊的地图上有 n n n 个景点,每个景点有分数 s i s_i si

n − 1 n-1 n1 个点对之间有双向直达的公交线路,每条线路有收费 w i w_i wi

现在小熊在 a a a 景点,总司令在 b b b 景点,他们要沿简单路径 a → b a\to b ab 路径上的 p p p 景点汇合,然后沿简单路径一起去 q q q 景点。( q q q 为任意点,每个人不会游览两次 p p p 景点)

m m m 次询问,给定 a , b a,b a,b,求 p , q p,q p,q,使得小熊和总司令花费之和最小的前提下他们经过的景点分数之和最大,输出他们经过的景点分数之和。(指小熊经过的景点分数之和 + + + 总司令经过的景点分数之和)

重复经过的线路收费重复计算,重复经过的景点分数重复计算。

输入格式

第一行两个整数 n , m n,m n,m。分别表示景点个数和询问次数。

接下来一行 n n n 个整数 第 i i i 个整数 s i s_i si 表示第 i i i 个景点的权值。

接下来 n − 1 n-1 n1 行,每行 3 3 3 个整数 u , v , w u,v,w u,v,w,表示 u u u 节点和 v v v 节点之间有一条收费 w w w 的双向公交路线。

接下来 m m m 行,每行两个整数 a a a b b b,表示小熊和总司令所在的景点位置。

输出格式

对于每组询问,每行输出一个整数表示结果。

输入输出样例 #1

输入 #1

7 1
1 1 1 1 1 1 1
1 2 3
3 6 -4
2 5 2
1 3 6
2 4 1
3 7 5
4 7

输出 #1

8

输入输出样例 #2

输入 #2

10 10
786755 -687509 368192 154769 647117 -713535 337677 913223 -389809 -824004 
1 2 -785875
1 3 -77082
1 4 -973070
3 5 -97388
2 6 -112274
3 7 657757
4 8 741733
3 9 5656
4 10 -35190
3 3
3 10
7 3
5 1
2 10
10 10
1 6
7 2
8 9
9 1

输出 #2

971424
-1257332
1309101
3420605
-2313033
-2567048
-2467802
352646
759321
1368370

说明/提示

样例说明

对于第一组样例,小熊的地图如图所示:

其中 a = 4 , b = 7 a=4,b=7 a=4,b=7,令 p = 3 , q = 6 p=3,q=6 p=3,q=6

小熊的路径为 4 → 2 → 1 → 3 → 6 4\to2\to1\to3\to6 42136,花费之和为 1 + 3 + 6 + ( − 4 ) = 6 1+3+6+(-4)=6 1+3+6+(4)=6,景点分数之和为 1 + 1 + 1 + 1 + 1 = 5 1+1+1+1+1=5 1+1+1+1+1=5

总司令的路径为 7 → 3 → 6 7\to3\to6 736,花费之和为 5 + ( − 4 ) = 1 5+(-4)=1 5+(4)=1,景点分数之和为 1 + 1 + 1 = 3 1+1+1=3 1+1+1=3

小熊和总司令花费之和为 6 + 1 = 7 6+1=7 6+1=7,经过的景点分数之和为 5 + 3 = 8 5+3=8 5+3=8

可以证明此时小熊和总司令花费之和最小的前提下他们经过的景点分数之和最大。


数据范围

本题采用捆绑测试。

Subtask n , m n,m n,m s i , w i s_i,w_i si,wi特殊性质分数
1 1 1 = 3 × 1 0 5 =3\times10^5 =3×105 ∈ [ 0 , 1 0 6 ] \in\lbrack0,10^6\rbrack [0,106] 10 10 10
2 2 2 = 3 × 1 0 5 =3\times10^5 =3×105 ∈ [ − 1 0 6 , 1 0 6 ] \in\lbrack-10^6,10^6\rbrack [106,106]小熊的地图是一条链 10 10 10
3 3 3 = 3 × 1 0 2 =3\times10^2 =3×102 ∈ [ − 1 0 6 , 1 0 6 ] \in\lbrack-10^6,10^6\rbrack [106,106] 5 5 5
4 4 4 = 3 × 1 0 3 =3\times10^3 =3×103 ∈ [ − 1 0 6 , 1 0 6 ] \in\lbrack-10^6,10^6\rbrack [106,106] 15 15 15
5 5 5 ≤ 3 × 1 0 5 \le 3\times10^5 3×105 ∈ [ − 1 0 6 , 1 0 6 ] \in\lbrack-10^6,10^6\rbrack [106,106] 60 60 60

对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 3 × 1 0 5 1\le n,m\le 3\times 10^5 1n,m3×105 ∣ s i ∣ , ∣ w i ∣ ≤ 1 0 6 \vert s_i\vert,\vert w_i\vert\le10^6 si,wi106,小熊的地图是一棵树。

(小熊都可以游览景点了,公交价格和景点分数怎么不可以是负数呢?)

P9487 图论+LCA

向量a记录各景点,点权。
p不会被游览两次,指的是p等于q时,路径是{p},而不是{p,p}。
f(a,b)=节点a到节点b的简单路径边权之和。就是本题的花费。
g(a,b)=节点a到节点b的简单路径点权之和。就是本题的积分。
本题的花费:
cost = f(a,p)+f(b,p)+2f(p,q)。
由于p在a、b的简单路径上,故f(a,p)+f(b,p)=f(a,b)。即cost = f(a+b)+2f(p,q)
本题的积分:
s=g(a,p)+g(b,p)+2g(p,q) - 2a[p] = g(a,b)+a[p]+2g(p,q)-2a[p] = g(a,b)+2g(p,q)-a[p];
令s1 = 2g(p,q)-a[p]。则s = g(a,b)+s1。
要想cost最小,必须f(p,q)最小,注意f(p,q)可以是负数。
要想积分最大,必须s1最大。
以p为起点的路径分为以下三类:
1,只有一个节点,即{p}。
2,第二个节点是p的子节点child。我们用pc[p]记录所有此类路径。
2.1 只有两个节点,即{p,child}。
2.2 另个或更多节点,即{p,任意pc[child]}
3,路径的第二个节点时p的父节点par。我们用pp[p]记录所有此类路径。
3.1,只有两个2个节点{p,par}。
3.2 第三个节点是par的父节点。即{p,任意pp[par]}
3.3 第三个节点是p的兄弟节点,即{p,par,任意pc[p的兄弟]]}。
注意
一,3.3不需要枚举所有孩子,如果最优解不是p,则采用最优解;否则采用次优解。故pc只需记录最优和次优两个元素。
二,pp只需要记录最优解。
三,pc和pp的元素包括:最小f(p,q)、最大s1,第二个节点(如果不存在-1)。
令d[cur] 记录cur到根的点权之和 ,则 f(a,b)=d[a]+d[b]-2d[p]+a[p] ,p是a、b的最近公共祖先LCA。
令e[cur]记录cur到根的边权之和,则g(a,b) = e[a]+e[b]-2e[p]。
四,本题节点从1开始,我习惯从0开始,预处理的时候,各节点–。
五,为了避免重写小于,pc和pp的第二个元素,用相反数。即-s1 = a[p]-2g(p,q)
六,chi是cur的孩子,pc[chi]的某条路径-s1是s2,令此路径加上cur后的-s1是s3,则:
s2 = a[chi] - 2g(chi,q)
s3 = a[cur] -2 (a[cur]+g(chi,q))
s3-s2=-a[cur]-2g(chi,q)-a[chi]+2g(chi,q)=-a[cur]-a[chi] 即 s3=s2-a[chi]-a[cur]
七,chi和chi2是cur的孩子,pc[chi2]的最优解的-s1是s2,chi → \rightarrow cur → \rightarrow 此路径的-s是s3。
s2 = a[chi2] - 2g(chi2,q)
s3 = a[chi] -2 (a[chi]+a[cur]+g(chi2,q))
s3-s2=-a[chi]-2a[cur]-a[chi2] 即:** s3=s2-a[chi]-2*a[cur]-a[chi2] **
八,s1可以这样理解 p → q p\rightarrow q pq,路径,p点一倍点权,其它点两倍点权。此理解可以验证六七。

倍增

ppc[cur]记录pp[cur]和pc[cur]的较优方案。如果取ppc[a…ab]的最优解?倍增。
h[cur][i] 记录ppc[cur,cur的父节点 ⋯ \cdots cur 的2i-1级祖先]的最大值。倍增时:每个元素的初始化,时间复杂度O(logn),查询也是。
此题比较繁杂,故分两步走:
第一步:不用倍增,直接枚举p。分两次枚举,p 在a到ab的路径上,p在b到ab的路径上。时间复杂度:O(nn)
第二步:用倍增。时间复杂度:O(nlogn)
树上倍增查询最近公共祖先的时间复杂度是:O(logn),我得封装类是O(lognlogn),现已更正。

代码

核心代码

template<class TSave>
class CMultMin {
public:
	CMultMin(const vector<int>& next, CParents& pars, const vector<TSave>& vals) :m_vNext(next), m_pars(pars) {
		const int N = next.size();
		const int M = pars.GetBitCnt();
		m_data.assign(N, vector<TSave>(M));
		for (int i = 0; i < N; i++) {
			m_data[i][0] = vals[i];
		}
		for (int i = 0; i + 1 < M; i++) {
			const int len = 1 << i;
			for (int j = 0; j < N; j++) {
				m_data[j][i + 1] = m_data[j][i];
				int j2 = pars.GetPow2Parent(j, i);
				if (-1 != j2) {
					m_data[j][i + 1] = min(m_data[j][i], m_data[j2][i]);
				}
			}
		}
	}
	TSave Query(int cur, int len, TSave def) {
		for (int iBit = 0; iBit < m_data[0].size(); iBit++)
		{
			if (len & (1 << iBit))
			{
				def = min(def, m_data[cur][iBit]);
				cur = m_pars.GetPow2Parent(cur, iBit);
				if (-1 == cur) { return def; }
				len -= (1 << iBit);
			}
		}
		return def;
	};
	const vector<int>& m_vNext;
	vector<vector<TSave>> m_data;
	CParents& m_pars;
};

class Solution {
public:
	vector<long long> Ans(vector<int>& pw, vector<tuple<int, int, int>>& edge, vector<pair<int, int>>& que) {
		const int N = pw.size();
		vector<vector<int>> neiBo(N);
		for (auto& [n1, n2, w] : edge) {
			n1--, n2--;
		}
		for (auto& [a, b] : que) {
			a--, b--;
		}
		for (const auto& [n1, n2, w] : edge) {
			neiBo[n1].emplace_back(n2);
			neiBo[n2].emplace_back(n1);
		}
		auto leves = CBFSLeve::Leve(neiBo, { 0 });
		vector<int> pars(N, -1), ew(N);//pars[cur]记录cur父节点,ew[cur]记录边cur、pars[cur]的权。
		for (int i = 0; i < N; i++) {
			for (const auto& j : neiBo[i]) {
				if (leves[j] < leves[i]) { continue; }
				pars[j] = i;
			}
		}
		for (const auto& [n1, n2, w] : edge) {
			if (leves[n1] < leves[n2]) {
				ew[n2] = w;
			}
			else {
				ew[n1] = w;
			}
		}
		auto nodeByLeveSort = CBFSLeve::LeveSort(leves);
		vector<long long> pw0(pw.begin(), pw.end()), ew0(N);//d[i]记录i到0的简单路径的点权之和,e记录边权。
		for (const auto& node : nodeByLeveSort)
		{
			if (-1 == pars[node]) { continue; }
			pw0[node] += pw0[pars[node]];
			ew0[node] += ew0[pars[node]];
		}
		vector<vector<tuple<long long, long long, int>>> pc(N);
		vector<pair<long long, long long>> pp(N);
		for (int cur = 0; cur < N; cur++) {
			pp[cur] = make_pair(0, -pw[cur]);
			pc[cur].emplace_back(make_tuple(0, -pw[cur], -1));
		}
		for (auto it = nodeByLeveSort.rbegin(); it != nodeByLeveSort.rend(); ++it) {
			auto& v = pc[*it];
			if (v.size() > 1) {//只保留最优次优
				nth_element(v.begin(), v.begin() + 1, v.end());
				v.erase(v.begin() + 2, v.end());
			}
			const auto& t = pc[*it].front();
			if (-1 == pars[*it]) { continue; }
			pc[pars[*it]].emplace_back(make_tuple(get<0>(t) + ew[*it], get<1>(t) - pw[*it] - pw[pars[*it]], *it));
		}
		for (const auto& cur : nodeByLeveSort)
		{
			const int iPar = pars[cur];
			if (-1 == iPar) { continue; }
			auto t = make_pair(ew[cur] + get<0>(pp[iPar]), get<1>(pp[iPar]) - pw[iPar] - pw[cur]);
			pp[cur] = min(pp[cur], t);//经过父节点,不经过兄弟节点
			for (const auto& [f, s1, i] : pc[iPar]) {
				if ((-1 == i) || (i == cur)) { continue; }
				t = make_pair(get<0>(pc[i].front()) + ew[i] + ew[cur], get<1>(pc[i].front()) - pw[i] - 2 * pw[iPar] - pw[cur]);
				pp[cur] = min(pp[cur], t);//经过父节点,经过兄弟节点
				break;
			}
		}
		vector<pair<long long, long long>> ppc(N);
		for (int i = 0; i < N; i++) {
			ppc[i] = min(pp[i], make_pair(get<0>(pc[i].front()), get<1>(pc[i].front())));
		}
		C2Parents par2s(pars, leves);

		/*{
			vector<pair<long long, long long>> ppc1(N,make_pair(LLONG_MAX/2,0));
			for (int a = 0; a < N; a++) {
				for (int b = 0; b < N; b++) {
					const int ab = par2s.GetPublicParent(a, b);
					pair<long long, long long> abr(0,0);
					for (int i = a; i != ab; i = pars[i]) {
						abr.first += ew[i];
						abr.second += -2 * pw[i];
					}
					for (int i = b; i != ab; i = pars[i]) {
						abr.first += ew[i];
						abr.second += -2 * pw[i];
					}
					abr.second += pw[a];
					abr.second -= 2 * pw[ab];
					ppc1[a] = min(ppc1[a], abr);
				}
			}
			for (int i = 0; i < N; i++) {
				AssertEx(-ppc1[i].second, ppc[i]);
			}
		};*/
		CMultMin<pair<long long, long long>> mult(pars, par2s, ppc);
		vector<long long> ans;
		for (const auto& [a, b] : que) {
			const int ab = par2s.GetPublicParent(a, b);
			const auto curAns1 = mult.Query(a, leves[a] - leves[ab], ppc[ab]);
			const auto curAns2 = mult.Query(b, leves[b] - leves[ab], ppc[ab]);
			const auto curAns3 = min(curAns1, curAns2);
			//AssertEx(curAns3, curAns);
			ans.emplace_back(pw0[a] + pw0[b] - 2 * pw0[ab] + pw[ab] - curAns3.second);
		}
		return ans;
	}
};

单元测试

vector<int> a;
		vector<tuple<int, int, int>> edge;
		vector<pair<int, int>> que;
		TEST_METHOD(TestMethod1)
		{
			a = { 1,1,1,1,1,1,1 },edge = { {1,2,3},{3,6,-4},{2,5,2},{1,3,6},{2,4,1},{3,7,5} },
				que = { {4,7} };
			auto res = Solution().Ans(a, edge,que);
			AssertV(vector<long long>{ 8 }, res);
		}
		TEST_METHOD(TestMethod12)
		{
			a = { 786755,-687509,368192,154769,647117,-713535,337677,913223,-389809,-824004 },
				edge = { {1,2,-785875},{1,3,-77082},{1,4,-973070},{3,5,-97388},{2,6,-112274},{3,7,657757},{4,8,741733},{3,9,5656},{4,10,-35190} };
			que = { {3,3},{3,10},{7,3},{5,1},{2,10},{10,10},{1,6},{7,2},{8,9},{9,1} };
			auto res = Solution().Ans(a, edge, que);
			AssertV(vector<long long>{ 971424,- 1257332,1309101,3420605,-2313033,- 2567048,	- 2467802,
				352646,759321,1368370 }, res);
		}

扩展阅读

我想对大家说的话
工作中遇到的问题,可以按类别查阅鄙人的算法文章,请点击《算法与数据汇总》。
学习算法:按章节学习《喜缺全书算法册》,大量的题目和测试用例,打包下载。重视操作
有效学习:明确的目标 及时的反馈 拉伸区(难度合适) 专注
闻缺陷则喜(喜缺)是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛
失败+反思=成功 成功+反思=成功

视频课程

先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771
如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

测试环境

操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。

评论 53
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

软件架构师何志丹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值