問題
$1,2,\dots,N$ の順列 $p_1,p_2,\dots,p_N$ のうち、次をすべて満たすものの個数を $10^9+7$ で割った余りを出力せよ。
- $p_1=s$、$p_N=f$
- 両端以外のすべての $i$($2\le i\le N-1$)について、$p_i$ は両隣より大きいか、両隣より小さい(上下が交互に入れ替わる「ジグザグ」)
入力形式
N s f
制約
| # | 条件 |
|---|---|
| 1 | $2\le N\le 2000$ |
| 2 | $1\le s,f\le N$、$s\ne f$ |
入出力例
入力例1
4 2 3出力例1
2入力例2
7 3 6出力例2
14入力例3
2000 1 2000出力例3
677229625(例1: $(2,1,4,3)$ と $(2,4,1,3)$ の 2 個)
この問題は CEOI 2016 の「Kangaroo」(oj.uz などに収録)と同じ内容。本セッションでは原文を参照しておらず、入力例1 は $N\le9$ の全順列探索で確かめた値。
概念図:塊をつないで順列を作る
ヒント(クリックで段階的に開示)
ヒント1(方向性)
位置 $1..N$ を左から埋めると「どの数を使ったか」の集合が状態に入り $2^N$ になる。逆に 数を小さい順に 置くことを考える。数 $i$ を置く時点で、置いた数はまだ最終的な並びで隣り合っているとは限らない。
ヒント2(アプローチ)
$1..i$ を置いた時点の状態を「隣同士であることが確定した塊(連結成分)の個数 $j$」で表す。塊と塊の間には、あとで より大きい数 が入る。数 $i$(端以外)の置き方は 2 通りだけ:
- 新しい塊として隙間に置く → 後から両隣に大きい数が来るので $i$ は 谷
- 2 つの塊の間に置いてつなぐ → 両隣は既に置いた小さい数なので $i$ は 山
塊の端に「くっつける」置き方は、$i$ の片側が小さく片側が大きくなりジグザグを壊すので 禁止。
ヒント3(誘導)
dp[j] を $1..i$ を置いて塊が $j$ 個の場合の数とする。
- $i=s$ または $i=f$: 左端(右端)に新しい塊を作る
dp[j-1]、または左端(右端)の塊の外側にくっつくdp[j] - それ以外: 新しい塊
dp[j-1] * (j - [i>s] - [i>f])、つなぐdp[j+1] * j
答えは $N$ 個置き終えて塊が 1 個 dp[1]。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
MOD = 10**9 + 7
def solve(n, cs, cf):
# dp[j] = 1..i を置き終えたとき、塊(まだつながっていない連結成分)が j 個ある並べ方の数
dp = [0] * (n + 2)
dp[0] = 1
for i in range(1, n + 1):
nd = [0] * (n + 2)
if i == cs or i == cf:
# 端に固定された数: 端に新しい塊を作る / 端の塊の外側にくっつく
for j in range(1, i + 1):
nd[j] = (dp[j - 1] + dp[j]) % MOD
else:
fixed = (i > cs) + (i > cf) # 既に置かれた端(その外側には置けない)
for j in range(1, i + 1):
# 新しい塊として隙間に置く(i は両隣より小さい谷になる)
v = dp[j - 1] * (j - fixed) if j > fixed else 0
# 2 つの塊の間に置いて 1 つにつなぐ(i は両隣より大きい山になる)
v += dp[j + 1] * j
nd[j] = v % MOD
dp = nd
return dp[1]
def main():
n, cs, cf = map(int, sys.stdin.read().split())
print(solve(n, cs, cf))
main()
計算量: $O(N^2)$ 時間、$O(N)$ 空間。実測で $N=2000$ が 0.27 s、$N=5000$ が 2.94 s。
Step-by-Step 解説
ジグザグの条件は「$p_i$ が両隣より大きいか小さいか」で、比べる相手との 大小だけ が効く。小さい数から置けば、ある数を置いた瞬間に「既に置いた数はすべて自分より小さい、後から来る数はすべて大きい」が確定する。これで谷(両隣が後から来る)と山(両隣が既に置いた数)を置いた瞬間に決められる。
最終的な並びで、既に置いた数同士が隣り合うかどうかだけが問題で、塊の 中身や位置 はもう変わらない。塊の間には必ず 1 個以上の大きい数が入る。よって状態は塊の個数 $j$ で足り、$N\times N$ の表で済む。
塊が $j-1$ 個あるところに新しい塊を作るとき、置ける隙間は $j$ か所(塊の間 $j-2$ か所+左右の外側 2 か所)。ただし $s$ が既に置かれていれば、$s$ を含む塊の左側には何も置けないので 1 減る。$f$ も同様。よって係数は $j-[i>s]-[i>f]$。
つなぐ場合は塊が $j+1$ 個の間の $j$ か所のどこかに置く。$s,f$ の外側は隙間ではないので減らさない。
$s$ は必ず左端。新しい塊を左端に作るか、既にある左端の塊の 左側にくっつける。くっつけても $s$ は端なのでジグザグの条件は課されない(端は両隣を持たない)。したがって係数は 1。
$N\le9$ の全 $(s,f)$ の組 240 通りで全順列探索と一致。さらに、$(s,f)$ について合計すると「ジグザグ順列の総数」$2E_N$($E_N$ は Euler のジグザグ数、OEIS A000111: $1,1,1,2,5,16,61,272,1385,7936$)になるはずで、$N=2..9$ の合計 $2,4,10,32,122,544,2770,15872$ は $2E_N$ と一致した。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 全順列 | $O(N!\,N)$ | 検算用のみ | $N=9$ で 36 万通り |
| 位置を左から埋めるビット DP | $O(2^N N^2)$ | 不採用 | 「どの数を使ったか」が状態に要る。$N\approx18$ が限界 |
| 「残りの数の中で何番目か」の相対順位 DP(Entringer 数の漸化式) | $O(N^2)$ | 不採用 | 端点固定なしのジグザグ数え上げでは定番で、同じ $O(N^2)$。ただし $s,f$ の両方を固定すると、左端の相対順位と右端の値を同時に追う必要があり式が込み入る |
| 挿入 DP(連結成分 DP、本解) | $O(N^2)$ | 採用 | 「隣が小さいか大きいか」の条件を、置き方 2 種類(新しい塊/つなぐ)で自然に表せる。端点の固定は係数を 1 ずつ減らすだけ |
前提が変わったら: 「隣との差が $K$ 以下」のような 値の差 の条件は、塊の数だけでは表せない(差の合計を状態に足す。JOI Open 2016「Skyscraper」がこの型として知られる。本セッションでは原文未参照)。「$p_i<p_{i+1}$ か $>$ かが文字列で指定される」型(例: EDPC T「Permutation」)は相対順位 DP の方が素直で $O(N^2)$(累積和で)になる。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 新しい塊の係数を $j$ のままにする | 置かれた端 $s,f$ の外側には置けない | $j-[i>s]-[i>f]$。入力例1 では 2 と正しく出るのに、$N\le9$ の 240 組中 178 組で誤る(実測) |
| 塊の端にくっつける遷移を入れる | 片側が小さく片側が大きい数はジグザグの中間に来られない | 端以外の数は「新しい塊」「つなぐ」の 2 種類だけ |
| $s,f$ で「くっつく」を禁止する | $s,f$ は端なので条件が課されない | $s,f$ は dp[j-1] + dp[j] |
j > fixed の確認を忘れ負の係数を掛ける | 塊が 1 個で $s,f$ の両方が置かれていると、新しい塊を置ける場所が無い | if j > fixed else 0 |
答えを sum(dp) にする | 全部つながった状態だけが 1 本の順列 | dp[1] |
次のステップ
- 発展1: 「$|p_i-p_{i+1}|$ の総和が $L$ 以下」の順列を数える(Skyscraper 型)。塊の数に加えて、差の合計を「まだ埋まっていない隙間の数 × 次の値の差」で前払いする
- 発展2: 本問を $N\le10^5$ にするには、$s,f$ を固定しない場合の Euler ジグザグ数が $\sec x+\tan x$ の係数であることを使い、形式的冪級数で扱う方法がある
- 扱わなかった範囲: $s=f$ の扱い(本問では禁止)、円順列版
検証ログ(本セッションで実行した数値)
- 全順列探索との一致(実測): $N=2..9$ の全 $(s,f)$、240 組で不一致 0
- ジグザグ総数(実測): $(s,f)$ で合計した値 $2,4,10,32,122,544,2770,15872$($N=2..9$)が $2E_N$(OEIS A000111 の値の 2 倍)と一致
- 係数を $j$ のままにした誤り(実測): 入力例1 は 2 で通るが、240 組中 178 組で不一致
- 入力例1・2・3 の出力
2、14、677229625を実行で確認 - 性能(実測): $N=500$ 0.02 s、$N=1000$ 0.07 s、$N=2000$ 0.27 s、$N=5000$ 2.94 s
- 出所ラベル: 数値は実測(Python 3.14.7)。CEOI 2016 の出題であることと OEIS の数列は文献の記述で、本セッションでは原文を参照していない(数列の値は全探索で独立に再現)
- 検証スクリプト: スクラッチパッドの
d161/q3.py・q3_test.py