目次
1、階段を上る
1. 動的プログラミング
class Solution {
public:
int climbStairs(int n) {
int p = 0, q = 0, r = 1;
for (int i = 1; i <= n; ++i) {
p = q;
q = r;
r = p + q;
}
return r;
}
};
2. 行列の高速累乗
class Solution {
public:
vector<vector<long long>> multiply(vector<vector<long long>> &a, vector<vector<long long>> &b) {
vector<vector<long long>> c(2, vector<long long>(2));
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
}
}
return c;
}
vector<vector<long long>> matrixPow(vector<vector<long long>> a, int n) {
vector<vector<long long>> ret = {
{1, 0}, {0, 1}};
while (n > 0) {
if ((n & 1) == 1) {
ret = multiply(ret, a);
}
n >>= 1;
a = multiply(a, a);
}
return ret;
}
int climbStairs(int n) {
vector<vector<long long>> ret = {
{1, 1}, {1, 0}};
vector<vector<long long>> res = matrixPow(ret, n);
return res[0][0];
}
};
3,通项公式
class Solution {
public:
int climbStairs(int n) {
double sqrt5 = sqrt(5);
double fibn = pow((1 + sqrt5) / 2, n + 1) - pow((1 - sqrt5) / 2, n + 1);
return (int)round(fibn / sqrt5);
}
};
要約する
ここで形成される数列はたまたまフィボナッチ数列であり、答えに必要な f(n)f(n) はフィボナッチ数列の nn 番目の項目です (添字は 0 から始まります)。フィボナッチ数列の nn 番目の項の解をまとめてみましょう。
n が比較的小さい場合、再帰的手法は記憶操作なしで直接問題を解くことができ、時間計算量は O(2^n) で、冗長な計算が多くなります。
通常の状況では、この伝達方程式を実装するために「メモリ検索」または「反復」方法が使用され、時間計算量と空間計算量は両方とも O(n) になります。
空間の複雑さを最適化するために、f(x−2) の前に項目を保存する必要はありません。f(x)、f(x−1)、f(x−2) を維持するために 3 つの変数のみを使用します。 Cheng が「ローリング配列のアイデア」を動的プログラミングに適用していることがわかります。これは一種の再帰として理解することもでき、その結果、空間計算量が O(1) に最適化されます。
n が増加し続けると、O(n) ではニーズを満たせなくなる可能性があるため、「行列高速累乗」メソッドを使用して、アルゴリズムを O(logn) まで高速化できます。
フィボナッチ数列の一般式に n を代入して結果を計算することもできますが、浮動小数点計算を使用して実装すると精度誤差が発生する可能性があります。
2. 強盗と強盗
1. 動的プログラミング
まず最も単純なケースを考えてみましょう。家が 1 つしかない場合は、盗める合計金額の上限までその家を盗みます。家が2つしかない場合、2つの家は隣接しているため、同時に盗むことはできず、どちらかの家しか盗めないため、盗む金額が高い家を選択すると、最大合計まで盗むことができます額。
家が2軒以上ある場合、盗難に遭う合計金額の上限はどのように計算すればいいのでしょうか?k 番目 (k>2) ハウスには、2 つのオプションがあります。
k 番目の家を盗んだ場合、k-1 の家を盗むことはできません。盗まれた総額は、最初の k-2 の家のうち最も高い合計金額と k 番目の家の金額の合計になります。
k 番目の家が盗まれていない場合、盗まれた合計金額は、最初の k-1 番目の家のうち最も高い合計金額になります。
2 つの選択肢のうち、盗難総額が大きい方を選択し、その選択肢に対応する盗難総額が、最初の k 軒の家から盗める金額の合計の上限となります。
最初の i 軒の家から盗まれる最大合計金額を dp[i] とすると、次の状態遷移方程式が成り立ちます。
dp[i]=max(dp[i−2]+nums[i],dp[i−1])
境界条件は次のとおりです。
dp[0]=nums[0] 家は 1 つだけなので、家を盗みます。
dp[1]=max(nums[0],nums[1]) 家は 2 つしかありません。より高い金額の家を選択してください。
窃盗
最終的な答えは dp[n−1] です。n は配列の長さです。
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty()) {
return 0;
}
int size = nums.size();
if (size == 1) {
return nums[0];
}
vector<int> dp = vector<int>(size, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < size; i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[size - 1];
}
};
上記のメソッドでは、配列を使用して結果を保存します。各ハウスの最大合計金額は、そのハウスの最初の 2 つのハウスの最大合計金額にのみ関連することを考慮すると、ローリング配列を使用して、各瞬間の最初の 2 つのハウスの最大合計金額のみを格納できます。
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty()) {
return 0;
}
int size = nums.size();
if (size == 1) {
return nums[0];
}
int first = nums[0], second = max(nums[0], nums[1]);
for (int i = 2; i < size; i++) {
int temp = second;
second = max(first + nums[i], second);
first = temp;
}
return second;
}
};
複雑さの分析
時間計算量: O(n)、n は配列の長さです。配列を走査する必要があるのは 1 回だけです。
空間複雑度: O(1)。ローリング配列を使用すると、配列全体の結果を保存せずに、最初の 2 つのハウスの最大合計額のみを保存できるため、空間の複雑さは O(1) になります。
3. 三角形の最小経路和
この問題は非常に古典的で古くからある動的プログラミングの問題であり、アルゴリズムの問題として登場し、その起源は 1994 年の IOI (国際情報オリンピック) の三角形にまで遡ることができます。時が経つのは早いもので、20 年以上の蓄積を経て、過去の国際コンテストの問題は現在、入門レベルの動的プログラミングの必須問題となり、常にアルゴリズムの学習と定着を促しています。
この問題では、指定された三角形の行数は n で、i 番目の行 (00 から始まる番号が付けられます) には i+1 個の数値が含まれます。各行の左端を揃えると、以下に示すように直角二等辺三角形が形成されます。
[2]
[3,4]
[6,5,7]
[4,1,8,3]