涂色游戏 题解
bsoj6412 没找到出处。。。
题图
题意简述:给一颗树,一次操作定义为随机选择一个点,染掉该点和它周围一圈的点,问期望多少次染黑所有点。
这是道好题啊!全面考察了容斥、反演、期望和dp,有许多值得注意的细节。
一、做法1(容斥/二项式反演+dp)
1.1 化式子
首先肯定第一个想到的式子就是
这是根据期望的定义直接得到的。
然后发现这个
于是这里有一个套路化法
(
就是改了改枚举的方式,随便想一想应该能够明白了吧(
总之,根据上式我们就可以得到
我们成功把
发现虽然选点可以进行无数次,但是最多只会选有限个点,许多选点是重复的。用实际选择的点的个数,我们可以在不可计算的无限和可计算的有限之间搭上一座桥梁。我们考虑将上面的
注意式子中“某”的含义。可以这样理解这个式子:
我钦定了某
首先我想知道:进行
然后再判断这
将所有可能钦定的情况合起来就是
好,理解了上式,我们来仔细研究
1.2 容斥/二项式反演
首先研究
但这样计算显然是有问题的。因为可能出现有点一次都没有被选中的情况,而这不满足我们“恰好选中”的要求。换句话说,我们只能计算
容易想到容斥掉它。
考虑枚举一次都没有被选中的点,经过仔细思考,我们能够艰难的得到
我无力解释这个式子…各位自己尝试理解一下吧…
把某个点一次都没有被选中画成一个圆圈,用Venn图的形式可能有助于理解。
虽然难以理解…不过好在可以用二项式反演推导。
套入本题
这样就好懂多了,,
不管怎么说,我们终于搞到了
1.3 回到答案式
为方便书写,令
由于当
故
(当
直接枚举是
1.4 树形dp
首先可以做一步简单容斥简化问题
求
实际上就是在树上分配选点,也就是一个背包,而方案数背包的实质是卷积,所以就是用树形dp维护卷积合并。
开始写状态。
随便写写就有转移方程了。
没有父亲援助,自己也不选,只能靠儿子。儿子节点只需要有一个选就可以养活自己。也就是儿子随便乱选减去儿子一个都不选的情况。
自己选了,上下随便。
父亲选了,自己不选,下面随便。
自己选了,上下随便。
照着dp即可。
顺着之前倒着带回去行了。
1.5 时间复杂度
1.5.1 答案式
显然是
1.5.2 树形dp
一次卷积
诶?这不是
实则不然。
设
于是考虑每个节点
后面减去的和式,将抵消掉所有儿子节点产生的时间复杂度!
所以真正的复杂度是
妙啊
这里提供另一道题 loj6289 花朵 ,其部分分解法也是这种方式证明时间复杂度
还要优化的话,可以用NTT来做卷积,还可以用堆来实现从小到大合并减少浪费的时间,类似分治NTT。但都只是常数级优化。
1.6 总结
爆拆期望得无穷级数,尝试去掉无穷,套路化法将
拆掉
1.7 Code
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<ctime>
#include<cstdlib>
#include<algorithm>
#include<queue>
#include<vector>
#include<map>
#include<set>
typedef long long ll;
using namespace std;
(){
ll Rd=0;char c=getchar();
ll answhile(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') ans=ans*10+c-'0',c=getchar();
return ans;
}
const ll MOD=998244353;
(ll x,ll up){
ll QPow=(x+MOD)%MOD;
x=1;
ll answhile(up)
if(up%2==0) x=x*x%MOD,up/=2;
else ans=ans*x%MOD,up--;
return ans;
}
(ll x){
ll Invreturn QPow(x,MOD-2);
}
const ll PTN=1005;
;
ll N[PTN],facInv[PTN];
ll facvoid FacInit(){
[0]=1;for(ll i=1;i<=N;i++) fac[i]=fac[i-1]*i%MOD;
fac[N]=Inv(fac[N]);for(ll i=N-1;i>=1;i--) facInv[i]=facInv[i+1]*(i+1)%MOD;
facInv[0]=1;
facInv}
(ll n,ll m){
ll Cif(n<m) return 0;
return fac[n]*facInv[m]%MOD*facInv[n-m]%MOD;
}
struct Edge{
,v;ll nxt;
ll u}edge[PTN*2];
,last[PTN];
ll graMvoid GraphInit(){graM=0;for(ll i=0;i<PTN;i++) last[i]=0;}
void AddBscEdge(ll u,ll v){
[++graM]=(Edge){u,v,last[u]};
edge[u]=graM;
last}
void AddDbEdge(ll u,ll v){
(u,v);AddBscEdge(v,u);
AddBscEdge}
class Func{public:
[PTN];ll len;
ll sav(){}
Func(ll len){
Functhis->len=len;
for(ll i=0;i<=len;i++) sav[i]=0;
}
void Resize(ll nwLen){
for(ll i=len+1;i<=nwLen;i++) sav[i]=0;
=nwLen;
len}
& operator [] (ll idx){return sav[idx];}
ll/*void Debug(){
cout<<len<<":";
for(ll i=0;i<=len;i++) cout<<sav[i]<<",";cout<<endl;
}*/
};
operator + (Func A,Func B){
Func (max(A.len,B.len));
Func C.Resize(C.len);B.Resize(C.len);
Afor(ll i=0;i<=C.len;i++) C[i]=A[i]+B[i]%MOD;
return C;
}
operator - (Func A,Func B){
Func (max(A.len,B.len));
Func C.Resize(C.len);B.Resize(C.len);
Afor(ll i=0;i<=C.len;i++) C[i]=A[i]-B[i]%MOD;
return C;
}
operator * (Func A,Func B){
Func (A.len+B.len);
Func Cfor(ll i=0;i<=A.len;i++)
for(ll j=0;j<=B.len;j++)
[i+j]=(C[i+j]+A[i]*B[j])%MOD;
Creturn C;
}
(){
Func I(1);A[1]=1;return A;
Func A}
(){
Func E(0);A[0]=1;return A;
Func A}
[PTN][2][2];
Func fvoid FDFS(ll u,ll father){
,s00_01,s10_11;
Func s00=s00_01=s10_11=E();
s00for(ll i=last[u];i!=0;i=edge[i].nxt){
=edge[i].v;if(v==father) continue;
ll v(v,u);
FDFS=s00*f[v][0][0];
s00=s00_01*(f[v][0][0]+f[v][0][1]);
s00_01=s10_11*(f[v][1][0]+f[v][1][1]);
s10_11}
[u][0][0]=s00_01-s00;
f[u][0][1]=I()*s10_11;
f[u][1][0]=s00_01;
f[u][1][1]=I()*s10_11;
f}
[PTN];
ll Avoid Solve(){
(1,0);
FDFSfor(ll k=0;k<=N;k++) A[k]=(C(N,k)-(f[1][0][0][k]+f[1][0][1][k])%MOD+MOD)%MOD;
=0;
ll Ansfor(ll k=0;k<N;k++){//注意<N
=0;
ll tfor(ll p=0;p<=k;p++){
;
ll alphaif((k-p)%2==0) alpha=1;
else alpha=(-1+MOD)%MOD;
=(t+C(k,p)*alpha%MOD*N%MOD*Inv(N-p)%MOD)%MOD;
t}
=t*A[k]%MOD;
t=(Ans+t)%MOD;
Ans}
<<Ans;
cout}
int main(){
=Rd();FacInit();
N();
GraphInitfor(ll i=1;i<N;i++){
=Rd(),v=Rd();
ll u(u,v);
AddDbEdge}
();
Solvereturn 0;
}
二、做法2(minmax容斥+dp)
2.1 minmax容斥
minmax容斥标准式:
由于期望具有线性性,它可以拓展到期望:
然后把这个式子映射到本题中来。(下文把操作了多少次称为“时间”)
套上期望后:
捋一下思路,由于期望不满足
2.2 处理
于是思考
其中
解得
补充一点,显然
将表达式带入原式中
枚举子集是复杂度的瓶颈。不过发现枚举中许多项的
也就是说,现在我们只需快速求得
思考这式子的意义。其实它就是一个带上了容斥系数的所有
尝试通过树形dp解决
2.3 树形dp
首先简单考虑一下所需的状态。考虑以某个点为根的子树,我们需要记录
可以想到转移大概的形式是在
2.3.1 状态压缩
首先有一个小Trick,可以压掉记录
根据上式,我们在dp时求
这么做的原因是可以压缩掉记录
2.3.1.1 对于转移
比如有两个对象
如果将对象改写为单个变量记录:
而对象的值就是我们所求的(考虑容斥系数的方案数)。成功压缩。
如果不做上面那一个Trick的转化,压缩状态需要 正*正=负 和 负*负=正,而这显然是不成立的。
2.3.1.2 对于新增
如果所有情况的
如果不压缩,操作应该是交换
如果压缩,只需对dp值乘上
2.3.2 设置状态并处理转移
我们的dp实际上是用背包分配
状态压缩后,剩下的主要问题在于合并时
由于转移情况复杂,而背包的本质是卷积,所以用封装性良好的卷积实现。
令
定义
我们开始处理转移。
意思是若
若
若
最后算答案,参考早前化出的答案式即可。
2.4 时间复杂度
答案式部分显然
树形dp卷积的时间复杂度为
2.5 总结
首先我们发现这道题适用于minmax容斥的套路,于是将难求的
然后我们再想办法优化枚举子集,发现
回头观察题面发现是树状结构,必然有其特殊性质,于是猜想用树形dp解决。讨论合并时
2.6 Code
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<cstdlib>
#include<algorithm>
#include<queue>
#include<vector>
#include<map>
#include<set>
using namespace std;
typedef long long ll;
(){
ll Rd=0;char c=getchar();
ll answhile(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') ans=ans*10+c-'0',c=getchar();
return ans;
}
const ll MOD=998244353;
(ll x,ll up){
ll QPow=(x+MOD)%MOD;
x=1;
ll answhile(up)
if(up%2==0) x=x*x%MOD,up/=2;
else ans=ans*x%MOD,up--;
return ans;
}
(ll x){
ll Invreturn QPow(x,MOD-2);
}
const ll PTN=1E3+5;
struct Edge{
,v;ll nxt;
ll u}edge[PTN*2];
,graM,last[PTN];
ll Nvoid GraphInit(){graM=0;for(ll i=0;i<PTN;i++) last[i]=0;}
void AddBscEdge(ll u,ll v){
[++graM]=(Edge){u,v,last[u]};
edge[u]=graM;
last}
void AddDbEdge(ll u,ll v){
(u,v);AddBscEdge(v,u);
AddBscEdge}
class Func{public:
[PTN];
ll sav;
ll len& operator [] (ll idx){return sav[idx];}
ll(){}
Func(ll len){
Functhis->len=len;
for(ll i=0;i<=len;i++) sav[i]=0;
}
void Expand(ll nwLen){
for(ll i=len+1;i<=nwLen;i++) sav[i]=0;
=nwLen;
len}
operator - (){
Func ;B.len=len;
Func Bfor(ll i=0;i<=len;i++) B[i]=(-sav[i]+MOD)%MOD;
return B;
}
/*void Debug(){
cout<<len<<":";
for(ll i=0;i<=len;i++) cout<<sav[i]<<',';cout<<endl;
}*/
};
(){
Func E(0);A[0]=1;
Func Areturn A;
}
(){
Func I(1);A[1]=1;
Func Areturn A;
}
operator + (Func A,Func B){
Func (max(A.len,B.len));
Func C.Expand(C.len);B.Expand(C.len);
Afor(ll i=0;i<=C.len;i++) C[i]=(A[i]+B[i])%MOD;
return C;
}
operator - (Func A,Func B){
Func (max(A.len,B.len));
Func C.Expand(C.len);B.Expand(C.len);
Afor(ll i=0;i<=C.len;i++) C[i]=(A[i]-B[i]+MOD)%MOD;
return C;
}
operator * (Func A,Func B){
Func (A.len+B.len);
Func Cfor(ll i=0;i<=A.len;i++)
for(ll j=0;j<=B.len;j++)
[i+j]=(C[i+j]+A[i]*B[j])%MOD;
Creturn C;
}
[PTN][3];
Func fvoid DFS(ll u,ll fa){
[u][0]=f[u][1]=f[u][2]=E();
ffor(ll i=last[u];i!=0;i=edge[i].nxt){
=edge[i].v;if(v==fa) continue;
ll v(v,u);
DFS[u][0]=f[u][0]*(f[v][0] +f[v][1]);
f[u][1]=f[u][1]*(f[v][0] +f[v][1]+f[v][2]);
f[u][2]=f[u][2]*(f[v][0]*I()+f[v][1]+f[v][2]);
f}
[u][1]=(f[u][1]-f[u][0])*I();
f[u][2]=-(f[u][2]*I());
f}
void Solve(){
(1,0);
DFS=0;
ll Ansfor(ll i=1;i<=N;i++){//注意从1开始,因为minmax容斥不包含空集
=(Ans+N*Inv(i)%MOD*(f[1][0][i]+f[1][1][i]+f[1][2][i]))%MOD;
Ans}
<<(-Ans+MOD)%MOD;
cout}
int main(){
=Rd();
N();
GraphInitfor(ll i=1;i<N;i++){
=Rd(),v=Rd();
ll u(u,v);
AddDbEdge}
();
Solvereturn 0;
}
三、总结
这道题简直人类智慧(
解题思路很具有参考价值,实为一道期望好题!
上周末看到这道题,因为全网都找不到出处也没题解,硬是对着一张题解截图(解法1)和先比我写出来的Waper爷的代码(解法2)杠出来了
我现在感觉我整个人都升华了.jpg