蓝桥杯2024年第十五届省赛A组-零食采购

题目描述

小蓝准备去星际旅行,出发前想在本星系采购一些零食,星系内有 n 颗星球,由 n − 1 条航路连接为连通图,第 i 颗星球卖第 ci 种零食特产。小蓝想出了 q 个采购方案,第 i 个方案的起点为星球 si ,终点为星球 ti ,对于每种采购方案,小蓝将从起点走最短的航路到终点,并且可以购买所有经过的星球上的零食(包括起点终点),请计算每种采购方案最多能买多少种不同的零食。

输入格式

输入的第一行包含两个正整数 n, q ,用一个空格分隔。

第二行包含 n 个整数 c1, c2, · · · , cn ,相邻整数之间使用一个空格分隔。

接下来 n − 1 行,第 i 行包含两个整数 ui, vi ,用一个空格分隔,表示一条航路将星球 ui 与 vi 相连。接下来 q 行,第 i 行包含两个整数 si, ti ,用一个空格分隔,表示一个采购方案。

输出格式

输出 q 行,每行包含一个整数,依次表示每个采购方案的答案。

样例输入

4 2
1 2 3 1
1 2
1 3
2 4
4 3
1 4

样例输出

3

2

提示

【样例说明】

第一个方案路线为 {4, 2, 1, 3} ,可以买到第 1, 2, 3 种零食;第二个方案路线为 {1, 2, 4} ,可以买到第 1, 2 种零食。

【评测用例规模与约定】

对于 20% 的评测用例,1 ≤ n, q ≤ 5000 ;对于所有评测用例,1 ≤ n, q ≤ 10^5,1 ≤ ci ≤ 20,1 ≤ ui, vi ≤ n,1 ≤ si, ti ≤ n。

整体思路

看到了树与路径的题目,可以考虑倍增 LCA 算法,零食的种类数用二进制维护,每一位表示一种不同的零食,用或运算模拟两段路径的合并,最终的零食种类数就是二进制中含有 1 的个数。

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;

const int N = 1e5 + 5;
int num[N]; // 零食编号 
vector<int> edge[N];
int dep[N], fa[N][25][2], lg[N];
// fa[i][j][k]中 k 为 0 表示 2^j 级祖先结点编号,为 1 表示该路径上的零食种类的二进制 

void dfs(int u, int father)
{
	dep[u] = dep[father] + 1;
	fa[u][0][0] = father;
	fa[u][0][1] = num[father];
	for(int i = 1; i <= lg[dep[u]]; i++)
	{
		fa[u][i][0] = fa[fa[u][i - 1][0]][i - 1][0];
		fa[u][i][1] = fa[fa[u][i - 1][0]][i - 1][1] | fa[u][i - 1][1];
	}
	for(int v : edge[u])
	{
		if(v != father)
		{
			dfs(v, u);
		}
	}
}

int cnt(int x)
{
	int r = 0;
	while(x)
	{
		if(x & 1)
		{
			r++;
		}
		x >>= 1;
	}
	return r;
}

int lca(int u, int v)
{
	int tmp = num[u] | num[v]; // 起点和终点在深搜的过程中没有被统计,需要手动添加 
	if(dep[u] < dep[v])
	{
		swap(u, v);
	}
	for(int i = lg[dep[u] - dep[v]] - 1; i >= 0; i--)
	{
		if(dep[fa[u][i][0]] >= dep[v])
		{
			tmp |= fa[u][i][1];
			u = fa[u][i][0];
		}
	}
	if(u == v)
	{
		return cnt(tmp);
	}
	for(int i = lg[dep[u]] - 1; i >= 0; i--)
	{
		if(fa[u][i][0] != fa[v][i][0])
		{
			tmp |= fa[u][i][1];
			tmp |= fa[v][i][1];
			u = fa[u][i][0];
			v = fa[v][i][0];
		}
	}
	tmp |= fa[u][0][1];
	return cnt(tmp);
}

int main()
{
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n, q;
	cin >> n >> q;
	for(int i = 1; i <= n; i++)
	{
		int c;
		cin >> c;
		num[i] = 1 << (c - 1);
	}
	for(int i = 0; i < n - 1; i++)
	{
		int u, v;
		cin >> u >> v;
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	for(int i = 1; i <= n; i++)
	{
		lg[i] = lg[i - 1] + ((1 << lg[i - 1]) == i);
	}
	dfs(1, 0);
	for(int i = 0; i < q; i++)
	{
		int s, t;
		cin >> s >> t;
		cout << lca(s, t) << endl;
	}
	return 0;
}

具体步骤

1. 从起点走最短的航路到终点的问题,可以等效为从起点通过两个结点的最近公共祖先到达终点。

2. 从一个结点到其 2^k 级祖先结点的路径上的零食种类,可以分解为以下两段:

  • 从该结点的 2^(k−1) 级祖先结点到 2^k 级祖先结点的路径上的零食种类。

  • 从该结点到该结点的 2^(k−1) 级祖先结点的路径上的零食种类。

3. 在寻找公共祖先的同时,利用或运算的特性记录跳跃路径上的零食种类,最终返回零食种类计数器 tmp 二进制中 1 的个数。

  • 12
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值