题目大意
给你一棵树,每个节点有一个权值 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_size∗nlogn),
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,甚至不用重儿子都能过。