树上启发式合并(dsu on tree)

395 阅读6分钟

算法

大体分这么几步:

  • 递归地解决所有轻儿子,并删除记录的信息
  • 解决重儿子,不删除记录
  • 重新把所有轻儿子合并过来 复杂度为O(NlogN)O(N\log N)

Codeforces 208E Blood Cousins

题目链接

思路

离线询问,对于每次询问,通过倍增找到viv_ipiancestorp_i-ancestor,在这个点上挂上这一次询问。在原树上进行一次树上启发式合并,记录深度为ii的点的个数,据此来回答当前点上挂的询问。

代码

#include<bits/stdc++.h>
#define rep(i,st,ed) for(int i=st;i<=ed;++i)
#define bl(u,i) for(int i=head[u];i;i=e[i].nxt)
#define en puts("")
#define LLM LONG_LONG_MAX
#define LLm LONG_LONG_MIN
#define pii pair<ll,ll> 
typedef long long ll;
typedef double db;
using namespace std;
const ll INF=0x3f3f3f3f;
void read() {}
void OP() {}
void op() {}
template <typename T, typename... T2>
inline void read(T &_, T2 &... oth)
{
    int __=0;
    _=0;
    char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            __=1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        _=_*10+ch-48;
        ch=getchar();
    }
    _=__?-_:_;
    read(oth...);
}
template <typename T>
void Out(T _)
{
    if(_<0)
    {
        putchar('-');
        _=-_;
    }
    if(_>=10)
       Out(_/10);
    putchar(_%10+'0');
}
template <typename T, typename... T2>
inline void OP(T _, T2... oth)
{
	Out(_);
	putchar('\n');
	OP(oth...);
}
template <typename T, typename... T2>
inline void op(T _, T2... oth)
{
	Out(_);
	putchar(' ');
	op(oth...);
}
/*#################################*/
const ll N=1E5+10;
ll n,m;
ll siz[N],f[N][20],dep[N],son[N],ans[N],t[N];
vector<ll> go[N];
vector<pii> qu[N];
void dfs(ll u)
{
	siz[u]=1;
	rep(i,0,16)
		f[u][i+1]=f[f[u][i]][i];
	for(auto v:go[u])
	{
		dep[v]=dep[u]+1;
		f[v][0]=u;
		dfs(v);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])
			son[u]=v;
	}
}
ll find(ll u,ll dis)
{
	for(int i=17;i>=0;--i)
	{
		if(dep[u]-dep[f[u][i]]<=dis)
		{
			dis-=dep[u]-dep[f[u][i]];
			u=f[u][i];
		}
	}
	return u;
}
void update(ll u,ll opt)
{
	t[dep[u]]+=opt;
	for(auto v:go[u])
		update(v,opt);
}
void dsu(ll u,ll opt)
{
	for(auto v:go[u])
	{
		if(v==son[u])
			continue;
		dsu(v,0);
	}
	if(son[u])
		dsu(son[u],1);
	for(auto v:go[u])
	{
		if(v==son[u])
			continue;
		update(v,1);
	}
	++t[dep[u]];
	for(auto it:qu[u])
	{
		ll absdep=it.first+dep[u];
		ans[it.second]=t[absdep]-1;
	}
	if(!opt)
		update(u,-1);
}
void print()
{
	rep(i,1,n)
		op(find(i,1));
	en;
}
int main()
{
	read(n);
	ll fa,u,v;
	rep(i,1,n)
	{
		read(fa);
		if(!fa)
			continue;
		go[fa].emplace_back(i);
	}
	rep(i,1,n)
	{
		if(!dep[i])
		{
			dep[i]=1;
			dfs(i);
		}
	}
	read(m);
	rep(i,1,m)
	{
		read(u,v);
		if(dep[u]<=v)
		{
			ans[i]=0;
			continue;
		}
		ll anc=find(u,v);
		qu[anc].emplace_back(pii(v,i));
	}
	rep(i,1,n)
	{
		if(dep[i]!=1)
			continue;
		dsu(i,0);
	}
	rep(i,1,m)
		op(ans[i]);
	en;
}

Codeforces 246E Blood Cousins Return

题目链接

思路

离线询问,启发式合并时为每个深度维护一个setset,把所有名字插入对应深度的setsetsetset的大小就是该深度的答案。

代码

#include<bits/stdc++.h>
#define rep(i,st,ed) for(int i=st;i<=ed;++i)
#define bl(u,i) for(int i=head[u];i;i=e[i].nxt)
#define en puts("")
#define LLM LONG_LONG_MAX
#define LLm LONG_LONG_MIN
#define pii pair<ll,ll> 
typedef long long ll;
typedef double db;
using namespace std;
const ll INF=0x3f3f3f3f;
void read() {}
void OP() {}
void op() {}
template <typename T, typename... T2>
inline void read(T &_, T2 &... oth)
{
    int __=0;
    _=0;
    char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            __=1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        _=_*10+ch-48;
        ch=getchar();
    }
    _=__?-_:_;
    read(oth...);
}
template <typename T>
void Out(T _)
{
    if(_<0)
    {
        putchar('-');
        _=-_;
    }
    if(_>=10)
       Out(_/10);
    putchar(_%10+'0');
}
template <typename T, typename... T2>
inline void OP(T _, T2... oth)
{
	Out(_);
	putchar('\n');
	OP(oth...);
}
template <typename T, typename... T2>
inline void op(T _, T2... oth)
{
	Out(_);
	putchar(' ');
	op(oth...);
}
/*#################################*/
const ll N=1E5+10,base=233,P=998244353;
ll n,m;
ll siz[N],son[N],dep[N],a[N],ans[N];
vector<ll> go[N];
set<pii> se[N];
vector<pii> qu[N];
string s;
void dfs(ll u)
{
	siz[u]=1;
	for(auto v:go[u])
	{
		dep[v]=dep[u]+1;
		dfs(v);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])
			son[u]=v;
	}
}
void insert(ll u)
{
	auto it=se[dep[u]].lower_bound(pii(a[u],0));
	if(it!=se[dep[u]].end() && (*it).first==a[u])
	{
		ll tmp=(*it).second;
		se[dep[u]].erase(it);
		se[dep[u]].insert(pii(a[u],tmp+1));
	}
	else
		se[dep[u]].insert(pii(a[u],1));
}
void update(ll u,ll opt)
{
	if(opt==1)
		insert(u);
	else
		se[dep[u]].clear();
	for(auto v:go[u])
		update(v,opt);
}
void dsu(ll u,ll opt)
{
	for(auto v:go[u])
	{
		if(v==son[u])
			continue;
		dsu(v,0);
	}
	if(son[u])
		dsu(son[u],1);
	for(auto v:go[u])
	{
		if(v==son[u])
			continue;
		update(v,1);
	}
	insert(u);
	for(auto it:qu[u])
	{
		ll absdep=dep[u]+it.first;
		if(absdep<=1e5)
			ans[it.second]=se[absdep].size();
	}
	if(!opt)
		update(u,-1);
}
int main()
{
	read(n);
	ll u,v,fa;
	rep(i,1,n)
	{
		cin>>s;
		ll hash=0;
		for(auto ch:s)
			hash=(hash*base+ch)%P;
		a[i]=hash;
		read(fa);
		if(fa==0)
			continue;
		go[fa].emplace_back(i);
	}
	rep(i,1,n)
	{
		if(!siz[i])
		{
			dep[i]=1;
			dfs(i);
		}
	}
	read(m);
	rep(i,1,m)
	{
		read(u,v);
		qu[u].emplace_back(pii(v,i));
	}
	rep(i,1,n)
		if(dep[i]==1)
			dsu(i,0);
	rep(i,1,m)
		OP(ans[i]);
	en;
}

Codeforces 741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

题目链接

思路

能够重新打乱形成回文串的字符集必然满足要么所有字母出现次数均为偶数,要么仅有一种字母出现了奇数次。

因为只有2222个字母,且仅考虑它们出现次数的奇偶性,可以把每个字母的奇偶性压缩成intint型二进制的一位,如果第ii个字母出现了奇数次,那么第ii位为11,反之为00

考虑对于树上每个节点pp维护一个二进制下22位的前缀状态sumpsum_p,表示从根节点到当前点路径上每一种字母出现次数的奇偶性状态。那么对于树上任何两个点u,vu,v,它们之间路径上每一种字母的出现次数显然是sumusumvsum_u\oplus sum_v,只要该值为00,那么以这两个点为端点的路径一定可以重新组合成为回文串。

另一种情况是异或后仅剩一位是11,这种情况也是可行的。

现在进行树上启发式合并,对于已经合并过的节点uu,维护前缀状态为sumusum_uuu的最大深度(因为我们要找最长链,显然深度越大链越长)。每次合并一个轻儿子上的节点vv,先找之前节点前缀状态为sumvsum_v的最大深度来维护答案,然后枚举每一位多异或一个11(也就是上面说的另一种情况)得到的最大深度。

代码

#include<bits/stdc++.h>
#define rep(i,st,ed) for(int i=st;i<=ed;++i)
#define bl(u,i) for(int i=head[u];i;i=e[i].nxt)
#define en puts("")
#define LLM LONG_LONG_MAX
#define LLm LONG_LONG_MIN
#define pii pair<ll,ll> 
typedef long long ll;
typedef double db;
using namespace std;
const ll INF=0x3f3f3f3f;
void read() {}
void OP() {}
void op() {}
template <typename T, typename... T2>
inline void read(T &_, T2 &... oth)
{
    int __=0;
    _=0;
    char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            __=1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        _=_*10+ch-48;
        ch=getchar();
    }
    _=__?-_:_;
    read(oth...);
}
template <typename T>
void Out(T _)
{
    if(_<0)
    {
        putchar('-');
        _=-_;
    }
    if(_>=10)
       Out(_/10);
    putchar(_%10+'0');
}
template <typename T, typename... T2>
inline void OP(T _, T2... oth)
{
	Out(_);
	putchar('\n');
	OP(oth...);
}
template <typename T, typename... T2>
inline void op(T _, T2... oth)
{
	Out(_);
	putchar(' ');
	op(oth...);
}
/*#################################*/
const ll N=5E5+10,M=(1<<22)+5;
ll n,tot;
ll head[N],sum[N],siz[N],son[N],dep[N],ans[N],pre[30],ma[M];
struct Edge{
	ll nxt,to,w;
}e[N];
void add_edge(ll u,ll v,ll w,int flag)
{
	e[++tot]=(Edge){head[u],v,w};
	head[u]=tot;
	if(flag)
		add_edge(v,u,w,0);
}
void dfs(ll u,ll sta)
{
	sum[u]=sta;
	siz[u]=1;
	bl(u,i)
	{
		ll v=e[i].to;
		dep[v]=dep[u]+1;
		dfs(v,sta^pre[e[i].w]);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])
			son[u]=v;
	}
}
void merge(ll u,ll top)
{
	if(ma[sum[u]])
		ans[top]=max(ans[top],ma[sum[u]]+dep[u]-2*dep[top]);
	rep(i,1,22)
		if(ma[sum[u]^pre[i]])
			ans[top]=max(ans[top],ma[sum[u]^pre[i]]+dep[u]-2*dep[top]);
	bl(u,i)
	{
		ll v=e[i].to;
		merge(v,top);
	}
}
void update(ll u,ll opt)
{
	if(opt==1)
		ma[sum[u]]=max(ma[sum[u]],dep[u]);
	else
		ma[sum[u]]=0;
	bl(u,i)
	{
		ll v=e[i].to;
		update(v,opt);
	}
}
void dsu(ll u,ll opt)
{
	bl(u,i)
	{
		ll v=e[i].to;
		if(v==son[u])
			continue;
		dsu(v,0);
		ans[u]=max(ans[u],ans[v]);
	}
	if(son[u])
	{
		dsu(son[u],1);
		ans[u]=max(ans[u],ans[son[u]]);
	}
	ans[u]=max(ans[u],ma[sum[u]]-dep[u]);
	rep(i,1,22)
		ans[u]=max(ans[u],ma[sum[u]^pre[i]]-dep[u]);
	ma[sum[u]]=max(ma[sum[u]],dep[u]);
	bl(u,i)
	{
		ll v=e[i].to;
		if(v==son[u])
			continue;
		merge(v,u);
		update(v,1);
	}
	if(!opt)
		update(u,-1);
}
void print()
{
	rep(i,1,n)
		op(sum[i]);
	en;
	rep(i,1,n)
		op(dep[i]);
	en;
}
int main()
{
	read(n);
	rep(i,1,22)
		pre[i]=1ll<<(i-1);
	ll fa;
	char ch;
	rep(i,2,n)
	{
		read(fa);
		scanf("%c",&ch);
		add_edge(fa,i,ch-'a'+1,0);
	}
	dfs(1,0);
	dsu(1,1);
	rep(i,1,n)
		op(ans[i]);
	en;
}

2019 ICPC 南昌站 K Tree

题目链接

思路

树上启发式合并,从小到大维护权值为ii的节点的深度,每次把一个权值为xx新节点合并到权值为yy的节点上,就在权值为2yx2y-x的节点中二分一个深度最大且与当前点距离不超过kk的位置,小于这个点的所有点都可以和当前点贡献答案。因为这一段操作涉及到增删,查rankrank,所以开NN个平衡树来维护。

代码

#include<bits/stdc++.h>
#include<bits/extc++.h>
using namespace __gnu_pbds;
#define rep(i,st,ed) for(int i=st;i<=ed;++i)
#define bl(u,i) for(int i=head[u];i;i=e[i].nxt)
#define en puts("")
#define LLM LONG_LONG_MAX
#define LLm LONG_LONG_MIN
#define pii pair<int,int> 
typedef long long ll;
typedef double db;
using namespace std;
const ll INF=0x3f3f3f3f;
const ll N=2E5+10;
int n,k;
ll ans;
int w[N],son[N],siz[N],dep[N];
vector<int> go[N];
tree<pii,null_type,less<pii>,rb_tree_tag,tree_order_statistics_node_update> t[N];
void dfs(int u)
{
	siz[u]=1;
	for(auto v:go[u])
	{
		dep[v]=dep[u]+1;
		dfs(v);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])
			son[u]=v;
	}
}
void merge(int u,int lca)
{
	int x=2*w[lca]-w[u];
	if(x>=0)
		ans+=t[x].order_of_key(pii(2*dep[lca]-dep[u]+k,2e9));
	for(auto &v:go[u])
		merge(v,lca);
}
void update(int u,int opt)
{
	if(opt==1)
		t[w[u]].insert(pii(dep[u],u));
	else if(opt==-1)
		t[w[u]].erase(pii(dep[u],u));
	for(auto v:go[u])
		update(v,opt);
}
void dsu(int u,bool opt)
{
	for(auto v:go[u])
	{
		if(v==son[u])
			continue;
		dsu(v,0);
	}
	if(son[u])
		dsu(son[u],1);
	for(auto v:go[u])
	{
		if(v==son[u])
			continue;
		merge(v,u);
		update(v,1);
	}
	t[w[u]].insert(pii(dep[u],u));
	if(!opt)
		update(u,-1);
}
int main()
{
	scanf("%d%d",&n,&k);
	rep(i,1,n)
		scanf("%d",w+i);
	int u;
	rep(v,2,n)
	{
		scanf("%d",&u);
		go[u].push_back(v);
	}
	// dep[1]=1;
	dfs(1);
	dsu(1,1);
	printf("%lld\n",ans*2ll);
}