動的計画法: 後続問題
序文
動的プログラミングに関する以前の記事:
後続の問題
1. 最長の増加サブシーケンス (中)
リンク:最長の増加サブシーケンス
-
質問の説明
-
質問を行う手順
-
状態表現
線形 dp の場合、通常、次の 2 つの表現を使用します。
(1) 特定の位置で終了...
(2) 特定の位置から開始...
通常、これら 2 つの方法のうち最初の方法を使用します。特定の位置が終了であり、質問の要件と組み合わせると、状態表現を dp[i] として定義できます。これは、i 位置で終了するすべてのサブシーケンスの中で最も長く増加するサブシーケンスの長さです。 -
状態遷移方程式
位置 i で終わる部分列については 2 つの可能性があります:
(1) 他人に従わず、自分だけ dp[i] = 1
(2) [0, 1, 2, ... に従う] , i - 1] をこれらの位置の後ろに配置し、0 <= j <= i - 1 と仮定すると、サブシーケンスの増分 (nums[j] < nums[i]) を維持できる場合は、この位置の後に接続できます。
0~i - 1 の j を列挙して、どの位置が最大の長さを持っているかを確認します。
つまり、 dp[i] = max(dp[i], dp[j] + 1) -
各位置を最小値 1 に初期化すると、すべての位置が 1 に初期化されます。
-
フォームに入力する順序:現在の状態を入力するときに、必要な状態が計算されていること、およびフォームに入力する順序が左から右であること
を確認してください。 -
戻り値
最長サブシーケンスの終了位置を直接決定する方法はなく、最大値は dp 中に更新されます。
- コード
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
//dp[i]表示以i位置为结尾的最长递增子序列
vector<int> dp(n, 1);
int ret = 1;
for(int i = 1; i < n; i++)
{
//从[0, i-1]看一圈,找接在那个符合条件的位置后面可以让子序列最长
for(int j = 0; j < i; j++)
if(nums[j] < nums[i])
dp[i] = max(dp[i], dp[j] + 1);
//看看能不能更新最大
ret = max(ret, dp[i]);
}
return ret;
//时间复杂度:O(N ^ 2)
//空间复杂度:O(N)
}
};
2. スイングシーケンス(中)
リンク:スイングシーケンス
-
質問の説明
-
質問を行う手順
-
状態表現
これまでの経験に基づいて、状態表現を dp[i]、つまりi 位置で終わるすべてのスイング シーケンスの最大長として定義できます。 -
状態遷移方程式
長さが 1 より大きいスイング シーケンスには、次の 2 つの状況があります。
(1) (1、7、4、9) などの上昇状態。
(2) (1, 17, 10) のように衰退している状態。
したがって、2 つの状態を同時に記録する必要があります。ここで、f[i] は位置 i で終了する上昇状態にある最長のスイング シーケンスの長さを表し、g[i] は下降状態を表します。スイングシーケンスを分析した後、単一のポジションを分析してみましょう (1) 他の人の後ろに接続せず、単独でプレーし、dp[i] = 1 (2) [0, 1, 2,... の後に接続する] の
2 つの可能性があります。, i - 1] これらの位置の後ろでは、0 <= j <= i - 1 とします。① j 位置に接続後、上昇状態(nums[i] - nums[j] > 0) の場合、j 位置で終了し、下降状態、つまりf[i]になる必要があります。 ] = g[j] + 1。② j 位置に接続後、下降状態になっている場合 (nums[i] - nums[j] < 0)、j 位置で終了し、上昇状態である必要があります。つまり g[ i ] = f[j] + 1。 -
初期化
シーケンスの最小長は 1 で、すべての位置が 1 に初期化されます。 -
フォームに入力する順序:現在の状態を入力するときに、必要な状態が計算されていること、およびフォームに入力する順序が左から右であること
を確認してください。 -
戻り値
最長スイングシーケンスの終了を直接判断する方法はないため、dp 中に最大値が更新されます。
- コード
class Solution {
public:
int wiggleMaxLength(vector<int>& nums)
{
//dp[i]表示以i位置为结尾的最长摆动序列长度
int n = nums.size();
vector<int> f(n, 1);//处于上升状态
vector<int> g(n, 1); //处于下降状态
int ret = f[0]; //记录最终结果
for(int i = 1; i < n; i++)
{
for(int j = 0; j < i; j++)
{
int gap = nums[i] - nums[j];
//处于上升
if(gap > 0)
f[i] = max(f[i], g[j] + 1);
//处于下降
else if(gap < 0)
g[i] = max(g[i], f[j] + 1);
//相同的情况为1不用处理
}
ret = max({
ret, f[i], g[i]});
}
return ret;
//时间复杂度:O(N ^ 2)
//空间复杂度:O(N)
}
};
3. 最長増加部分列の数 (中)
リンク:最長増加部分列の数
-
質問の説明
-
質問を行う手順
-
状態表現
これまでの経験に基づいて、状態表現 dp[i]:位置 i で終了する最も長く増加するサブシーケンスの数を定義できます。 -
状態遷移方程式は
、現在の位置で最も長く増加する部分列の数を更新する必要があります。それは、どの位置が最も長いかを調べるだけです。しかし、問題は、以前の位置と、前の位置にある列の数だけが存在することです。長さがないため、長さを記録するテーブルを追加する必要があります:
(1) count[i]: i 位置で終わる最長の増加サブシーケンスの数
(2) len[i]: 最長の増加サブシーケンスの長さi 位置で終了
len[i ] 前に述べたように、count[i] を分析します:
(1) 他の後に接続されていない場合、最大長は 1、count[i] = 1
(2) 後に接続されます。 [0, 1, 2,..., i - 1] これらの位置の後に、0 <= j <= i - 1 と仮定すると、増加し続ける部分列 ( nums [j] < nums[i]) を接続できます。この位置の後。
j を 0~i - 1 まで列挙し、その位置以降の長さに従って解析します。
① 元の長さ (len[i] > len[j] + 1) より小さい場合は、気にしないでください。
② 元の長さより大きい(len[i] < len[j] + 1) 元のシーケンス番号がいくら多くても大幅にカットされ、番号が長くなるように更新されます。 count[i] = count[j ] ]。③元と同じ長さ(len[i] == len[j] + 1)で、 count[i] += count[j] と
カウントが増加します。 -
初期化
シーケンスの最小長は 1 で、すべて 1 に初期化されます。 -
フォームに入力する順序:現在の状態を入力するときに、必要な状態が計算されていること、およびフォームに入力する順序が左から右であること
を確認してください。 -
戻り値
(1) で前の作業が完了します。各位置で終わる最長の増加部分列の長さと数はわかっていますが、どの位置で終わる最長列かはわかりません。そのため、片側 dp で最大長を更新する必要があります。 max_length。
(2) 最大長がわかっているので、カウント テーブルを 1 回走査し、max_length の長さのシーケンスをカウントするだけで済みます。
- コード
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> count(n, 1); //f[i]表示以i位置为结尾的最长子序列个数
auto len = count; //g[i]表示以i位置为结尾的最长递增子序列长度
int max_length = len[0];
for(int i = 1; i < n; i++)
{
for(int j = 0; j < i; j++)
{
if(nums[i] > nums[j])
{
//找到了更加长的
if(len[i] < len[j] + 1)
{
len[i] = len[j] + 1;
count[i] = count[j];
}
else if(len[i] == len[j] + 1) //长度相同
count[i] += count[j];
}
}
max_length = max(max_length, len[i]);
}
int ret = 0; //返回值
//遍历一次,计算最长序列个数
for(int i = 0; i < n; i++)
if(len[i] == max_length)
ret += count[i];
return ret;
//时间复杂度:O(N ^ 2)
//空间复杂度:O(N)
}
};
4. 最長ペアチェーン(中)
リンク:リンクのペアの最長数
-
質問の説明
-
質問を行う手順
-
状態表現
これまでの経験に基づいて、状態表現 dp[i] を定義します。これは、位置 i で終わる最長の数値ペア チェーンの長さです。 -
状態遷移方程式
この質問の分析は、実際には、前の最長増加部分列と基本的に同じです。
(1) 他の人の後ろには接続せず、自分でプレイします。 dp[i] = 1
(2) 位置 [0, 1, 2,..., i - 1] の後ろに接続します。0 <= j <= i と仮定します。 - 1.ペアチェーンの数の要件が満たされている場合(pairs[j][1] <pairs[i][0])、この位置の後に接続できます。
0~i - 1 の j を列挙して、どの位置が最大の長さを持っているかを確認します。
つまり、 dp[i] = max(dp[i], dp[j] + 1) -
最小の初期化
長は 1 で、すべて 1 に初期化されます。 -
フォームに入力する順序:現在の状態を入力するときに、必要な状態が計算されていること、およびフォームに入力する順序が左から右であること
を確認してください。 -
戻り値
最長ペアチェーンの終端を直接判断する方法はないため、dp の実行中に最大値が更新されます。
- コード
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
sort(pairs.begin(), pairs.end()); //先排序
int n = pairs.size();
//dp[i]表示以i位置为终点的最长长度
vector<int> dp(n, 1);
int ret = 1; //记录最长
for(int i = 1; i < n; i++)
{
for(int j = 0; j < i; j++)
if(pairs[j][1] < pairs[i][0]) //如果可以接在后面
dp[i] = max(dp[i], dp[j] + 1);
ret = max(ret, dp[i]);
}
return ret;
//时间复杂度:O(N ^ 2)
//空间复杂度:O(N)
}
};
5. 最長の明差部分列(中)
リンク:最長明差部分列
-
質問の説明
-
質問を行う手順
-
状態表現
これまでの経験に基づいて、状態表現 dp[i] を定義します。これは、添字 i 位置で終わる最長の算術部分シーケンスの長さです。 -
状態遷移方程式
この問題を解決する最善の方法は、増分サブシーケンスを使用することですが、この方法で記述するとタイムアウトになります。その理由を分析できます:
(1) 増加するサブシーケンスの後に多くの位置が続く可能性があります。
(2) 算術部分列の後には (1、2、3、4) などの固定位置のみが続き、差は 1 で、その中の 4 の後には 3 のみが続き、その他の判断は冗長です。
次に、考え方を変えてみましょう。つまり、(1, 2, 3, 4) の差は 1 です。 4 の位置を埋めるとき、3 で終わるものを直接見つけることができれば (arr[i] - 差) 最長になります。サブシーケンスを増やすのは問題ありません。要素 arr[i] を dp[i] にバインドして
、ハッシュ テーブルのハッシュを作成できます。このハッシュ テーブルで直接動的プログラミングを行うことができます。状態遷移方程式は次のとおりです: hash[i] = hash[ arr[i] -差] + 1。
-
初期化
フォームに入力するときに、事前状態が存在しない場合は、個別に処理されません (0 プラス 1 は 1 になり、それ自体の 1 つに対応します)。したがって、ハッシュ テーブルに最初の要素を入れる必要があるのは、 hash[arr[0]] = 1 だけです。 -
フォームに入力する順序:現在の状態を入力するときに、必要な状態が計算されていること、およびフォームに入力する順序が左から右であること
を確認してください。 -
戻り値
最長部分演算シーケンスの終わりは不定であるため、dp の実行中に最大値が更新されます。
- コード
class Solution
{
public:
int longestSubsequence(vector<int>& arr, int difference)
{
// 创建⼀个哈希表
unordered_map<int, int> hash; // {arr[i], dp[i]}
hash[arr[0]] = 1; // 初始化
int ret = 1;
for(int i = 1; i < arr.size(); i++)
{
hash[arr[i]] = hash[arr[i] - difference] + 1;
ret = max(ret, hash[arr[i]]);
}
return ret;
//时间复杂度:O(N)
//空间复杂度:O(N)
}
};
6. 最長のフィボナッチ部分列の長さ (中)
リンク:最長のフィボナッチ部分列の長さ
-
質問の説明
-
質問を行う手順
-
状態表現
経験に基づいて、状態表現を i 位置で終わる最長のフィボナッチ数列の長さとして定義できますが、この定義には致命的な問題があります。ある位置の行為順序を確認します。
1 つの要素を決定することはできませんが、フィボナッチ数列の最後の 2 つの要素がわかっていれば、前の要素を推定できるため、前述の問題を解決できます。
そこで、2 次元テーブルdp[i][j] を定義します。 i と j の位置を、最後の 2 つの要素の最長のフィボナッチ数列の長さとしてとります。 -
状態遷移方程式
では、i が j より小さいことが規定されており、j は [2, n - 1] から列挙を開始し、i は [1, j - 1] から列挙を開始します。
nums[i] = b, nums[j] = c と仮定すると、このシーケンスの最初の要素は a = c - b になります。 a の状況に基づいて議論します:
(1) a が存在し、その添字を k とします。そして a < b の場合、このとき c は a と b で終わるフィボナッチ数列の後に接続でき、dp[i][j] = dp[k][i] + 1 となります。
(2) a は存在しますが、 b < a < c このとき、 b と c だけで構成できますdp[i][j] = 2。(3) a は存在せず、この時点では b と c、 dp[i][j] = 2
のみで構成できます。状態遷移プロセスでは、a 要素の添字を決定する必要があることがわかりました。したがって、 dp の前にすべての「要素 + 添え字」をバインドして、ハッシュ テーブルに入れることができます。 -
最小の初期化
長は 2 で、すべて 2 に初期化されます。 -
フォームに記入する順序は
最後の番号に固定されており、最後から 2 番目の番号が列挙されます。 -
戻り値
最長のフィボナッチ部分列の終わりは不定であるため、dp 中に最大値が更新されます。
- コード
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr)
{
int n = arr.size();
//i->j
dp[i][j]表示以i,j为后两个的斐波那契数列最长长度
vector<vector<int>> dp(n, vector<int>(n, 2));
unordered_map<int, int> hash;
for(int i = 0; i < n; i++) hash[arr[i]] = i;
int ret = 2;
for (int j = 2; j < n; j++)
{
for (int i = 1; i < j; i++)
{
int former = arr[j] - arr[i];
//a b c,a < b 并且a存在
if (former < arr[i] && hash.count(former))
{
dp[i][j] = dp[hash[former]][i] + 1;
}
ret = max(ret, dp[i][j]);
}
}
//斐波那契序列最小为3,为2的情况返回0
return ret > 2 ? ret : 0;
//时间复杂度:O(N)
//空间复杂度:O(N ^ 2)
}
};
7. 最長の算術シーケンス (中)
リンク:最長の算術シーケンス
-
質問の説明
-
質問を行う手順
-
状態表現
は前の質問と似ています。1 つの要素だけでは等差数列の外観を決定できません。それを決定するには次の 2 つの要素が必要なので、2 次元のテーブル dp[i][j] を定義します。最後の 2 つは j 要素の最長の算術サブシーケンスの長さ。 -
状態遷移方程式
では、i が j より小さいと規定されており、nums[i] = b、nums[j] = c と仮定すると、このシーケンスの最初の要素は a = 2 * nums[i] - nums[j] (算術演算) (1 ) a が
存在し、その添字を k とするとき、a と b で終わる数列に c を接続すると、dp[i][j] = dp[k][i] + 1。
(2) a は存在せず、この時点では b と c のみで構成できます ( dp[i][j] = 2 )。
状態遷移方程式では、要素 a の添字を決定する必要があることがわかります。したがって、すべての「要素 + 添え字」をバインドしてハッシュ テーブルに入れることができます。この質問のハッシュ テーブルには 2 つのオプションがあります:
(1) dp の前にハッシュ テーブルに直接配置します。重複要素が表示される可能性があります (この質問は順序が正しくありません、前の質問は厳密に増分です)。これらの重複要素は次のとおりです。を記録するには、その添え字を配列に形成する必要があります。フォームに記入する前に、配列を走査して必要な添え字を見つける必要があります。これには非常に時間がかかるため、この解決策を渡すことはできません。
(2)ハッシュ テーブルに格納できるのは DP 使用中のみであり、i 位置が使用された後はハッシュ テーブルに格納されますが、テーブルを埋める順序は最後から 2 番目と固定である必要があります。列挙の最初から最後まで。、最初の質問を修正し、前の質問から 2 番目の質問を列挙してフォームに記入することはできません。この例を見てみましょう: [0, 2, 4, 4, 4, 6, 8, 4, 9, 4, 4]. 最後の 4 は固定です。最初の 4 が最後から 2 番目の場合、次の値を探す必要があります。前の 4. 添え字 (ここでは [0,2] が前に付いていますが、4 はありません。これは、この数値がハッシュ テーブルに含まれるべきではなく、固定の逆数 1 と逆数 2 形式の列挙であることを意味します) fill メソッドにより、ハッシュ テーブルに確実に保存されます。この時点では完全に混乱しています) -
最小の初期化
長は 2 で、すべて 2 に初期化されます。 -
フォームに入力する順序は
最後から 2 番目に固定されており、フォームに入力する順序は列挙型の最後から最後になります。 -
戻り値
最長等差数列の終了は不定であるため、dp 実行中に最大値を更新します。
- コード
class Solution {
public:
//dp[i][j]表示以i,j为结尾的最长等差数列长度
int longestArithSeqLength(vector<int>& nums) {
int n = nums.size();
unordered_map<int, int> hash;
hash[nums[0]] = 0;
vector<vector<int>> dp(n, vector<int>(n, 2));
int ret = 2;
for (int i = 1; i < n; i++) //倒数第二个
{
for (int j = i + 1; j < n; j++)
{
int former = 2 * nums[i] - nums[j];
if (hash.count(former))
dp[i][j] = dp[hash[former]][i] + 1;
ret = max(ret, dp[i][j]);
}
hash[nums[i]] = i;
}
return ret;
//时间复杂度:O(N ^ 2)
//空间复杂度:O(N ^ 2)
}
};
8. 等差数列の割り算Ⅱ~部分数列(難解)
リンク:等差数列部門 II - 部分数列
-
質問の説明
-
質問を行う手順
-
状態表現
は前の質問と一致しています。1 つの要素だけでは等差数列の外観を決定できません。それを決定するには次の 2 つの要素が必要なので、2 次元テーブル dp[i][j] を定義します。最後の 2 つは j 要素の算術部分列の数。 -
状態遷移方程式
まず、この問題では算術部分列の繰り返しは存在しません、要素の位置が異なれば異なる部分列とみなされます 例えば配列 [7,7,7,7,7] ] には 16 もの算術部分列があります。
nums[i] = b、nums[j] = c と仮定すると、i は j より小さいと規定されており、このシーケンスの最初の要素は a = 2 * nums[i] - nums[j] になります。 a の状況に基づく:
(1) a が存在するこのとき、c の後に a と b で終わるシーケンスが続くことができます。a の添字を k とします。ここでの添字の状況は、複数の a が存在する可能性があるため、前の状況とは異なります。添字配列を使用して、a の添字をさまざまな位置に記録する必要があります。k < i の場合 (a はi (前)、dp[i][j] += dp[k][i] + 1、ここでの +1 はグループ [a, b, c] を表します。条件を満たすすべての a を合計するだけです。。
(2) a は存在せず、この時点では b と c のみで構成できます ( dp[i][j] = 2 )。
状態遷移方程式では、要素 a の添字を決定する必要があることがわかります。したがって、すべての「要素 + 添字配列」をバインドしてハッシュ テーブルに入れることができます。 -
初期化
初期化は必要ありません。デフォルトは 0 です。 -
フォームへの入力順序:
フォームへの入力順序は最初に固定され、次に列挙が続きます。 -
戻り値
変数 sum を定義し、dp 中に累積します。
- コード
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int n = nums.size();
//dp[i][j]表示以i,j为结尾的等差数列个数,规定j > i
//前置可能有存在多个,需要一一加起来
vector<vector<int>> dp(n, vector<int>(n));
unordered_map<long long, vector<int>> hash; //数据和下标数组绑定
for(int i = 0; i < n; i++)
hash[nums[i]].push_back(i);
int sum = 0;
for(int j = 2; j < n; j++)
{
for(int i = 1; i < j; i++)
{
long long former = (long long)nums[i] * 2 - nums[j]; //处理数据溢出
if(hash.count(former))
{
for(auto k : hash[former])
{
//former必须在左边
if(k < i)
dp[i][j] += dp[k][i] + 1; //这里的1表示[a,b,c]单独一组
else //当前a下标不满足,后面的也一定不满足,可以直接跳出
break;
}
}
sum += dp[i][j];
}
}
return sum;
//相同数据不多的情况下
//时间复杂度:O(N ^ 2)
//空间复杂度:O(N ^ 2)
}
};