Advertisement
Fastrail08

Partition Equal Subset Sum Leetcode 416

Jul 8th, 2025 (edited)
75
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 23.37 KB | None | 0 0
  1. /*
  2. ******************* SUPER IMPORTANT *********************
  3. https://chatgpt.com/canvas/shared/686d6b2645788191bc42b686b5773df1
  4.  
  5. */
  6.  
  7. #include <bits/stdc++.h>
  8. using namespace std;
  9. // INDEX BASED APPROACH - process each element sequentially(INDIVDIUAL
  10.     // ELEMENT CHOICE DIAGRAM) TRY FILLING ALL PARTITIONS PARALLELY/AT ONCE,
  11.     // here both partitions So in base case need to compare both of them to see
  12.     // if valid path or not(equal sum)
  13.  
  14.     // Generally not a good approach when you fill all the groups at once,
  15.     // because then sum of each group needs to be monitored throughout
  16.     // recursion, i.e. CHANGING,as they are INFLUENCING THE DECISION in base
  17.     // case on what to return. The sum for each partition is also NOT DERIVABLE
  18.     // at any point of time by any other variable in index based recursion
  19.    
  20. long long getEqualPartitionSum(int level, int part1, int part2, vector<int> &nums, unordered_map<string, long long> &memo){
  21.     if(level >= nums.size()){
  22.         if(part1 == part2){
  23.             return 1;
  24.         }
  25.         return 0;
  26.     }
  27.    
  28.     string key = to_string(level) + '|' + to_string(part1) + '|' + to_string(part2);
  29.    
  30.     if(memo.count(key) > 0){
  31.         return memo[key];
  32.     }
  33.     long long count = 0;
  34.     count += getEqualPartitionSum(level + 1, part1 + nums[level], part2, nums, memo);
  35.     count += getEqualPartitionSum(level + 1, part1, part2 + nums[level], nums, memo);
  36.     return memo[key] = count;
  37. }
  38.  
  39. bool getEqualSumPartitionSingleGroupTracking(int index, int currSum, int &targetSum, vector<int> &nums, vector<vector<int> > &memo){
  40.     //base case
  41.     if(index >= nums.size()){
  42.         if(currSum == targetSum){
  43.             return true;
  44.         }
  45.         return false;
  46.     }
  47.     //Assert #1: currSum should never exceed targetSum
  48.     // you can assert with meaningful messages too
  49.     assert(currSum <= targetSum && "currSum exceeded targetSum");
  50.    
  51.     //Assert #2: safe indexing in memo table
  52.     assert(index >= 0 && index < memo.size());
  53.     assert(currSum >= 0 && currSum < memo[0].size());
  54.    
  55.     if(memo[index][currSum] != -1){
  56.         return memo[index][currSum];
  57.     }
  58.     //levels = each element
  59.     //options = whether to include in group1 or not(if not taken in group1, it already becomes a part of group2)
  60.    
  61.     bool inc = false, exc = false;
  62.     //include only if does not exceed the sum limit of group
  63.     if(currSum + nums[index] <= targetSum){
  64.         inc = getEqualSumPartitionSingleGroupTracking(index + 1, currSum + nums[index], targetSum, nums, memo);
  65.     }
  66.     //exclude
  67.     exc = getEqualSumPartitionSingleGroupTracking(index + 1, currSum, targetSum, nums, memo);
  68.    
  69.     return memo[index][currSum] = inc || exc;
  70. }
  71.  
  72. //Just remember to use where  N <= 20 (it is best in this range because it reduces other params in key)
  73. bool getEqualSumPartitionSingleGroupTrackingWithMASK(int bitmask, int currSum, int &targetSum, vector<int> &nums, vector<int> &memo){
  74.     //base case
  75.     // We can't use the condition like builtin_popcount(mask) = nums.size() as we are not filling ALL the groups, As we are only filling 1 group, so some elements will be left which will logically go into the other group. So as soon as we reach the condition currSum = targetSum, that means we were able to fill that single group, so k = 0 (groups left to fill) but and used the elements required that were required to do so. So this base will suffice.
  76.     if(currSum == targetSum){
  77.         return true;
  78.     }
  79.     if(memo[bitmask] != -1){
  80.         return memo[bitmask];
  81.     }
  82.     bool partitionPossible = false;
  83.     //levels - group to filled
  84.     //options - any unused element that on being added doesn't exceed the limit(targetSum) of the current group
  85.     for(int i = 0; i < nums.size(); i++){
  86.         int elementIndexInMask = (1 << i);
  87.         int element = nums[i];
  88.        
  89.         //check if element has been used or not in filling of the current or any previous group
  90.         //bitmask represents a configuration where our current element 'i' has a corresponding bit in mask (ith bit)
  91.         //We take elementIndexInMask which has all 0 bits in 31 out 32 total bits. The only bit that is set is the ith bit.
  92.         // We take '&' of these...so no matter what was present in bitmask in other 31 positions, it will become 0 as those position bits is 0 in elementIndexInMask. So the '&' would result in some non zero value only if ith bit is set in bitmask, and (1 & 1) = 1, so the resultant value would be some nonzero value. But if ith bit is OFF in bitmask(signfying that ith element has not been used yet), so it would result in 0 as ith bit of elementIndexInMask is 1 but the ith bit in bitmask 0.
  93.         if((bitmask & elementIndexInMask) == 0){
  94.             //ith element can be used in filling the group as it is unused, but using it should not exceed the sum limit of current group
  95.             if(currSum + element <= targetSum){
  96.                 //unused element and not violating the sum limit
  97.                 //mark the element as used
  98.                 int mask = bitmask | elementIndexInMask; // will set the ith bit in the new mask
  99.                 partitionPossible |= getEqualSumPartitionSingleGroupTrackingWithMASK(mask, currSum + element, targetSum, nums, memo);
  100.             }
  101.         }
  102.     }
  103.     return memo[bitmask] = partitionPossible;
  104. }
  105.  
  106.  
  107. // BESTEST APPROACH (USING BITSET AND SINGLE GROUP TRACKING) (with bitset, you can only track single group)
  108.     bool canPartition(vector<int>& nums) {
  109.     /*
  110.     USED BITSET instead of bitmask!!!
  111.    
  112.     BITSET could be used only where CUMULATIVE property or CUMULATIVE EFFECT of the elements(SUM, XOR) is to be tracked instead of IDENTITY of each elements(which element, how many element, size of group etc).
  113.    
  114.     It is VALUE-ACHIEVABILITY TRACKING
  115.    
  116.     BITSET can be used where the VALUE that is being tracked is of order <= 1e5.
  117.     Here in this question Max Sum of a group was 10000 or 1e4.
  118.     OR
  119.     If you see large N >= 24 (Often eliminating the solution via bitmask) and CUMULATIVE PROPERTY needs to be tracked.
  120.    
  121.     Only required 10001 bits in space and time complexity is also greatly reduced because it involves bit operations only.
  122. */
  123.         int totalSum = 0;
  124.         for(int i : nums){
  125.             totalSum += i;
  126.         }
  127.         if(totalSum % 2 != 0){
  128.             return false;
  129.         }
  130.         int targetSum = totalSum / 2;
  131.         //each bitset[i] = represents if SUM(group/partition) = i is possible or not.
  132.         //if possible, bitset[i] = 1
  133.         //if not possible, bitset[i] = 0
  134.         //So we need to maintain an bitset array of size targetSum + 1, to see if sums[targetSum] is possible or not
  135.         // bitset<targetSum + 1> sums;
  136.  
  137.         //Above declaration is wrong, although we need to just maintain if targetSum + 1 is achievable or not, DYNAMIC SIZE(RUNTIME SIZE) BITSET ARRAY IS NOT POSSIBLE. IT CAN ONLY BE INITIALIZED WITH A CONSTANT.
  138.         //So to declare one, check the constraints, and calcuate what is the maximum possible sum that can be achieved .
  139.         //Here N = 200, nums[i] <= 100, So the biggest totalSum = 200 * 100 = 20000
  140.         //But We don't need a SUM(group) = 20000 sum, we just need to see if 1 group of totalSum/2 is possible or not, as the other group would automatically equal totalSum/2.
  141.         //So the max targetSum that is achievable by a group in worst case is 10000. So a BITSET of size 10001 required.
  142.         bitset<10001> sums;
  143.         //sums[24] = represents if sum(GROUP) = 24 is possible or not(we don't care about the actual element used to meet the sum criteria, we just want to know if SOMEHOW A COMBINATION OF ELEMENTS EXIST that makes a particular sum possible), here 24
  144.         //sum 0 is alaways possible, don't select any element
  145.         sums[0] = 1;
  146.  
  147.          //for each element in nums, add nums to the previously achievable sums to get new sums that are possible
  148.             // (previously achievable sums + if current element added to that group) = new sum that is also achievable
  149.             //suppose previous sums that are possible for the group = 2,3,6,7 and now we have element as 5
  150.             // Now if we add 5 to all of these possible sums(much like a backtrack solution, add 5 explore that path, then add 5 to next ...), we can generate 2 + 5 = 7, 3 + 5 = 8,....11, 12
  151.             // It is simple technique,
  152.             /*
  153.             IMPORTANT
  154.                 If some sums are possible using the combination of previously used elements, then
  155.                 If we add the current element to all of those previously possible sums, then the new sums are also possible .
  156.                 If somehow sum = 4 was achievable before, now if we the add element 5 to it(basically whatever elements help the group form the sum = 4 via any combination(we just want to know if it is POSSIBLE regardless of what element formed the group), if we add 5 in that group too, that makes sum = 9 also POSSIBLE)
  157.  
  158.                 suppose if the sum 4 was possible via the elements {1, 3}
  159.                 Now if we add 5 in that group it becomes {1, 3, 5}, which makes the sum of group 9. So now we know that a group can be formed with sum = 9.
  160.                 That is what the question demands after breaking it down to the problem where we want to know if we can form a group with sum = targetSum.
  161.             */
  162.         for(int i : nums){
  163.             //(sums << i) shift all the previously set bits(possible sum) by position 'i'.
  164.             // basically for all sums[k] == 1, left shift those bits by 'i', saying if sums[k] = 1, i.e. sum = k is possible then a new sum, sums[k + i] = 1, sum = k + i is also possible.
  165.             //(sums << i) shifted the bit corresponding to the previously possible sum to some new bit, saying sums that can be formed by adding 'i' to all the previous sums is also possible
  166.             //Now we 'OR' it with the original BITSET sums, to update the new sums that are possible now (by adding the current element to all the previously possible sums).
  167.             // By 'OR' we maintained the previous possible sums and also updated the new possible sum.  
  168.            sums = sums | (sums << i);
  169.         }
  170.         return sums[targetSum] == 1 ? true : false;
  171.     }
  172.  
  173. int main() {
  174.     // your code goes here
  175.     int n;
  176.     cin >> n;
  177.     vector<int> nums(n);
  178.     for(int i = 0; i < n; i++){
  179.         cin >> nums[i];
  180.     }
  181.     /*
  182.     NAIVE APPROACH - (INDEXED BASED, MULTIPLE GROUPS TRACKING)
  183.   //  1) Index Based Recursion (making choice for each element sequentially, valid subset formed at base case)
  184.  //   2) Tracking all groups parallely, and making decision in base case (because only there all the elements have been processed, all the elements have expressed their choices, and the sum of each partition are finalised.
  185.   //3) Memo Key = memo(index, sum1, sum2)
  186.       as index is CHANGING, INFLUENCING decision(index >= nums.size() i.e. letting  us know if all the elements have been processed or not) in base case, and is NOT DERIVABLE
  187.       sum1 and sum2 are changing, INFLUENCING decision in base case(sum1 == sum2), and is NOT DERIVABLE from any other parameters
  188.     unordered_map<string, long long> memo;
  189.     cout << getEqualPartitionSum(0, 0, 0, nums, memo) << '\n';
  190.      */
  191.      
  192.    
  193.     /*
  194.     // BETTER APPROACH (INDEXED BASED, SINGLE GROUP TRACKING)
  195.         // 1) Index Based Recursion
  196.         // 2) Tracking one group at a time, filling one group then the next and see if we are able to fill all the groups. If we are not able to fill a group, we backtrack to the previous groups try different combination to fill them up in hopes that it would free up the used element that can be used further down the line to succesfully fill the group that we were stuck on.
  197.         // 3) In filling one group at a time, you should know when it is completely full. So read the question again to figure out what can be the INDIVDIUAL group sum.
  198.         // 4) We want ALL the elements of array to be partitioned in 2 groups with equal sum SUM(part1) = SUM(part2)[Quesetion Constraint]
  199.         // So TOTALSUM(elements)  = 2 * SUM(part1) => SUM(part1) = SUM(part2) = SUM(TOTALSUM)/2
  200.         // 3) Memo key here would be (index, currSum, k) as all 3 are CHANGING, INFLUENCING & NOT DERIVABLE
  201.        
  202.          int totalSum = 0;
  203.     for(int i : nums){
  204.         totalSum += i;
  205.     }
  206.    
  207.     //if the sum itself is not divisible by 2, then it is impossible to PARTITION in 2 groups
  208.     if(totalSum % 2 != 0){
  209.         cout << "NOT POSSIBLE TO PARTITION INTO 2 GROUPS OF EQUAL SUM" << '\n';
  210.         return false;
  211.     }
  212.    
  213.     // It might be possible to partition, and it is only possible when both of the partition equal totalSum / 2;    
  214.     int targetSum = totalSum/2;
  215.    
  216.     //Clever trick - Even if you are able to partition 1 group with sum equal to totalSum/2, then the other half automatically partitions with sum = targetSum/2. So memo(index, currSum, k) => memo(index, currSum).
  217.     // We were able to remove the dependency from 'k' because instead of filling both the groups, we can just check if one of the group can achieve sum = targetSum/2.
  218.     vector<vector<int> > memo(n, vector<int>(targetSum + 1, -1));
  219.     //************IMPORTANT
  220.     //assert is a powerful tool to check if some logical condition is true or not while code is getting executed
  221.     //Used as safe access to index (both level param/recursion driving param such as index or level and memo table declaration)
  222.     //Use it before or after a risky/sensitive array access
  223.    
  224.     //Assert #1: memo size should always be these dimensions
  225.     assert(memo.size() == n && "memo table should be of size n");
  226.     assert(memo[0].size() == targetSum + 1);
  227.     cout << boolalpha << getEqualSumPartitionSingleGroupTracking(0, 0, targetSum, nums, memo) << '\n';
  228.     */
  229.    
  230.    
  231.     /*
  232.     //BEST APPROACH - (MASK BASED, SINGLE GROUP TRACKING)
  233.     //1) Use mask instead of index, mask configuration represents a VALID COMPLETE SINGULAR PATH IN CHOICE TREE DIAGRAM or SUBSET OF CHOICES EXPRESSED BY ALL THE ELEMENTS
  234.     //2) //Unlike index based recursion, where all the elements are processed at the base case/leaf node of the recursion tree, we are only able to the full choice diagram at base case. But in MASK for N elements, a particular state of mask (0th bit to N - 1th bit)  , represents the whole choice. We recurse with mask, and a MASK at any STATE OR TIME fully REPRESENTS A VALID CHOICE made by all the elements out of 2^N choices. We don't need to wait for the base case to see what were the choices made by the elements.
  235.    
  236.     Think of it as - mask from (0th bit to N - 1th bit, every bit is going to be either 0 or 1 in this range, some bits in this range are 0 and some of them are 1). Those which are 1, are the elements which decided to COME in the subset, those which are 0, decided NOT TO COME in the subset.
  237.    
  238.     ****** IMPORTANT
  239.      The CONFIGURATION of MASK at any point of time represents a choice out of all the 2^N different choices that can be formed by N elements
  240.      
  241.      
  242.     ******** VERY IMPORTANT
  243.      SINGLE GROUP TRACKING is the staple, we will do it with index based as well as mask based recursion.
  244.      It reduces the time complexity as well as the space by not tracking multiple groups at once and having to linearize multiple values to form a key and do memoisation.
  245.      It reduces the space because we are only tracking 1 group sum at a time and checking if it equals to some targetSum. So our memo key instead of looking like
  246.      memo(mask, sum1, sum2,.....sum k) it looks like memo(mask, currSum, k), where currSum represents the sum of the current group getting filled and k represents the number of groups left to filled.
  247.      So no matter how many group we have to fill, we can always maintain it with 2 variables,
  248.      currSum & K.
  249.      But in multiple group tracking we have to monitor all the K groups at once making the number of keys directly dependent on K.
  250.      So as K increases, length of memo key also increases memo(sum1, sum2.....sum3) as we have to maintain all k groups together.
  251.      So SINGLE GROUP TRACKING makes sure we can always track any number of groups with just 2 variables, instead of K variables in multiple groups
  252.  
  253.     Mask helps by reducing the number of variables further, because mask at any moment stores the whole information about the CONFIGURATION, and we can deduce of lot of things from that configuration such as sum, size etc.
  254.     So in index based recursion key would look like memo(index, currSum, k). All 3 are CHANGING, INFLUENCING and NOT DERIVABLE from any parameter, because index does not remember any configuration state so far, it only represents where we are are and how much we need to go further to reach base case.
  255.     But this is where mask shines, as mask represents the COMPLETE SINGULAR PATH IN INDIVDIUAL ELEMENT CHOICE DIAGRAM, it represents a VALID SUBSET formed by choices made by ALL THE ELEMENTS, unlike index which just tells that we have processed/made decision TILL ith index and we still need to reach base case where WE CAN MAKE DECISION based on some other parameter that we are maintaining in our recursion state (such as currSum and k to make a decision in base case) because from index alone we cannnot derive currSum or k, IT JUST TELLS WHERE YOU ARE, NOT WHAT YOU ARE
  256.    
  257.     With index based recursion, key was memo(index, currSum, k) but with mask this reduces to memo(mask), as the other 2 can be derived from mask.
  258.     SUMOFSETBITSELEMENTS(mask) / targetSum = NUMBER OF VALID GROUPS FORMED, which is what k is
  259.     SUMOFSETBITSELEMENTS(mask) % targetSum = The PORTION OF SUMOFSETBITSELEMENTS(mask) that has not been used to fill any previous VALID GROUP, but it is getting accumulated in the current group getting filled, essentially telling how much full the current group is, or what is the currSum
  260.     So K and currSum will be used in recursion just to drive it (help writing conditional or help in decision making in base case) but it would be treated as RECURSIVE SCAFFOLDING.
  261.     As mask can derive the other 2 at any point of time, there is no need to memoise them, which means no matter which path we take to reach 2 different states, if their mask is same, the other 2 params would also be same for those states. It does not need specification based on params that can be derived. So -
  262.     R1 (mask, a1, b1) and R2(mask, a2, b2).
  263.     as we can see R1 and R2, both states have the same mask, so a1 = a2 = f(mask) & b1 = b2 = f(mask)
  264.     So a1 = a2 and b1 = b2 at any state where mask1 = mask2 .
  265.     */
  266.    
  267.     /*
  268.   SINGLE GROUP TRACKING (SGT) is better than multi-group tracking because:
  269.     - In multi-group tracking, we need to track sum1, sum2, ..., sumK β†’ O(k)-sized memo keys.
  270.     - As k increases, memo size increases exponentially.
  271.  
  272.   In contrast, SGT fills one group at a time. When a group reaches `targetSum`, we move to the next group.
  273.     - Memo key reduces to: (currSum, k) β†’ only 2 parameters, regardless of how many groups we track.
  274.     */
  275.    
  276.     /*
  277.   ============================================
  278.   πŸ” MASK vs INDEX β€” SINGLE GROUP TRACKING DP
  279.   ============================================
  280.  
  281.   🎯 Goal: Fill one group at a time (say group1), each of size `targetSum`.
  282.            If we can successfully fill `k` such groups.
  283.  
  284.   ===========================================================
  285.   βœ… INDEX-BASED DP: memo(index, currSum, k) β€” WHY IT WORKS
  286.   ===========================================================
  287.   - `index` = position in array => tells us how many elements have been *processed*
  288.   - `currSum` = sum of elements added to current group
  289.   - `k` = how many groups are still left to fill
  290.   - This state tracks:
  291.       β€’ Which elements are *considered* (via index)
  292.       β€’ How much we have *filled* in current group (currSum)
  293.       β€’ How many groups we *still need* to fill (k)
  294.  
  295.   ❗ BUT `index` itself doesn't track which elements were actually *included* or *excluded*
  296.       ➀ So we can't derive currSum or k from index alone
  297.       ➀ Every parameter must be explicitly passed and memoized
  298.       ➀ Total states: O(N * targetSum * k) β†’ large memo
  299.  
  300.   ======================================================================
  301.   βœ… MASK-BASED DP: memo(mask) β€” WHY IT’S OFTEN SUPERIOR FOR N <= 24
  302.   ======================================================================
  303.   - `mask` = bitmask where i-th bit is 1 if nums[i] is already used in any group
  304.       β€’ For N = 4 β†’ mask = 0b1101 means nums[0], nums[2], nums[3] are used
  305.  
  306.   βœ… ADVANTAGE: Full configuration is captured by mask β†’ we can DERIVE:
  307.       β€’ `currSum` = sum of all elements in current partial group
  308.                   = total sum of used elements % targetSum
  309.       β€’ `k` = how many *full* groups formed
  310.             = total sum of used elements / targetSum
  311.  
  312.   So:
  313.       β€’ State is fully determined by `mask`
  314.       β€’ No need to track or memoize currSum or k separately
  315.  
  316.   🎯 Result: memo(mask) is sufficient!
  317.       ➀ Saves space: O(2^N)
  318.       ➀ Reduces overlapping subproblems (merges many (currSum, k) into 1 mask)
  319.       ➀ Faster lookups with integer keys (if using vector or unordered_map)
  320.  
  321.   =====================================================================
  322.   ⚠ LIMITATION OF MASK METHOD β€” USE ONLY WHEN N ≀ 24
  323.   =====================================================================
  324.   - There are 2^N different masks for N elements
  325.   - At N = 24 β†’ 2^24 β‰ˆ 16.7 million states β†’ typically acceptable
  326.   - At N > 24 β†’ 2^N becomes too large to store/compute efficiently
  327.  
  328.   βœ… Summary:
  329.       β€’ For small N (≀ 24), mask-based DP is often more efficient and elegant
  330.       β€’ For large N and SMALLER SUM, prefer index-based DP (memo(index, currSum)), which runs in O(N * targetSum)
  331.  
  332.   🧠 PRO TIP:
  333.     Use `mask` when:
  334.       β€’ You need to track *subset usage*
  335.       β€’ You can derive all needed parameters from it (e.g., sum, size, group count)
  336.     Use `index` when:
  337.       β€’ You need sequential decisions (e.g., combinations, fixed order)
  338.       β€’ You care about positions, not configurations
  339.  
  340.   Example:
  341.     Index DP  : memo(index, currSum)   β†’ O(N * targetSum)
  342.     Mask DP   : memo(mask)             β†’ O(2^N)
  343.  
  344. */
  345.  
  346.    
  347.     int totalSum = 0;
  348.     for(int i : nums){
  349.         totalSum += i;
  350.     }
  351.    
  352.     if(totalSum % 2 != 0){
  353.         cout << "NOT POSSIBLE TO PARTITION INTO 2 GROUPS OF EQUAL SUM" << '\n';
  354.         return false;
  355.     }
  356.    
  357.     int targetSum = totalSum/2;
  358.    
  359.     //here we don't need to maintain k, as there are only 2 groups. So just fill 1 group with sum = targetSum, other group would automatically equal targetSum.
  360.     //SIZE OF MEMO ?
  361.     // RANGE(MASK) => mask represents a configuration in the FORM OF AN INTEGER, i.e. each bit represents the choice of an element. There are N elements, so N bits needed. 0th bit to (N - 1)th bit. A configuration is flipping of bits between that range.
  362.     //N bits are need, so we left shift 1 by n, to acquire N bits.
  363.     vector<int> memo((1 << n), -1);
  364.     //IF there N bits in consideration - what is the minimum value and the maximum value that mask can take
  365.     //The integer value of mask, represents a valid configuration where some bits are on of off.
  366.     // such as mask = 3 represents 0b011 or mask = 10 = 0b1010
  367.     // min(mask) = 0b00000 = all N bits OFF = no element taken
  368.     //max(mask) = 0b11111....N times = all N bits ON = each element taken
  369.     cout << getEqualSumPartitionSingleGroupTrackingWithMASK(0, 0, targetSum, nums, memo) << '\n';
  370.    
  371.    
  372. }
  373.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement