1.強盗:
動的規制の 5 つの部分の分析は次のとおりです。
- dp 配列 (dp テーブル) と添字の意味を決定する
dp[i]: 添え字 i (i を含む) 内の家を考慮すると、盗まれる最大量は dp[i] です。
2. 再帰式を決定する
dp[i] を決定する要素は、i 番目の部屋が盗まれているかどうかです。
i 番目の部屋を盗んだ場合、dp[i] = dp[i - 2] + nums[i]、つまり、i-1 番目の部屋は考慮されてはならず、添字 i-2 (i を含む) を見つけます。 -2) 内の家の場合、盗まれる最大金額は dp[i-2] に i 番目の部屋から盗まれたお金を加えたものです。
i 番目の部屋を盗まない場合は、dp[i] = dp[i - 1]、つまり i-1 の部屋を考慮します (これは考慮事項であり、必ずしも i-1 の部屋を盗むわけではないことに注意してください) 、これは多くの学生が混同しやすい点です)
次に、dp[i] は最大値をとります。つまり、 dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
私の考えを使って、i 番目の部屋を盗まない場合、盗む代わりに i-1 の部屋を考慮する理由を説明します。
1. 私の思考回路はサイズよりも 2 つのルートです。
正直に言うと、1つのルートは1番目の場所から始まり、2番目のルートは2番目の場所から始まり、1つの家にジャンプして強盗するか、2つの家にジャンプして強盗するかを選択できます(3つ以上の家にジャンプするのは意味がありません)連続して一軒の家を盗む人はたくさんいます)つまり、部屋 i を盗まなければ、部屋 i-1 を盗む必要があるという意味ではありません
2. では、なぜソリューションのコードが 2 件の強盗にジャンプしなかったのでしょうか?
dp[i]=dp[i-1]が入っているので、まず私の考えでは、1軒にジャンプして強盗する場合は元のルートにあり、2軒にジャンプして強盗する場合は別のルートにジャンプし、私の初期化は dp[1]=nums[0]; dp[2]=nums[1]; 私の再帰式は dp[i]=max(dp[i-3]+nums[i-1],dp[ i-2 ]+nums[i-1]); は 2 つのルートの値を比較するもので、解の初期化コードは dp[1]=nums[0]; dp[1] = max( nums[0], nums[1]); 彼は最初から 2 つのルートを結合し、i が最初のルート、i-2 も最初のルート、i-1 が 2 番目のルートであると仮定しました。再帰式は次のとおりです: dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); また、2 つのルートの値も比較します。
3. dp配列の初期化方法
再帰式 dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); から、再帰式の基礎は dp[0] と dp[ 1]
dp[i] の定義から、dp[0] は nums[0] でなければならず、dp[1] は nums[0] と nums[1] の最大値です。 dp[1] = max(nums[0] ]、nums[1]);
コードは以下のように表示されます。
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
4. 走査順序を決定する
dp[i] は dp[i - 2] と dp[i - 1] から派生するため、前から後ろにたどる必要があります。
コードは以下のように表示されます。
for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
1
5. dp 配列の導出例
例 2 では、例として [2,7,9,3,1] を入力します。
赤いボックス dp[nums.size() - 1] が結果です。
上記の分析後の C++ コードは次のようになります。
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
2. 強盗 ||:
配列の場合、リング形成には主に 3 つのケースがあります。
- ケース 1: 最初と最後の要素を含めないことを検討する
- ケース 2: 最初の要素を含め、末尾要素を含めないことを検討する
- ケース 3: 最初の要素ではなく、末尾の要素を含めることを検討します。
なお、ここでは「考慮」としていますが、例えばケース3の場合、末尾要素を考慮していますが、末尾要素を選択する必要はありません。ケース 3 では、nums[1] と nums[3] が最大になります。
ケース 2 とケース 3 には両方ともケース 1 が含まれるため、ケース 2 とケース 3 のみが考慮されます。
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
int result1 = robRange(nums, 0, nums.size() - 2); // 情况二
int result2 = robRange(nums, 1, nums.size() - 1); // 情况三
return max(result1, result2);
}
// 198.打家劫舍的逻辑
int robRange(vector<int>& nums, int start, int end) {
if (end == start) return nums[start];
vector<int> dp(nums.size());
dp[start] = nums[start];
dp[start + 1] = max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[end];
}
};
ノート:
1. start と end を渡しても、dp 配列の長さは依然として nums.size() です。robRange 関数では、start は 0 として想定され、end は nums.size()-1 として想定されます。
3.強盗III:
暴力的な再帰 (タイムアウト):
class Solution {
public:
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
// 偷父节点
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left,相当于不考虑左孩子了
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了
// 不偷父节点
int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
return max(val1, val2);
}
};
考え方は Home Robbery のときと同じです| 2 つのルートを比較すると、最初のルートは 1 階から開始し (ヘッド ノードを盗む)、2 番目は 2 階から開始します (ヘッド ノードを盗むことはありません)。
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
これは、再帰ノードを盗んで、その孫を考慮するためです。孫を盗む必要はありません。上の図のオレンジ色の線を見てください。これは、バイナリ ツリーのヘッド ノードを盗むことですが、ヘッド ノードの孫は盗みません。中間に2つの層があり、最初の層は親ノードを再帰的に考慮し、2番目の層は親ノードを再帰的に考慮しません。
もちろん、上記のコードはタイムアウトになり、この再帰的なプロセスでは実際に計算が繰り返されます。
ルートの4つの孫(左右の子の子)がヘッドノードの部分木である場合を計算し、次にルートの左右の子がヘッドノードの部分木である場合を計算しました。 . 左右の子を計算する際に、実際に孫を入れて再度計算します。
メモ化された再帰
したがって、マップを使用して計算結果を保存できるため、孫を計算した場合は、子の計算時に孫ノードの結果を再利用できます。
コードは以下のように表示されます。
class Solution {
public:
unordered_map<TreeNode* , int> umap; // 记录计算过的结果
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回
// 偷父节点
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
// 不偷父节点
int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
umap[root] = max(val1, val2); // umap记录一下结果
return max(val1, val2);
}
};
なぜ親を盗まないノードにレコード結果を置くのでしょうか?
実際、これについては上で説明しましたが、これが何に役立つのか推測してみましょう。
動的プログラミング:
- 再帰関数のパラメータと戻り値を決定する
ここでは、盗む場合と盗まない場合の 2 つの状態からお金を盗むようにノードに依頼すると、戻り値は長さ 2 の配列になります。
パラメータは現在のノードで、コードは次のとおりです。
vector<int> robTree(TreeNode* cur) {
実際、ここで返される配列は dp 配列です。
dp 配列 (dp テーブル) と添字の意味は、ノードを盗まないことで得られる最大金額を記録する場合は添字が 0、ノードを盗むことで得られる最大金額を記録する場合は添字が 1 です。
したがって、この質問の dp 配列は長さ 2 の配列です。
それでは、長さ 2 の配列はツリー内の各ノードの状態をどのようにマークするのでしょうか?と疑問に思う学生もいるかもしれません。
再帰プロセス中に、システム スタックが再帰の各層のパラメーターを保存することを忘れないでください。
それでも理解できない場合は、コードを見て理解してください。
2. 終了条件の決定
トラバーサルの過程で空のノードに遭遇した場合、それが盗まれているかどうかに関わらず0であることは明らかなので、戻り値を返します。
if (cur == NULL) return vector<int>{0, 0};
これは dp 配列の初期化にも相当します。
- 走査順序を決定する
まず明確なことは、ポストオーダートラバーサルを使用することです。次の計算は再帰関数の戻り値を通じて行われるためです。
左のノードを再帰することで、左のノードが盗む、または盗まないお金を取得します。
正しいノードを再帰することによって、正しいノードが盗む、または盗まないお金を取得します。
コードは以下のように表示されます。
// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 中
3. 単一レベル再帰のロジックを決定する
現在のノードを盗む場合、左右の子は盗むことができません、 val1 = cur->val + left[0] + right[0]; (添字の意味がわからない場合は、意味を確認してください) dp 配列の)
現在のノードを盗まない場合、左と右の子がそれを盗むことができます。それを盗むかどうかに関しては、最大のものを選択する必要があるため、次のようになります: val2 = max(left[0], left[1 ]) + max(right[0], right [1]);
ここでは例として max(left[0], left[1]) を使用します。max が left[0] の場合、中央に 2 つのレイヤーがあります。left[1] が使用される場合、中央に 1 つのレイヤーがあります。真ん中。
最後に、現在のノードの状態は {val2, val1} になります。つまり、{現在のノードから得られる最大のお金を盗むのではなく、現在のノードから得られる最大のお金を盗む}
コードは以下のように表示されます。
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 偷cur
int val1 = cur->val + left[0] + right[0];
// 不偷cur
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
- 導出 dp 配列の例
例 1 を例にとると、dp 配列の状態は次のとおりです (これは事後走査によって推定されることに注意してください)。
最後のヘッド ノードは、添字 0 と添字 1 の最大値を取得します。これは、盗まれた最大金額です。
再帰的 3 部作と動的 5 部構成の 3 部作を分析した後の C++ コードは次のようになります。
class Solution {
public:
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
// 长度为2的数组,0:不偷,1:偷
vector<int> robTree(TreeNode* cur) {
if (cur == NULL) return vector<int>{0, 0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
// 偷cur,那么就不能偷左右节点。
int val1 = cur->val + left[0] + right[0];
// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};