コードカプリス 48日目

1.強盗:

動的規制の 5 つの部分の分析は次のとおりです。

  1. 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);
    }
};

なぜ親を盗まないノードにレコード結果を置くのでしょうか?

実際、これについては上で説明しましたが、これが何に役立つのか推測してみましょう。

 

動的プログラミング:
 

  1. 再帰関数のパラメータと戻り値を決定する

ここでは、盗む場合と盗まない場合の 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 配列の初期化にも相当します。

  1. 走査順序を決定する

まず明確なことは、ポストオーダートラバーサルを使用することです。次の計算は再帰関数の戻り値を通じて行われるためです。

左のノードを再帰することで、左のノードが盗む、または盗まないお金を取得します。

正しいノードを再帰することによって、正しいノードが盗む、または盗まないお金を取得します。

コードは以下のように表示されます。

// 下标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};
  1. 導出 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};
    }
};

おすすめ

転載: blog.csdn.net/2201_75793783/article/details/130942765