N頂点の木の特徴
「木」は連結で閉路を持たない(無向)グラフです。
頂点の木は、本の辺を持ちます。
1本の辺は、両端の頂点の次数にカウントされるので、頂点の次数を合計した値は になります。
また、連結しているという条件から、次数 0 の頂点はありえず、各頂点の次数は 1以上です。
頂点が2つ以上ある木には少なくとも2個の端末点(次数1の頂点)があります。
そこで、良い木が存在するために必要な条件は、 の条件の下で
となります。
ここで、直径が最大、つまり端から端までの距離が最大になる木を考えます。
のうち、 の頂点の個数を とします。
これらの次数2以上の頂点を一列に並べて、両端に端末点を置くと、 頂点を結ぶ道ができます。
この道の距離は、辺の数 です。
ここで、
を満たす数列 が与えられたときに、直径の良い木が作れるか考えます。
次数2以上の頂点の個数は、次数1の頂点の個数は です。
次数1の頂点の次数の合計は 、次数2以上の頂点の次数の合計は です。
次数2以上の個の頂点と両端の次数1の端末点で距離 の道を作ります。
道に使われなかった頂点は、次数1の頂点 個で、次数2以上の頂点の次数のうち、道を作るときに使われるのは
であり、残り の次数が使われずに残っています。
次数1の頂点の残りと次数3以上の頂点の次数の残りが一致しているため、次数が3以上の頂点と次数1の頂点の間に辺をつくることで余りなく木を作ることができます。
場合の数の計算
以下、 とします。
を満たす整数数列 としてありうるものすべてについて数え上げる問題になります。
このうち、 の個数が 個の数列の数を数えてみます。
まず、個のうち、個の と個の に分ける場合の数を考えます。
この場合の数は、
です。
個の となる の合計は です。このような としてありうるものの数は、
ボールを箱に分けるときの場合の数と同じ問題とみなすことができます。最初に各箱に2個ずつボールを入れて、(残り)
「 個のボールを個の箱に分ける分け方の場合の数」(ボールは区別しない、箱は区別する、ボールが 0個の箱があってもよい)
という問題に帰着できます。
この答えは、
です。
以上から、場合の数に、最大直径 をかけて和をとった
が答えになります。
しかし、この式のままでは時間オーバーになります。そこで、さらに式の変形を考えます。
ヴァンデルモンドの畳み込みの利用
使えそうな公式として、「ヴァンデルモンドの畳み込み」があります。
https://manabitimes.jp/math/622
和の範囲を 0 ~ n にするため、まずは , と置き換えると、
となります。
さらに、公式
を利用すると、
と変形できます。 から、
を最終的な計算式とします。
多くのテストケースで答えを求めますが、
の値は、あらかじめ を mod 998244353 で計算しておくことで計算時間を短縮できます。
コード例 (Julia)
const MOD_ = 998244353 const NMAX = 2_000_000 fact = Vector{Int}(undef, NMAX) invfact = Vector{Int}(undef, NMAX) function setup_fact( ) fact[1] = 1 invfact[1] = 1 for i = 2:NMAX fact[i] = (i * fact[i-1]) % MOD_ # N! invfact[i] = invmod(fact[i],MOD_) # 1/N! end end function Comb( N, M ) if N < 0 || M < 0 || N - M < 0 return 0 end if M == 0 || N - M == 0 return 1 end ret = (invfact[M] * invfact[N-M])%MOD_ ret *= fact[N] return ret % MOD_ end function solve_test(N) ans = N * Comb(2N-4,N-3) ans %= MOD_ ans += Comb(2N-3,N-1) ans %= MOD_ return ans end function solve() # あらかじめ n!, 1/n! を計算 setup_fact() # 入力 readInt() = parse(Int,readline()) T = readInt() for i = 1:T println(solve_test(readInt())) end end # function solve # main solve()