2020牛客暑期多校训练营Infinite Tree(虚树,质因数分解,线段树/树状数组,树形DP,换根)

Infinite Tree

题目描述

在这里插入图片描述

输入描述:

在这里插入图片描述

输出描述:

在这里插入图片描述

示例1

输入

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

输出

3
17
0

题目大意

定义了一个函数 m i n d i v ( n ) mindiv(n) mindiv(n)返回 n n n的最小非 1 1 1因子。现在有一棵无限的树,其连边的规则是对于所有大于 1 1 1 n n n n m i n d i v ( n ) \frac{n}{mindiv(n)} mindiv(n)n之间有连边,所有质数都与 1 1 1相连,这样就构造出一棵以 1 1 1为根的无限的树。
现在要求你找到一个点 u u u使得 u u u到给出的 m m m个点的阶乘点乘上权值和最小。
即求一个 u u u,使得 ∑ i = 1 m w i δ ( u , i ! ) \mathop{\sum}\limits_{i=1}^m w_i\delta(u,i!) i=1mwiδ(u,i!)最小,其中 w i w_i wi题目中给出,函数 δ ( u , v ) \delta(u,v) δ(u,v)表示树上 u , v u,v u,v之间的距离。

分析

这题需要的算法比较多,汇总放到了注脚,算法名称已经加粗。
首先手动画出一部分的树试试。比如,画到 14 14 14
在这里插入图片描述
可以发现树的增长是很快的,如果全部建出来跑的话肯定是会 T L E TLE TLE的,所以肯定不是把这棵树给建出来。

不妨我们先考虑如果建出来了,要用什么来算呢。
首先肯定是想到树形 d p dp dp1,因为很容易联想到树形 d p dp dp的板子题,求一个点到所有的节点路径之和最小。那么这题就是树形 d p dp dp
先看怎么 d p dp dp
对于根节点而言,求出这个答案很简单,相信有过树形 d p dp dp经验的人都会。
定义 d p 1 [ i ] dp1[i] dp1[i]表示以 i i i为根的子树到 i i i w w w和, d p 2 [ i ] dp2[i] dp2[i]表示乘上距离之后的答案。
d e p [ i ] dep[i] dep[i]表示 i i i的深度, x x x为当前节点, s o n son son为子节点。
则有:
d p 1 [ x ] = w [ x ] , d p 2 [ x ] = 0 dp1[x]=w[x],dp2[x]=0 dp1[x]=w[x],dp2[x]=0
d p 1 [ x ] + = d p 1 [ s o n ] dp1[x]+=dp1[son] dp1[x]+=dp1[son]
d p 2 [ x ] + = d p 2 [ s o n ] + ( d e p [ s o n ] − d e p [ x ] ) ∗ d p 1 [ s o n ] dp2[x]+=dp2[son]+(dep[son]-dep[x])*dp1[son] dp2[x]+=dp2[son]+(dep[son]dep[x])dp1[son]

那么要把当前的根状态移到其他的子节点,就要用到换根2

如果 x x x为根转移到 s o n son son为根,则有:
d p 2 [ x ] − = d p 2 [ s o n ] + ( d e p [ s o n ] − d e p [ x ] ) ∗ d p 1 [ s o n ] dp2[x]-=dp2[son]+(dep[son]-dep[x])*dp1[son] dp2[x]=dp2[son]+(dep[son]dep[x])dp1[son]
d p 1 [ x ] − = d p 1 [ s o n ] dp1[x]-=dp1[son] dp1[x]=dp1[son]
d p 2 [ s o n ] + = d p 2 [ x ] + ( d e p [ s o n ] − d e p [ x ] ) ∗ d p 1 [ x ] dp2[son]+=dp2[x]+(dep[son]-dep[x])*dp1[x] dp2[son]+=dp2[x]+(dep[son]dep[x])dp1[x]
d p 1 [ s o n ] + = d p 1 [ x ] dp1[son]+=dp1[x] dp1[son]+=dp1[x]
如此,就可以解决。但是这不是本题的重点。


我们可以来想一下,最后 u u u肯定是会选在什么节点上的。由于是到所有阶乘点的距离和,所以如果当前点不在阶乘点上,那么肯定是移动到阶乘点上更优(这里各位可以自己画下理解)。因此,答案肯定是在所有的阶乘点,比如 1 , 2 , 6 , 24 … 1,2,6,24\dots 1,2,6,24
所以这些点是关键的,其它都是混混。所以我们可以将这些节点视为 k e y key key。看到这里,你想到了什么?没错,虚树3。只要保留 k e y key key就可以了。

根据学习的虚树的知识,我们可以想到, k e y key key其实是比较好找的,主要是 L C A LCA LCA,由于实树建不动,所以不能简单地用倍增解决了,要直接从相邻两个数的阶乘怼出 L C A LCA LCA的深度等信息。可是发现好像并没有什么公式可以直接推出其 L C A LCA LCA,因此,我们需要更深一步的探究。

首先考虑 a ! a! a! ( a + 1 ) ! (a+1)! (a+1)! d f n dfn dfn那个大。由于 ( a + 1 ) ! (a+1)! (a+1)!的因子中肯定包含了 a ! a! a!的因子,所以不断除以 m i n d i v mindiv mindiv之后,肯定是 ( a + 1 ) ! (a+1)! (a+1)!的深度更大。所以我们可以不用对这些阶乘数排序,因为它们本来就是 d f n dfn dfn从小到大的。

其次,我们不妨列一个表格,记录每个阶乘数质因数分解后有多少个分别质数。

235711
2!10000
3!11000
4!31000
5!31100
6!42100
7!42110
8!72110

列出上面的表格,然后再求相邻的 L C A LCA LCA试试。
2 ! 2! 2! 3 ! : 3!: 3!: 1,深度为1
3 ! 3! 3! 4 ! : 4!: 4!: 6,深度为3
4 ! 4! 4! 5 ! : 5!: 5!: 1,深度为1
5 ! 5! 5! 6 ! : 6!: 6!: 15,深度为3
6 ! 6! 6! 7 ! : 7!: 7!: 1,深度为1
7 ! 7! 7! 8 ! : 8!: 8!: 5040,深度为7
我们可以发现,对于 a ! a! a! ( a + 1 ) ! (a+1)! (a+1)!来说,它们的 L C A LCA LCA是从大到小公共的质因子的乘积,遇到不同的就停止,深度是其个数。所以,对于每个阶乘,都可以快速地算得 L C A LCA LCA。这里用到了质因数分解或者素数筛4


但是还是不够快,如果对于每个数都扫一次的话,还是很慢。所以,需要一个快速地查找求和,修改的算法,那就是用到了线段树或树状数组5

这样就可以存入,快速修改,快速求和,代码中用线段树。
还有一点,可以发现,对于相邻的阶乘,其质因子的差别在于多出来那个数。所以,其实线段树插入时,只要考虑最后一个数。


这样终于解决了这题所有的问题,建起了虚树。难点在于,不能建实树,搞得很慌。 d p dp dp很简单就是简单的板子,所以这题的重点在于建立虚树。

代码

#include<bits/stdc++.h>
#define ll long long
#define inf 1ll<<60
using namespace std;
const int MAXN=1e5+10;
int num[MAXN],w[MAXN<<1],d[MAXN<<1],stk[MAXN];
int ldfn[MAXN],rdfn[MAXN],dep[MAXN],lcad[MAXN],m;
int mndv[MAXN],pcnt=0;
ll dp1[MAXN<<1],dp2[MAXN<<1],ans;
vector<int> vir[MAXN<<1];
ll tr[MAXN<<2];//注意开long long
void Build(int i,int l,int r)
{
    tr[i]=0;
    if(l==r) return;
    int mid=l+r>>1;
    Build(i<<1,l,mid);
    Build(i<<1|1,mid+1,r);
}
void Change(int i,int l,int r,int x)
{
    if(l==r){
        tr[i]++;
        return;
    }
    int mid=l+r>>1;
    if(x<=mid) Change(i<<1,l,mid,x);
    else Change(i<<1|1,mid+1,r,x);
    tr[i]=tr[i<<1]+tr[i<<1|1];
}//x是插入的质数,要将其计数+1
int Query(int i,int x,int l,int r)
{              
    if(l>=x) return tr[i];
    int mid=l+r>>1;
    if(x<=mid) return Query(i<<1,x,l,mid)+Query(i<<1|1,x,mid+1,r);
    else if(x>mid) return Query(i<<1|1,x,mid+1,r);
}//查找当前质数的个数
void build()
{//^不等于,相当于!=
    dep[1]=1;
    for(int i=2;i<=m;i++){
        dep[i]=dep[i-1];
        int j=i;
        while(j^mndv[j]) j/=mndv[j];
        lcad[i]=Query(1,j,1,m)+1;
        for(j=i;j^1;dep[i]++,j/=mndv[j]) Change(1,1,m,mndv[j]);
    }
    int top=0,tot=m;stk[++top]=1;
    for(int i=2;i<=m;i++){
        if(top==1||lcad[i]==dep[stk[top]]){
            stk[++top]=i;continue;
        }
        while(top>1&&lcad[i]<=dep[stk[top-1]]){
            vir[stk[top-1]].push_back(stk[top]);
            top--;
        }//建虚树的基本操作,不会的建议去学习一下
        if(lcad[i]^dep[stk[top]]){
            dep[++tot]=lcad[i];
            w[tot]=0;
            vir[tot].push_back(stk[top]);
            stk[top]=tot;
        }
        stk[++top]=i;
    }
    while(top>1){
        vir[stk[top-1]].push_back(stk[top]);
        top--;
    }
}//原理同上,供参考
void dfs1(int x,int fa)
{
    dp1[x]=w[x];
    dp2[x]=0;
    for(int i=0;i<vir[x].size();i++){
        int son=vir[x][i];
        if(son==fa) continue;
        dfs1(son,x);
        dp1[x]+=dp1[son];
        dp2[x]+=dp2[son]+(dep[son]-dep[x])*dp1[son];
    }
}
void dfs2(int x,int fa)
{
    ans=min(ans,dp2[x]);
    for(int i=0;i<vir[x].size();i++){
        int son=vir[x][i];
        if(son==fa) continue;
        ll x1=dp1[x],x2=dp2[x],son1=dp1[son],son2=dp2[son];
        dp2[x]-=dp2[son]+(dep[son]-dep[x])*dp1[son];
        dp1[x]-=dp1[son];
        dp2[son]+=dp2[x]+(dep[son]-dep[x])*dp1[x];
        dp1[son]+=dp1[x];
        dfs2(son,x);
        dp1[x]=x1,dp2[x]=x2,dp1[son]=son1,dp2[son]=son2;
    }
}//树形dp+换根,非重点,且是模板一套的问题,上面已经分析
int main()
{
    mndv[1]=1;
    for(int i=2;i<MAXN;i++)
        if(!mndv[i])
            for(int j=i;j<MAXN;j+=i)
                if(!mndv[j]) mndv[j]=i;//预处理出每个数的mindiv,之后分解时可以用
    while(~scanf("%d",&m)){
        for(int i=1;i<=m;i++)
            scanf("%d",&w[i]);
        for(int i=0;i<=m*2;i++){
            vir[i].clear();
            dp1[i]=dp2[i]=0;
        }
        Build(1,1,m);
        build();
        dfs1(1,0);
        ans=dp2[1];
        dfs2(1,0);
        printf("%lld\n",ans);
    }
}

END

叉姐的题目,好难。
这里再放虚树的笔记。
是时候交作业了。


  1. 树形 d p dp dp ↩︎

  2. 换根 ↩︎

  3. 虚树 ↩︎

  4. 质因数分解,素数筛 ↩︎

  5. 线段树或树状数组 ↩︎

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值