“蔚来杯“2022牛客暑期多校训练营6 F题: Hash

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=1nj=i+1nXiYjZlca(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(k1)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=2psumLCA(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(p1))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;
}
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值