【C++コード】等和部分集合の分割、目標和、1と0、変更交換、動的プログラミング - コード考察

タイトル:等和部分集合の分離

  • 正の整数のみを含むを与える空でない 配列nums。 2 つのサブセットの要素の合計が等しくなるように、この配列を 2 つのサブセットに分割できるかどうかを判断してください。

  • 最初のアイデアは、ソート後にダブル ポインターを使用することでしたが、それが機能しないことがわかりました。

    • class Solution {
              
              
      public:
          bool canPartition(vector<int>& nums) {
              
              
              if(nums.size()<2){
              
              
                  return false;
              }
              sort(nums.begin(),nums.end());
              int left=0,right=nums.size()-1;
              int left_sum=nums[left],right_sum=nums[right];
              while(left<right-1){
              
              
                  if(left_sum>right_sum){
              
              
                      right--;
                      right_sum += nums[right];
                  }else{
              
              
                      left++;
                      left_sum += nums[left];
                  }
              }
              cout<<left_sum<<" "<<right_sum<<endl;
              return left_sum==right_sum;
          }
      };//nums = [2,2,1,1]
      
  • 集合内で sum / 2 が現れるサブセットの合計を見つければ、それを 2 つの同一の要素とサブセットに分割できます。バックトラッキングを使用してすべての答えを激しく検索することもできますが、最終的にはタイムアウトになり、これ以上最適化したくないので、バックトラッキングをあきらめて、01 バックパックに進みます。

    • ナップザック問題には、N 個のアイテムと、最大 W の重量を運ぶことができるナップザックがあります。 i 番目の項目の重みをweight[i]、得られた値をvalue[i]とします。各アイテムは 1 回しか使用できません。合計価値が最も高いバックパックに入れるアイテムを見つけてください。バックパッキングの問題には多くのバックパッキング方法がありますが、一般的なものは次のとおりです: 01 バックパック、完全なバックパック、複数のバックパック、グループ バックパック、混合バックパックなど。
  • dp 配列と添字の意味を確認します。 dp[j] は、次のことを意味します。容量 j のバックパックは、最大値 dp[j] のアイテムを運ぶことができます。

  • 再帰式を決定します: 01 バックパックの再帰式は次のとおりです: dp[j] = max(dp[j], dp[j -weight[i]] + value[i]); これは、値を次のように入力するのと同じです。バックパックの場合、アイテム i の重量は nums[i] で、その値も nums[i] です。したがって、再帰式は次のようになります。 dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

  • dp 配列を初期化する方法: まず、dp[0] は 0 でなければなりません。

  • class Solution {
          
          
    public:
        bool canPartition(vector<int>& nums) {
          
          
            // int sum=0;
            // vector<int> dp(10001,0);
            // for(auto i:nums){
          
          
            //     sum+=i;
            // }
            // if(sum % 2 == 1){
          
          
            //     return false;
            // }
            // int half_sum = sum/2;
            // for(int i=0;i<nums.size();i++){
          
          
            //     for(int j=half_sum;j>=nums[i];j--){
          
          
            //         dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
            //     }
            // }
            // if(dp[half_sum]==half_sum){
          
          
            //     return true;
            // }
            // return false;
    };
    
  • わかりにくいですが、横軸がsum/2の探索、縦軸が要素の探索となる2次元配列を使うことができます。

    • class Solution {
              
              
      public:
          bool canPartition(vector<int>& nums) {
              
              
              if(nums.size()<2){
              
              
                  return false;
              }
              int sum=0;
              for(int i:nums){
              
              
                  sum+=i;
              }
              if(sum%2==1){
              
              
                  return false;
              }
              int target = sum/2;
              vector<vector<bool>> dp(nums.size(),vector<bool>(target+1,false));
              if(nums[0]<=target){
              
              
                  dp[0][nums[0]] = true;
              }
              for(int i=1;i<nums.size();i++){
              
              
                  for(int j=0;j<dp[0].size();j++){
              
              
                      dp[i][j] = dp[i-1][j];
                      if(nums[i]==j){
              
              
                          dp[i][j]=true;
                          continue;
                      }
                      if(nums[i]<j){
              
              
                          dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
                      }
                  }
                  if(dp[i][target]==true){
              
              
                      return true;
                  }
              }
              return false;
          }
      };
      

トピック:最後の石の重さ II

  • 整数配列stonesで表される石の山があります。ここで、 stones[i]i 石の重量を表します。各ラウンドで、任意の 2 つの石を選択し、それらを一緒に砕きます。石の重さがそれぞれ xyx <= y であるとします。 x == y の場合、両方の石は完全に粉砕されます。x != y の場合、重量 x の石は完全に粉砕され、重量 y の石は完全に粉砕されます。 y-x の新しい重み。最終的には、多くても 1 個の石だけが残ります。この石の可能な最小重量を返します。石がなくなった場合は、0 に戻ります。

  • この質問のアイテムの重量は石[i]であり、アイテムの価値も石[i]です。 01 バックパック内のアイテムの重量[i]と価値[i]に対応します。

    • dp 配列と添え字の意味を確認します。dp[j] は、容量 j のバックパックを表します (ここでの容量はより鮮明です。実際には、運ぶことのできる最大の重みは dp[j] です。

    • 再帰式を決定します: 01 バックパックの再帰式は次のとおりです: dp[j] = max(dp[j], dp[j -weight[i]] + value[i]); この質問は: dp[j] = max(dp[j], dp[j - stone[i]] + stone[i]);

    • dp 配列の初期化方法: 重みは負の数にはならないため、dp[j] を 0 に初期化するだけで十分です。したがって、再帰式では dp[j] = max(dp[j], dp[j - stone[i] ] + stone[i]); dp[j] は初期値で上書きされません。

    • 走査の順序を決定する: 1 次元 dp 配列を使用する場合、アイテム走査の for ループは外側の層に配置され、バックパックを走査するための for ループは内側の層に配置され、内側の for ループが走査されます。逆の順序で!

    • ここに画像の説明を挿入します

    • 次に、それを 2 つの石の山に分割し、一方の山の石の重さの合計を dp[目標]、もう一方の石の重さの合計を合計 - dp[目標]とします。 target を計算する場合、切り捨てられるため target = sum / 2 となるため、sum - dp[target] は dp[target] 以上である必要があります。この場合、衝突後に残る石の最小重量は、(sum - dp[target]) - dp[target] となります。

  • class Solution {
          
          
    public:
        int lastStoneWeightII(vector<int>& stones) {
          
          
            int sum=0;
            for(int i:stones){
          
          
                sum += i;
            }
            int target = sum / 2;
            int dp[1501] = {
          
          0};
            for(int i=0;i<stones.size();i++){
          
          
                for(int j=target;j>=stones[i];j--){
          
          
                    dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
                }
            }
            return sum-dp[target]-dp[target];
        }
    };
    
  • 時間計算量: O(m × n)、m は石の総重量 (正確には総重量の半分)、n は石ブロックの数、空間計算量: O(m)

  • まず、バックパックの問題点の中で、01バックパックが最も基本的なものであり、他のバックパックも01バックパックをベースに若干の改良が加えられています。

    • ここに画像の説明を挿入します
  • dp 配列と添字の意味を確認します。 dp[i][j] は、添字 [0-i] を持つアイテムを取り出し、それを容量 j のバックパックに入れる場合、最大合計はいくらになることを意味します。価値。

  • 再帰式を決定します。 dp[i][j] = max(dp[i - 1] [j], dp[i - 1] [j -weight[i]] + value[i]);

  • dp配列を初期化する方法

  • 走査順序を決定する: 01 バックパックの 2 次元 dp 配列の走査順序では、外層がアイテムを走査し、内層がバックパックの容量を走査し、外層がバックパックの容量を走査し、内層がアイテムを走査することがすべて可能です。

  • 例: バックパックの最大重量は 4 です。項目は次のとおりです: [項目 0; 1; 15] [項目 1; 3; 20] [項目 2; 4; 30] 対応する dp 配列値は図に示すとおりです。

    • ここに画像の説明を挿入します

题目:目标和

  • 負でない整数の 配列 nums と整数 target が与えられます。配列内の各整数の前に '+' または '-' を追加し、すべての整数を連結して の数を返します。 target として評価される個別の: たとえば、nums = [2, 1] の場合、2 の前に「+」を追加し、1 の前に「-」を追加して、それらを連結して式「+2-1」を取得できます。上記のメソッドで構築でき、

  • 加算の合計を x とすると、減算に対応する合計は sum - x となります。したがって、必要なのは x - (sum - x) = target; いくつかのメソッドです。ここの x は、後で必要になるバックパックの容量です。

  • dp 配列と添字の意味を決定します: dp[j] は、j (j を含む) と同じ大きさの体積をバッグに充填することを意味します。dp[j] メソッドがあります。実際には、2 次元のメソッドも使用できます。この問題を解決する dp 配列 dp[i ] [j]: nums[i] を添え字 [0, i] で使用すると、j (j を含む) と同じくらい大きな容量のパッケージを埋めることができます。 [j]メソッド。

  • 漸化式を決定します。 nums[i] が得られる限り、dp[j] を作成するには dp[j - nums[i]] 通りの方法があります。

  • dp 配列の初期化方法: 初期化中、dp[0] は数式内のすべての再帰結果の原点であるため、dp[0] は 1 に初期化される必要があります。dp[0] が 0 の場合、再帰結果は 0 になります。 。

  • class Solution {
          
          
    public:
        int findTargetSumWays(vector<int>& nums, int target) {
          
          
            int sum=0;
            for(int i:nums){
          
          
                sum+=i;
            }
            if(abs(target)>sum){
          
          
                return 0;
            }
            if((target+sum) & 1){
          
          
                return 0;
            }
            int left=(target+sum)/2;
            vector<int> dp(left+1,0);
            dp[0]=1;
            for(int i=0;i<nums.size();i++){
          
          
                for(int j=left;j>=nums[i];j--){
          
          
                    dp[j]+=dp[j-nums[i]];
                }
            }
            return dp[left];
        }
    };
    
  • 時間計算量: O(n × m)、n は正の数、m はバックパックの容量; 空間計算量: O(m)、m はバックパックの容量

  • class Solution {
          
          
    public:
        int count=0;
        void track(vector<int> &nums,int target,int sum,int start){
          
          
            if(start==nums.size()){
          
          
                if(sum==target){
          
          
                    count++;
                }
            }else{
          
          
                track(nums,target,sum+nums[start],start+1);
                track(nums,target,sum-nums[start],start+1);
            }
    
        } 
        int findTargetSumWays(vector<int>& nums, int target) {
          
          
            track(nums,target,0,0);
            return count;
        }
    };//回溯
    
  • 时间复杂度: O ( 2 n ) O(2^n) O(2n),其中 n 是数组 nums \textit{nums} nums の長さ。バックトラッキングには、 2 n 2^n 2n 個の異なる式。各式の評価には O(1) 時間がかかるため、合計の時間計算量は です。空間計算量: O(n)、n は配列 nums の長さです。空間の複雑さは主に再帰呼び出しのスタック空間に依存し、スタックの深さは n を超えません。

  • ストレージ用の 2 次元配列を構築する方が理解しやすいです。

    • 配列の要素の合計は sum、- 符号が付いた要素の合計は neg、+ が追加された残りの要素の合計は sum-neg となり、式の結果は ( 合計)−負< a i=6>=合計−2⋅=;ターゲット
    • 配列 nums の要素はすべて非負の整数であるため、neg も非負の整数でなければなりません。したがって、上記の式が成り立つ前提は、sum-target が非負の偶数であるということです。この条件が満たされない場合は、直接 0 を返すことができます。
    • 上記の式が真の場合、問題は、これらの要素の合計が neg に等しくなるように配列 nums 内の要素の数を選択し、要素を選択するためのオプションの数を計算することに変換されます。動的計画法を使用してそれを解決できます。
    • 2 次元配列 dp を定義します。 dp [ i ] [ j ] \textit{dp}[i][j] dp[i][ j] は、配列 nums の最初の i 番目の数値から要素を選択し、これらの要素の合計が j に等しくなるようにすることを意味します。配列 nums の長さを n とすると、最終的な答えは $ \textit{dp}[n][\textit{neg}]$ になります。
  • class Solution {
          
          
    public:
        int findTargetSumWays(vector<int>& nums, int target) {
          
          
            int sum=0;
            for(int &num:nums){
          
          
                sum+=num;
            }
            int diff=sum-target;
            if(diff<0 || diff%2!=0){
          
          
                return 0;
            }
            int n=nums.size(),neg=diff/2;
            vector<vector<int>> dp(n+1,vector<int>(neg+1));
            dp[0][0]=1;
            for(int i=1;i<=n;i++){
          
          
                int temp=nums[i-1];
                for(int j=0;j<=neg;j++){
          
          
                    dp[i][j]=dp[i-1][j];
                    if(j>=temp){
          
          
                        dp[i][j]+=dp[i-1][j-temp];
                    }
                }
            }
            return dp[n][neg];
        }
    };
    

题目:一和零

  • バイナリ文字列配列 strs と 2 つの整数 mn が与えられます。 が最大 < a i= である strs の最大のサブセットの長さを見つけて返してください。 7> および のすべての要素が の要素でもある場合、集合 は集合 になります。 a>サブセットm0n1xyxy

  • **この質問の strs 配列の要素はアイテムであり、各アイテムは 1 つです。 ** dp 配列 (dp テーブル) と添字の意味を決定します:dp[i][j]: 最大 i 0 と j 1 を持つ str の最大サブセットのサイズdp [i][j] です。

  • 再帰式を決定します: dp[i][j] は、前の strs 内の文字列から推定できます。strs 内の文字列には、zeroNum 0 と oneNum 1 があります。 dp[i][j] は dp[i - zeroNum] [j - oneNum] + 1 になります。次に、走査プロセス中に dp[i][j] の最大値を取得します。したがって、再帰式: dp[i][j] = max(dp[i][j], dp[i - zeroNum] [j - oneNum] + 1); 01 バックパックの再帰式を思い出してください: dp[j] ] = max(dp[j], dp[j -weight[i]] + value[i]); 文字列の zeroNum と oneNum がアイテムの重量 (weight[i]) に等しいことがわかります。 、および文字列自体の数値は項目の値 (value[i]) に相当します。

  • dp配列を0に初期化する方法

  • class Solution {
          
          
    public:
        int findMaxForm(vector<string>& strs, int m, int n) {
          
          
            vector<vector<int>> dp(m+1,vector<int>(n+1,0));
            for(string str:strs){
          
          
                int one_count=0,zero_count=0;
                for(char c:str){
          
          
                    if(c=='0'){
          
          
                        zero_count++;
                    }else{
          
          
                        one_count++;
                    }
                }
                for(int i=m;i>=zero_count;i--){
          
          
                    for(int j=n;j>=one_count;j--){
          
          
                        dp[i][j] = max(dp[i-zero_count][j-one_count]+1,dp[i][j]);
                    }
                }
            }
            return dp[m][n];
        }
    };
    
  • 時間計算量: O(kmn)、k は strs の長さ、空間計算量: O(mn)。古典的なナップザック問題には異なる容量が 1 つだけありますが、この問題には 2 つの容量、つまり、選択された文字列サブセット内の 0 と 1 の数の上限があります。

バックパック一式

  • N 個のアイテムと最大 W の重量を運ぶことができるバックパックがあります。 i 番目の項目の重みをweight[i]、得られた値をvalue[i]とします。 各アイテムは無限にあります (つまり、バックパックに何度も入れることができます)。どのアイテムを入れる必要があるかを調べてください。合計値が最も大きいバックパック。 完全なバックパックの問題と 01 バックパックの問題の唯一の違いは、各アイテムのピースが無限に存在することです。

题目:零钱兑换 II

  • さまざまな額面のコインを表す整数配列 coins と、合計金額を表す別の整数 amount を与えます。合計金額を構成できるコインの組み合わせの数を計算して返してください。合計金額に達しないコインの組み合わせがある場合は、 0 を返します。各金種のコインが無限にあると仮定します。質問データは、結果が 32 ビット符号付き整数に準拠していることを保証します。

  • ただし、この質問は純粋な完全なバックパックとは異なります。**純粋な完全なバックパックはバックパックの最大値はいくらかに関するものですが、この質問は合計金額を構成するために必要なアイテムの組み合わせの数に関するものです。 ! **タイトルの説明は、合計金額を構成するコインの組み合わせの数を参照していることに注意してください。 組み合わせは要素間の順序を重視しませんが、配置は要素間の順序を重視します。

  • dp 配列と添え字の意味を確認します。 dp[j]: 合計金額 j を構成する通貨の組み合わせの数は dp[j] です。

  • 再帰式を決定します。 dp[j] は、すべての dp[j - Coins[i]] の合計です (coins[i] の場合を考慮します)。したがって、再帰式は次のようになります。 dp[j] += dp[j - Coins[i]];

  • したがって、再帰式: dp[j] += dp[j - Coins[i]]; まず、dp[0] は 1 でなければならず、dp[0] = 1 が再帰式の基礎です。 dp[0] = 0 の場合、後続のすべての派生値は 0 になります。 dp[0]=1 も状況を示しています。coins[i] が選択されている場合、つまり j-coins[i] == 0 はこのコインを選択できることを意味し、dp[0] が 1 はそのようなコインが存在することを意味します。コイン[i]のみを選択する選択方法。

  • class Solution {
          
          
    public:
        int change(int amount, vector<int>& coins) {
          
          
            vector<int> dp(amount+1,0);
            dp[0]=1;
            for(int i=0;i<coins.size();i++){
          
          
                for(int j=coins[i];j<=amount;j++){
          
          
                    dp[j]+=dp[j-coins[i]];
                }
            }
            // for(auto i:dp){
          
          
            //     cout<<i<<endl;
            // }
            return dp[amount];
        }
    };
    
  • 時間計算量: O(mn)、m はコインの量、n は長さ; 空間計算量: O(m)

  • バックパックに荷物を詰める方法が複数ある場合、その順序を理解することが非常に重要です。

    • 組み合わせの数を調べたい場合は、外側の for ループでアイテムを走査し、内側の for ループでバックパックを走査します
    • 順列の数を調べたい場合は、外側の for ループでバックパックを走査し、内側の for ループで項目を走査します

题目:组合总和 Ⅳ

  • 異なる整数numsの配列とターゲット整数targetnums から target の合計が となる要素の組み合わせの数を求めて返します。質問データにより、回答が 32 ビット整数の範囲内に収まることが保証されます。

  • この質問の説明では、組み合わせを見つけることについて書かれていますが、同じ要素が異なる順序で含まれる組み合わせは 2 つの組み合わせとして数えられるとも書かれています。**実際には、これは順列を求めることについてのものです。 **この組み合わせは順序を重視していません。(1,5) と (5,1) は同じ組み合わせです。配置は順序を重視しており、(1,5) と (5,1) は 2 つの異なる配置です。本質は、この質問がすべての順列ではなく、順列の合計を求め、順列の数のみを求めるということです。 すべての順列をリストした場合、検索にはバックトラッキング アルゴリズムのみを使用できます

  • dp 配列と添字の意味を確認します:dp[i]: ターゲットの正の整数 i を構成する順列の数は dp[i]

  • 再帰式を決定します。 dp[i] (nums[j] を考慮) は、dp[i - nums[j]] (nums[j] を考慮しない) から導出されます。

  • dp 配列を初期化する方法: 再帰式 dp[i] += dp[i - nums[j]] のため、他の dp を再帰するときに数値基準が存在するように、dp[0] を 1 に初期化する必要があります。 [私] 。

  • 横断の順序を決定します。その数は無制限で、これが完全なバックパックであることを示します。使用できるアイテムの数に制限はなく、これが完全なバックパックであることを示しています。

    • 組み合わせの数を調べたい場合は、外側の for ループでアイテムを走査し、内側の for ループでバックパックを走査します
    • 順列の数を調べたい場合は、外側の for ループでバックパックを走査し、内側の for ループで項目を走査します
  • この例の例を使用して、次のことを推測してください。

    • ここに画像の説明を挿入します
  • class Solution {
          
          
    public:
        int combinationSum4(vector<int>& nums, int target) {
          
          
            vector<int> dp(target+1,0);
            dp[0]=1;
            for(int i=0;i<target+1;i++){
          
          
                for(int j=0;j<nums.size();j++){
          
          
                    if(i-nums[j]>=0 && dp[i] < INT_MAX-dp[i-nums[j]]){
          
          
                        // C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
                        dp[i] += dp[i-nums[j]];
                    }
                }
            }
            return dp[target];
        }
    };
    

おすすめ

転載: blog.csdn.net/weixin_43424450/article/details/134149144