概要
数列の各要素について「LIS に使われる可能性があるか、使われる場合 LIS の何番目か」を計算しておくといろいろ便利です。
前提
増加部分列は「値」というよりは「添字」をとってきたものと思うのが見通しがよいです。この記事では次のように、増加部分列を添字の列として定義します:
- 長さ
の数列
の増加部分列とは、添字の列
であって、
かつ
を満たすもの。最長増加部分列 (LIS) とは、増加部分列のうち、長さ
が最大のもの。
また、LIS 長を求める 種類の DP(二分探索、セグ木)は既知として進めます。
本題
まず、添字 が LIS に使われる場合、その LIS 上での位置は一意です(そうでないとしたら、より長い増加部分列がとれて矛盾)。
列
の増加部分列のうち
を含むものの長さの最大値
とすると、添字 が LIS に使われる場合の LIS 上での位置は
に一致します。
ところで、LIS に絶対に使われない添字が存在することもあります。そこで、 を次のように定義します:
- 添字
が使われる LIS が存在する場合、
とする。
- 添字
が使われる LIS が存在しない場合、
とする。
また、(
:LIS 長)に対して
を、
を満たす
たち(つまり、LIS の
番目になりうる添字の候補)を昇順に並べた列とします。
この (と
)を求めて、LIS の性質に関するいろいろな問題に応用するのが今回やりたいことです。
の性質
(記法の補足: は
の末尾要素を表すとします。)
- LIS は必ず、
それぞれから
つずつ選んできてできる列のどれかです。ただし、そのような列が必ず LIS になっているわけではありません。
の中身は添字の昇順ですが、これは値の降順になっています。つまり、
です。
- そうでないと仮定すると、LIS より長い増加部分列がとれてしまいます。
それぞれの先頭をとってきた列は LIS です。つまり、
かつ
です。これは添字の辞書順最小、値の辞書順最大の LIS です。
- 添字が単調増加でないと仮定すると、ある
が存在して
となりますが、
より
が使われる LIS が存在しないことになり、矛盾します。
- 値が単調増加でないと仮定すると、ある
が存在して
となりますが、
より
が使われる LIS が存在しないことになり、矛盾します。
- 添字が単調増加でないと仮定すると、ある
それぞれの末尾をとってきた列は LIS です。つまり、
かつ
です。これは添字の辞書順最大、値の辞書順最小の LIS です。
- 増加部分列になっていることの証明は上と同様です。
- 上記の辞書順最小・最大の LIS の性質より、辞書順最小 [resp. 最大] の LIS は、列を反転したときの辞書順最大 [resp. 最小] になります。
の求め方
は LIS 長を求める DP と同時に求めることができます(二分探索・セグ木のどちらのバージョンでも OK、簡単なので略)。
さて、 を含む LIS が存在する(
である)ための必要十分条件は、次のいずれかを満たすことです:
- ある
が存在して、
この事実を用いると、 を利用して
が
時間で計算できます。
を後ろから見つつ、各
について「今まで見た
のうち
である最小の(つまり、
が最大の)
」を管理していけばよいです。(なお下の実装例では、これを管理するついでに
を求めています。)
実装例
#include <algorithm> #include <vector> using namespace std; // idx[i] := A の i 番目が LIS に使われ得るなら LIS の何番目か、使われ得ないなら -1 // cand[j] := LIS の j 番目になりうる A の要素の添字の配列 (添字の昇順、値の降順) // cand[j].front() たちをとってきたもの: 添字の辞書順最小、値の辞書順最大の LIS // cand[j].back() たちをとってきたもの: 添字の辞書順最大、値の辞書順最小の LIS struct LongestIncreasingSubsequence { vector<int> idx; vector<vector<int>> cand; LongestIncreasingSubsequence() {} template <class T> LongestIncreasingSubsequence(const vector<T> &a) : idx(a.size()) { // LIS 長を求める DP (ここで idx' を求める) const int n = a.size(); vector<T> dp; for (int i = 0; i < n; i++) { auto it = lower_bound(dp.begin(), dp.end(), a[i]); idx[i] = it - dp.begin(); if (it == dp.end()) dp.emplace_back(a[i]); else *it = a[i]; } // idx, cand を求める const int len = dp.size(); cand.resize(len); for (int i = n - 1; i >= 0; i--) { if (idx[i] == len - 1 || (!cand[idx[i] + 1].empty() && a[i] < a[cand[idx[i] + 1].back()])) cand[idx[i]].emplace_back(i); else idx[i] = -1; } for (auto &candj : cand) reverse(candj.begin(), candj.end()); } };
問題例
ABC354 F - Useless for LIS
LIS に使われうる要素をすべて求めてください。
解答
の求め方の一部そのままです。
Library Checker - Longest Increasing Subsequence
LIS をひとつ復元してください。
解答
それぞれの先頭・末尾をとってくることで、辞書順最小・最大が復元できます。
yukicoder No.1804 Intersection of LIS
LIS に必ず使われる要素をすべて求めてください。
解答
の要素が唯一であるところが答えになります。解説は「辞書順最小と辞書順最大の共通部分」という書き方をしていますが、同じことです。
ABC360 G - Suitable Edit for LIS
整数列の
要素を他の整数に書き換えて、LIS 長を最大化してください。
解答
の先頭に
を、末尾に
を付け加えておきます。LIS 長を
増やせるための必要十分条件は、ある添字
が存在して、
となることです。
を全探索します。
としてありうるのは、
として
の要素のどれかです。
内では添字が昇順、値が降順になっていることに注目すると、
が条件を満たすような
の集合は区間になります。よってこの区間を二分探索か尺取り法(
を
に現れる順番で探索すれば尺取りできます)で求めればよいです。
yukicoder No.992 最長増加部分列の数え上げ
LIS の個数を(適当な mod のもとで)数え上げてください。(列は添字が異なれば区別します)
解答
DP をします。
LIS の
番目を決めて、なおかつ
番目を
とするような方法の数
内では添字が昇順、値が降順になっていることに注目すると、配る DP で考えた場合は配る先が区間に、貰う DP で考えた場合は貰う先が区間になります。いずれにしても、この区間を二分探索か尺取り法で求めて累積和や imos 法を利用すればよいです。
答えが にはならないことには注意してください。
なお、もし「値が同じであれば添字が異なっても同じ列とみなす」という設定だった場合は結構面倒になりますが、部分列 DP のように考えればなんとかなります(略、後で追記するかも)。
「みんなのプロコン」本選 D - KthLIS
辞書順
番目の LIS を求めてください。(リンク先の問題では 値の辞書順・値が同じであれば同じものとみなす という設定ですが、以下では簡単のため添字の辞書順で考えます)
解答
数え上げの DP と同じことをします。ただし、その後に辞書順 番目を求めたいので、後ろから DP します。
LIS の
番目を決めて、なおかつ
番目を
とするような方法の数
このテーブルを求めた後は、テーブルの情報にしたがって答えを前から決めていくことができます。
ところで、テーブルに載せる数が非常に大きくなる場合があります。これは、 との min をとった値を計算することにすれば対処できます。モノイド
の区間積だと考えて、セグ木に載せることで計算できます(追記:尺取りになることを考えると SWAG でもできますね)(ところで、このモノイドは逆元がないので累積和だとうまくいかない気がするのですが、解説には累積和と書いてあるのでできるのかもしれないです。ちゃんと読んでいないのでよくわかりません)。