E - 小紅的樹上移動 (Nowcoder85687 E)
題目大意
小紅有一棵 𝑛 個點的樹,根節點為 1,有一個物塊在根節點上,每次它會等機率隨機移動到當前節點的其中一個子節點,而後等機率隨機傳送到一個同深度節點,若此時它位於葉子節點,則停止移動。
求其移動到子節點的次數的期望值,答案對 998244353 取模。
解題思路
我們可以分別對每一層的情況進行討論。首先使用dfs求出樹在每一個深度時具有的節點數量和葉子節點數量。然後根據題意,我們可以發現,每一層達到葉子節點的期望可以表示為,上一層到達下一層的機率這一層中葉子節點的機率再遍歷次數(樹的深度),即(\(cnt/sum\times p\times\ deep)\)%$mod $然後將每一層的期望累加皆可。
需要注意的是,在計算除法的時候,也需要對結果進行取餘數。但是會遇到一些問題。
常見的取餘:
- (a + b) % p = (a%p + b%p) %p
- (a - b) % p = ((a%p - b%p) + p) %p
- (a * b) % p = (a%p)*(b%p) %p
當我們要計算(\(a\times b)\)%$mod $時,較大的資料往往會超出資料範圍。這時使用快速乘法可以解決這個問題。
typedef long long LL;
LL q_mul(LL a, LL b, LL p){//快速乘法取模
LL ans = 0;
while (b){
if(b&1LL) ans=(ans+a)%p;
//or ans=(ans+(b%2*a)%p)%p;
a = (a +a) % p;
b >>= 1;
}
return ans;
}
對於除法取餘,常用的取餘方式是不成立的,所以要使用費馬小定理(不做證明)。
- a / b=a * pow_mod(b,mod-2,mod)
int mi(int a,int b,int MOD){
int res = 1;
while(b){
if(b&1)res=res*a%MOD;
b>>=1;
a=a*a%MOD;
}
return res%MOD;
}
////////////////////////////////////////////////////
int main(){
int op=((cnt[deep]%mod)*(mi(sum[deep],mod-2,mod))%mod);
//(cnt[deep]/sum[deep])%mod
}
然後使用乘法取餘即可。
對於這道題,我們就可以使用費馬小定理解決上述的問題。
#pragma GCC optimize("O2")
#pragma GCC optimize("O3")
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000500;
int h[N*2],e[N*2],ne[N*2],idx;
int n,max_deep,cnt[N],sum[N];
int mod=998244353;
int ans;
int mi(int a,int b,int MOD){
int res = 1;
while(b){
if(b&1)res=res*a%MOD;
b>>=1;
a=a*a%MOD;
}
return res%MOD;
}
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs1(int u,int fu,int deep){
//避免訪問到父節點,更新最大深度
max_deep=max(max_deep,deep);
bool flag=false;
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(j==fu)continue;
flag=true;
dfs1(j,u,deep+1);
}
if(!flag){
cnt[deep]++;
}
sum[deep]++;
}
//能夠到達下一層的機率p:上一層非葉子節點的比例
//對於每一層,q:葉子節點的機率為這一層葉子節點的比例
//每一層落入葉子結點的期望為q*p*deep,累加得到整棵樹上到達葉子節點的期望
void dfs2(int deep,int zi,int mu){
//zi:到達上一層所有非葉子節點的情況
//mu:到達上一層所有節點的情況
//zi/mu就是到達當前層的機率
//計算完成後更新當前層的zi和mu,進入下一層即可
if(deep>max_deep)return;
if(cnt[deep]){
//有葉子節點的情況
int op=((cnt[deep]%mod)*(mi(sum[deep],mod-2,mod))%mod);
op=(op*(zi%mod))%mod*mi(mu,mod-2,mod)%mod;
op=op*deep%mod%mod;
ans=(ans+op)%mod;
}
dfs2(deep+1,zi*(sum[deep]-cnt[deep]+mod)%mod,mu*sum[deep]%mod);
}
signed main(){
scanf("%d",&n);
memset(h,-1,sizeof(h));
for(int i=1;i<n;i++){
int u,v;
scanf("%d %d",&u,&v);
add(u,v);
add(v,u);
}
dfs1(1,-1,0);
dfs2(0,1,1);
cout<<ans%mod<<endl;
return 0;
}
於是我們使用了兩次dfs和部分最佳化解決了這個問題。
對於這個問題,我們也可以使用期望dp去解決。
對於每個節點i,我們可以發現i節點能夠向下轉移(即到達i+1節點的機率)\(dp[i]=dp[i-1]*(a-b)/a\)
a為全部節點的個數,b為葉子節點的個數
則轉移到i層停止的期望次數就是\(dp[i-1]*a/b*i\)
#pragma GCC optimize("O2")
#pragma GCC optimize("O3")
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000500;
int h[N*2],e[N*2],ne[N*2],idx;
int n,max_deep,cnt[N],sum[N],dp[N];
int mod=998244353;
int ans;
int mi(int a,int b,int MOD){
int res = 1;
while(b){
if(b&1)res=res*a%MOD;
b>>=1;
a=a*a%MOD;
}
return res%MOD;
}
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs1(int u,int fu,int deep){
//避免訪問到父節點,更新最大深度
max_deep=max(max_deep,deep);
bool flag=false;
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(j==fu)continue;
flag=true;
dfs1(j,u,deep+1);
}
if(!flag){
cnt[deep]++;
}
sum[deep]++;
}
//對於每一層,到達這一層的機率為 dp[i]=dp[i-1]*(a-b)/a
//dp[i]為到第i層能夠向下轉移的機率,a為總點數,b為葉子點數
void solve(){
dp[0]=1;//根節點一定可以向下轉移
for(int i=1;i<=max_deep;i++){
int a=sum[i],b=cnt[i];
ans+=dp[i-1]%mod*b%mod*mi(a,mod-2,mod)%mod*i%mod;
ans%=mod;
dp[i]=dp[i-1]%mod*(a-b+mod)%mod*mi(a,mod-2,mod)%mod;
dp[i]%=mod;
}
}
signed main(){
scanf("%d",&n);
memset(h,-1,sizeof(h));
for(int i=1;i<n;i++){
int u,v;
scanf("%d %d",&u,&v);
add(u,v);
add(v,u);
}
dfs1(1,-1,0);
solve();
cout<<ans%mod<<endl;
return 0;
}