ダイナミックプログラミングダイナミックプログラミング
方法論
コンピュータの本質は、メモリに格納されているすべてのデータは、CPUのみを利用することができ、現在の状態を構成するステートマシンでの現在の状態を計算次の状態(彼らは唯一の状態の拡大を検討する場合にも、外部記憶装置のハードディスクをもつれしませんストレージ容量は)だけで、この状態では変更されないだけで、この鉄のルールのうち、現在の状態から計算することができ
ますが、課題を解決するためにコンピュータを使用しようとすると、この問題を配置する方法について考え、実際には、状態として表現された変数に格納されています(データ)とする方法状態を転送する(変数はいくつかの他の変数に応じて算出する方法)。いわゆる宇宙の複雑さは、初期状態から最終中間状態に到達するために必要なステップ数は、いわゆる時間の複雑さの何を状態までの保管のために必要なあなたの計算をサポートすることです!
例:という非波証書の数を計算するには、各非フィボナッチ数の状態が状態ごとに2つだけの新しいデジタル・ニーズを追求する前に、この問題です。だから、同じ時間は、たった2つの状態の最大値を保存し、空間的な複雑さが一定であり、新しい状態を一定にして直線的に増加する状態であることが要求されるたびに、計算され、時間の複雑さが線形です。この状態計算は簡単です、ちょうどライン上の古い状態から新しい状態を計算するために固定パターンに従う必要がありa[i]=a[i-1]+a[i-2]
、あなたがより多くの状態、新しい状態を計算するために選択する必要はありません古い状態のためかどうかの必要性を検討する必要はありません。そのような解決のために、我々は再帰と呼ばれます。
- 各段の最適な状態は、の段階に最適な状態にすることによって得られる - >貪欲。
各位相の最適な状態は、すべての状態の前に組み合わせ相から得られる - >検索。
- 最適な状態は、各段階で得ることができる前にかかわらず、状態が特定の状態または前に特定の段階から直接取得する方法であるかどうかの - >動的計画法。
- 各段の最適な状態が特定の状態又は直接前に、特定の段階から得ることができ、このプロパティは、最適な下部構造と呼ばれています。
- この状態が取得する方法である前に、どんなにこのプロパティは、後に効果がないと呼ばれています。
なぜDPは速くなりますか?
DPや暴力のいずれかが、私たちのアルゴリズムが最適解を見つける、解空間内で可能です。暴力のアプローチは、これが最も可能性の高い解空間で、すべての可能な解決策を列挙することです。DPは、列挙は(最適下部)の溶液への答えであることを約束です。このスペースは、暴力よりもはるかに小さいです。言い換えれば、DPは剪定が来ます。回答の多くをあきらめ最適解することはできません。だから我々は、DPの核となるアイデアを得ることができます:可能な解空間を絞り込むしよう。我々はDPを使用している場合、その解空間のサイズダウン多項式可能であり、アルゴリズムにおける暴力は、それが解空間の大きさは、多くの場合、指数関数的であるかもしれません。一般的には、解空間は解決策を見つけるために、より速く、小さくなっています。これは、最適化を完了します。
アルゴリズムDPを設計する方法
まず、Xのように表さ状況に直面しています。このステップでは、設計状態と呼ばれています。状態xのために、私たちはその答え(例えば最小コスト)は、f(x)は尋ねた。私たちの目標は、F(T)を見つけることです。Fを探す(x)は(Pで示す)に関する状況は、書き込みされて覚えていますF(p)とF(X)を起動するための式(状態遷移方程式と呼ばれます)。
DPステップ
DPアルゴリズムの設計、多くの場合、三つのステップ:
- 私は誰ですか? - 設計状態、状況を示します
- 私はから来ましたか?
- 私は行きたいですか? - 両方のデザインの転送後
設計条件DPの基礎です。デザインの次の転送、2つの方法があります。一つは、私はどこから来たのかを検討することであり、もう一方はどこへ行く私を考慮することで、Fにおいて一般的である(X)を、得た後のxからの溶液の一部を更新するために行ってきました。また、これはDPがたくさんあり、我々は後に発生します。すべてのすべてで、「私は、来る」と「私が行きたい、」1のみを明確に考える必要があり、問題を解決するためにコードを書くための状態遷移方程式を設計することができるであろう。また、プル型転送として知られ、前者は、後者はまた、転移プッシュタイプとして知られています。
テンプレートの例
/*
题意:求n个元素的数组的m个连续子段的和的最大值
dp[i][j] = 选择i个连续子段时 前j个数的最大值
状态转移方程:max[i-1][j-1]=max{ dp[i - 1][k] }(i - 1 <= k <= j - 1)
dp[i][j] = max{dp[i][j - 1],max[i-1][j-1]} + a[j]
解释:第j个元素可以和前面的连接在一起,构成i段dp[i][j-1]+a[j],
或者独自成为一段,dp[i-1][k]+a[j] 因为分成i-1段,所以k>=i-1
前j - 1个数必须组合出i - 1段,选择多种情况里的最大值,
注:最后由于数据较大,所以要压缩空间,使用滚动数组
*/
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
using namespace std;
#define ull unsigned long long
#define ll long long
#define MAX 1000005
const int inf = 0x3f3f3f3f;
int t, n, m, x, y, z;
int a[MAX],mx;
int dp[MAX];
int ma[MAX];
int main()
{
while (scanf("%d%d",&m,&n)!=EOF)
{
memset(dp, 0, sizeof(dp));
memset(ma, 0, sizeof(ma));
for (int i = 1; i <= n; i++)
scanf("%d",a+i);
for (int i = 1; i <= m; i++)
{
mx = -inf;
for (int j = i; j <= n; j++)
{
dp[j] = max(dp[j - 1], ma[j - 1]) + a[j];
ma[j-1]= mx; //为下一轮i循环保留max[i-1][j-1]
mx = max(mx,dp[j]); //mx为dp[i][i]到dp[i][j]的最大值
}
}
printf("%d\n", mx); //因为循环到n所以mx最终为结果
}
return 0;
}
/*
题意:求奇数个元素的数组的出现次数大于一半的数
此数出现的次数大于其他数出现次数的总和,
状态转移方程:新增数==现有最多次数的数,次数+1,else 次数-1
次数为0时转移到新增数
解释:每增加一个数做一次决策判断之前的数是否是新增状态中的次数过半的数,
若判定越过的数当前不可能次数过半,由于一定有次数过半的数,
因此可以暂定新增数为次数过半的数
注:此题没有卡内存,因此还可以用桶装,输出第一个次数过(n+1)/2的数
for (int i = 1; i <= n; i++)
{
cin >> x;
a[x]++;
if (a[x] == (n + 1) / 2)
y = x;
}
cout << y << endl;
*/
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
using namespace std;
#define ull unsigned long long
#define ll long long
#define MAX int(1e6+5)
const int inf = 0x3f3f3f3f;
int t, n, m, cnt, x, y, z;
int a[MAX];
int dp[MAX];
int ma[MAX];
int main()
{
while (cin >> n)
{
cnt = 0;
for (int i = 1; i <= n; i++)
{
cin >> x;
if (cnt == 0)
{
m = x;
cnt = 1;
}
else
{
if (m == x)
cnt++;
else cnt--;
}
}
cout << m << endl;
}
return 0;
}
最长上升子序列(LIS)
/*
**题意:求n个元素的数组中最长的由依次增大的元素组成的子序列
**状态转移方程:if (a[i] > a[j])
** dp[i] = max(dp[i], dp[j] + 1);
** sum = max(dp[i], sum);
**解释:整体最长上升子序列的从头开始的每个部分都是特点末元素的最长上升子序列
** 每增加一个元素,判断以此元素是否比以其他元素大
** 若大则以j元素结尾的最长上升子序列dp[j]+1
** 且得到所有以i元素结尾的子序列,比较得以i元素结尾的最长上升子序列
** 一直到第n个元素,得到以n元素结尾的最长上升序列
** 比较所有元素结尾的最大子序列,得到最长上升子序列
** 注: if (a[i] > a[j])
** dp[i] = max(dp[i], dp[j] + a[i]);
** sum = max(dp[i], sum);
** 此方程得到最大上升子序列(所有元素的和最大)
*/
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
using namespace std;
#define ull unsigned long long
#define ll long long
#define MAX int(1e6+5)
const int inf = 0x3f3f3f3f;
int t, n, m, cnt, sum, flag;
int x, y, z;
int a[MAX];
int dp[MAX];
int main()
{
while (cin >> n)
{
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++)
cin >> a[i];
a[0] = 0;
sum = 0;
for(int i=1;i<=n;i++)
for (int j = 0; j < i; j++)
{
if (a[i] > a[j])
dp[i] = max(dp[i], dp[j] + 1);
sum = max(dp[i], sum);
}
cout << sum << endl;
}
return 0;
}
演習のコアコード
最も長いシーケンスと上昇変形を見つけるnlogn
- 羅区P1439最長共通サブテンプレート
の一般的なソリューション:我々が使用することができp[i][j]
、私は文字列をビット前に、最初を表すために、LCS長jビット2番目の文字列の前に、我々は容易に状態遷移方程式を想像することができます。現在の場合A1[i]
とA2[j]
同じ(すなわち、新しい公共の要素がある)ので、dp[i][j]=max(dp[i][j],dp[i−1][j−1]+1);
同じでない場合は、共通の要素を更新することができないという、継承を考慮してくださいdp[i][j]=max(dp[i−1][j],dp[i][j−1]
。しかし、この問題のために、単純なアルゴリズムである\(N ^ 2 \)ため\(10 ^ 5 \)データのサイズは、時間範囲を超えた2つの完全な置換配列は1からnであるように、アルゴリズムのセットは、2つの考慮nlogn互いに異なる配列と同一の要素には、すなわち、わずかに異なる位置である我々意志によってアレイB配列である配列中の位置の数表さ-最長共通サブシーケンスであるので、少し後方に位置合わせインクリメントされたシーケンスbは、bは、部分的にこの番号の後の位置の全体数に記載される場合、各要素の配列における位置が、次に包含のために考慮LCS--することができるように、記録用変換nlogn尋ねますLISの新しい場所のマップ配列。
n = 0;
cin >> n;
for (i = 0; i < n; ++i)
cin >> a1[i];
for (i = 0; i < n; ++i)
cin >> a2[i];
for (i = 0; i < n; ++i)
dp[a1[i]] = i; //将a1数组中的数的顺序载入
int len = 0;
for (i = 1; i <n; ++i)
{
if (dp[a2[len]] < dp[a2[i]])
a2[++len] = a2[i];
else //二分查找
{
int j = 0, k = len,mid;
while(j<k)
{
mid = (j + k) / 2;
if (dp[a2[mid]] > dp[a2[i]])
k = mid;
else j = mid + 1;
}
if(dp[a2[j]]>dp[a2[i]])
a2[j] = a2[i];
}
}
cout << len+1<< endl;
- 羅区P1020ミサイルインターセプトは、
この質問は、と(迎撃システムが最小を必要とどのように多くの)最長の単調に増加するサブシーケンスの1(迎撃ミサイルの数まで)単調最長のシーケンスを上昇しない子供を依頼することです。
int a[MAX],a2[MAX],dp[2][MAX];
n = 0;
while ((cin >> a[n]))
++n;
dp[0][0] = a[0];
dp[1][0] = a[0];
int len1 = 0, len2 = 0;
for (i = 1; i <n; ++i)
{
if (dp[0][len1] >= a[i])
dp[0][++len1] = a[i];
else
{
int p= upper_bound(dp[0], dp[0]+ len1, a[i], greater<int>()) - dp[0];
dp[0][p] = a[i];
}//最长单调不升序列
if (dp[1][len2] < a[i])
dp[1][++len2] = a[i];
else
{
int p= lower_bound(dp[1], dp[1]+ len2, a[i]) - dp[1];
dp[1][p] = a[i];
}//最长单调递减序列
}
cout << len1 + 1 << '\n' << len2 + 1 << endl;
- バレーシティロサンゼルスP2782友情
北サウスバンクの銀行または片側に分類への単一の最も長いシーケンスにより、他の側に単調に増加、需要を持っています
struct node
{
int n, s;
}a[2*MAX+10];
bool cmp(struct node x, struct node y)
{
return x.n < y.n;
}
int main()
{
cin >> n;
for (i = 1; i <= n; ++i)
scanf("%d%d", &a[i].n,&a[i].s);
int len= 1;
sort(a + 1, a + n + 1,cmp);//排序
for (i = 2; i <= n; ++i)//二分求最长单增子序列
{
if (a[i].s > a[len].s)
{
a[++len].s = a[i].s;
}
else
{
j = 1, k = len;
int mid;
while(j<k)
{
mid = (j + k) / 2;
if (a[mid].s > a[i].s)
k = mid;
else j = mid + 1;
}
a[j].s = min(a[j].s, a[i].s);
}
}
printf("%d", len);
return 0;
}
- OpenJ_Bailian - 2995
#include <iostream>
#include <cstdio>
using namespace std;
int n,i,j;
int a[1200],f1[1200],f2[1200],maxn;
int main()
{
scanf("%d",&n);
for (i=1;i<=n;i++)
scanf("%d",&a[i]);
for (i=1;i<=n;i++)
f1[i]=f2[i]=1;
for (i=1;i<=n;i++)
for (j=1;j<i;j++)
if (a[i]>a[j])
f1[i]=max(f1[i],f1[j]+1);//求首元素开始到某一元素结束的最长递增序列长度
for (i=n;i>=1;i--)
for (j=n;j>i;j--)
if (a[i]>a[j])
f2[i]=max(f2[i],f2[j]+1);//求某一元素开始到尾元素的最长递减序列长度
for (i=1;i<=n;i++)
maxn=max(maxn,f1[i]+f2[i]-1); //遍历求除最大值
printf("%d\n",maxn);
return 0;
}
印刷DPパス
- 羅DIG鉱山P2196谷
状態定義されf[i]
、次いで、i番目のノードの終わりに最大値をf[i]=max{f[j]}+a[i] (g[j][i]=1)
、
void print(int x)//打印前驱节点
{
if (pre[x] == 0)//起始节点无前驱节点
{
printf("%d", x);
return;
}
else print(pre[x]);
printf(" %d", x);
}
int main()
{
cin >> n;
for (i = 1; i <= n; ++i)
cin >> a1[i];
for(i=1;i<n;++i)
for (j = i + 1; j <= n; ++j)
cin >> a[i][j];
for (i = 1; i <= n; ++i)
{
for (j = 1; j <= n; ++j)
if (a[j][i] == 1&&dp1[j]>dp1[i])
{
dp1[i] =dp1[j];
pre[i] = j;//i点由j点到达
}
dp1[i] += a1[i];
if (dp1[i] > sum)
{
sum = dp1[i];
k = i;//记录结束的节点
}
}
print(k);
cout << '\n'<< sum << endl;
}
- 羅区P2066マシン配布
F(i、j)は、最大の利益は1〜I年代を与えるjのマシンを表しw[i][j]
、I-会社の利益移転式jのマシンがさを表すf(i,j)=max(k∈[0,j]){f(i+1,j-k)+w[i][j]}
。このタイトルデータが(配列のサイズを最適化する方法に焦点を当て)最小の、それだけに注目F(I + 1、L)に伝達方程式を使用され 、(l∈[0、j]) であれば下降列挙jとiは、この次元配列DPに除去することができます
次の出力は、kはANS格納された配列F(I、J)で最大値をとる場合、順次F(i、j)を求め、である(I + 1?)Fであるプログラムの出力から転送することができます。最小の正のシーケンス列挙Kので、比較例F [J]とを辞書式にf [JK] + [I W ] [K] との大きさを取ることができないような、正の配列片が正のシーケンス出力を容易にする場合、私は(そう列挙降順答えのためにあなたは、配列を持つことになり、正シーケンス出力を保存したい場合は、)、n個の出力を開始(N、M)のi Fです。
int main()
{
cin >> n>>m;
sum = 0;
for (i = 1; i <= n; ++i)
for (j = 1; j <= m; ++j)
scanf("%d", &a[i][j]);
dp1[0] = 0;
for (i = n; i > 0; --i)
for (j = m; j >=0; --j)
for (k = 1; k <= j; ++k)
if (dp1[j-k] + a[i][k] > dp1[j])
{
dp1[j] = dp1[j-k] + a[i][k];
a1[i][j] = k;
}
cout << dp1[m];
for (i = 1, j = m; i <= n; ++i)
{
cout << endl << i << " " << a1[i][j];
j -= a1[i][j];
}
return 0;
}
ナップザック問題
バックパック9つのストレス- 01バックパック
が特徴最も基本的なナップザック問題、:各アイテムは、選択または除外することができ、唯一のものです。
サブ問題状態によって定義される:すなわち、F [i]は[V]フロントIアイテムを示し、正確に利用可能な最大容量値Vバックパックを置きました。状態遷移方程式は、f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
「私にVのバックパックの容量に入れアイテムの前に」サブ問題、唯一のiアイテム(又は離脱)の戦略を考えると、それだけに変換することができます。 I-1は、項目の前に問題がありました。あなたは私のアイテムを保持する場合、問題は、「I-1バックパックVに配置されたアイテムの能力の前に」に変換され
、時間と上記のプロセスの空間計算量は、O(Nの*のV)である、の時間複雑基本的にはもはや最適化することができないが、それはO(V)に空間的複雑度を最適化することができます。
達成するために、上記の状態遷移方程式を検討する方法を、間違いなくメインループi = 1..N、それぞれ算出された2次元アレイがあるf[i][0..V]
すべての値。唯一の配列を使用している場合f[0..V]
、あなたはi番目のサイクルの終了後ことを確認する必要がありf[v]
、当社の状態表現で定義されているf[i][v]
ので、f[i][v]
によるf[i-1][v]
と、f[i-1][v-c[i]]
再発からの二つのサブ問題をプッシュすることを確実にするために、f[i][v]
とき(すなわち、i番目のメインループを押してくださいf[v]
時間)を得ることができるf[i-1][v]
とf[i-1][v-c[i]]
値。実際には、各メインループは、我々は、V = V..0順押すf[v]
ごとにプッシュすることを保証するf[v]
ときf[v-c[i]]
の状態退避f[i-1][v-c[i]]
値を。
我々はタイトルに表示さナップザック問題の最適解は、実際には、同じ質問が2つ以下があります。いくつかの質問には、最適解の間に「ちょうど埋めバックパック」を聞くと、いくつかのトピックは、バックパックを充填する必要はありません。両者の違いを実現することは、初期化時に聞かれる別の質問です。最初の質問が尋ねられる場合、単に充填袋を必要とし、Fを除い次に初期化[0] [1..V] F他方が得られたFを確保するために、-∞に設定されている0 [N]まさに最適なソリューションで満たさバックパック。バックパックを充填する必要はなく、唯一のできるだけ大きなへの価格を希望されない場合は、[0..V]すべて0に設定されているfは、初期化がなければなりません。他のすべてのボリュームの前の最大値が満たされた袋を得るために、0から取得する必要があり、後者は、空のバックパックを出発物質として、任意の容積であってもよく、残りは任意の容積とすることができます。このトリックは、ナップザック問題の他のタイプに拡張することができ
羅区のハーブP1048
scanf("%d%d", &m, &n);
for (i = 1; i <= n; ++i)
scanf("%d%d", a1 + i, a2+i);
for (i = 0; i <= m; ++i)
dp[i] = 0;
for (i = 1; i <= n; ++i)
for (j =m ; j >= 0; --j)
if (a1[i]<=j)
dp[j] = max(dp[j], dp[j-a1[i]] + a2[i]);
printf("%d\n",dp[m]);
羅区P2871 [USACO07DEC]ブレスレットチャームブレスレット
scanf("%d%d", &n, &m);
for (i = 1; i <= n; ++i)
scanf("%d%d", a[0] + i, a[1] + i);
for (i = 0; i <= n; ++i)
for (j = m; j >= a[0][i]; --j)
dp[j] = max(dp[j - a[0][i]] + a[1][i], dp[j]);
printf("%d\n",dp[m]);
デジタル組み合わせOpenJ_Bailian - 4004
dp1[0] = 1;\\初始化和为0的方式为1
for (i = 0; i < n; ++i)
for (j = m; j >= 1; --j)
if (a1[i] <= j && dp1[j - a1[i]] != 0)
dp1[j] += dp1[j - a1[i]];\\得到和为j的组合方式
printf("%d\n", dp1[m]);
- フルバックパック
フル01ナップザック問題のナップザックは、各項目が無制限のピースを取ることができることを除いて、非常によく似ています。F [i]が[V]リュックvの最大重量容量に直前に私の記事を意味するように。状態遷移方程式は次のように、各項目ごとに異なる戦略に書くことができますf[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
。
最適な方法は、そのようなものが、ゼロサイクルから2番目のフィールドサイクルの構造を変更することでf[i][v]
形成することができるf[i][v-c[i]]+w[i]
与えるために
狂ったハーブロスバレーP1616を
for (i = 1; i <= n; ++i)
scanf("%d%d", a[0] + i, a[1] + i);
for (i = 1; i <= n; ++i)
for (j = a[0][i]; j<= m; ++j)\\从a[0][i]开始
dp[j] = max(dp[j], dp[j- a[0][i]] + a[1][i]);
その他の利用
- 最大のサブ行列のHDU - 1559
int dp[1100][1100];
int MAX;
int main()
{
int i,j,n,m,T,x,y;
scanf("%d",&T);
while(T--)
{
scanf("%d %d %d %d",&n,&m,&x,&y);
MAX=0;
memset(dp,0,sizeof(dp));
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
{
scanf("%d",&dp[i][j]);
dp[i][j]+=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1];//状态转移方程
if(i>=x&&j>=y) //矩阵大小为x*y
{
MAX=max(MAX,dp[i][j]-dp[i][j-y]-dp[i-x][j]+dp[i-x][j-y]);
}
}
printf("%d\n",MAX);
}
return 0;
}
- 抽象ピーナッツOpenJ_Bailian - 3727
cin >> t;
while (t--)
{
cin >> m >> n;
sum = 0;
memset(a, 0, sizeof(a));
for (j = 1; j <= m; ++j)
for (i = 1; i <= n; ++i)
{
cin >> a[j][i];
a[j][i] += max(a[j - 1][i], a[j][i - 1]);//只能由左方或上方走到
}
cout << a[m][n] << endl;
}