F题: Hash
原题链接:https://ac.nowcoder.com/acm/contest/33191/F
题目大意
树 T T T 包含不超过 50 50 50 个节点,根节点为 1 1 1 。
定义
F
(
T
)
=
(
∑
i
=
1
n
∑
j
=
i
+
1
n
X
i
Y
j
Z
l
c
a
(
i
,
j
)
)
m
o
d
998244353
F(T)=(\sum_{i=1}^n\sum_{j=i+1}^nX^iY^jZ^{lca(i,j)})\mod 998244353
F(T)=(i=1∑nj=i+1∑nXiYjZlca(i,j))mod998244353
已知 F ( T ) , X , Y , Z F(T),X,Y,Z F(T),X,Y,Z ,求 T T T 。
题解
不妨将树
T
T
T 分为根节点
1
1
1 与两棵子树(左子树和右子树)。
当左子树和右子树的大小确定时,其中根与左子树,根与右子树,左子树与右子树间对哈希函数的贡献都很容易统计, 因此我们只需要考虑左子树内部和右子树内部的贡献即可即可。
我们通过随机得到大量的可能左子树内哈希值,然后随机生成右子树,检查是否有左子树与其合并后哈希值恰好为 F ( T ) F(T) F(T)(类似于折半搜索的思想)。该算法在通常情况下的复杂度很优秀,可以通过此题。
那么我们考虑如何快速计算一棵子树内的哈希值,以下给出一个
O
(
n
2
)
O(n^2)
O(n2) 的做法:
我们按照编号从小到大枚举每个节点,对于节点
a
a
a ,枚举是可能是
a
a
a 与其他节点的
L
C
A
LCA
LCA 的位置(其实就是
a
a
a 到根的路径上的点,即
a
a
a 的祖先),计算
a
a
a 与编号小于
a
a
a 的节点对哈希函数的贡献,同时更新某种标记以便计算编号大于
a
a
a 的节点与
a
a
a 对答案的贡献。
设节点
a
a
a 与节点
b
b
b (
a
<
b
a<b
a<b )的深度为
k
k
k 的公共祖先为
L
C
A
(
k
)
LCA(k)
LCA(k) ,其中最近公共祖先为
L
C
A
(
p
)
LCA(p)
LCA(p) 。
(设根节点
1
1
1 的深度为
1
1
1 )
我们可以在枚举到
a
a
a 时对辅助数组
s
u
m
sum
sum 进行更新:
s
u
m
L
C
A
(
k
)
+
=
{
X
a
Z
L
C
A
(
k
)
k
=
2
X
a
Z
L
C
A
(
k
)
−
X
a
Z
L
C
A
(
k
−
1
)
o
t
h
e
r
w
i
s
e
sum_{LCA(k)}+= \begin{cases} X^aZ^{LCA(k)}&k=2\\ X^aZ^{LCA(k)}-X^aZ^{LCA(k-1)}&otherwise \end{cases}
sumLCA(k)+={XaZLCA(k)XaZLCA(k)−XaZLCA(k−1)k=2otherwise
(第一种情况即
L
C
A
(
k
)
LCA(k)
LCA(k) 的祖先为根节点
1
1
1 ,即
L
C
A
(
k
)
LCA(k)
LCA(k) 为该子树的根时)
那么在枚举到
b
b
b 时,对哈希函数的贡献可计算为
Y
b
∑
i
=
2
p
s
u
m
L
C
A
(
i
)
Y^b\sum_{i=2}^p sum_{LCA(i)}
Ybi=2∑psumLCA(i)
其中与
a
a
a 有关的项可展开为:
Y
b
(
X
a
Z
L
C
A
(
2
)
+
X
a
Z
L
C
A
(
3
)
−
X
a
Z
L
C
A
(
2
)
+
.
.
.
+
X
a
Z
L
C
A
(
p
)
−
X
a
Z
L
C
A
(
p
−
1
)
)
=
Y
b
X
a
Z
L
C
A
(
p
)
\begin{aligned} &Y^b(X^aZ^{LCA(2)}+X^aZ^{LCA(3)}-X^aZ^{LCA(2)}+...+X^aZ^{LCA(p)}-X^aZ^{LCA(p-1)})\\ =&Y^bX^aZ^{LCA(p)} \end{aligned}
=Yb(XaZLCA(2)+XaZLCA(3)−XaZLCA(2)+...+XaZLCA(p)−XaZLCA(p−1))YbXaZLCA(p)
与哈希函数中的形式相同,这正是我们想要的。
最坏情况下子树退化为链,此时对于每个节点枚举祖先的复杂度为
O
(
n
)
O(n)
O(n) ,计算子树内贡献的复杂度为
O
(
n
2
)
O(n^2)
O(n2) 。
参考代码
#include<bits/stdc++.h>
using namespace std;
template<class T>inline void read(T&x){
char c,last=' ';
while(!isdigit(c=getchar()))last=c;
x=c^48;
while(isdigit(c=getchar()))x=(x<<3)+(x<<1)+(c^48);
if(last=='-')x=-x;
}
#define ll long long
const int MAXN=55,MAXT=4e4+5,P=998244353;
int n=50;
ll F,X,Y,Z;
ll pX[MAXN],pY[MAXN],pZ[MAXN];//幂表
ll sum[MAXN];//辅助数组,用于计算哈希值
int f[MAXN];//存储当前计算的子树
int Fa[MAXT][MAXN];//用于存储左子树
int stk[MAXN],top;//一个栈,用于维护祖先
vector<int>A,B;//左/右子树
map<int,int>mp;//记录对于一个哈希值,是否存在对应的左子树
void init(){
pX[0]=pY[0]=pZ[0]=1;
for(int i=1;i<=n;++i){//对X,Y,Z的幂进行打表
pX[i]=pX[i-1]*X%P;
pY[i]=pY[i-1]*Y%P;
pZ[i]=pZ[i-1]*Z%P;
}
vector<int>().swap(A);
vector<int>().swap(B);
for(int i=2;i<=n/2;++i){
A.push_back(i);//左子树
F=(F-X*pY[i]%P*Z)%P;//左子树内每个点与根的贡献
}
for(int i=n/2+1;i<=n;++i){
B.push_back(i);//右子树
F=(F-X*pY[i]%P*Z)%P;//右子树内每个点与根的贡献
}
for(int i=0;i<A.size();++i){
for(int j=0;j<B.size();++j){
int x=A[i],y=B[j];
F=(F-pX[x]*pY[y]%P*Z)%P;//左右子树间(LCA为根节点1)的贡献
}
}
F=(F+P)%P;//可能为负,注意化为正数
mp.clear();//清空原先的左子树可能哈希值
}
void Rand(vector<int>V){//以V中节点随机生成一棵树
f[V[0]]=1;
for(int i=1;i<V.size();++i){
f[V[i]]=V[rand()%i];//选取一个下标小于他的作为父亲,保证最终构成一棵树
}
}
int Hash(vector<int>V){//计算V构成的树内部的哈希值
int ret=0;
memset(sum,0,sizeof(sum));//初始化
sort(V.begin(),V.end());//根据标号从小到大
for(int i=0,u,v;i<V.size();++i){
u=v=V[i],top=0;
while(v!=1){
stk[++top]=v;v=f[v];//用栈存储它的祖先们
}
int pre=0,now;
while(top){
v=stk[top--];
ret=(ret+pY[u]*sum[v])%P;//计算对哈希值的贡献
now=pX[u]*pZ[v]%P;
sum[v]=(sum[v]+now-pre)%P;//更新sum数组
pre=now;
}
}
return (ret+P)%P;//因为sum数组在过程中可能为负数,所以ret也可能为负数,注意化为正数
}
int main()
{
int T;read(T);
while(T--){
read(F),read(X),read(Y),read(Z);
init();
for(int t=1;t<MAXT;++t){
Rand(A);//枚举40000次左子树
int val=Hash(A);
mp[val]=t;//记录下该棵左子树的哈希值
memcpy(Fa[t],f,sizeof(f));//存下这课左子树
}
while(1){
Rand(B);//枚举右子树
int val=(F-Hash(B)+P)%P;//需要的左子树哈希值
if(mp.find(val)!=mp.end()){//存在对应的左子树
int t=mp[val];
for(int i=2;i<=n/2;++i)f[i]=Fa[t][i];//将左子树取出
break;
}
}
cout<<n<<'\n';
for(int i=2;i<=n;++i)cout<<f[i]<<' '<<i<<'\n';//输出每个点的父亲到它的这条边,即输出一棵树
}
return 0;
}