2020牛客多校第一场B题 Infinite Tree 虚树

本文探讨了InfiniteTree问题的解决策略,介绍了一种基于虚树概念的高效算法,通过构建虚树来减少搜索空间,实现快速求解。文章详细解析了如何利用树状数组和深度优先搜索进行优化,并提供了完整的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Infinite Tree

题意

一颗无限结点的树,任意大于111的点kkk与点kmindiv(k)\frac{k}{mindiv\left(k\right)}mindiv(k)k相连,其中mindiv(k)mindiv\left(k\right)mindiv(k)kkk的最小质因子
δ(u,v)\delta\left( u, v \right)δ(u,v)为树上u−vu-vuv之间的距离,求min⁡u∑i=1mwiδ(u,i!)\min_u \displaystyle\sum_{i = 1} ^ {m} w_i \delta\left( u, i! \right)minui=1mwiδ(u,i!)

题解

不考虑本题的树

我们先考虑这个题在已经知道树的结构下怎么解
显然δ(u,v)=dis(u,v)\delta\left( u, v \right)=dis\left( u, v \right)δ(u,v)=dis(u,v)
min⁡u∑i=1mwiδ(u,i!)=min⁡u∑i=1mwidis(u,i!)\min_u \displaystyle\sum_{i = 1} ^ {m} w_i \delta\left( u, i! \right) = \min_u \displaystyle\sum_{i = 1} ^ {m} w_i dis\left( u, i! \right)minui=1mwiδ(u,i!)=umini=1mwidis(u,i!)
root=1root=1root=1的树中,假设现在u=1u=1u=1,那么当前答案ans=∑i=1mwidis(1,i!)ans=\displaystyle\sum_{i = 1} ^ {m} w_i dis\left( 1, i! \right)ans=i=1mwidis(1,i!)
f[u]=w[u]+∑v∈sonf[v]f[u]=w[u] + \displaystyle\sum_{v∈son}f[v]f[u]=w[u]+vsonf[v],那么在图中,显然f[1]=∑i=1mwif[1]=\displaystyle\sum_{i = 1} ^ {m} w_if[1]=i=1mwi
现在考虑当我们的点uuu转移到uuu的一个子节点vvv时,答案会发生什么变化
那么就有f[1]−f[v]f[1]-f[v]f[1]f[v]多一段移动距离dis(u,v)dis\left( u,v \right)dis(u,v)f[v]f[v]f[v]少一段移动距离dis(u,v)dis\left( u,v \right)dis(u,v)
所以当我们转移uuu点能够使答案变小的时候,即(f[1]−f[v])−(f[v])=f[1]−2f[v]<0(f[1]-f[v])-(f[v])=f[1]-2f[v]<0(f[1]f[v])(f[v])=f[1]2f[v]<0时,我们就会移动uuu点,当不能继续移动时我们就找到了最终的答案

void dfs(int u, int fa) {//第一个dfs结束后,w就是子树的w之和,就是上面讲的f
   	f[u] = w[u]
    for (auto &v: g[u])
        if (v != fa) {
            dfs(v, u);
            f[u] += f[v];
        }
}
void dfs2(int u, int fa) {//如果rt移动之后答案变小就一直移动下去,直到答案不在变小
    for (auto &v: g[u])
        if (v != fa) {
            //rt从u转移到v的代价
            //+(w[1] - w[v]) - w[v]
            if (w[1] - 2 * w[v] < 0) {
                ans += 1ll * (w[1] - 2 * w[v]) * (dep[v] - dep[u]);//一步的代价*距离
                dfs2(v, u);
            }
        }
}

或者如果觉得这里路径不是单一的,可以用dpdpdp数组来记录以每一个点为中心的答案,最后找到最小的就行

本题树的结构

接下来我们将本题构造的树画出来,下面是m=6m=6m=6的时候建的树
因为任意大于111的点kkk与点kmindiv(k)\frac{k}{mindiv\left(k\right)}mindiv(k)k相连,所以这里我更直观的将mindivmindivmindiv标了出来,就是图上的边权
picture1
对于结点iii作质因数分解,记为i=p1k1p2k2⋅⋅⋅pnkni=p_1^{k_1}p_2^{k_2}···p_n^{k_n}i=p1k1p2k2pnkn
这棵树从根节点1!1!1!到点iii的路径中,质因子由大变小,即经过的路径边上的质因数是形如5,5,5,3,3,2,2,2,25,5,5,3,3,2,2,2,25,5,5,3,3,2,2,2,2
且有dis(i,1!)=∑i=1nkidis(i,1!)=\displaystyle\sum_{i=1}^{n}k_idis(i,1!)=i=1nki
而本题最大的点m!,1≤m≤1e5m!, 1\leq m \leq1e5m!,1m1e5是一个非常大的点,要将整棵树全部保存下来是不可能的

虚树

先来看看这个图(假设所有wi=1w_i=1wi=1,蓝色的字为结点的fff值)
picture2

除了绿色的点之外,其他所有的点的fff值都和他们的子节点相等!
也就是说,如果我们能够移动到点vvv,即有f[1]−2f[v]<0f[1]-2f[v]<0f[1]2f[v]<0,如果f[vv]=f[v]f[vv]=f[v]f[vv]=f[v]那么肯定会继续移动下去
所以这些fff值不变的点都是不重要的
只有我们的目标点i!i!i!和他们的最近公共祖先lcalcalca是有用的,这个就是虚树的概念

这样我们保留了绿色的点,新建的树就是这样:
picture3
现在考虑怎么建虚树
目标点i!i!i!好说,就是他们之间的lcalcalca比较难求
之前我们说了:
这棵树从根节点1!1!1!到点iii的路径中,质因子由大变小,即经过的路径边上的质因数是形如5,5,5,3,3,2,2,2,25,5,5,3,3,2,2,2,25,5,5,3,3,2,2,2,2
所以当i!=p1k1p2k2⋅⋅⋅pnkni!=p_1^{k_1}p_2^{k_2}···p_n^{k_n}i!=p1k1p2k2pnkn,当变成(i+1)!(i+1)!(i+1)!时,这条路径最先改变的地方就是(i+1)(i+1)(i+1)的最大质因子
2432532^43^25^324325321312^13^12131
原来的路径:5,5,5,3,3,2,2,2,25,5,5,3,3,2,2,2,25,5,5,3,3,2,2,2,2
现在的路径:5,5,5,3,3,3,2,2,2,2,25,5,5,3,3,3,2,2,2,2,25,5,5,3,3,3,2,2,2,2,2
可以看到前面555个数5,5,5,3,35,5,5,3,35,5,5,3,3都一样的,也就是说在树上他们是公用这些节点的
那么2432532^43^25^32432532533532^53^35^3253353的最近公共祖先lcalcalca的深度就等于555
可以得到dep[lca((i+1)!,i!)]=sum(maxdiv(i+1),n)dep[lca((i+1)!,i!)]=sum(maxdiv(i+1), n)dep[lca((i+1)!,i!)]=sum(maxdiv(i+1),n)
其中sum(maxdiv(i+1),n)sum(maxdiv(i+1), n)sum(maxdiv(i+1),n)为原来i!i!i!中大于等于maxdiv(i+1)maxdiv(i+1)maxdiv(i+1)的因子个数
这样我们能够顺利找到两个相邻点之间的lcalcalca
为什么不用考虑所有点呢,因为构造这棵树的时候已经是按dfsdfsdfs序排序了,所以考虑相邻的两个点即可

虚树的构造部分代码:

void buildVirtualTree() {
    tot = n; st[top = 1] = 1;
    for (int i = 2; i <= n; i++) {
        dep[i] = dep[i - 1] + 1; int j = i;
        for (; j != mindiv[j]; j /= mindiv[j]) dep[i]++;
        //一般的树都是直接找lca的,但是这里是这样找的
        //上面操作完成后,j = maxdiv(i)
        lcadep[i] = query(n) - query(j - 1);//这一步就是查找sum(maxdiv(i), n)
        //可以按照上面的图中的5!和6!两个点模拟一下
        for (j = i; j != 1; j /= mindiv[j]) upd(mindiv[j], 1);
    }
    //下面和一般的虚树板子类似
    for (int i = 2; i <= n; i++) {
        while (top > 1 && dep[st[top - 1]] >= lcadep[i])
            add_edge(st[top - 1], st[top]), top--;
        if (dep[st[top]] != lcadep[i]) {
            dep[++tot] = lcadep[i];
            add_edge(tot, st[top]);
            st[top] = tot;
        }
        st[++top] = i;
    }
    while (top > 1) add_edge(st[top - 1], st[top]), top--;
}

代码

#include<bits/stdc++.h>
#define lowbit(x) x&-x
using namespace std;
typedef long long ll;
const int MAX = 2e5 + 10;
//建立虚树点数tot < 2n, 空间开两倍

int n, w[MAX];
ll ans;

//树状数组
int c[MAX];
void upd(int p, int k) { for (; p <= n; p += lowbit(p)) c[p] += k; }
int query(int p) { int res = 0; for (; p; p -= lowbit(p)) res += c[p]; return res; }

int mindiv[MAX];
void sieve(int siz) {//筛mindiv
    for (int i = 2; i <= siz; i++)
        if (!mindiv[i])
            for (int j = i; j <= siz; j += i)
                if (!mindiv[j])
                    mindiv[j] = i;
}

int lcadep[MAX], dep[MAX];
int st[MAX], top, tot;//stack, top, tot:虚树点数
vector<int> g[MAX];//虚树
void add_edge(int u, int v) { g[u].push_back(v), g[v].push_back(u); }

void buildVirtualTree() {
    tot = n; st[top = 1] = 1;
    for (int i = 2; i <= n; i++) {
        dep[i] = dep[i - 1] + 1; int j = i;
        for (; j != mindiv[j]; j /= mindiv[j]) dep[i]++;
        lcadep[i] = query(n) - query(j - 1);
        for (j = i; j != 1; j /= mindiv[j]) upd(mindiv[j], 1);
    }
    //建树
    for (int i = 2; i <= n; i++) {
        while (top > 1 && dep[st[top - 1]] >= lcadep[i])
            add_edge(st[top - 1], st[top]), top--;
        if (dep[st[top]] != lcadep[i]) {
            dep[++tot] = lcadep[i];
            add_edge(tot, st[top]);
            st[top] = tot;
        }
        st[++top] = i;
    }
    while (top > 1) add_edge(st[top - 1], st[top]), top--;
}

void dfs(int u, int fa) {
    ans += 1ll * w[u] * dep[u];//ans最开始是rt = 1时的答案
    for (auto &v: g[u])
        if (v != fa) {
            dfs(v, u);
            w[u] += w[v];
        }
}

void dfs2(int u, int fa) {//如果rt移动之后答案变小就一直移动下去,直到答案不在变小
    for (auto &v: g[u])
        if (v != fa) {
            //rt从u转移到v的代价
            //+(w[1] - w[v]) - w[v]
            if (w[1] - 2 * w[v] < 0) {
                ans += 1ll * (w[1] - 2 * w[v]) * (dep[v] - dep[u]);//一步的代价*距离
                dfs2(v, u);
            }
        }
}

void init() {
    ans = top = 0;
    for (int i = 1; i <= tot; i++) g[i].clear(), c[i] = w[i] = lcadep[i] = dep[i] = 0;
}

int main() {

    sieve(1e5);
    while (~scanf("%d", &n)) {
        init();
        for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
        buildVirtualTree();
        int rt = 1; dfs(rt, 0); dfs2(rt, 0);
        printf("%lld\n", ans);
    }

    return 0;
}
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值