「CTS2019」氪金手游
题目
点这里看题目。
分析
不难发现题目给出的边的结构是一棵树。题目要求的是在有向边限制下,每张牌第一次出现构成的序列是这棵树的一种拓扑序的方案数。
首先,对于这类题目,一个经典的结论是:
第 \(i\) 张牌有 \(W_i\) 的概率被抽出来。那么对于 \(S\subseteq U,S\not=\varnothing\),一张牌 \(x\in S\) 是 \(S\) 中第一个被抽出来的概率为:
\[\sum_{k\ge 0}\left(\frac{\sum_{z\in U}W_z-\sum_{y\in S}W_y}{\sum_{z\in U}W_z}\right)^k\cdot \frac{W_x}{\sum_{z\in U}W_z}=\frac{W_x}{\sum_{y\in S}W_y} \]这个结论正向使用或者逆向使用都比较常见。
一个想了半天的错误做法:
注意到最后答案只和 \(W\) 和一种拓扑序 \(\sigma\) 有关,我们可以直接写出答案:
\[\sum_\sigma\sum_W\left(\prod_{k=1}^np_{\sigma_k,W_k}\prod_{k=1}^nW_k\prod_{k=1}^n\left(\sum_{j=k}^nW_j\right)^{-1}\right) \]然后可以提取公因数,外层枚举 \(W\),内层只需要枚举 \(\sigma\)。这个时候可能就可以开始 DP 了,只需要能够成功维护 \(\sum_\sigma \prod_k p_{\sigma_k,W_k}\) 即可,但是这个东西一点都不简单。
稍微研究一下这个不对的方向,我们发现,最大的错误出在没有联系树结构,而仅仅考虑了抽象的拓扑序 \(\sigma\)。现在我们应当回过头,重新审视一下“树”。
不妨从一个简单的情况开始考虑,我们可以完成“树是外向树”的问题吗?这个时候就要求,结点 \(u\) 必须早于它的所有儿子被取到。套用一下结论,如果 \(u\) 子树内包括 \(u\) 的 \(\sum W\) 为 \(S\),那么此时的概率就是 \(\frac{W_u}{S}\)。现在我们只需要简单地维护 \(dp_{u,s}\),表示 \(u\) 的子树内,\(S=s\) 的情况的概率之和即可。
那么,一般的树和上述情况的区别就在于,一般的树可以有反向边。但是,如果我们根本不管反向边的限制,那么原树就被划分为了若干个独立的连通块,分别计算并乘起来;此时多半会有不合反向边限制的情况,我们可以钦定某些反向边性质被打破,于是反向边就变成了正向边......不难发现,这个算法描述的正是一个容斥的过程。当然,我们大可不必枚举某些反向边云云,直接在遇到反向边的时候,修改 DP 的转移方程,把容斥算进去就可以了。
小结:
- 充分利用已知的结构。比如此题,拓扑序背后还是树结构,如果忽略树结构那么拓扑序本身就会变得非常晦涩;当然,也要学会掉头😅。
不过话说回来,在答案的式子比较容易写出来的时候,对着式子本身进行分析不失为一种有效的方法,只不过本题之中 \(\sigma\) 脱离了树之后就难以处理罢了。 - 考虑简单的子问题。先可以尝试 \(\sigma\) 唯一的情况,接着就可以尝试外向树的情况。最好不要忽略了某些特殊的情况,跨度太大思维难度也比较大。
- 关注一下提到的概率结论和反向边容斥的方法。
代码
#include <cstdio>
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )
const int mod = 998244353;
const int MAXN = 1e3 + 5;
template<typename _T>
void read( _T &x ) {
x = 0; char s = getchar(); bool f = false;
while( s < '0' || '9' < s ) { f = s == '-', s = getchar(); }
while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
if( f ) x = -x;
}
template<typename _T>
void write( _T x ) {
if( x < 0 ) putchar( '-' ), x = -x;
if( 9 < x ) write( x / 10 );
putchar( x % 10 + '0' );
}
struct Edge {
int to, nxt;
}Graph[MAXN << 1];
int tmp[MAXN * 3], inv[MAXN * 3];
int dp[MAXN][MAXN * 3];
int siz[MAXN];
int possi[MAXN][4];
int head[MAXN], cnt = 1;
int N;
inline int Qkpow( int, int );
inline int Mul( int x, int v ) { return 1ll * x * v % mod; }
inline int Inv( const int a ) { return Qkpow( a, mod - 2 ); }
inline int Sub( int x, int v ) { return ( x -= v ) < 0 ? x + mod : x; }
inline int Add( int x, int v ) { return ( x += v ) >= mod ? x - mod : x; }
inline int Qkpow( int base, int indx ) {
int ret = 1;
while( indx ) {
if( indx & 1 ) ret = Mul( ret, base );
base = Mul( base, base ), indx >>= 1;
}
return ret;
}
inline void AddEdge( const int from, const int to ) {
Graph[++ cnt].to = to, Graph[cnt].nxt = head[from];
head[from] = cnt;
}
void Init( const int n ) {
inv[1] = 1;
rep( i, 2, n ) inv[i] = Mul( inv[mod % i], mod - mod / i );
}
void DFS( const int u, const int fa ) {
siz[u] = 1;
rep( j, 1, 3 ) dp[u][j] = possi[u][j];
for( int i = head[u], v ; i ; i = Graph[i].nxt )
if( ( v = Graph[i].to ) ^ fa ) {
DFS( v, u ); int s = siz[u] + siz[v];
if( ! ( i & 1 ) ) {
rep( j, 1, s * 3 ) tmp[j] = 0;
rep( j, 1, siz[u] * 3 )
rep( k, 1, siz[v] * 3 )
tmp[j + k] = Add( tmp[j + k], Mul( Mul( dp[u][j], dp[v][k] ), Mul( j, inv[j + k] ) ) );
rep( j, 1, s * 3 ) dp[u][j] = tmp[j];
} else {
rep( j, 1, s * 3 ) tmp[j] = 0;
rep( j, 1, siz[u] * 3 )
rep( k, 1, siz[v] * 3 ) {
tmp[j + k] = Sub( tmp[j + k], Mul( Mul( dp[u][j], dp[v][k] ), Mul( j, inv[j + k] ) ) );
tmp[j] = Add( tmp[j], Mul( dp[u][j], dp[v][k] ) );
}
rep( j, 1, s * 3 ) dp[u][j] = tmp[j];
}
siz[u] += siz[v];
}
}
int main() {
read( N );
rep( i, 1, N ) {
int su = 0;
rep( j, 1, 3 )
read( possi[i][j] ),
su = Add( su, possi[i][j] );
rep( j, 1, 3 )
possi[i][j] = Mul( possi[i][j], Inv( su ) );
}
rep( i, 1, N - 1 ) {
int u, v;
read( u ), read( v );
AddEdge( u, v ), AddEdge( v, u );
}
Init( N * 3 );
DFS( 1, 0 );
int ans = 0;
rep( j, 1, 3 * N )
ans = Add( ans, dp[1][j] );
write( ans ), putchar( '\n' );
return 0;
}