题目描述
输入描述:
输出描述:
示例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=1∑mwiδ(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从小到大的。
其次,我们不妨列一个表格,记录每个阶乘数质因数分解后有多少个分别质数。
2 | 3 | 5 | 7 | 11 | |
---|---|---|---|---|---|
2! | 1 | 0 | 0 | 0 | 0 |
3! | 1 | 1 | 0 | 0 | 0 |
4! | 3 | 1 | 0 | 0 | 0 |
5! | 3 | 1 | 1 | 0 | 0 |
6! | 4 | 2 | 1 | 0 | 0 |
7! | 4 | 2 | 1 | 1 | 0 |
8! | 7 | 2 | 1 | 1 | 0 |
列出上面的表格,然后再求相邻的
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
叉姐的题目,好难。
这里再放虚树的笔记。
是时候交作业了。