Day 162-Q4 — 2026-09-27 — 転倒数がちょうど K の順列を数える(挿入 DP の遷移を累積和で O(1) にする)

2026-09-27 赤色 Master (Phase 8+) ★★★★★★★★★ DP 高速化(焦点帯 M1)。「幅 $i$ の窓の和」になる遷移を累積和で $O(NK)$ に。$K\le N$ なら五角数定理で $N=10^{18}$ でも解ける別解つき

問題

$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」が同じ「窓の和を累積和で」の遷移を持つ。

概念図:差し込みと窓の和

① (2,1,3)(転倒数 1)に最大値 4 を差し込む:右にある個数だけ転倒が増える (4,2,1,3)転倒 +3 → 4 (2,4,1,3)転倒 +2 → 3 (2,1,4,3)転倒 +1 → 2 (2,1,3,4)転倒 +0 → 1 長さ i の順列 ⇔ (長さ i−1 の順列, 増分 t ∈ {0,…,i−1}) が 1 対 1 ② dpᵢ[k] = dpᵢ₋₁[k−i+1] + … + dpᵢ₋₁[k](幅 i の窓の和) 012345678910111213 dpᵢ₋₁ の添字(i = 4、k = 9 の例) 幅 i=4 の窓 → dp₄[9] 累積和 S[t] = dp[0]+…+dp[t−1] を使うと dpᵢ[k] = S[k+1] − S[k−i+1] で O(1) 左端を k−i にずらす(幅 i+1)と、N ≤ 8 の 108 組中 71 組で誤る(実測)

ヒント(クリックで段階的に開示)

ヒント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最大値を差し込む DP

$1,\dots,i-1$ の順列の転倒数を保ったまま、$i$ を右端から $t$ 個目の隙間に入れると転倒が $t$ 増える($i$ はそれまでの誰よりも大きいので、右にいる $t$ 個すべてと転倒)。$t$ は $0$ から $i-1$ まで 1 つずつ取れるので、長さ $i$ の順列と「(長さ $i-1$ の順列, $t$)」が 1 対 1 に対応する。

2窓の和を累積和で

$\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 組で誤った(実測)。

3Python で速く書く

[... 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) で引く。

4上限の打ち切り

長さ $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$ を半分以下にできる。

5素直な三重ループとの比較(実測)
$(N,K)$本解三重ループ $O(N^2K)$
$(200, 2000)$0.09 s8.15 s
$(400, 10^4)$0.58 s177.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 原典との照合

自己評価

自分の回答

気づき・メモ