wangmaocheng
2025-01-04 15:17:56
一种复杂度很大但是常数小到可以过题的做法。
题意:给你一颗有颜色的树,求有多少个联通块满足内部至多只有两种颜色。
到这个题要我们求的是符合要求的联通块个数,自然而然的想到了点分治,那么问题转化为了必须包含根节点的联通块个数。
于是联通块必须包含根节点所在的颜色。
我们可以先把根节点所在的且所有点的颜色都和根节点相同的最大联通块
这一个联通块里的点是无论另一种颜色怎么选择都一定可以被选到的。
而与这个联通块相邻的那些点是比较特殊的,因为如果要选择这个点子树内的点,就一定会选择这个点,所以对于这些点的子树而言,联通块所包含的两种颜色是已经确定了的。
这意味着,我们可以把所有非
然后我们就可以考虑如何 DP 了。
假设已经确定了哪两种颜色是可行的,那么是简单的,假设定义
转移就是
对于不同的选颜色方案,可以选择的点是不同的。
不过根据前文可以得到如下结论。
无论如何,
不在
于是做法也顺理成章的出来了:
我们先把所有和
再把
接着按颜色枚举与
那么对于每一种颜色,其对于
于是这个题就做完了,不过要注意的是,对于每一种颜色,我们求出的方案数是根节点颜色加当前颜色的所有方案的数量,因此还需要减去
都做这种题了应该都会 DDP 吧。
复杂度是
code:
#include <bits/stdc++.h>
#define ll long long
#define pii pair<int,int>
using namespace std;
const int N=5e5+10,M=998244353;
int n,ans,res,old_res,rt;
int a[N];
void do_mod(int &x){
if(x>=M)x-=M;
}
vector<int>e[N];
int ksm(int x,int y){
int res=1;
while(y)
{
if(y&1)res=1ll*res*x%M;
x=1ll*x*x%M;
y>>=1;
}
return res;
}
struct barycenter/*求重心单独开结构体防止变量名重名*/{
int siz[N],msiz[N];
int minn=n,id,sum;
void dfs(int x,int fd){
siz[x]=1;
msiz[x]=0;
for(auto y:e[x])
{
if(y==fd)continue;
dfs(y,x);
siz[x]+=siz[y];
msiz[x]=max(msiz[x],siz[y]);
}
// msiz[x]=max(siz[x],sum-siz[x]-1);
msiz[x]=max(msiz[x],sum-siz[x]);
if(msiz[x]<minn)
{
minn=msiz[x];
id=x;
}
}
void rebuild(int &x,int sig){
sum=sig;
minn=sig;
id=0;
dfs(x,sig);
x=id;
}
void ddfs(int x,int fd){
siz[x]=1;
for(auto y:e[x])
{
if(y==fd)continue;
dfs(y,x);
siz[x]+=siz[y];
}
}
}B;
int deep[N],siz[N],fa[N],son[N];//描述树性质的数组
int dfn[N],id[N],top[N],cnt,len[N];//描述树链性质的数组
int g[N],f[N];//dp数组
int old_g[N];//为了撤销用
pii point[N];int poi;//所有的与S相邻的点
int croot,ccol;//对于某个与S相邻的点,其子树内可以选择的两种颜色
void dfs_dp(int x,int fd)/*预处理的dfs*/{
f[x]=1;
g[x]=1;
siz[x]=1;
son[x]=0;
fa[x]=fd;
deep[x]=deep[fa[x]]+1;
top[x]=0;
for(int y:e[x])
{
if(y==fd)continue;
if(a[y]!=a[x])
{
point[++poi]={y,x};
continue;
}
dfs_dp(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])
{
son[x]=y;
}
f[x]=1ll*f[x]*(f[y]+1)%M;
}
for(int y:e[x])
{
if(y==fd)continue;
if(y==son[x]||a[y]!=a[x])continue;
g[x]=1ll*g[x]*(f[y]+1)%M;
}
}
void dfs_dfn(int x){
dfn[x]=++cnt;
id[cnt]=x;
len[x]=0;
if(!top[x])top[x]=x;
len[top[x]]++;
if(son[x])
{
top[son[x]]=top[x];
dfs_dfn(son[x]);
}
for(int y:e[x])
{
if(y==fa[x]||y==son[x]||a[y]!=a[x])continue;
dfs_dfn(y);
}
}
void dfs_y(int x,int fd){
f[x]=1;
for(int y:e[x])
{
if(y==fd)continue;
if(a[y]!=croot&&a[y]!=ccol)continue;
dfs_y(y,x);
f[x]=1ll*f[x]*(f[y]+1)%M;
}
}
struct mac/*矩阵*/{
int a[2][2];
}empty,ifg[N*4],*root[N],*tree;
//为了减小常数,每一个链开一个线段树
mac *inc;
int add;
mac operator *(mac x,mac y){
mac res=empty;
for(int i=0;i<2;i++)
{
for(int k=0;k<2;k++)
{
for(int j=0;j<2;j++)
{
res.a[i][j]=(res.a[i][j]+1ll*x.a[i][k]*y.a[k][j])%M;
}
}
}
return res;
}
struct seg{
void build(int l,int r,int p){
if(l==r)
{
tree[p].a[0][0]=g[id[add+l]];
tree[p].a[1][0]=g[id[add+r]];
tree[p].a[1][1]=1;
return;
}
int mid=(l+r)/2;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
tree[p]=tree[p*2+1]*tree[p*2];
}
void change(int pos,int num,int l,int r,int p){
if(l==r)
{
tree[p].a[0][0]=tree[p].a[1][0]=num;
return;
}
int mid=(l+r)/2;
if(mid>=pos)
{
change(pos,num,l,mid,p*2);
}
else
{
change(pos,num,mid+1,r,p*2+1);
}
tree[p]=tree[p*2+1]*tree[p*2];
}
}T;
void dfs_build(int x,int fd){
if(top[x]==x)
{
root[x]=inc;
inc+=len[x]*4;
add=dfn[x]-1;
tree=root[x];
T.build(1,len[x],1);
}
for(int y:e[x])
{
if(y==fd||a[y]!=a[x])continue;
dfs_build(y,x);
}
}
bool cmp(pii x,pii y){
return a[x.first]<a[y.first];
}
void change(int x,int num)/*将g[x]的值*num */{
if(old_g[x]==-1)old_g[x]=g[x];
g[x]=1ll*g[x]*num%M;
while(x)
{
add=dfn[top[x]]-1;
tree=root[top[x]];
int ni=ksm(tree[1].a[1][0]+1,M-2);
T.change(dfn[x]-dfn[top[x]]+1,g[x],1,len[top[x]],1);
int s=tree[1].a[1][0];
if(top[x]==rt)
{
res=s;
break;
}
x=fa[top[x]];
int new_f=s;
if(old_g[x]==-1)
{
old_g[x]=g[x];
}
g[x]=1ll*g[x]*ni%M*(new_f+1)%M;
}
}
void back(int x)/*DDP的撤销*/{
if(~old_g[x])
{
g[x]=old_g[x],old_g[x]=-1;
}
while(x)
{
add=dfn[top[x]]-1;
tree=root[top[x]];
T.change(dfn[x]-dfn[top[x]]+1,g[x],1,len[top[x]],1);
if(top[x]==rt)
{
break;
}
x=fa[top[x]];
if(~old_g[x])
{
g[x]=old_g[x];
old_g[x]=-1;
}
}
}
void solve(int x,int sum)/*点分治的主要函数*/{
if(sum==1)
{
ans++;
return;
}
if(sum==2)
{
ans+=3;
return;
}
B.rebuild(x,sum);
B.ddfs(x,sum);//找到重心
poi=0,cnt=0,inc=ifg,rt=x;
dfs_dp(x,0);//求出S这颗子树的f值,顺便预处理DDP的树剖
do_mod(ans+=f[x]),old_res=f[x];
dfs_dfn(x);//DDP的树剖预处理
for(int i=1;i<=poi;i++)
{
croot=a[x],ccol=a[point[i].first];
dfs_y(point[i].first,point[i].second);//找到与S相邻的点的f值
}
dfs_build(x,0);//DDP的建线段树
sort(point+1,point+1+poi,cmp);
for(int l=1;l<=poi;l++)
{
int r=l;
while(r<poi&&a[point[r+1].first]==a[point[l].first])r++;
//颜色相同的块一起处理
for(int i=l;i<=r;i++)
{
change(point[i].second,f[point[i].first]+1);
}
do_mod(ans+=(res-old_res)<0?(res-old_res+M):res-old_res);
//要减去S内部的贡献
for(int i=l;i<=r;i++)
{
back(point[i].second);
}l=r;
}
// cout<<x<<' '<<ans<<'\n';
for(int y:e[x])
{
for(int o=0;o<(int)e[y].size()-1;o++)
{
if(e[y][o]==x)
{
swap(e[y][o],e[y][(int)e[y].size()-1]);
}
}
e[y].pop_back();
solve(y,B.siz[y]);
}
}
signed main()
{
// freopen("in.in","r",stdin);
// freopen("1.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr);
//
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
memset(old_g,-1,sizeof old_g);
solve(1,n);
cout<<ans;
return 0;
}
/*
考虑DDP部分的细节
f[i]代表答案,g[i]代表轻儿子的答案
转移为
f[i]=g[i]*(f[son]+1)
换成矩阵的形式
[f[son],1]*U=[f[i],1]
于是转移矩阵 U就是
g[i],0
g[i],1
而DDP的撤销部分是用一种记录的方式实现的
在树剖对g值进行改变时,记录下原本的g值
然后做完之后再复原
*/