\(LIS \) (最長の上昇シーケンス)
長さを求めます
\(DP \) - \(O(N ^ 2)\)
動的なプログラミングアプローチ
オーダー\(\ [I] F)部で表される\(Iは\)素子端番目の\(LIS \)の長さ
则有:\(F [I] = MAX(F [I]、F [J] + 1)、([J] <[i]は、J <I)\)
列挙することによって\を(\ [I] F)と\([J] F \)連続状態を転送するには、連続的に最大列挙を更新します
\(コード\) :
#include<cstdio>
using namespace std;
#define maxn 10005
int f[maxn],a[maxn];
int main()
{
int n,Max = 0;
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);
for(int i = 1;i <= n;i ++)
{
f[i] = 1;
for(int j = 1;j < i;j ++)
if(a[j] < a[i] && f[i] < f[j] + 1) f[i] = f[j] + 1;
}
for(int i = 1;i <= n;i ++) if(f[i] > Max) Max = f[i];
printf("%d",Max);
return 0;
}
アルゴリズムは、我々は一般的により良く考慮することができ、特定の配列の最大増加を決定するが、最も長い系列の長さを必要なときだけ増加してもよい(O(N \ LG電子の\ \ \)n)のアプローチを
\(DP \) +フェンウィックツリー\(O(N \ LG電子の\ N)\)
我々は列挙する状態で移動するとき私たちは気づい([J] \ F)\を転送する最大値のそれは、単一の最もvalue変更すると、クエリのデータ範囲をサポートしているとして、我々は限り、それを最適化するために維持するためのデータ構造を使用することを検討することができますこれは、構造、ブロック行うことができる\((O(N \ SQRT n)を)\) とフェンウィックツリー\((O(N LG \ N-を\))\) 、セグメントツリー\((O(N \ LG \ n個))\)などを行いますが、フェンウィックツリーを書くために優れているので、私たちは唯一のフェンウィック木の文言を説明
- プリプレス重みは、シリアル番号、最大の後にソートチェック\(fは[J] \)にシフトするのではなく、一つのことには注意することは、私たちはLISは、厳密に増加するので、私たちが出会った繰り返しの重みであることを見つけます端部にワンタイム契約を配置、またはバック繰り返さなければならない場合([] F \)\最終的な答えが得られることは間違っている、シフトする同じ前部要素を使用することができるであろう
#include <cstdio>
#include <algorithm>
using namespace std;
#define maxn 1000007
int n,Dp[maxn],Ans,Max[maxn];
struct Node{int w,i;}A[maxn];
#define lowbit(x) ((x) & (-x))
inline bool cmp(Node A , Node B){return A.w < B.w;}
inline void Update(int Pos , int w)
{
for(int i = Pos;i <= n; i += lowbit(i))
Max[i] = max(Max[i] , w);
}
inline int Query(int Pos)
{
int Ret = 0;
for(int i = Pos ; i ; i -= lowbit(i))
Ret = max(Ret , Max[i]);
return Ret;
}
int main()
{
scanf("%d" , &n);
for(int i = 1;i <= n;i ++) scanf("%d" , &A[i].w) , A[i].i = i;
sort(A + 1 , A + 1 + n ,cmp);
int Last = 1;//为了处理权值相同时的情况
for(int i = 1;i <= n;i ++) //确保权值的大小关系正确
{
if(A[i].w != A[i - 1].w && i - 1) //处理前面权值相同的情况
{
for(int j = Last;j <= i - 1;j ++) Update(A[j].i , Dp[j]);
//如果不是到了最后再更新的话,后面重复的就会用前面重复的值来更新
Last = i;//处理完转移过来
}
Dp[i] = Query(A[i].i) + 1;//转移
Ans = max(Ans , Dp[i]);
}
printf("%d" , Ans);
return 0;
}
- メンテナンス\(F [] \)の配列が、配列の添字として正しい値では、する必要はありません\(ソート\) 、その上に列挙の順序、値の大きさについては、我々は直接見つけることができます(フェンウィックツリー)、範囲が大きいことを指摘し、我々は離散化することができます
#include <cstdio>
#include <algorithm>
using namespace std;
#define maxn 1000007
int n,ans,f[maxn];
struct Node{int val,num;}z[maxn];
#define lowbit(x) ((x) & (-x))
inline void modify(int x,int y)
{
for(;x < maxn ;x += lowbit(x))
f[x] = max(f[x],y);
return ;
}
inline int query(int x)
{
int res = 0;
for(;x;x -= lowbit(x)) res = max(res,f[x]);
return res;
}
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%d",&z[i].val);
for(int i = 1;i <= n;i ++)
{
int Max = query(z[i].val - 1);
modify(z[i].val , ++ Max);
ans = max(ans,Max);
}
printf("%d",ans);
return 0;
}
ピギー半分+ \(O(N \ LG電子の\ n)は、\)
貪欲なアプローチ
単調スタック維持し、スタックは、現在の要素と定義し、最適な戦略を選択するための比較要素に応じて要素が与えられる(スタックは[] \)\を単調スタック、スタック要素である\(スタック[トップ] \) 、現在のシーケンス最初の\(Iは\)番目の要素\([I] \)
- 場合\(スタック[トップ] < [それ] \) 次に単調直接スタックにプッシュすることができます
- もし\(スタック[トップ] \ geqslant [i]は\) 、この時間は単調に挿入されていない、我々は、最初の発見、単調なスタック内のバイナリ検索を考える(スタック\を[J] \ geqslant A [I]は\)に置き換えられます
Q:なぜ、この貪欲な戦略が実現可能である置き換えますか?
:その代わりに、それはスタックの長さを増加させない、ので、次は実際には、感情的な理解は、スタック内の2以上のものとして理解することができ、複数のシーケンスで、より良いソリューションを持つことができるので、圧入は、配列の各要素に適用してもよいした後、次の例のように、貢献します
\(入力\) :
5
1 4 2 5 3
このような手順を起動するには、上記の決定貪欲少し難しいによると:
- スタック[1] = {1}。
- [2] = {1,4}スタック。
- [2] = {1,2}スタック。
- [3] = {1,2,5}をスタック。
- [3] = {1,2,3}スタック。
セクション参照\を(3 \)は、プロセスステップ、置換\(4 \する)となる\(2 \) 、実際には\(4 \)が存在する{1,4,5}
最長の配列におけるこのような増加は、しかしバックデジタルがある場合なし\(2 \)私たちが交換した後、より良い、\(4 \)は、実際に関与しているが、我々は、より優れたので\(2 \) 、そう\(4 \)よりも間違いなく良い貢献を\ (2 \)小への貢献、直接スワップを置き換えることができます
\(警告\)
\(O(N \ LG電子の\ N)\) アルゴリズムは、実際に得られないことは、エレメントを交換するために中断された元のシーケンスを交換する過程で中間体として、最長の特定の配列を増やします私は後ろの最長のシーケンスの値を大きくするかどうかわからない、それは可能ではありません
\(コード\) :
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 100005
int a[maxn],d[maxn];
int main()
{
int n,len = 1;
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);
d[1] = a[1];
for(int i = 2; i <= n; i ++)
{
if(d[len] < a[i]) d[++ len] = a[i];
else *lower_bound(d + 1 , d + 1 + len , a[i]) = a[i];
}
printf("%d",len);
return 0;
}
詳細なシーク\(LIS \)シーケンス
この問題は\(DP - O(N ^ 2)\) アイデアはそれに対応して適用することができ、我々は、アルゴリズムの時間計算量を最適化するために、フェンウィックツリーに追加することができます。(O(N \ LG電子の\ \ n)を\) 、するアルゴリズムのスペースの複雑さを最適化するために、前駆体の対応するシーケンスを記録の終わりまでに(O(N)を\ \) 、その後、書き込み用の2つの方法で、作者がいるので、非常に、フェンウィックツリーを行います一つのアプローチが唯一の解決策を記述することです、別の解決策は、読者が自分で考えることができますが、それは離散を見ることが最善です
#include <cstdio>
#include <algorithm>
using namespace std;
#define maxn 1000007
int n,Dp[maxn],Ans,Max[maxn],Max_num[maxn],pre[maxn],num = 0,a[maxn];
struct Node{int w,i;}A[maxn];
#define lowbit(x) ((x) & (-x))
inline bool cmp(Node A , Node B){return A.w < B.w;}
inline void Update(int Pos , int w,int Num)
{
for(int i = Pos;i <= n; i += lowbit(i))
if(Max[i] < w) Max[i] = w,Max_num[i] = Pos;
return ;
}
inline int Query(int Pos)
{
int Ret = 0;num = 0;
for(int i = Pos ; i ; i -= lowbit(i))
if(Ret < Max[i]) Ret = Max[i],num = Max_num[i];
return Ret;
}
inline void Output(int first)
{
if(!first) return ;
else Output(pre[first]);
printf("%d ",a[first]);
return ;
}
int main()
{
scanf("%d" , &n);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]),A[i].w = a[i],A[i].i = i;
sort(A + 1 , A + 1 + n ,cmp);
int Last = 1,first = 0;
for(int i = 1;i <= n;i ++)
{
if(A[i].w != A[i - 1].w && i - 1)
{
for(int j = Last;j <= i - 1;j ++) Update(A[j].i , Dp[j],j);
Last = i;
}
Dp[i] = Query(A[i].i) + 1;
pre[A[i].i] = num;
if(Ans < Dp[i]) Ans = Dp[i],first = A[i].i;
}
printf("%d\n" , Ans);
Output(first);
return 0;
}
\(LCS \) (最長共通部分列)
長さを求めます
\(O(nm)を\)
動的なプログラミングアプローチ、実際には、原則としては、非常に簡単ですそらすために状況に応じて問題は、その後、現在の位置を一致されているかどうかを確認することです
長さと\(N- \)文字列\(S \)との長さ\(m個\)文字列\(T \)と、([I、J] Fは\ \) を表し\(S \)文字列を前に\(私は\)文字と\(T \)文字列の前に\(J \)文字\(LCS \)があります:
\(F [I、J] = MAX(F [I - 1、j]、F [I、J - 1](F [I - 1、J - 1] + 1)* [S_I = T_J]) \)
いずれにしても、存在しなければならない、ということ
\(F [I、J] = MAX(F [I - 1、j]、F [I、J - 1])\)
特別な事情について話し合う:とき\(S_I = T_J \)とき
\(F [I、J] = MAX(F [I、J]、F [I - 1、J - 1] + 1)\)
だから我々は、その後、この結果を起動することができます\(F [N、M] \) 最終的な答えであります
以来\(F [i]が\)は常にからになります\(F [I - 1] \) この次元転送のオーバーので、我々は、空間的複雑さを最適化するための配列を転がり考えることができます
\(コード\) :
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn = 5e3 + 7;
int n,m,tmp;
char a[maxn],b[maxn];
int f[2][maxn];
int main()
{
scanf("%s",a + 1);
n = strlen(a + 1);
scanf("%s",b + 1);
m = strlen(b + 1);
int now = 0;
for(int i = 0;i <= n;i ++)
{
now ^= 1;
for(int j = 1;j <= m;j ++)
{
f[now][j] = max(f[now ^ 1][j],f[now][j - 1]);
tmp = f[now ^ 1][j - 1] + 1;
if(a[i] == b[j] && f[now][j] < tmp) f[now][j] = tmp;
}
}
printf("%d",f[now][m]);
return 0;
}
\(O(N \ LGする\ n)\)
実際には、私が話を\(LIS \)の理由で、我々はこれらの2つの問題の間のリンクを探求しようとすることができます
私は別のシーケンスに元のシーケンスを配置し、見つけることができる場合(LIS \)\得るために、\(LCS \)をこれら2つの問題を変換する方法を、うまく?
私たちは簡単に見つけることができます\(LISを\)最長の上昇シーケンスを見つけるために使用されているので、シーケンスは昇順であるとき、注文のうちシーケンス、二つの配列を求める他の時に\(LCSは、\)を求めることですスクランブルシーケンス\(LIS \)昇順があるので、シーケンスが存在している必要があり、シーケンススクランブル配列最長の上昇昇順、最長共通部分列を持つための出そうすることができます
そこで、入力開始\(\)のシーケンスは、シーケンス番号のどの要素によるものであろうときに、符号化中の対応する要素にB列、\(\)シーケンス番号である(\しますB \)要素の配列で(\)\配列中の位置
たとえば、
1 5 4 3 2
5 3 1 2 4
数は、後
1 2 3 4 5
2 4 1 5 3
その後、変更しようとする\(B \)シーケンス\(LIS \を)のような、それは答えである2 4 5
、長さ\(3 \)
\(警告\)
2つの配列がそうでない場合は、削除する様々な要素を扱う、さまざまな要素を持っている場合は、\(LIS \)は、別のシーケンスの要素を含んでいてもよい誤った答えにつながりません
配列内の2つの反復要素が存在する場合、この方法によって治療することができません
例えば:
abacc
cbabb
私たちは、これら二つの配列を見つけることができます\(LCS \)があるab
か、ba
長さ2
しかし、治療割り当ての過程で少し問題で
abacc
cbabb
1 2 3 4 5
5 2 3 2 2
我々は理由に初めて、処理された順に番号の重複を発見した(A \)\要素の割り当ては、我々は重複要素のための値の2倍を割り当てたので、彼は順序を率いたときに複数に対応する位置の数は、プロセスで上書きします
次に、あなたが言うかもしれない:私はそれを望んで上書きしません
そして、あなた\(ナイーブ\)いくつかのケースでは、関係なく、あなたが割り当てるか、実際には、必ずしも昇順に配列でもないないので、アイデアは、別のシーケンスを求めて、この時間を無駄にされる可能性があります\(LIS \)あなたが自分自身の配列を決定するので、意味がありません昇順されていない、私は別のシーケンスとは何の関係に昇順再び尋ねます
- アルゴリズムは、特定の判断ができない\(LCS \)配列を被験体は、アルゴリズムの特定の配列を必要とする場合、同じ理由、使用することができない\(LIS \)\(N \ LG \ n \ ) アプローチ
\(コード\) :
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 100005
int a[maxn],b[maxn];
int d[maxn],stack[maxn],len = 0;
int main()
{
int n;
scanf("%d",&n);
for(int i = 0;i < n;i ++)
{
scanf("%d",&a[i]);
d[a[i]] = i + 1;
}
for(int i = 0;i < n;i ++)
{
scanf("%d",&b[i]);
b[i] = d[b[i]];
}
stack[1] = b[0],len = 1;
for(int i = 0;i < n;i ++)
{
if(stack[len] < b[i]) stack[++ len] = b[i];
else *upper_bound(stack + 1,stack + 1 + len,b[i]) = b[i];
}
printf("%d",len);
return 0;
}
詳細なシーク\(LCS \)シーケンス
我々は考えるそれから学ぶことができる(LIS \)\の文言、および最大値は、前駆体の先頭に記録した後、再帰的に出力を逆に、私たちはこの時間\(DP [] \)ので、データ構造を最適化するために使用することはできません私たちのアルゴリズムは\(O(nm)を\) 、特定のコード・リーダーが自分で考えます
\(LCIS \) (最長共通部分列上昇)
私たちは、上記の組み合わせることができます\(LIS \)と\(LCS \)にこの質問を熟考すると考えられています
\(LIS:\)
オーダー\(\ [I] F)部で表される\(Iは\)素子端番目の\(LIS \)の長さ
则有:\(F [I] = MAX(F [I]、F [J] + 1)、([J] <[i]は、J <I)\)
\(LCS:\)
長さと\(N- \)文字列\(S \)との長さ\(m個\)文字列\(T \)と、([I、J] Fは\ \) を表し\(S \)文字列を前に\(私は\)文字と\(T \)文字列の前に\(J \)文字\(LCS \)があります:
\(F [I、J] = MAX(F [I - 1、j]、F [I、J - 1](F [I - 1、J - 1] + 1)* [S_I = T_J]) \)
私たちは少しの組み合わせを見つけることができると思います。
場合\(F [I、J] \) を表し\(A \)プレシーケンス\(Iは\)要素および\(B \)プレシーケンス\(J \)要素\(LCISは\)、\ ( T \)を表す\(LCISある\)要素の終了位置があります。
\(F [I、J] Fを= [I - 1、j]は、a_iを\ NE B_j \)
\(F [I、J] = MAX(F [I - 1、j]、F [I - 1、T] + 1)、a_iを= B_j \)
また、発見した\(F [i]が\)この次元から毎回の\(F [I - 1] \) この寸法の転送が終わったので、我々は、配列の最適化を転がり得るためにそれを使用することができます。
\(F_iと\)配列の代表\(\)の前に\(Iは\)配列の番目の要素\(B \)の\(LCISが\である)、\ (T \) 、場所を終了します
\(F_J = F_T + 1、a_iを= B_j \)
計算\を(LCISは\)の長さの間、我々は記録前駆体をドロップすることができ、特定の順序で出力され、それは使用することができる\(O(NM)\)計算時間計算\を(LCISが\である)長さ\(LCISであります\)特定のシーケンスの
\(コード\) :
#include<cstdio>
using namespace std;
#define maxn 505
int a[maxn],b[maxn],f[maxn],pos[maxn];
void output(int x)
{
if(!x) return;
output(pos[x]);
printf("%d ",b[x]);
}
int main()
{
int n,m,Max = 0;
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);
scanf("%d",&m);
for(int i = 1;i <= m;i ++) scanf("%d",&b[i]);
for(int i = 1,t = 0;i <= n;i ++,t = 0)
for(int j = 1;j <= m;j ++)
{
if(a[i] == b[j]) f[j] = f[t] + 1,pos[j] = t;//f[j] 的结尾
if(a[i] > b[j] && f[t] < f[j]) t = j;// 保证t在结尾位置
}
for(int i = 1;i <= m;i ++)
if(f[i] > f[Max]) Max = i;
printf("%d\n",f[Max]);
output(Max);
return 0;
}
これまでのところ、我々が議論してきた\(LIS \)、\ (LCS \)と\(LCIS \) 、動的プログラミングの3つの基本タイプは、アルゴリズムとアルゴリズムとの間の接続を見ることができます
謝辞
- ロス・バレーでの問題にはいくつかの高品質なソリューションのおかげで、私は多くの教えやすい問題の解決からたくさんのアイデアを見つけました
- \(梁\)\(シャイン\) \(スカイ\)ギャングのアドバイス