2021 ICPC区域赛(上海)H-Life is a Game(kruskal重构树、lca)

游戏人生

2021 ICPC区域赛(上海)H-Life is a Game(kruskal重构树、lca)_第1张图片
题目大意

  • 给你一个图,图上有点权和边权。以及q个查询:每个查询给你一个初始位置x和初始能量k;
  • 你每到一个新点上即可获得该点的能量(即点权),但是如果想通过一条边,你的能量总数需要大于边权(可以来回走)
  • 求可以获取的最大能量数。

引用 lwz_159的题解博客

首先贪心地想,能走到最多的点肯定能获得更多能量,但边权会是限制,在可以来回走的前提下,我们显然可以保留限制最小的边让这个图变成一颗树(最小生成树)

但仅仅有 最小生成树 并不能很好地处理 q 次询问的时间复杂度,这里我们再次引入 Kruskal重构树 来解决

什么是kruskal重构树?

类型题:洛谷:P2245 星际导航

在一颗 kruskal重构树 上,我们拥有神奇的性质:

  • 这颗树上叶子节点都代表原图中的,其余结点都是代表原图中的
  • 任意原最小生成树中的两点之间路径上 最大 的边权是他们在重构树上 最近公共祖先 lca 这个点的权值(这个点必定代表边)
  • 这颗树有权值的点都代表边,他们会构成一个二叉堆,即一个子树的根节点权值必定大于他内部所有结点的权值(这里的权值是针对边的)

拥有了这样一颗重构树,我们就能用倍增的方式每次处理查询

2021 ICPC区域赛(上海)H-Life is a Game(kruskal重构树、lca)_第2张图片
每次询问我们都会得到一个起始点(根据原则必定是叶子节点)和起始值,假设为图中的 a 点;在这颗重构树上我们每次尽可能往根节点跳,判断能不能跳过去就是判断 a点的值 是否大于等于 b点的值(边权限制)

一旦我们能跳到 b点,因为二叉堆的性质 b这条边 会是 b这个子树中最大的一条边,所以我们必定能得到 b子树中所有点的权值(最大的边都能走,其他小边肯定也能走过去)(这里的权值针对点)

跳的过程用倍增压缩,这样总时间就可以在 O ( q l o g n ) O(qlogn) O(qlogn) 内,一旦某次怎么都跳不上去我们就可以退出了(止步于此)

C o d e : Code: Code:

#include
#include
#define mem(a,b) memset(a,b,sizeof a)
#define cinios (ios::sync_with_stdio(false),cin.tie(0),cout.tie(0))
#define sca scanf
#define pri printf
#define forr(a,b,c) for(int a=b;a<=c;a++)
#define rfor(a,b,c) for(int a=b;a>=c;a--)
#define endl "\n"
//[博客地址]:https://blog.csdn.net/weixin_51797626?t=1
using namespace std;
inline void read(int& x) { x = 0; int f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + (ch - '0'); ch = getchar(); } x *= f; }
void write(int x) { if (x < 0) putchar('-'), x = -x; if (x >= 10) write(x / 10); putchar(x % 10 + '0'); }

typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> PII;

const int N = 200010, M = 100010, MM = N;
int INF = 0x3f3f3f3f, mod = 1e9 + 7;
ll LNF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, T, S, D;
struct edge
{
	int a, b, w;
	bool operator <(const edge& ee)const { return w < ee.w; }
}ed[M];
vector<int> e[N];//重构树,重构树最多会比原最小生成树多一倍的点
int a[N], p[N], down[N];
ll d[N];
int fa[N][18];

int find(int x) {
	return p[x] == x ? p[x] : p[x] = find(p[x]);
}

int kruskal() { //先创建最小生成树
	sort(ed + 1, ed + 1 + m);
	forr(i, 1, 2 * n)p[i] = i;
	int cnt = n;
	forr(i, 1, m) {
		int a = find(ed[i].a), b = find(ed[i].b);
		if (a ^ b) {
			d[++cnt] = ed[i].w;//同时用并查集建重构树
			p[a] = p[b] = cnt;
			e[cnt].push_back(a);
			e[cnt].push_back(b);
		}
	}
	return cnt;
}

int build(int x) { //递归预处理倍增,队列也可,不容易爆栈
	int sum = a[x];
	for (auto j : e[x]) {
		fa[j][0] = x;
		for (int i = 1; i <= 17; i++)
			fa[j][i] = fa[fa[j][i - 1]][i - 1];
		sum += build(j);
	}
	down[x] = sum;//以及以某点为根的子树内所有点权和
	return sum;
}

int main() {
	cinios;

	cin >> n >> m >> k;
	forr(i, 1, n)cin >> a[i];

	forr(i, 1, m) {
		int a, b, x;
		cin >> a >> b >> x;
		ed[i] = { a,b,x };
	}

	n = kruskal();
	build(n);
	d[0] = 1e18;//边界要给个极值

	forr(i, 1, k) {
		int x, q;
		cin >> x >> q;

		ll ans = 1ll * a[x] + q;
		while (x != n)//跳到根节点为止
		{
			int x1 = x;
			for (int i = 17; i >= 0; i--)
				if (d[fa[x][i]] <= ans)x = fa[x][i];//能跳我们就跳上去
			if (x == x1)break;//如果某轮怎么都跳不动就退出
			ans = 1ll * down[x] + q;
			//随时更新携带的能量
		}
		cout << ans << '\n';
	}

	return 0;
}
/*
*/

你可能感兴趣的:(#,生成树,#,LCA,算法,数据结构,kruskal,树堆,LCA)