問題
$1,2,\dots,N$ の順列 $p$ のうち、転倒数($i<j$ かつ $p_i>p_j$ となる組 $(i,j)$ の個数)がちょうど $K$ であるものの個数を $998244353$ で割った余りを求めよ。
入力形式
N K
制約
| # | 条件 |
|---|---|
| 1 | $1\le N\le 1000$ |
| 2 | $0\le K\le 10^4$ |
| 3 | 入力はすべて整数 |
入出力例
入力例1
3 1出力例1
2(転倒数 1 の順列は $(2,1,3)$ と $(1,3,2)$)
入力例2
4 3出力例2
6入力例3
10 20出力例3
230131入力例4
1000 10000出力例4
507792345背景と出典
転倒数ごとの順列の個数は Mahonian 数(OEIS A008302)と呼ばれ、母関数は $\prod_{i=1}^{N}(1+x+\dots+x^{i-1})$。Knuth『The Art of Computer Programming』第 3 巻 5.1.1 節などで扱われる(本セッションでは原典を参照していない)。
AtCoder では Educational DP Contest の「M - Candies」が同じ「窓の和を累積和で」の遷移を持つ。
概念図:差し込みと窓の和
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$1,\dots,i-1$ の順列に、いちばん大きい $i$ を差し込むと考える。差し込む位置によって転倒数はいくつ増えるか。
ヒント2(アプローチ)
$i$ を右から $t$ 番目の隙間($t=0,\dots,i-1$)に入れると、$i$ より右にある $t$ 個との転倒がちょうど $t$ 個増える。他の組の転倒は変わらない。よって
$$\mathrm{dp}_i[k]=\sum_{t=0}^{i-1}\mathrm{dp}_{i-1}[k-t]$$
素直に書くと $O(N\cdot K\cdot N)=10^{10}$。
ヒント3(誘導)
和は $\mathrm{dp}_{i-1}$ の 連続した幅 $i$ の区間 の和。累積和 $S[t]=\mathrm{dp}_{i-1}[0]+\dots+\mathrm{dp}_{i-1}[t-1]$ を作れば
$$\mathrm{dp}_i[k]=S[\min(k,L-1)+1]-S[\max(0,k-i+1)]$$
で 1 項 $O(1)$。Python では itertools.accumulate とリストの一括計算で書くと速い。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from itertools import accumulate
from operator import sub
MOD = 998244353
def main():
n, K = map(int, sys.stdin.readline().split())
if K > n * (n - 1) // 2:
print(0)
return
dp = [1] # 長さ 1 の順列: 転倒数 0 が 1 通り
for i in range(2, n + 1):
# 新しい最大値 i を差し込む位置で、転倒数が 0..i-1 のどれか 1 つだけ増える
top = min(K, i * (i - 1) // 2) # K を超える転倒数は答えに効かない
L = len(dp)
S = [0]
S += accumulate(dp) # S[t] = dp[0] + ... + dp[t-1]
# new[k] = dp[max(0,k-i+1)] + ... + dp[min(k,L-1)] = hi[k] - lo[k]
hi = S[1:] + [S[L]] * (top + 1 - L) # S[min(k+1, L)]
lo = [0] * (i - 1) + S[:top + 2 - i] # S[max(0, k-i+1)]
dp = [x % MOD for x in map(sub, hi[:top + 1], lo[:top + 1])]
print(dp[K])
main()
計算量: $O(N\cdot\min(K,N^2))$。実測(Python 3.14.7、M1 Mac)で $N=1000,K=10^4$ が 1.31 s、$K=5000$ が 0.60 s。
Step-by-Step 解説
$1,\dots,i-1$ の順列の転倒数を保ったまま、$i$ を右端から $t$ 個目の隙間に入れると転倒が $t$ 増える($i$ はそれまでの誰よりも大きいので、右にいる $t$ 個すべてと転倒)。$t$ は $0$ から $i-1$ まで 1 つずつ取れるので、長さ $i$ の順列と「(長さ $i-1$ の順列, $t$)」が 1 対 1 に対応する。
$\mathrm{dp}_i[k]$ は $\mathrm{dp}_{i-1}[k-i+1..k]$ の和(範囲外は 0)。累積和の差で $O(1)$。
窓の左端 は $k-i+1$(幅 $i$)。幅を $i+1$ にずらした版は $N\le8$ の全 108 組中 71 組で誤った(実測)。
[... for k in range(top+1)] の中で min / max 相当の条件分岐をすると遅くなる。最初に書いた分岐版は $(1000,10^5)$ で 23.67 s、本解は 12.35 s(約 2 倍、実測。$(1000,2\times10^4)$ では 3.95 s と 3.87 s でほぼ差なし)。本解は
hi: $S[1..L]$ の後ろに $S[L]$ を並べる($k\ge L-1$ では右端が固定)lo: 先頭に $0$ を $i-1$ 個並べ、その後に $S[0..]$($k<i-1$ では左端が 0)
と 分岐の無い 2 本のリスト にして map(sub, hi, lo) で引く。
長さ $i$ の順列の転倒数は最大 $i(i-1)/2$ なので、配列の長さは $\min(K,i(i-1)/2)+1$ で十分(ただし速さへの効果は小さく、打ち切らない版でも $(1000,10^4)$ で 1.19 s と差は無かった、実測)。$K>N(N-1)/2$ なら答えは 0。対称性 $\mathrm{dp}[k]=\mathrm{dp}[N(N-1)/2-k]$(順列を左右反転)を使えば $K$ を半分以下にできる。
| $(N,K)$ | 本解 | 三重ループ $O(N^2K)$ |
|---|---|---|
| $(200, 2000)$ | 0.09 s | 8.15 s |
| $(400, 10^4)$ | 0.58 s | 177.9 s |
| $(1000, 10^4)$ | 1.31 s | 未計測($(400,10^4)$ から約 18 分と見積もり) |
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 三重ループ | $O(N^2K)$ | 検算用のみ | $(400,10^4)$ で 177.9 s(実測) |
| 母関数 $\prod(1-x^i)/(1-x)^N$ を FPS の log/exp で | $O(K\log K)$ | 不採用 | NTT を Python で書くと定数倍が重く、$K\le10^4$ では累積和 DP の方が短く速いと判断(実測はしていない) |
| $K\le N$ のとき五角数定理 | $O(\sqrt K\cdot K)$(本実装) | 別解として記載 | 下の「前提が変わったら」。$K\le N$ という条件が付くので本問の制約全体は覆えない |
| 累積和 DP(本解) | $O(NK)$ | 採用 | 全列挙と全件一致、$N=1000,K=10^4$ で 1.31 s |
前提が変わったら:
- $K\le N$ で $N$ が巨大(例 $N=10^{18}$)なら、$\prod_{i=1}^{N}(1-x^i)$ の $x^K$ までの係数は $\prod_{i\ge1}(1-x^i)$ と同じで、Euler の五角数定理から $\pm1$ が $O(\sqrt K)$ 個だけ立つ。答えは$$\sum_{t}e_t\binom{N-1+K-t}{K-t}$$($e_t$ は五角数 $t=j(3j\mp1)/2$ で $(-1)^j$)。$N\le58$・$K\le N$ の 610 組で本解の DP と全件一致し、$N=10^{18},K=10^4$ が 0.16 s(実測)。Day156 Q2 の五角数定理と同じ道具
- $N$ が大きく $K$ も大きい($N,K\sim10^5$)なら $O(NK)$ は無理。母関数を FPS の log/exp で扱う(M3 の範囲)
- 法が素数でない なら累積和の差はそのまま使える(割り算が無い)が、五角数の別解は二項係数の逆元が要るので使えない
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 窓の幅を $i+1$ にする | 差し込み位置は $i$ 通り($t=0..i-1$) | 左端 $k-i+1$(ずらすと 108 組中 71 組誤り) |
引き算のあと % MOD を忘れる | 累積和の差が負になりうる | x % MOD(Python の % は常に非負) |
内包表記の中で min/max 分岐 | 1 要素ごとに Python の分岐が走る | 分岐の無い 2 本のリストを作って map(sub, ...)($K$ が大きいと約 2 倍差、実測) |
次のステップ
- 発展1: 転倒数が $K$ 以下 の順列の個数(最後に累積和を 1 回取るだけ)
- 発展2: 「$p_i>p_{i+1}$ となる位置(降下)がちょうど $K$ 個」の順列=Euler 数 $\langle{N\atop K}\rangle$。挿入 DP の遷移が変わる(Day161 Q3 の挿入 DP と比べる)
- 次回予告: 不等式の連立を最短路で解く(牛ゲー)→ Q5
検証ログ(本セッションで実行した数値)
- 全列挙との一致(実測): $N=1..8$、$K=0..N(N-1)/2+2$ の 108 組で、プログラム全体(標準入出力)の出力が全順列の転倒数の集計と全件一致
- 窓の幅を 1 ずらした誤り(実測): 同じ 108 組中 71 組で不一致
- 五角数定理の別解(実測): $N=1,4,7,\dots,58$、$K\le N$ の 610 組で本解と全件一致。$N=10^{18},K=10^4$ で 0.16 s、$K=10^5$ で 5.40 s(二項係数を項ごとに $O(K)$ で計算しているため)
- 実行時間(実測): 本解 $(200,2000)$ 0.09 s、$(400,10^4)$ 0.58 s、$(1000,5000)$ 0.60 s、$(1000,10^4)$ 1.31 s。三重ループ $(200,2000)$ 8.15 s、$(400,10^4)$ 177.9 s。$(1000,10^5)$ は本解でも 12.35 s だったので制約を $K\le10^4$ にした
- 書き方の差(実測): 内包表記の中で分岐する版 $(1000,2\times10^4)$ 3.95 s・$(1000,10^5)$ 23.67 s → 本解 3.87 s・12.35 s
- 扱っていない範囲: FPS log/exp 版の実装と実測、TAOCP 原典との照合