把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【洛谷4075】[SDOI2016] 模式字符串(点分治+哈希)

点此看题面

大致题意: 给你一棵树,每个节点上有一个字母。求有多少对节点(有序),满足其树上路径中所有节点的字母依序连接起来所得的字符串是由给定模式串重复若干次得到的。

前言

打算复习点分治,于是就找上了这道题,应该算比较简单的吧。

看完题面差不多就有了大致想法,写完调了调就过了样例,然后一交爆零。。。

下载了数据发现无比巨大,只好疯狂手玩造小数据。搞了快半个小时,最后找到了一个极其智障的错误(我都没脸说这是什么错误了),改完就过了。。。

太久没写代码了,码力果然不太行啊,一写这种码量稍微有点大的题目就要调半天,唉。

因为这道题是自己做出来的,做完也懒得再去翻题解了,所以也不知道我的方法会不会太复杂,反正姑且记一记吧。

大致思路

题目中要求路径字符串是由给定模式串重复若干次得到的,这个条件看看都麻烦。

考虑到我们在点分治的时候,一条路径一般被拆成:一个子树中的路径(设长度为\(l1\))——当前的根——另一个子树中的路径(设长度为\(l2\))。

那么,我们假设长度为\(l1\)的这条路径是前缀,\(l2\)是后缀,则显然可以通过哈希来判断是否合法。

由于是重复若干次得到的,因此显然就有\(l1+1+l2\equiv 0(mod\ m)\)

也即:\((l1\ mod\ m)+1+(l2\ mod\ m)=m\)。(注意,在\(l1=m-1\)这种边界情况下,\(l2\)可以为\(0\),反之同理)

然后我们就可以发现,对于一条已知长度为\(l\)的路径,在判断其是否为一个前缀或一个后缀之后,只需记下\(l\ mod\ m\)的值,就可以方便地统计答案了。

而其余部分就是点分治板子了吧,具体实现详见代码。

代码

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1000000
#define LL long long
#define ull unsigned long long
#define CU Con ull&
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
#define Gmax(x,y) (x<(y)&&(x=(y)))
using namespace std;
int n,m,a[N+5],ee,lnk[N+5];struct edge {int to,nxt;}e[N<<1];string s;
struct Hash//双哈希
{
	ull x,y;I Hash() {x=y=0;}I Hash(CU a):x(a),y(a){}I Hash(CU a,CU b):x(a),y(b){}
	I Hash operator + (Con Hash& o) Con {return Hash(x+o.x,y+o.y);}
	I Hash operator - (Con Hash& o) Con {return Hash(x-o.x,y-o.y);}
	I Hash operator * (Con Hash& o) Con {return Hash(x*o.x,y*o.y);}
	I bool operator == (Con Hash& o) Con {return x==o.x&&y==o.y;}
}S(233,23333),P[N+5],H1[N+5],H2[N+5];
class FastIO
{
	private:
		#define FS 100000
		#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
		#define tn (x<<3)+(x<<1)
		#define D isdigit(c=tc())
		char c,*A,*B,FI[FS];
	public:
		I FastIO() {A=B=FI;}
		Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
		I void reads(string& x) {x="*";W(isspace(c=tc()));W(x+=c,!isspace(c=tc())&&~c);}
		#undef D
}F;
class DotSolver
{
	private:
		int Rt,Sz[N+5],Mx[N+5],used[N+5],t1[N+5],t2[N+5];LL ans;
		int T,K[N+5],O[N+5];
		I void GetRt(CI x,RI sum,CI lst=0)//找重心
		{
			Sz[x]=1,Mx[x]=0;for(RI i=lnk[x];i;i=e[i].nxt) !used[e[i].to]&&
				e[i].to^lst&&(GetRt(e[i].to,sum,x),Sz[x]+=Sz[e[i].to],Gmax(Mx[x],Sz[e[i].to]));
			Gmax(Mx[x],sum-Sz[x]),Mx[x]<Mx[Rt]&&(Rt=x);
		}
		I void dfs(CI x,CI lst,CI l=1,Hash h=0)//遍历子树,统计路径
		{
			h=h*S+a[x],h==H1[l]&&(K[++T]=(l-1)%m+1,O[T]=1),h==H2[l]&&(K[++T]=(l-1)%m+1,O[T]=2);//判断是否可以作为前缀和后缀
			for(RI i=lnk[x];i;i=e[i].nxt) !used[e[i].to]&&e[i].to^lst&&(dfs(e[i].to,x,l+1,h),0);//继续处理子树
		}
		I void Solve(RI x)//以x为根统计子树内答案
		{
			RI i,j,T_=1;for(used[x]=1,i=lnk[x];i;i=e[i].nxt) if(!used[e[i].to])
			{
				for(dfs(e[i].to,x),j=T_;j<=T;++j) switch(O[j])//统计答案
				{
					case 1:a[x]==(s[K[j]%m+1]&31)&&(ans+=K[j]==m-1?t2[m]+1:t2[m-K[j]%m-1]);break;//注意路径长度为m-1的特殊情况
					case 2:a[x]==(s[m-K[j]%m]&31)&&(ans+=K[j]==m-1?t1[m]+1:t1[m-K[j]%m-1]);break;
				}
				W(T_<=T) O[T_]==1?++t1[K[T_]]:++t2[K[T_]],++T_;//更新计数器
			}
			W(T) O[T]==1?--t1[K[T]]:--t2[K[T]],--T;//清空计数器
			for(i=lnk[x];i;i=e[i].nxt) !used[e[i].to]&&(Rt=0,GetRt(e[i].to,Sz[e[i].to]),Solve(Rt),0);//继续处理子树
		}
	public:
		I void Solve()
		{
			for(RI i=1;i<=n;++i) used[i]=0;//注意初始化,不然除第一问外答案就全是0了。。。
			ans=0,Mx[Rt=0]=1e9,GetRt(1,n),Solve(Rt),printf("%lld\n",ans);
		}
}D;
int main()
{
	RI Tt,i,x,y;for(P[0]=1,i=1;i<=N;++i) P[i]=P[i-1]*S;
	F.read(Tt);W(Tt--)
	{
		for(F.read(n),F.read(m),F.reads(s),ee=0,i=1;i<=n;++i) a[i]=s[i]&31,lnk[i]=0;//计算点权,清空数组
		for(i=1;i^n;++i) F.read(x),F.read(y),add(x,y),add(y,x);F.reads(s);//建树
		for(i=1;i<=n;++i) H1[i]=H1[i-1]+P[i-1]*(s[(i-1)%m+1]&31);//前缀
		for(i=1;i<=n;++i) H2[i]=H2[i-1]+P[i-1]*(s[(m-i%m)%m+1]&31);D.Solve();//后缀
	}return 0;
}
posted @ 2020-05-06 09:41  TheLostWeak  阅读(151)  评论(0编辑  收藏  举报