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
で終了するまで繰り返せば良いです。