Wednesday, June 8, 2022

Leetcode discuss: 416. Partition Equal Subset Sum

 Here is the link. 

The post is written by GeorgeChryso. I like to copy and paste the content in the following, and read more carefully and learn a few things. 

-Buckle up Jimbo, this one's gonna be a doozy

So, as I found this problem and the variations of its solution rather interesting, I will be explaining every possible approach.

Let's start by the definition of our problem.
image

However, we can manipulate the data in order to get to easier requirements.
image

It is now clear, that all I need is to find a combination(subset) of my elements that sums up to the total sum of the array, divided by two.

First comes the easiest approach:
image
Here's how to implement a simple hashmap where I store every possible combination's sum and a short example:
image

var canPartition = A => {
    let totalSum = A.reduce((acc, curr) => acc + curr);
    if (totalSum % 2) return false;

    const target = totalSum / 2;
    const memo = new Set([0]);

    for (let number of A) {
        let possibleSums = Array.from(memo);
        for (let possibleSum of possibleSums) {
            memo.add(possibleSum + number);
        }
    }
    return memo.has(target);
};

BACKTRACKING/DFS
image

So I deduce that every possible combination is a series of choices for every item-candidate. That is whether to take the item, or ignore it.
image

Let's demonstrate our solution with an example:
image

//clear backtracking, TLE
var canPartition = (candidates) => {
    
    //sumA
    let target=candidates.reduce((acc, curr) => acc + curr)
    if (target%2) return false; //As we said our sum has to be dividible by two
    target/=2

    
    const backtracking = (currSum, index) => {
        //if our Sum is bigger than my target there's no reason to continue expanding
        if (currSum > target || index>=candidates.length)return false
        // when I reach my target, return true
        if (currSum === target)return true

        return backtracking(currSum + candidates[index],index+1)||backtracking(currSum,index+1)

    }
  
    return backtracking(0,0)
  } 

  //get's TLE'd on 
  //[1,1,...,1,1,100]

As you can see, this solution doesnt pass a certain case, we can easily remedy that by modifying our method a bit

image

So If I start from my target and follow the first approach, I see that I pass all my cases:

  var canPartition=candidates=>{
  
    candidates.sort((a, b) => b- a); //key for TLE :Essentially means: If you fail,do it fast
    let target=candidates.reduce((acc, curr) => acc + curr)
    if (target%2) return false;
    target/=2

    
    const backtracking = (remaining, index) => {
        if (remaining <candidates[index]  || index>=candidates.length)return false
        if (remaining === candidates[index])return true

        return backtracking(remaining-candidates[index],index+1)||backtracking(remaining,index+1)

    }
  
    return backtracking(target,0)
}

KNAPSACK SOLUTION
Here follows some theoretical background for the knapsack problem, It is key to understand this in order to fully grasp the solutions that follow.
image

So, essentially, my task is to find the maximum value combination, given a capacity constraint.
image
Let's dive into the formula
image
And here's a quick example for demonstration purposes
image
And here's the matrix completed
image

Back to my problem
image
image
As you can see, my outputs are not identical. Here's how that affects my formula.
image

var canPartition = function(A) {
    //calculate the sum of my Array
    var sumA = A.reduce((acc, curr) => acc + curr);

    if (sumA % 2) return false;

    //create Rows
    // i want a row for each of my candidate elements+ one for my
    // 0th element( no element ) which I know for a fact can add up to 0 if selected
    var B = new Array(A.length + 1).fill(null);
    // create Columns
    // My final total sum ranges from 0 to sumA, which are totally sumA+1 candidate weights(sums)
    B = B.map(d => Array((sumA/2)+1).fill(false));

    // now that the matrix is created I have to use my base case which is:
    // If there is a way for me to get sum=0, with 0 elements
    B[0][0] = true;    // of course there is



    //here i=0 cos everything other column (sum) of this row cannot be created with 0 elements
    for (let i = 1; i <= A.length; i++) {
        for (let j = 0; j <= sumA / 2 ; j++) {
            //I know that i-1>=0 so i dont need an extra check for that
            if (j - A[i - 1] >= 0){
                B[i][j] = B[i - 1][j - A[i - 1]]||B[i - 1][j];
            }
            else{
                B[i][j] = B[i - 1][j];

            }
            
        }
    }
    
    
    return B[A.length][sumA/2];
};

KNAPSACK OPTIMIZATIONS

There's room for improvement on my last solution
image

2ROW APPROACH

var canPartition = function(A) {
    var sumA = A.reduce((acc, curr) => acc + curr);

    if (sumA % 2) return false;
  
    var previousRow = new Array((sumA/2)+1).fill(false);
    var currentRow= new Array((sumA/2)+1).fill(false);
    
    previousRow[0] = true; // base case  


    for (let i = 1; i <= A.length; i++) {
        for (let j = 0; j <= sumA / 2 ; j++) {
           
            if (j - A[i - 1] >= 0){
                currentRow[j] = previousRow[j - A[i - 1]]||previousRow[j];
            }
            else{
                currentRow[j] = previousRow[j];

            }
            
        }
        previousRow=currentRow.slice(0) // make previous=current
    }

    return currentRow[sumA/2];
};

1 ROW APPROACH
image

var canPartition = function(A) {
    var sumA = A.reduce((acc, curr) => acc + curr);

    if (sumA % 2) return false;
  
    var row = new Array((sumA/2)+1).fill(false);
    
    row[0] = true; // base case  


    for (let i = 1; i <= A.length; i++) {
        for (let j = sumA / 2; j >= 0; j--) { //start from right to left
            if (j - A[i - 1] >= 0){
                row[j] = row[j - A[i - 1]]||row[j];
            }
        }
    }

    return row[sumA/2];
};

FORWARD DYNAMIC PROGRAMMING STYLE
Essentially the two row approach, but a state [i][j] contributes to [i+1][j+A[i]] instead.

var canPartition = function(A) {
    let totalSum=A.reduce((a,c)=>a+c),n=A.length
    if(totalSum&1)
        return false
    let target=totalSum/2,
        dp=[...Array(target+1)].map(d=>0),
        dp2=[...Array(target+1)].map(d=>0)
    dp[0]=1,dp[1]=1
    for(let i=0;i<n;i++,dp=[...dp2])
        for(let j=0;j<=target;j++)
            if(j+A[i]<=target)
                dp2[j+A[i]]|=dp[j]
    return dp[target]
};

BIT SOLUTION

Here follows the final and more elegant approach using bitwise operations
image

image

image

var canPartition = function(A) {
    var sumA = A.reduce((acc, curr) => acc + curr)
	 /* to start with, i want the number with 1 as its first element so i can mimic the previous[0]=1 state, and length of bits= the length of bits of my desired sum (sumA/2)*/
    if (sumA % 2) 
		return false;
	let row = 1n << BigInt(sumA / 2 );
    for (const weight of A) 
        row = row | (row >> BigInt(weight));
    // check the the column corresponding to my target by bitwise ANDing it with just 1
	// so if the first bit is 1, it will return true, otherwise false
    return row&1n;
};


If you got this far, thank you for your attention. I hope I cleared up this problem. Sorry for my sloppy handwriting. Cheers.

hashmapknapsackrecursiondfs-bfsbitwise operation
Comments: 63
mateatomico's avatar
Read More

I commend you for the hard work put on explaining this problem. Very helpful. Thank you!

150
Reply
Share
Report
themistoklik's avatar
Read More

best editorial ever!

39
Reply
Share
Report
__Prudhvi__Raj's avatar
Read More

you should definitely try writing editorials for problems on leetcode 😉

33
Reply
Share
Report
rhq66's avatar
Read More

wow my dude, thank you

12
Reply
Share
Report
leetcode_lover's avatar
Read More

OMG!!! This is heaven. Leant a lot from you mate! Hats off to your great work and thanks :)

10
Reply
Share
Report
oots's avatar
Read More

That's so much of hard work done for helping the rest of us. Thank you!

6
Reply
Share
Report
Heuit's avatar
Read More

Explained thoroughly.
Out of context:
Congratulations on maintaining the leetcode streak for 1 year straight. (I don't know how many years, since we can only see 1 year streak on leetcode. )
Sheer will!!

5
Reply
Share
Report
chinmayswaroop's avatar
Read More

Appreciates the amount of work you put in man <3

4
Reply
Share
Report
amc1996's avatar
Read More

Hi... regarding the fail fast solution, while it does pass the OJ test cases, it gives wrong result for [6,4,4,3,1]
Even if sum < arr[i], the smaller elements to the right might make the required sum. returning false give wrong answer.
Correcting this mistake again gives TLE on the previous test case.

4
Show 3 replies
Reply
Share
Report
Arzz13's avatar
Read More

Backtracking with recursion fails for [14,9,8,4,3,2]

Leetcode discuss: A Full year with no days off- My thoughts

I came cross this post called "A Full year with no days off- My thoughts", and then I like to write my own one. 

Here is the link.  

  • The number of problems you have solved means absolutely nothing, and leetcode alone will not make you great at problem solving.
  • Making a post about something, doesn't always mean that you understand it.
  • You can't force learning topics that you don't like. The only thing you're achieving by forcing it is temporarily remembering it.
  • Half-assing a concept will most certainly eventually hurt your progress and waste your time.
  • You are not meant to understand every problem's solution. You need a certain level of algorithmic maturity for some problems. A lot of times this maturity comes from practice/ concepts that you have not studied yet. For example, no matter how much we discuss/practice a problem that requires the Convex Hull optimization, you still won't get it until you ve studied its theory, or the underlying concepts like dp/monoq.
  • High rated people are not always the best teachers.
  • Low rated people over long periods of time are most certainly not your best resource.
  • Posts like aatalyk's Dynamic Programming Patterns are trash. 2 weeks in you will have forgotten most of what you've learned, because you haven't really learned anything other than a few lines of code.
  • Setting time-schedules for a concept that you're studying is lamentable. Your brain doesnt respond to stimuli the way you expect it to.
  • Not having a schedule for the concepts you must study makes you a slave of occasion.
  • You can't really "finish" studying a challenging concept like dp, but you can attain some basic knowledge first, that will enable you to move on to other conecpts. BFS- style learning will be better in long term.
  • Ideas survive in your memory, implementations do not.
  • Just because you have implemented something, doesn't mean that you understand it.
  • You don't really understand things that you haven't implemented yet.
  • If you don't look at your old code and cringe, are you really improving?
  • Most likely, you don't even know what you don't know yet.
  • You shouldn't blidnly trust anything that you havent researched yourself from multiple sources.
  • Single contest ratings don't mean that much.
  • You're not really improving if your rating stays the same for long periods of time.
  • Copy-pasting solutions that you don't understand is for trash in denial who've hit rock bottom.
  • Avoiding contests to preserve rating is also for trash in denial.
  • Skipping a contest to study and remain on schedule won't hurt you.
  • Upsolving contest problems may hurt you and waste your time, when you lack knowledge of underlying basic concepts which you're better off studying first.
  • Your mental can break at some point. Spending more than 3 days to understand an approach can be stressful and disappointing.
  • If your mental is intact and your progress flatlines, consider doing harder problems/ studying more challenging topics.
  • Consistency on DSA can and will affect your personal life, especially if you're commited. The degree however depends on how much you let it affect it. You're not Gennady, and you know it. You haven't spent 10 years solving this kind of problems so it's only natural to struggle for many of them. This however should not discourage you from trying to be uncomfortable. Excuses are easy to find, especially when they' re de facto important, like work, family, money you name it. At these moments you have to remember that not everybody starts this race with the same tools, but it's the same race for everyone.

I like to review and write my own statements: 

  • The number of problems you have solved is a very good metric; and it takes time to build skills, but a lot of DSA practice will help you to prepare for future challenges.
  • It takes time to learn how to write a very good discussion post. I was too shy before to write my own discussion post. I am no longer a shy person, I love to write and I write to share, and I love to learn from my own experience as well. 
  • It is a difficult job to learn a hard level algorithm. I try so many hours to learn one hard algorithm sometimes. Learning is fun and I do believe that hard level algorithm may not be the best choice, since I can learn easy level or medium level first, and it may help me a lot to solve hard level later on. 
  • There are so many ways to learn and improve problem solving skills. Do not get frustrated. 
  • Build a good habit. Try to document the practice and find your own weakness. 
  • Look up high rated people and see what I can learn from their success.
  • Posts like aatalyk's Dynamic Programming Patterns definitely should be good resource. I will try to read again. 
  • How to learn better on algorithms by topics? I will think about it more later on. 
  • Learn to make plans.
  • DP, BFS are very good topics to work on. Work on BFS first, afterwards, work on DP. 
  • Ideas, implementation, small example, case study are the four most important components in problem solving. I try to work on simple things. Make it simple as possible. A small example with steps to solve will help me to learn much more quickly and make things clearer.
  • It takes time to improve problem solving skills, and I often review your own discussion post, and try to write a new one.
  • It is hard to manage contest rating. I prefer to learn better by studying algorithms, and work on classical algorithms first. 
  • I did spend around six months to work on weekly contest, and then I had difficult time to improve my rating. I chose to move on, and work on other ideas first. 

Leetcode: Google tagged | Medium level algorithms | Last six months | 10 algorithm problems - total 120 algorithms | Page 12

 

1570
807
366
1506
2128
419
702
2174
1525
370
841


Leetcode: Google tagged | Medium level algorithms | Last six months | 10 algorithm problems - total 120 algorithms | Page 11

 

1244
362
1167
1706
427
1268
1101
238
1306
323

Leetcode: Google tagged | Medium level algorithms | Last six months | 10 algorithm problems - total 120 algorithms | Page 10

 

62
131
12
286
430
900
540
1504
1094
1048

Leetcode: Google tagged | Medium level algorithms | Last six months | 10 algorithm problems - total 120 algorithms | Page 9

 

740
2178
348
384
1296
2184
846
539
752
729