yukicoder No.1051 PQ Permutation
No.1051 PQ Permutation - yukicoder
※当初の公開時点での解説およびリンク先コードが誤っていたため、修正しました。
解法
※以下の解説中で「辞書順で1つ進める」と書いた時には、「もし進められない(既に辞書順最大である)ときには -1 を出力して終了する」という意味を含むものとします。実装上はC++であれば next_permutation を用いると楽です。
答え は与えられた
より辞書順で大きくないといけないので、まず与えられた
を辞書順で1つ進めます。この状態で
の位置をチェックして、
が
より左にあればこれが答えです。
のほうが左にある場合、先頭から
までの並びがこのままである限りは条件を絶対に満たせません。よって先頭から
までの並びが崩れるまで順列を進めます。具体的には
より後ろ(
を含まない)を逆順ソートして、さらに辞書順で1つ進めます。
この操作後に順列がどうなるかを場合分けします。(-1 で終了するパターンは書いていません)

細かく分けるとこんな感じですが、パターン1とパターン3、パターン2とパターン4はほぼ同じです。 より後ろを逆順ソートしてさらに1つ進めると、
もしくはそれより前の位置のどこかが「繰り上がり」、それ以降は余った値が昇順ソートされた順列になります。
これらのパターンにおける の位置関係を見ると、
- パターン1, 3のように「繰り上がり」に
が使われた場合、必ず
が
よりも左に来るので、これが答えとなる。
- パターン2, 4のように「繰り上がり」に
も
も使われなかった場合、
は昇順ソートされた領域に属するため、その位置関係は大小に依存する。
であればそれが答えであり、
であれば
を
のすぐ右隣に移動させるのが最適である。
- パターン5のように「繰り上がり」に
が使われた場合、必ず
が
よりも左に来る。
となります。パターン5以外の場合はこれで答えが求められます。
パターン5だけは決着が付かないので、「再度 より後ろを逆順ソートする→さらに辞書順で1つ進める→繰り上がりパターンチェック→パターン5なら再度…」という流れを繰り返すことになります。これは最悪ケースで約
回繰り返すことになるので、毎回ソートと
next_permutation を実行していると間に合いません。しかし次のようにやると間に合わせることができます。
そもそも「さらに辞書順で1つ進める」処理の時に繰り上がる位置はどこなのか考えます。それは が存在する位置から左に見ていって、初めて
となるような位置
です。そして
の中で
より大きいもののうち最小の値が位置
に来ることによって繰り上がります。
もし位置 に来る値が
以外であれば、先述のパターン5から逃れられます。そして
が来てしまう場合は、「辞書順で1つ進めて、再び
より後ろを逆順ソート」という一連の処理の代わりに、
と
をスワップしてしまうことができます。
その理由は以下の通りです。繰り上がり処理を行う前の状態では が降順ソートされていました。そして「
の中で
より大きいもののうち最小の値」が
だったので、値
があった位置に値
が代わりに入っても位置
以降における相対的な順序は変わりません。そのため「
より後ろが降順ソートされている」という条件が保たれます。

繰り上がりの位置 をずらしていくのは全体で
、繰り上がりで位置
に来る値の判定は
set 等を用いて1回あたり 、値のスワップは1回あたり
です。よってこの処理を約
回繰り返しても大丈夫です。
が繰り上がりに使われるパターン5を抜け出して答えに辿り着くか、または
となるような
が存在しなくなってしまい
-1 で終了するまで繰り返せば良いです。