牛客 NC201908 小睿睿的伤害(dsu on tree, 启发式合并)

传送门

题目大意

  给你一棵树,每个节点有一个权值 v a l val val,每一对点对 ( i , j ) (i,j) (i,j) 可以在他们的 l c a lca lca处造成 g c d ( v a l [ i ] , v a l [ j ] ) gcd(val[i],val[j]) gcd(val[i],val[j])的伤害,问对于每一个点,点对对它造成最大的伤害是多少,以及造成最大伤害的点对的数量。

解题思路

  这道题一个比较好想的做法就是暴力,我可以暴力枚举点对计算其在 l c a lca lca处的贡献(复杂度 O ( n 2 ) O(n^2) O(n2)),同时我也可以对每个点暴力枚举 l c a lca lca为这个点的点对,后者我们可以开一个桶 m m m,来表示因数的个数,即 m [ i ] m[i] m[i]表示因数 i i i的个数。

比如求下面两个序列 a a a, b b b g c d ( a i , b i ) gcd(a_i, b_i) gcd(ai,bi)的最大值。
a: 3 12 9
b: 18 18 8
首先枚举a中的元素更新m,
m: 3 1 3 1 0 1 0 0 1 0 0 1 [1~12]
枚举b的元素的每一个因数,比如18,枚举时要维护最大的公因数和其个数
第一个因数:1,此时发现m[1]=3说明,有三对数公因数为1
第二个因数:2,此时发现m[2]=1说明,有两对数公因数为2
第三个因数:3,此时发现m[3]=3说明,有三对数公因数为3
第四个因数:6,此时发现m[6]=1说明,有一对数公因数为6
第五个因数:9,此时发现m[9]=1说明,有一对数公因数为9
⋯ ⋯ \cdots \cdots
枚举完 b b b后再把 m m m更新,如果再来一组 c c c可以按照等同于 b b b的方式枚举 c c c

  将上述算法放在树中 a a a, b b b 相当于一个节点的2个分支,分别在两个分支上枚举点对,得到的点对的lca一定等于这个点。需要值得注意的是,lca也可以是这个点它子树中任意一个节点,此时单独将这个点看作一个分支处理即可。
  上述方法其实依旧是 O ( n 2 ) O(n^2) O(n2),因为对于每个点都暴力枚举其子树的每个点计算贡献。改进该算法,我们发现每一个分支的m数组其实已经在访问该节点孩子的时候求出来了,但是我们仍需要将每一个分支的m数组两两合并求出答案,唯一不用合并的就是第一个被访问的分支(类似于上面的a序列,它只用来更新m,但这时的m已经在子问题的时候解决了)。我们让第一个被访问的分支尽可能大,让其他小的分支往这个大的分支上合并。这里就是dsu on tree算法启发式合并的思想了,这一个优化能够直接让 O ( n 2 ) O(n^2) O(n2) 的复杂度降为 O ( n log ⁡ n ) O(n \log n) O(nlogn)
想加深对这个算法的理解可以看一下这个大佬的博客:https://www.cnblogs.com/zwfymqz/p/9683124.html

代码实现

  如果我们要保存每一个节点的m数组,一定会MLE,由于我们通过上面的分析发现,其实我们只需要用到子孙节点数最多的节点(重儿子)的m数组。所以我们全局只开一个m数组,对于重儿子,我们在访问完之后不清理m数组直接用来下次计算,而轻儿子我们访问完之后要清理m数组,避免对之后的计算产生影响,下次用的时候现用现算。
  还要注意预处理因数。
  整个算法的复杂度为 O ( 1 0 7.5 + m a x _ s i z e ∗ n log ⁡ n ) O(10^{7.5 } + max\_size * n\log n ) O(107.5+max_sizenlogn) m a x _ s i z e max\_size max_size 是1e5内数的最大因数个数,可以证明其小于100。

具体细节请参考代码:

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 5;
vector<int> ft[MAXN];
int ans[MAXN], cnt[MAXN];
int head[MAXN], nxt[MAXN << 1], to[MAXN << 1], val[MAXN], sze, n;
inline void AddEdge(int u, int v) {
	nxt[++sze] = head[u]; to[head[u] = sze] = v;
}
// 轻重链剖分 
int sum[MAXN], hson[MAXN];
void dfs(int u, int fa) {
	sum[u] = 1;
	for (int e = head[u]; e; e = nxt[e]) {
		if (to[e] == fa) continue;
		dfs(to[e], u);
		sum[u] += sum[to[e]]; 
		if (sum[to[e]] > sum[hson[u]]) 
			hson[u] = to[e];
	}
}
// m是一个桶,存放因数的个数用来记录影响, calc用来维护答案,add用来更新m数组,del用来清理m数组 
int m[MAXN];
void calc(int u, int fa, int rt) {
	for (auto w : ft[val[u]]) {
		if (m[w] && w > ans[rt]) 
			ans[rt] = w, cnt[rt] = m[w];
		else if (w == ans[rt]) 
			cnt[rt] += m[w];
	}
	for (int e = head[u]; e; e = nxt[e]) 
		if (to[e] != fa) calc(to[e], u, rt);
}
void add(int u, int fa) {
	for (auto w : ft[val[u]]) m[w]++;
	for (int e = head[u]; e; e = nxt[e]) 
		if (to[e] != fa) add(to[e], u);
} 
void del(int u, int fa) {
	// 删除最好不用memeset,会改变算法的复杂度
	for (auto w : ft[val[u]]) m[w]--;
	for (int e = head[u]; e; e = nxt[e]) 
		if (to[e] != fa) del(to[e], u);
}
// dsu on tree 的思路,删除轻儿子的影响保留重儿子的影响, opr=0表示计算完后要删除影响
void solve(int u, int fa, int opr) {
	for (int e = head[u]; e; e = nxt[e]) 
		if (to[e] != fa && to[e] != hson[u]) solve(to[e], u, 0);
	if (hson[u]) solve(hson[u], u, 1);
	// 对于u来说,任意一个孩子节点与u的lca都是u,所以单独计算节点u对它自身的影响 
	for (auto w : ft[val[u]]) {
		if (m[w] && w > ans[u]) 
			ans[u] = w, cnt[u] = m[w];
		else if (w == ans[u]) 
			cnt[u] += m[w];
	}
	for (auto w : ft[val[u]]) m[w]++;
	// 分别计算所有轻链的影响 
	for (int e = head[u]; e; e = nxt[e]) 
		if (to[e] != fa && to[e] != hson[u]) calc(to[e], u, u), add(to[e], u);
	
	if (!opr) del(u, fa);
}
int main() {
	for (int i = 1; i < MAXN; i++) 
		for (int j = 1; j * j <= i; j++) {
			if (i % j == 0) {
				ft[i].emplace_back(j);
				if (j * j != i) ft[i].emplace_back(i / j);
			}
		}
	scanf("%d", &n);
	for (int u, v, i = 1; i < n; i++) {
		scanf("%d%d", &u, &v); AddEdge(u, v); AddEdge(v, u);
	}
	for (int i = 1; i <= n; i++) 
		scanf("%d", val + i);
	dfs(1, 0);
	solve(1, 0, 1);
	
	for (int i = 1; i <= n; i++) 
		printf("%d %d\n", ans[i], cnt[i]);
	return 0;
} 

注:这个题目数据随机性很强,没有卡long long,甚至不用重儿子都能过。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值