Transcript for:
Dynamic Programming and Tabulation

Hey programmers, I'm Alvin l come to our course on dynamic programming. So dynamic programming is one of my most favorite topics to teach. But unfortunately, I feel like dynamic programming also has a reputation for being a very difficult topic. That being said, I think dynamic programming can be very intuitive. If we actually take a nice gradual progression through the material, right? A lot of students have this habit of just trying to attempt a pretty hard dynamic programming problems, without going through the necessary steps of really understanding the material, right? It goes without saying, if you want to do well on those data structure and algorithm interviews, you definitely need to know dynamic programming. So I hope to give you all the background knowledge, you need to really crush those types of problems. Now, that being said, What problems are we going to solve throughout the course? Here are a few examples. So one question I can ask you is to calculate the fourth number on the Fibonacci sequence seems like a very easy problem, can also ask you to count the number of different ways to move through a six by nine grid, or something different, like given a set of coins? How can we make 27 cents in the least number of coins? A final example would be given a set of substrings? What are the possible ways to construct a string potent pot? And all of these questions really fall under the umbrella of dynamic programming. And this is really why I think the topic has such a bad reputation or being very difficult, because the problems ranged so much, right? It looks like these problems are totally different. And there may not be any underlying mechanics that we can use to tackle all of them. But the short answer is, we really can if we have the correct way of thinking about these problems. That being said, let's go over the overall format of this course, in this course, I think our key to victory is going to be to really visualize all of these algorithms, right? So we're going to spend a lot of time drawing things on the whiteboard, as well as visualizing things with animations. To me all the heavy lifting on an algorithm interview is really done when you come up with that picture, right? When you describe that process, and then translating it into some code is really the easy part. Right? The hard part is designing the algorithm in the first place, right? So we're going to draw out of things to make sure we understand the structure of the problem, as well as coming up with a solution. And then once we're really happy with that pen and paper process, then we'll hop into my editor, and we'll solve it in some code, we'll probably have to go back and forth, until we end up with an algorithm that runs in an efficient amount of time, right. So it goes without saying, we're also going to analyze the time and space complexity of all of our solutions. I'll be writing my code in JavaScript, but you'll find it very easy to translate our solutions into the language of your choice. So in this course, on dynamic programming, we're going to divide the material into two main parts. Part one is going to be about memoization. And then Part two is going to be about tabulation. And don't sweat it if you have no idea what those two terms refer to, don't worry, we'll reach all of that material step by step together, I think you're going to realize if we actually learn these things in all very logical progression, we're almost like discovering these algorithms. And I don't just have to tell us what the algorithms are. So in terms of prerequisites, I won't assume you know anything about dynamic programming, but I will assume that you understand a little basic recursion, as well as some basics of complexity analysis, right. So you're sort of familiar with big O notation. And I'm sure that we'll be able to review some of that notation as we move along. So I think with all that out of the way, and no further ado, let's hop into the course. Alright, so where should we start? Well, I want us to really ease into this new topic. And so what we'll do is we'll start by attacking a problem that you probably seen in the past that is I want to solve a Fibonacci problem. And so for us, we'll have a particular phrasing of the Fibonacci problem. What I want to do is write a function fib of n that takes in a number as an argument, and I need to return the nth number of the Fibonacci sequence. And just to review, how does the sequence work? Well, the first and second number of the sequence is just one. And then at any point in time to generate the next number of the sequence, you can just sum the previous two. So for example, these are the first few numbers of the Fibonacci sequence, right starts 112358, and so on. What I'm saying is your number needs to take in like a position of the sequence. In other words, if I asked you for the seventh Fibonacci number, you need to return the answer 13, right? Because the seventh bonacci number is 13. And how do we actually calculate that 13? Well, logically, it's just a sum of the previous two elements, that is five plus eight gives me the 13. So very, very classic function here. And to really get us going for what we do later on in the lesson, I'm going to require us to solve this one recursively, right, it's really going to be at the heart of a topic for today. Why don't we kick things off by quickly implementing the classic recursive implementation of a Fibonacci function, probably in this function a few times in your programming career, usually one of the earliest examples of recursion that we face. So we'll just lay out the classical notation here. So I want to take in a number and and I want to return the number of the Fibonacci sequence. Like we expect, the base case is just about, you know, the first two numbers of the sequence. In other words, if I'm given some n, that's less than or equal to two, then what I should do is just returned one. And I'm reading this because hey, the first two numbers of Fibonacci sequence are exactly one. But in the recursive case, in general, what I can do is just return the sum of the Fibonacci number right before the one I'm asking for, as well as the Fibonacci number two before the one I'm asking for, right? Again, the kind of baked in recursive nature Fibonacci is to calculate some Fibonacci number, you can take the sum of the previous two numbers in the sequence right. Now, of course, we should test our code for some correctness, what I'll do is call a few examples of this fib function. So I'll try fib of six, seven, and eight. And I shouldn't get the answers of 813 and 21, respectively, right. So let's give this a go. I'll run this in some JavaScript. There we have it right. 813 and 21. So this is a very, very classic implementation of Fibonacci, you've probably seen this many times before. And we did solve it recursively. And really, what I want to do is give some a larger number to this fib function. So what if I asked for, let's say, I don't know, the 15th Fibonacci numbers seems like something reasonable to ask for. So if I give this code a shot, it looks like the first three calls of bonacci do work fine, right, I get 813 and 21. But the fourth call is actually still running, right, my program actually hasn't finished. And this is a big issue with this type of implementation with Fibonacci. So obviously, this Fibonacci function needs some work. Let's go ahead and head to the drawing board. Alright, so it's apparent that our Fibonacci function is correct. And that returns a proper result. However, if we give it a large enough value of n, it's pretty slow. That is our function has correctness but lacks some efficiency. In the long run, we definitely want to improve this implementation of a recursive Fibonacci. But to do that, it's really important that we identify exactly where there's even room for improvement. And to do that, I think we should draw some things. This is something I think students actually need to work on. Students have this habit of trying to like picture everything in their mind. And that works for some easier problems. However, when we want to tackle more complex problems, if we just try to capture all this information, just mentally without drawing it on, like pen and paper, or marker and whiteboard, or chalk and chalkboard, you're going to really lose track of the finer details in these structures. So I want us to be very, very methodical, and we're going to draw really how you should visualize a problem like Fibonacci. And so over to my drawing, let's say that I wanted to trace through what happens when we call a fib with the number seven. That is I'm asking for the seventh number in the Fibonacci sequence, I know that in the long run, I should get back to 13. Right. 13 is the seventh number in the sequence. So I'll keep that goal in mind. But over to my drawing, let's say I called fib of seven, I'll denote that by really drawing a circle with my value for n inside. So I think about this call to fib of seven, what is it going to do? Well, I know that seven is not the base case, right? Seven is not less than or equal to two. So this call is going to branch out into some more recursive calls. In particular, on its left hand side, I'm going to call n minus one, which is six. On the right hand side, I'm going to do n minus two, which is five. And at this point, I carry over the same logic for the other nodes have the structure, right, if I look at the six node, if I'm routed right there, then that will have a left child of minus one, so five, it will also have a right child of minus two. So four, you can start to see a pattern where really this recursive structure just visualizes as a tree, which is really neat. I hop over to this five node, it will have some children, right, minus one on slept minus two on its right. And we will actually just continue this process for most of these nodes. But let's say we pause right here. So you'll notice that these notes that have pointed to in red, they're actually the base case, right? For those nodes, I have values of two or one. And I know that those function calls will return immediately. More importantly, that means they don't branch into any further calls. So I don't want to start flushing out the tree of those nodes. Instead, I look at things that are not the base case, right? That is these nodes in yellow. So I'll continue to flush out this tree, but not branch out further for the base cases. So at this point, I built out my entire tree, and I stopped fleshing out the tree whenever we had a base case scenario. So this is actually the full recursive tree. Remember that the numbers inside of the nodes here represent the end that we passed in. That being said, if we have this visualization, how does this tree really calculate the Fibonacci answer? Right, so let's start to break it down over here. Let's say I looked at some note, in particular, this base case note of two, right? I know that this note is a base case, so it's going to return the value of one according to my base case, when we say return, that really just means return to your caller right return to your parent. So this note of two is going to return one to its parent of three. In the same way, this node on the right hand side of one is also a base case, two will return one, both of those values that are returning, they go back to the parent of three, and three is actually going to add those two values up. One plus one is two. And this makes a lot of sense because we know that the third Fibonacci number is two. So we can continue this process. Okay, this node over here. This is also a base case. So it returns one. And now the parent node of four is going to sum up both of its children values. Two plus one is three. And that makes sense in itself, because the fourth Fibonacci number is three. So you probably got the picture. Now let's speed things up. For all of these base cases, I know that they're going to return one to their parent. And for all parent nodes that have both of their children ready, that is both of their children returned, they're just going to add up those values. And this process just continues all the way up the tree, right? Just adding our left and right children. To get the answer that we should return the very top of our tree at the root of our tree, we get the final result of 13, which makes a lot of sense, because at the start, we said that, hey, the seventh Fibonacci number is indeed 13. So now that we have a robust understanding of how to visualize this fib function, what do we actually know about its speed? What do we know about time complexity. And so you may have heard offhand people mentioned that a classic recursive implementation of fib is going to be two to the n in time complexity. And that is the case however, you really understand the reason why. So hidden in this picture is the reason why Fibonacci for us is going to be two to the n in terms of its time complexity. However, something that's kind of unfortunate about this drawing is it's asymmetric. And that's, I think, a big reason why students have a really hard time convincing themselves that a function like this has a two to the n power time complexity. So here's what we'll do, why don't we warm up and kind of go through some basic understanding of time complexity? And I promise we'll answer that Fibonacci question. So let's do a little warm up. Let's say that I gave you this foo function, notice that it's different from our fib function, right? It's similar in that recursive, this function is kind of arbitrary, it actually doesn't calculate or solve any particular problem. So if I wanted to visualize how this foo function behaves, let's draw it out. Let's say I initially call it the top level foo of five. I know five is not a base case. So it's going to call upon n minus one, or it's going to call upon for four calls three, three calls to two calls one, and then here, we've actually bottomed out at a base case. If you look at the number of calls I made, I basically made exactly five function calls. Which makes sense, because in terms of our base case, where do we stop once we hit a number less than or equal to one and every recursive step, we just subtract one from our current value of n. So overall, I have five calls here. But if I generalize that, for any arbitrary input, I know that in the long run, I'm going to have about n different function calls recursively. And so for that reason, the time complexity of this is really just O of n time, right? I have to evaluate O of n different function calls. While we're at it, why don't we take a look at the space complexity? Well, you may have heard in past that when we analyze the space complexity of our recursive functions, we should include any of the additional stack space that our function calls take up right, when we make a recursive call, we add that to the call stack, and those must be tracked by our computer. And so since we have about five or n different function calls added to the stack, before we hit our base case, you can see that the space complexity of this code is also O of n space, overall, we're looking at O of n time and open space for this function. Pretty straightforward stuff, right? Let's look at another more involved function. So let's say I gave you now this bar function, it's another arbitrary function, it's very, very similar to foo, what you should notice is, the only difference is when we make a recursive call, we do an n minus two instead of an n minus one. So how does that actually change the time complexity of this function? So let's say I wanted to trace through this and I made a top level call to bar of six, I know six is gonna call upon four four is gonna call upon to, to call zero and zero actually hits a base case. So this is very similar to our last example, except we kind of see that from one call to the next, we kind of take a bigger step in time, right. And so in a way, we can say that we're moving twice as far upon every recursive call. And this would actually half the number of recursive calls we need overall. So I guess we might be tempted to say that the time complexity of this one is N over two time, but a keen observer would note that according to our bego, you know, understanding, we can remove any multiplicative constants when we have a time complexity, so N over two is the same as one half times n. So this simplifies nicely, it's just an O of n time complexity. Using the same exact argument, we can also say that the space complexity from the stack is also open space. All right, so let's take a lay of the land, I showed you two functions that are very similar, they really only differ in how they made their recursive calls, right? One did minus one, the other did minus two. But in the grand scheme of things, we saw that they had an identical complexity class, right? We have O of n time and O of n space for both of these functions. So after these two examples, you may be able to see the reason I wanted to bring them up, right, maybe you're actually ready to make the logical leap and make some conclusion about our classic Fibonacci recursive function. That being said, I don't want to skip any steps. I want to be super methodical, I think, if we pay the cost of understanding fib right now, and I mean, truly, like Absolutely. Understanding fib. It's really going to pay off later on in the lesson when I slam me with some much harder problems. So let's be nice and methodical over here. Let's take a look at some other functions. Let's say I gave you this did function now. very particular Right, we'll pay it no mind. What do we notice about this did function? Well, it has two recursive calls now right inside of every single call. And they both do an n minus one. How should we visualize this one? Well, it's kind of similar to I guess, our initial fib drawing and shape, where if we start with some initial call, let's say five, five is going to branch to exactly two children, right? Because five is not yet the base case. And for this did function, it does a minus one on its left, and also it's a right child, right. So the next level is just for next levels, just threes than twos, then just ones which would actually hit our base case over here. This is a really nice, like, beautifully symmetric tree, right? Okay, so this is a visualization for our dib function. But what does it tell us about the time complexity of it? Something you'll hear me say a lot in this lesson is when we tackle a quote, unquote, new problem, or a new pattern we're really trying to do is just leverage our past experience, right? So when I look at this tree structure, I'm trying to notice anything familiar, right? Is there some strand over here that I can grasp that to really feel comfortable and kind of extend my previous learnings, right? Where can I find our base inside of this drawing? Boop, right here. So if I look at this path, I've highlighted in yellow, it's really just the path starting at the root node going down to some base case here, I just designated the leftmost path. And what's really nice about this structure is just referring to the notes in yellow. It's a linear structure that we saw before, right? If I start at the root, it just goes 5432. And one. And so I know that in general, based on my initial input of n, like the length of this path that has a number of nodes highlighted in yellow, there's going to be about n different nodes. If I kind of adapt that language for like the tree, I can also say that the height of this tree is n. So the height of a tree is really just the distance from the root node all the way to the far this leaf, in this case, that just means the distance from our top level called Five, all the way down to a base case, which is going to be exactly five here. Something you may also hear in passing is we can say that the number of levels in this tree is also n, this term is pretty straightforward, right? When we say the number of levels, a level is just a collection of nodes that are the same distance away from the root. So for example, here in yellow, I have highlighted level zero, this is level one, this is level two, this is level three, and so on. But if I rewind things a bit, I look at the very, very top level, there's one node here, on the next level, there's two nodes. On the next level, there's four nodes, then eight nodes, then 16 nodes, see the pattern? So let's try to generalize that. So I know no matter what, whenever we call some top level argument for dib, we know that we're going to have one node at the top level. But to get the number of nodes on the next level, we'll just multiply that by two. And the level after that would also multiply by two and multiply to again further levels after that. And I do this a total of n different times, right? Because I know that the height of the tree, or the number of levels in this tree is exactly n. And so what conclusion Can we make over here, we're basically saying that to get the total number of nodes, or the total number of calls a recursive function would make, you would just take the number two, and multiply it by itself about n times over. And that is really the definition of an exponent, right? This is the same as two to the nth power. So we can say that this tree structure, this recursive function has a two to the N, time complexity. Awesome. So we identified this dip function is having a two to the n time complexity. But what do we know about the space complexity over here, I think a common mistake I've seen people make is kind of automatically assumed that the space complexity of a recursive function will be the same as the time complexity. And that might be a reasonable trap to fall into, because we know that in the long run, we're gonna have to evaluate two to the n function calls. And so I guess that means you have to put two to the n function calls on the stack. But there's actually some nuance to this, the time complexity of this is not the same as space complexity. So let's jump in. Let's say that we made our top level call to dibba five, we know that that is added to the stack in the same way five calls for so it's that the stack, we add a stack frame for every call that we make down until just the base case, right. So at this point, I reached a scenario where I have a base case, and I'd have about five stack frames on my call stack. And the important insight is when we actually hit this base case that I have highlighted the left one over here, it actually will return when something returns when a function returns, its stack frame is actually removed or popped from the stack. And at this point, only after I have returned from that left one, what I actually add the right one to the stack to be explored. And this process continues. Notice that any point in time the most number of stack frames that we use up, it's exactly five, right, it's not as if we throw all of these function calls on the stack at once. And because we have such a nice tree visual, we know that the number of stack frames that we're ever going to use is really just the height of the tree, right, so the height of the tree is n like we said before, that means our maximum stack depth is also n. So we have in space complexity coming from the call stack. So overall, for a dip function, we're looking at two to the n time complexity, but only and space complexity. Alright, so let's look at one more function. See, I gave you this very similar lib function. Notice that in its recursive calls, it does an n minus two. So by now you should be able to visualize what structure like this would look like, say we initially called lib with a value of eight, what would the full tree look like? Well, it would just look like this. Notice that it's still a tree, right where every node branches to two children. But this time, we go by twos, right, so if I look for some familiar ground here, I've noticed that from one node to the next, I do one minus two. And this occurs all the way down to a base case. So we already know that, hey, we can identify this tree as having a height of about N over two. So I guess that means that the time complexity is going to be two to the N over two power, right, because from one level of the tree to the next, we double the number of nodes. So that's that two times two times two repeating pattern for the number of levels and our number of levels is N over two, right? However, we can actually simplify this time complexity, you can take that n over to in the exponent and simplify that to just an N. So overall, we're looking at a two to the n time complexity. And using our same arguments from last time, we know that the space complexity for this from the stack is also going to be N over two, which simplifies nicely to N space complexity. So we see that overall for our loop function, we're looking at a two to the n time, but no space complexity. Alright, so now it's time to look at the big picture, we looked at two toy functions of dibben lib. And we saw that the only difference in how they made the recursive calls, right did did a minus one and lived in a minus two. And we saw that despite their differences, both functions had an exponential two to the n time complexity, and a linear and space complexity. That being said, Where does our original Fibonacci function fit in this picture? Well, you can imagine that kind of falls right in between, we know that for our fib function, it has two recursive calls for the first recursive call, it makes it does n minus one like did did. But its second recursive call this n minus two like lib. So in a sense, it's kind of like smack in the middle, right? If you let me abuse the notation a little bit, and just talking about the time complexity, we can kind of say that the complexity of fib is somewhere between dibben lib, but we've already plugged in some values, right? You know, that, hey, the lower bound that as Deb has to to the end, and the upper bound, that is lib has to at the end as well. So that means that our fib function must have exactly two to the n time complexity as well, right? All three of these functions have an exponential time complexity. Awesome. So that was a really complete analysis of why we have this fib function, it's evident that it has a two to the n time complexity and an N space complexity. Right now, the bottleneck that we're experiencing is, of course, the time complexity, right two to the n overall, is not undesirable complexity. But do we really have a nice feel for what this really implies? So let's take a look at this to to the end. So what's the implication of this? Well, you could kind of imagine that, hey, if I asked for the 50th Fibonacci number, that would take you know, roughly two to the fifth power number of steps. And so if you punch this exponent into a calculator, you're gonna end up with a result like this, this is roughly a 16 digit number. So you should have a vi for you know, this being a very, very, very large quantity. But I think that, you know, to really understand the gravity of what we're really saying here, if we expand this number, that quantity is exactly this. That's over one quadrillion 125 trillion, which is really, really interesting, right? Because we just asked our Fibonacci function for just you know, something relatively modest, right, the 50th Fibonacci number, and it's gonna take quite literally a quadrillion steps to do that. And of course, we can probably do better. So let's work on making this faster. So if I recognize that the bottleneck for this fib function is the time complexity, I know that that comes from the number of recursive calls that I make. So what I want to do is look for any patterns that I see in the recursive nature of this problem, let's see a quick snapshot of what the recursion for fib of seven would look like, we know that it looks like this tree just like we saw before. So take a moment, look at this tree, do you notice any interesting patterns within it? Well, one thing that I noticed is I can see this subtree rooted at three, there, I have two on the left and one on the right. And if I look at that subtree, I can actually see it in many different places in this tree, right? This subtree rooted at three appears in a bunch of different places, it's very duplicate. In a similar way, I can look at other sub trees, let's say I root myself at four, and see that duplicate four sub tree all over the place. And it's even carries over for larger values of n like fib of five. So I see that this tree has a lot of duplicate sub trees, right? And I want to now draw a connection between this diagram and what happens in my code. I know that if I route myself in any of these sub trees of five, I know that each sub tree is trying to answer the question. Hey, what's the fifth Fibonacci number? I know that the Fibonacci number never changes, right? If I calculate it once on the left hand side, then the answer I should get back on the right hand side shouldn't differ at all. And so what I want to do is possibly reuse these calculations right? If I calculate the Fibonacci number over here, then I should just store that because later on it might be useful when need to recalculate it over here. I would basically get rid of a lot of the tree, I wouldn't have to travel down this entire recursive tree rooted at five. This pattern of overlapping subproblems is known as dynamic programming. And so for us dynamic program is going to be any instance where we have some larger problem, in this case, Fibonacci. And we can decompose it into smaller instances of the same problem, we also have an overlapping structure. So for us, right, now, I see that I have to calculate now let's say fib of five, twice over to calculate the larger a fib of seven a solution. And something we're going to be doing a lot in this lesson is really trying to visualize problems in terms of like their recursive nature. So we're going to be drawing a lot of trees. And what I'm always going to do is try to really recognize, hey, what pattern in this tree is duplicate, right? If I do some duplicate work, if I do some duplicate drawing that I know I can optimize that out later on. But that being said, let's go ahead and get to the punch line on this fib function and work on optimizing this solution. Alright, now let me discuss the plan, I think we're ready to actually implement some code that will actually carry that plan out. Here I have our classic bonacci implementation, this was the one that we ran earlier. And it's definitely too slow, right has an exponential time complexity. So I know that overall, when I want to do is kind of capture a duplicate subproblems, I want to store any results that I get that way, if I have to recalculate those subproblems. Later on, I can just use my stored data. And so the trick here, it's a very common programming pattern, we're going to implement some memorization. memorization is actually one of the overarching strategies we can use to solve any dynamic programming problems. And so just look at the name like why is it called memoization? Well, this refers to like, memo, right? So if I have like a memo, and let's say, like real life, it's really just like a reminder for myself. So using memoization, I'm looking to do is store some duplicate subproblems. That way, I can just get those results later on. I think a really neat way to implement memorization in JavaScript, as well as many languages is to use some sort of a fast access data structure usually be like your hash map equivalent in the programming language of your choice. For us, that'll be a JavaScript object. And so our plan is to use some JavaScript object. And so what do I want the keys to be, so the keys in the object are going to be the argument to our function, right. And then the value will be the return value, that we have a nice napping for a argument to the function that is a function call, as well as its return value, right. And so what I can do for my existing function is, I kind of just bake in some optional arguments. So my favorite strategy is to do this, and assign a memo to be an empty object. So if you're unfamiliar with this syntax, in JavaScript, it's pretty useful. What I'm saying is, if I were to call our fib function, and not pass in a second argument, by default, it will create this memo, as containing a new JavaScript object that is empty, of course, right? So it's gonna be useful that way, whoever is actually testing, my code doesn't have to deal with setting up any memo object. So well, I prefer this strategy over here. And what I want to do is treat it as if that, hey, this memo is going to store n as the key and values are going to be just the return values for this function. So what we're going to see us doing a lot in this lesson is at the start, we're going to first check for existence inside of our memo. So let's say that we're somewhere in the middle of recursion, the first thing I should do is kind of add an additional base case and say, Hey, is my current argument n inside of the memo, and if it is, then I can just get the stored value from that memo. And I'm done. So I'm going to do an early return here, I'm going to return the value that corresponds to that memos key, right, I'm just using the original argument and as a key in my memo, and this condition is really just some classic JavaScript syntax, I'm just checking if some key is inside of a JavaScript object. So really quick, maybe just to warm us up, if you haven't seen that syntax before. So let's say I had some object and had some properties inside and we had a name of Alvin, that's me, then it had a favorite color of like gray. And what I can do is check for an existence of a key in that object. So JavaScript keys are mostly strings, right? So I can check Hey, his name in the object. That's true, is the fav color in the object, make sure I spell it right. That is also true, I can check for a key that's not there, like I don't know, his location and the object that is false. And so here, I'm just using that same pattern, but for and right, which is going to be a number, technically, it'd be converted into a string key, which would still totally work here. Awesome. So I have my memo fetching logic, right? I check, Hey, is this argument in the memo, but if it's not, I'm gonna have to actually manually do the calculation, which is okay, because I know I need to do a subtree at least once. So what I'll do is I'm going to take the exact return value, right, so this is my return value in the original brute force. What I want to do is actually store that entire result inside of my memo and the key is course and write, the key you use to access is always just what your argument is. And I want to complete the original return. So I can just go ahead and return what I just put in that memo. So I'm not really changing any return values, or I'm returning exactly the expression that I returned before. But now I'm actually also saving that value inside of the memo object. What I want to do is make sure that all of these recursive function calls are accessing the same memo. So what I'll do is I'll pass in that object to both of these calls are really important pattern here is, I know that I only receive a new top level metal object, whenever I make a top level call, that's a fit, right, because I'm not passing in a second argument over here. However, if I look at my recursive calls, I do pass in explicit second arguments. And so they're actually going to receive the same memo object, and it would be like passed by reference, right, because when you pass a JavaScript object to a function, you actually receive kind of that exact object, right, you don't receive a copy of it, which is really neat. So basically, I'm giving my function calls a way to sort of communicate to each other, they all have some sort of global information to reference across all the recursive trees. So this is looking good. Again, I just want to emphasize I only added a new argument over here, I added a new base case on line five, then I added my memo storing logic on line seven, but I didn't change any of the functional logic here. Let's go ahead and run these test cases. And we'll see how our code is doing now. So moment of truth, I'm going to run this file. And notice how blazing fast our program was, I still get the results of 813 and 21. And the 50th Fibonacci number is indeed have this very large number. And it basically executed almost instantly. So let's go ahead and head back to the drawing board. And really understand you know, what happens when we execute this sort of code. Alright, so it looks like we implemented the improved version of Fibonacci by memorizing it. And it's clear by running the program that we definitely had an impact on the time complexity. However, I really don't want us to understand you know how the structure of the recursion tree changes, once we implemented this memoized version of the code. So let's say I want to step through an example of Fibonacci. And let's say I pass in a number six. So really, what I'm looking for here is to get back eight, because the six number of the bonacci sequence is indeed eight. So we know that we're going to have a tree that looks like this, right? This is a tree that would really be the full recursive nature. So this is how the tree would look like if I did not optimize it. And what impact do we have by actually adding this memo object? Alright, so let's start tracing through this, I know that when I call fib of six at the top level, an important thing to know is I'm not going to pass in some memo object. So by default, my code says it will initialize it to an empty object. And the really important aspect of this is I'm going to create a new object just for the top level call. But then that same object is going to be passed down to my recursive calls, right noticing line five. And so I'm going to travel down my left hand path, right, six calls five, five calls for four calls three, and three calls to, and right now I've hit a base case. So I know that this note of two is going to return one. So that's kind of business as usual, right? In the same way, now I need to evaluate this other note of one, which is also base case, it also returns one. And at this point, my parent of three is actually ready to compute the sum of its children, right. So one plus one just gives me two. So I just add up those values. And now three is returning two. However, looking at line five of my code, not only is this node going to return to to its parent, it's also going to store it in the memo object, right? So the key insight is, at this point, I would add a key to my memo of three, whose value is to basically in my memo, I kind of read as if I'm saying, hey, the third Fibonacci number is two, right? That in itself is logical. So I can just continue this pattern right? Now I start to evaluate what happens when I'm at this note of t, which is also a base case, it returns one. Now my call to four is ready to compute, right? It's going to take the sum of its both children. So it's going to do two plus one gives me three. But of course, it's also going to store that inside of the memo, right? It's going to cash that result out to be used later on. So that means I make the N my key and I make the return value, the value, right, so I'm going to have four points to three inside of my mental object. And now here's the beauty of this memoized solution. At this point, I need to compute what happens for fib of three. However, this is actually going to hit one of the new base cases I added, right? I know that three is in the memo object looking at line two of my code, right? It's in the memo object. And so I just immediately return the stored value. And so this call to fib of three is just going to return the stored value of two. And if I do that, I won't have to travel down the full, you know, recursive subtree rooted at three, right? I don't need to travel to these nodes at all. So I already used some stored value in my memo. At this point five is ready to return right five will take The summer both with children, three plus two is five. And it's going to store that in the memo, right? The fifth Fibonacci number happens to be five. And now the same thing happens for six is right child. So I have to evaluate this node now. So what is the fourth of bonacci number up, that's actually stored directly inside of my memo. So this actually early returns the stored value of three, right. And again, the key insight is, it will return that stored memo value without having to travel through that full subtree. And at this point, I can return for fib of six, so I do five plus three, that gives me eight, this would actually technically be stored in my memo as well. So I have the six of bonacci number as eight and very happy, right? The answer is eight. So it's evident that by memorizing our Fibonacci function, we definitely cut down on the number of recursive calls, we want to visualize that we ended up with this kind of structure. Here in light blue, I have a circle the nodes that we technically didn't need to do the full recursion at, right? So these nodes of two, three and four, for this small example of my initial fib of six, I was able to kind of fetch a value from my memo and kind of forego traveling down the recursive subtree. So what do we actually know about the time complexity of this function now? Well, I think it's really important that we always try to generalize things. So we just stepped through a relatively small example of fib of six. But how does this sort of scale? So this is the same tree? Let's see, I kind of tidy it up a little bit. So I know that in general, fib of six kind of has this structure. So I want you to take a moment, look at this. And in your brain picture, what fib of seven would look like if I memorize with with seven, what would its sub trees look like? Well, it really just looked like this. Notice if I route myself at the seven node, its left child is still six, right minus one. Its right, Charles, still five minus two. So this still obeys the laws of Fibonacci. If I asked for fib of eight memoized, the structure would be like this. And nine would look like this. See the pattern looks like at this point, we're just growing this like memoized chain in a linear kind of way. If you're not convinced by you know, that just like structural argument, let's say we were a little more methodical, we know that in this drawing, there is some common ground, right? Again, like I always say, when you tackle these new problems, or new patterns, try to find some familiar territory, right? Where can I recognize stuff I've seen previously. So I look at this chain, I go 98765432, right. So this goes all the way down in a very, very, very linear fashion, right, just counting down. So I know that if I just look at this highlighted chain in yellow, that's definitely just an notes, right, where n is my initial top level call. So I know that what I have highlighted in yellow is just a linear chain right there exactly n nodes. But I haven't accounted for everything in this picture, right, some of the nodes are still in white, meaning I need to kind of work them into my description of what the shape of this tree is, well, I know that each of those white nodes is actually connected to some of the nodes, I've colored in yellow, they're kind of paired off in a way, right? For every node in my ello chain, it has exactly kind of one partner node one right hand node on its right hand edge. And overall, if I have n pairs, that means I have to end things overall, right? There are two things in a pair. So overall, the number of nodes is roughly two n. And I know that that can actually be simplified, right? The time complexity here would be two n, which just simplifies to n time complexity. And using the same arguments we did for our space complexity, we know that the space is going to be n as well. And this is pretty powerful stuff. by memorizing our fib function, we brought it down from an exponential time complexity to just a linear time complexity. Pretty cool stuff, right? All right, I think we're ready to graduate from Fibonacci. And what we'll do is work on a more involved problem, this one has more of a narrative to it. So say that you're a traveler on a two dimensional grid, you begin in the top left corner, and your goal is to travel to the bottom right corner of that grid. And the rule is you can only move down or to the right, that means you can't move up or to the left, and you definitely can't move to AGL. That being said, In how many ways can you travel to the goal? That is how many ways can you travel to the bottom rate if you had a grid of dimensions, m by n. So the first thing I recognize here is looks like the grid, maybe a rectangle, not necessarily a square, right. And overall, what we want to do is implement a function that calculates this that is our function is to take the dimensions of the grid. And so before we hop into sketching the strategy for this one, I think it's really important that we make sure that we actually understand what this question is asking. So let's say they asked us to calculate grid traveller of two comma three, that means they're asking us in how many ways can you travel from the top left to the bottom right, of a two by three grid? I will tell you right now that the answer here should be three. Right? So there are three ways to do that. And in particular, you can imagine that we had, you know, a two by three grid, so that means two rows, as well as three columns. And we start in the top left, and our end goal is in the bottom right. And so why do we say that there are three ways to get from the top left to the bottom right? Well, they told us in the problem that we can only either take a right move or a down To move, so one of the ways would be going right right down, right, that would bring us from the start to the end. And it would kind of look like this. In a similar way, another path we can take is doing a write down, right? It would kind of look like this. And the only other way would be going down, right, right, which would look like this path. And that's why we say there are only three unique ways to travel not through a two by three grid. All right, so that's just one example of how we might call a grid traveler, I think what's really important to notice is try to also frame the problem as if you were given some relatively small inputs, but I think they're really important case to think about is, what happens if they give us like, basically, the smallest valid grid we can have that is a one by one grid. This one is kind of trivially simple, right? A one by one grid only has one unique way to travel from the start to the end. And it's kind of concise in that if you had a one by one grid, there's really only one position, right? So that means that the start is really the same as the end, and kind of you already have the problem solved out the gate, because to travel from the start to the end, you kind of just do nothing, right? So the logic here is in a one by one grid, there's only one way to travel from the start to the end, you're kind of already there. So you don't need to take any moves. Something else we might think about is what happens when one of our dimensions is zero. So let's say someone asked us to calculate grid traveller of zero comma one, this is a kind of strange question to ask because for one, if there are zero rows and one column, that kind of means that the grid is empty. So I would consider that as there being zero waste to travel through that grid because the grid is sort of invalid in a way. In a similar way, if I asked you for traveling through a one by zero grid, there's still no grid to be found here. Right? So it should also return zero. And likewise, when either of the dimensions is zero, the answer should just be zero, right? If one of your dimensions is empty, then there really is no grid. So maybe you're catching on into why I brought up those trivially small grid examples, right? Those kind of sound like base cases, which we can use to reconstruct a larger answer. But let's stay grounded and look at another grid. So let's say I asked you to calculate grid traveler of three comma three, right? So I want the answer for a three by three grid. Well, how can we reason that one out? At this point, what I'm looking to do is sort of frame the problem in a way where I can decrease the problem size, usually by mutating the arguments to my function call. So let's imagine this three by three grid, as always, I want to move from the top left to the bottom, right. So I begin at this position, let's say, and so I know that overall, I have this top level problem of saying, how many ways can I travel through this entire three by three grid, let's say I took one of the moves, right, I can move either right or downward. Let's say I made the decision to move downward. Well, if I move downward, then I would appear here now, I know in the future, I can only move to the right or downward. So now it's as if my playable area has been reduced to just this shaded region. And if I look at what I've actually done to this problem, I'm still sort of in the top left corner of some grid, where now I'm really trying to answer the question, hey, and how many ways can I travel through this two by three grid. And this is a really important way that we're shrinking the problem size, right, I had a three by three grid initially. Now I have a two by three grid. And this is really like the relative grid from my current position, we'll say, spanning all the way to the end position. So if you look at this coordinate of like two comma three, technically, that is like not the coordinate of the person within the larger grid. This is really the size of the rectangle that the person is trying to cover now. Right. So in a similar way, let's say that, hey, I want it to move to the right now. Well, that would also shrink the grid along a different dimension. So I'd appear over here. And now I have a two by two grid that I'm trying to solve. And if I keep following this pattern, we're gonna keep shrinking on the problem size over here. Now, I'm asking for a grid traveller of one by two. And finally, if I take a right move over here, I'm asking for grid traveller of one by one, which I know is one of those sort of base case scenarios that I had previously. So that's going to be really useful when we actually implement this in some code. And so the key insight here is when we make a move through the grid, that is when we go right or downward, are basically shrinking the effective size of the playable area, right, our grid gets smaller along one of its dimensions. Awesome. So I think now we're able to see that, hey, this grid traveler problem definitely has some overlapping subproblems. Right. Let's say I tried to take more of like a programmatic approach, something that we'll be doing a ton, like, basically, for every problem in this lesson is whenever we have a problem that we kind of know is going to be recursive. The move is to really structure it like a tree, right? I want to really visualize this because I know that if I had a tree structure on pen and paper, I can implement that with just some recursive code using some JavaScript, right. So let's say that I wanted to take more of a tree based visual understanding now a grid traveller of two comma three. So I know that usually the way we do this is we encode nodes of the tree Using the arguments to this function right? Now I have two arguments where I have the number of rows and number of columns. Whereas before for something simple like Fibonacci, I only had a single argument. So in the long run, I know that the answer I should get back is three. So just keep that goal in mind. Let's start to flesh out this diagram. So I'm going to start with that top level, column two comma three. And now I think about how this node can transition to other nodes, right? How does the State of my game actually change? Well, I know there are really two options I can take right? You can either have one child going downward, or another child going the right word, right? Those are the two options based on the gameplay here. And so if I go down, how does that change of the number I put in the node? That is, how does it change the dimensions of the playable grid area? Well, if I go downward, then I'm reducing the number of rows by one. So that means my left child is just one comma three. Now, if I had moved to the right, that would mean I'm reducing the number of columns by one. So my right child is just two comma two. Notice that from parent to child, all I'm doing is reducing a one of the dimensions by one, right, that means you're either going downward or going to the right, and I can just carry over this pattern recursively, right. So let's say I'm fleshing out this node now, five, mat one comma three, then its children would be just 03. And one, two. And a similar way. If I wrote myself at two, two, then I have children of one, two, and two, one. But now let's notice something important. Here. I've kind of spotted a node where I have zero comma three, remember what we trace through in terms of the visual drawing? What is this? No trying to answer? Well, let's notice saying in how many ways can you travel through a zero by three grid, but if you have zero rows, and there's really no grid to be dealt with. And so I think for this note of zero comma three, or really any node that contains zero, we don't need to actually flesh out its children. So instead, I'll work on recursively, drawing out these other nodes. So we'll carry over this pattern. At this point, we've actually hit I think all of our base scenarios, right. So if I look at these notes, that have one comma one that was exactly like the affirmative base case, meaning that I know, trivially I can solve the one by one grid. In other words, these function calls are going to return one, right, in a one by one grid, there's exactly one way to travel from start to end, right, you kind of just do nothing. And on the flip side of that, for all those notes that contain a zero, they're also a base case. But they're kind of like the negative base case, meaning that there is no way to actually travel from start to end. So for all of these notes containing zero, they should return zero to their parent, right? There are zero ways to travel through that grid. At this point, I just sum up these values at the parent node, right? So I just left these and then I add them together. And I forget what these things are saying, right? If I look at like the node one comma two, above, it is one meaning there is one way to travel through a one by two grid, right? You kind of just move rightward. And so I carry this pattern over, I keep adding the children nodes at their parents. And this carries up all the way to the root node. And what do I know, there are three ways to travel through a two by three grid. So although there's like a narrative, and there's some, you know, cute story behind this problem, it's really just a spin off of Fibonacci. And that's going to be the case with many of these dynamic programming problems. So we've confirmed that there are three ways to travel through a two by three grid. And there's also some other information encoded in this tree. I know that whenever I took like a left hand edge in this tree that I presented the choice of going downward. And whenever I took a right hand edge in this tree, that represented the decision of moving rightward. And so if I have that pattern in mind, that not only have I been able to count the number of ways that I can win the game, but I also know exactly which combination of moves lead to a solution. One of the ways to win this game is to go down, right, right. Another way is to go right down, right, and the final ways to go right right down. So we can glean a lot of information just from the same tree structure. Cool. So I think that's enough drawing for now let's go ahead and implement this in some code. Alright, programmers, here we are back in my text editor. Let's go ahead and implement this grid traveler function. So you want to go with that recursive strategy. So I'll start by just laying down my base cases, I already refer to the fact that hey, one of our base cases is we have a one by one grid, just go ahead and return to zero, right? So that's really easy to do. So I'll check Hey, if m is one, and n is one, then the answer is just trivially one. Right? Then along with that, I also have the base case when I had like an invalid grid. That means if either of my dimensions is zero, so I'm using an order here, right? If either dimension is zero, then your grid is empty, which means there's definitely no way to travel from top left to bottom right of that grid. Cool. Then my recursive scenario is very straightforward. All I need to do is get the sum between me going downward and me going rightward, right. So if I go Down number n is the number of rows, if I go down that I'm decreasing the number of rows affected rows, that is by one, but I keep the same number of columns. And then in a very symmetric way, if I move to the right, that means I still have the same number of rows. So I keep him, but I decrease n, right, I have one less column. So this code is looking pretty sharp, really just some reminiscent Fibonacci style code. Let's go ahead and give this a run. So I'm running a few examples over here. And these are the expected results. In some comments, we'll go ahead and bang this one out, grid traveller. Cool. So I get the first four results. 1336 looks like it's working just like a charm. However, for this last example of an 18 by 18 grid, looks like my programs hang right now, it's actually a little too slow to calculate an 18 by 18 grid, so you probably see where this one's heading. Let's go ahead back to the drawing board and really understand a why this code is fairly slow. Alright, so here we are back in the drawing board, because it looks like we hit a wall when it came to our recursive implementation of grid traveller. That being said, I think implementing the brute force solution here is actually a really logical first step. And now we can just focus in on where there is room for improvement. So our first question is, you know, what is the actual time complexity of this implementation? Well, I know when I call a grid traveller of two, three, the full tree would explore is this right. And like we said, in our Fibonacci, to understand the time complexity here, it's really about to understand the number of function calls we make, which is really the number of nodes in this tree. And of course, I want to generalize my understanding for any large inputs. So I know that this tree sort of looks, you know binary in a way, meaning that a node can branch to up to two children, which makes sense, because from one position of the grid, I have two choices to make, right, I either go down or I go to the right. But that being said, I need to realize what the height of this tree is. And this one's pretty interesting, because out the gate, we actually have two input arguments to our function, I'm given two numbers, m and n. And since this function contains two inputs, it shouldn't be the case that our final complexity analysis actually describes it in terms of those two number inputs. So what I'll do is try to recognize the height of this tree. And so I'll just choose some path from the root to the leaf. And kind of in the recursive sense, that means I take some path from my top level call all the way down to the base case, and preferably the far this base case. So I know my base cases are either when one of my arguments turns to zero, or when both of my arguments turn to one. And I think in general, the farther base case would be a scenario where both of my arguments are one. So here I have a path highlighted that ends in a one one. And in general, I know from one node to the next, what I do is either decrease the M by one, or I decrease the n by one, it's never the case that I can decrease both of the numbers by one, because that would kind of entail that you're moving diagonally across the grid, which is not allowed in the gameplay. So if I can either subtract one from em, or subtract one from n, and overall a path from my initial input down to a space, like one one is going to have a distance of n plus m, we basically had to subtract, you know, n and m from your initial node to reach that bottom level of one comma one, that kind of tells me the number of levels in this tree, right using the same arguments as before, it's still the case that most of this tree is going to be binary, meaning that a node branches to two children. And so I have that two times two times two pattern for the number of levels in this tree are saying that they're n plus m levels. So really, the time complexity is two to the n plus m power, right. So still some sort of exponential, that was just sort of the multi variable exponential, because I have two variables of m and n. And likewise, for the space complexity, you know, the space complexity, in general for recursive code is just going to be the height of the tree, the height of the tree gives us the maximum stack depth of the recursion. In this case, it'll just be the number of levels still, which is n plus m. Cool. So obviously, the limiting factor here seems to be that time complexity of two to the n plus m time. Alright, so you probably anticipated the time complexity of this one being exponential. That being said, I think it's really important to still have a nice argument to you know, say what the time complexity is. That way, you can get a massive buy in from your interviewer right. So don't skip the steps of trying to draw it out and actually defend your reason for why the time complexity is exponential. But let's try to improve this now. Is there any room to actually improve the runtime of this function? Well, here's that same drawing, right? Just sort of cleaned up. So I have grid traveller of two comma three, and this would be the full tree. We sort of already know now that I have this recursive tree structure. What I could possibly do is notice any duplicate work, right? Is there some work I can prevent myself from doing here. So take a moment and really remind yourself of the actions we took in the Fibonacci problem. And see if there's any patterns in this tree, probably from the gate, you notice that? Oh, I have some duplicate sub trees right? Have one comma two. So in these the two highlighted sub trees in blue, that's sort of asking the question, hey, what is the total number of ways I can travel through a one by two grid? That being said, I think there's even more sub trees that correspond to that particular problem. What if you look at this far right subtree of two comma one, if you really, really think about it, asking the number of ways to travel through a one by two grid is the same as the number of ways to travel through a two by one grid, you're just flipping the rows and the columns, but the total number of ways should be exactly the same. So that's actually a pretty interesting way you can optimize this solution. So I can still memorize it. That is I have these duplicate sub problems. But possibly a really cool insight we can make in this problem is, you can also sort of flip of the arguments. In other words, if someone asked you, for grid traveler have a comma b, then that would be the same answer for a grid traveler of B, comma a, write the order of the arguments technically doesn't matter here, until we can totally encode that in our memo object. That being said, punchline here is, we better memorize this one. So I think let's hop to it. All right, here we are back in my text editor. And hopefully, that diagram of how we can improve the time complexity for this one, make some sense, let me go ahead and kill this program still running. And my computer's fan is actually going crazy. Now it's running the entire time. Let's go ahead and memorize this one to the beauty of you know, solving these dynamic programming like problems is if we have the initial strategy of implementing, like the brute force recursion, and we wrote like a very well formed recursion, meaning that I actually use return values, and I reconstruct the sub solutions all the way up to the tippy top. Because I have a really nice recursion here. memoization is a very like formulaic pattern. So it's going to be almost the same strategy, we did have Fibonacci, even other problems that we do in this lesson. So we're going to bake in my default mental as an empty object. And then along with that, I'm going to add my memo checking logic. So I know we're here in general, and to check, hey, are the arguments in the memo, right, I need to key into my memory object using all of the arguments. Now I have two arguments. And since both of the arguments combined, sort of dictate the output, I need a key that sort of contains both of those in JavaScript keys are either strings or symbols. So for us right now, really, strings is the most relevant one. So what I'll do is I'll concatenate both of these integers together. That way, I have a string which I can key into the object with. And we'll all definitely want to do is also maybe separate them with a comma, let's say, so I'll say like key equals m plus a comma, plus, and, and the reason that you probably want like some sort of separator between these two arguments in your key is to make sure the numbers don't get misinterpreted. So I've actually ran into this issue in the past, can you imagine like, if I had a scenario where, let's say m, was 42. And then we'll say n was a number three. So what I don't want to do is just make my key, something like four to three, because this isn't going to uniquely identify the exact input argument, right? Because if this was just my key, then I guess I have the same key for a totally separate set of arguments. If I gave you four and 23, right, both of these combinations of very different arguments would lead to the same key, they would collide at the same key. So instead, I'll prefer to put a comma between them right now I know without a doubt that this key corresponds to four comma 23. Right. So that's why I put a separator between them. And depending on the language that you know, you're choosing for interviews, you can find a very similar construct. Cool. So now I'll go ahead and check, hey, if my key is in demo, then I have the Result Cache already. So I could just return net cash value. So return memo at key. And then what I want to do is look for my old return value, right? So here's where I actually do the manual recursive calculation. Before I return that I want to store it in my memo, using the same key and then return that thing, I just put in the memo just completing the old return logic. Cool. So you've seen this pattern before, I just want to emphasize a few things. Now it's the second time we're seeing it. memoization, for me, at least, is a very, very formulaic thing, right? So I always take the exact expression that I returned previously, and I put that entire thing in the memo object, right? Notice that my key encodes the arguments for this function, right? Mnn. Something that I've seen students do in the past is do like very heavy handed logic, where they try to, you know, check if the child call is in the memo. In other words, don't write any pre emptive logic where you check Hey, if you know m minus one And so if you like concatenate those two things together, don't check if like your child's key is in the memo, right? So imagine like, this was my key right now. Right? don't check if that is in a memo. When you write logic like that, it ends up being very, very duplicate and a little harder to debug, right? Instead of writing your function as if it's, you know, reasoning for its children calls, you know that once you actually evaluate those child calls, they're going to cache themselves, right, they're eventually going to check this if statement anyway, alright, so don't do any like the look before you leap logic, just make the recursive call. That way, every recursive call doesn't like self service, right? So I prefer this way implemented. And with that, let's go ahead and run this code. So I'll give this a shot grid tremor, I still have that 18 by 18 grid, which took quite long last time didn't actually finish while I was waiting. Looks like I'm still hanging here, because I'm actually missing a little bit in my code. So take a moment, see if you can spot it, I forgot one really important thing. So I have the logic of checking if my key exists in the memo. And I also have a logic of storing something in my memo if the key doesn't exist, but what I failed to do was actually passed down the memo object to all of the recursive calls, I want to pass it down over here. Remember, the trick is only top level when someone calls like two comma three for grid traveller, then we're going to initialize a brand new empty object, which will be shared for the rest of the recursion, because it's passed down by reference at this point. So that's a common mistake. Let's go ahead and run this now. Nice. And there we have it. Look how quick our last execution was over here now is the expected answer for an 18 by 18. grid. Cool. So here's what we'll do, it's evident that we definitely set up the execution of this one, let's head back to the drawing board to wrap up this problem and really see how we cut down the complexity here. All right, so it looks like we definitely improved the runtime of our function. But I want to really understand you know, what the big O complexity of our improved function is now. So sort of a way we constructively argue for what the new time complexity is, is to think about where the values of the nodes that will actually have to travel through. So let's say I looked at this example, I wanted to find the number of voice a traveled through a four by three grid, I know in general, there would be a top level node, of course, four comma three. But in general, that refers to m n, right? So I'm trying to think in a very general way right now. And so I know when it comes to the other nodes of this tree, they're all going to sort of at most be four comma three, but then probably be less than that, right? They have like a range of values for the nodes. It's not as if to solve grid traveler, four, three, I would have to solve 530, right, that would be a larger grid. That doesn't make sense here, right? I'm shrinking the subproblems, only in this rendition of grid traveler. And so if I think about some possible values here, I know that if M is for top level, then all of the values for m in the rest of the tree would be from zero all the way up to and including four, right, it's never going to be bigger than four. In a similar way, since n is three here, the only possible values for n in the rest of my tree are zero through three. Right? So roughly Mele, we're a little off by one here, because I have to include zero, because we know that that's a base case, in our edition of this problem. That being said, there are roughly n choices for the first number, node, and n choices for the second number in the node. And along with that, we know that we're not gonna have to travel through many duplicate sub trees, because we memorize them away. And so I think what I can say here is the total number of nodes you can possibly have is m times n, and I'll be the number of like distinct nodes, right? Because I have m choices for the first number in the node and n choices for the second number. I know that I'm going to really minimize any duplicate exploration through the memo object. So the really the implication here is we started out with our brute force recursive implementation, which looked like it was exponential in the time, right, it was two to the n plus m time. And then by memorizing this function, we were able to bring it down to n m times n, time complexity, which is much faster. Notice that the space complexity stays here, which is really fine, because n plus m is some sort of a multi linear function. Cool. So there, we have our nice optimal solution for this grid traveler problem. So key thing I want you to take away from this one is, although the initial narrative and the problem made it seem, you know, pretty specific, and pretty different from a previous dynamic programming problem, like Fibonacci, was really the same sort of story, the most important thing that we're going to sort of leverage throughout in this lesson that we leverage twice already is try to think about your recursive functions in terms of a tree, right, I get the most information out of the tree. And then from there, I can use that tree to not only implement a brute force, but to also recognize, hey, where can I optimize this brute force, that way you can reach for the optimal solution. Alright, so we've gone over two different problems together, and hopefully we're starting to notice how we tackled them in similar ways. I think it's about Tammy gave herself some guidelines for solving dynamic programming problems using a memoization strategy. So we'll call this our memorization recipe. And there are three different ways you can go about learning this topic of memorization. And you might have different recommendations. This is just my particular recommendation. So I think the most important thing to establish if you want to, you know, solve some dynamic programming problem using memoization, is to stick to to like high level steps, we definitely need to at first just have a solution that's recursive. So just make it work could be slow, that's okay. And after that, we make it efficient. I think this is where a lot of students sort of try to take on too much at once. They try to just solve it quickly all in one go. Right? To me, it should be a very separate process, right? First, I look for correctness in my solution. And then once I have correctness, then I look for efficiency in my solution. So when we go through step one, if I just want to get a working solution, then I have to start visualizing these problems as trees in dynamic programming problems, the gist of them are that we have some large problem that I can break down into smaller instances of the same problem. So when I visualize it as a tree, what I'm looking to do is figure out all right, in the nodes of the tree, that should represent a problem. And when I draw an edge between nodes, that should be shrinking the size of the problem. And depending on you know how your problem is stated, you'll have to figure out that logic, right? In the case of our Fibonacci, it was as simple as we know, we can decrement our values of n. But in the case of our grid traveler, what we had to do was travel rightward or downward. And once we do that, you want to implement that tree using recursion. What's great about a tree is it's already a recursive structure. Right? So how do you start to translate that kind of tree visualization to some recursive code? Well, you think about the leaves of that tree as your base case, right? Lately, for us, it's been about some small numbers, right? So like a grid of size one, or it could also be in the case of Fibonacci, just our initial seed values of n equals one, and n equals two. Now, once you have that baseline recursion, that's going to be your brute force solution. And so you'll want to test it. To me this testing step is really important, right? So if you pass inputs into your brute force recursion, it should give correct answers. Although possibly for large inputs, it may take a long time, right? To me, there's a big difference between code that is slow, and code that is wrong, right. So here, we should give back valid results, although maybe our code is a little slow. Once we have our working brute force solution, making it efficient using memorization is a very, very canned scenario. All we do is start by adding a memo object into the mix. So this memo object needs to have keys which represent arguments to our function. And the values of that object represent the return values for those function calls. We know that in our functions, a unique set of arguments should give me a particular result. So we're just having that sort of mapping inside of an object, you need to make sure that this object is shared among all of your recursive calls. One way you can do that is to pass them along as arguments. And lately, I've been doing that by giving myself a default empty object at the top of each of my recursive calls right through my top level call. And once we do that, we need to add a new base case into our code. So I'm not going to remove any of the old base cases, from my brute force solution to me, I'm just adding a new base case that captures the memo. In other words, if my arguments are in the memo object as a key, then I'll return the stored value, I refer to that as like my memo fetching logic, right? Looking up some stored value in my memo. And beyond that, the only thing we need to do is implement our memo storing logic. And it's as simple as going to exactly where we had our return values in our function. And then we just make sure that we add those return values into our memo object before returning, right, so I always look to the exact return expression, and just write some code around it, right storing that result into another mental object before I return it, right? Step two is actually very, very easy to implement, meaning it's very easy to memorize a brute force solution, it's really coming up with the brute force in the first place, that kind of feels more difficult. So as you're learning and practicing memorization for these dynamic programming problems, I highly, highly recommend you stay very methodical, and follow these steps, right? Don't try to efficiently implement an algorithm from the get go get a brute force working solution with recursion, and then implement it using memoization. Afterwards, right. And as you get more practice with this technique, soon, you'll be able to do everything all in one swoop, but I don't recommend that until you've definitely finished this course. So we'll be sure to follow these rules for falling problems. Alright, so I think it's tempting increase a difficulty and we're going to know Their dynamic programming problem. So let's work on this can some function, what I need to do here is take in a target some as an argument, as well as an array of numbers by function needs to return a Boolean. So true or false, indicating whether or not it is possible to generate the target some using some numbers from the array. And along with that, we have some constraints here, we can totally use an element of the array as many times as we want. And we can also assume that all input numbers, so the target sum as well as the numbers of the array are non negative. So let's try to understand what this question is asking. Let's say I gave you this example case. So it looks like our targets, I'm a seven, and the array of numbers is 534, and seven, here, the response should be true because it is possible to generate seven, by adding some amount of numbers from the array, one way you can generate seven is by just doing three plus four, another way would actually be to just take the loan seven, because seven is actually a member of the array. So it's definitely possible to generate seven using, you know, some amount of numbers from the array. So that's why we return true. Let's see, I gave you another example. Let's say I gave you a target sum of seven again, but I gave you a different array of just two and four, this is actually going to return false, because there is no possible way that combinations of two and four can actually sum to seven. Cool. So that's really what the question is asking. Now let's try to think about how we can frame this problem of recursively. Right? Hopefully, you've already gathered in your mind. If we have a smaller amount of targets some, then they'll tend to be a smaller, easier problem than a larger number for targets. All right, so let's start to think about the recursive structure. For the first example, we know in the long run, we should be able to derive the answer true from the string that we make. So like, we always say, we should encode the arguments to our function into the nodes of our drawing. That being said, since in the problem, they told us that we can reuse an element of the numbers array as many times as we need, I'm just going to omit that from the node drunk, because basically, every node every function call is going to receive the same array. So I'll just list the target sum in every node. So I start with seven, and I have to think about how I can transition to other nodes, right? How can I shrink the size of this problem? Well, I know that I only have, you know, four options I can take right have an option for every element of the array. So basically, if I'm at this seven node top level, I can branch to some children, and sort of the rule for my transition is you can either take a five, take a three, take a four, take a seven. And if I actually take you know those elements as a choice, they are going to decrease my current target sum. In other words, seven minus five is two, seven minus three is four, seven minus four is three. And of course, seven minus seven is zero. So notice, we have a very particular rule from traveling from a parent node to some child, I can just carry over this pattern. However, we have to watch out right, so let's say I tried to flesh out this note where my target sum is two, if I look at the options I have, right, I still have the options of 534, and seven, however, none of them are really compatible with this target sum of two, right, what I don't want to do is take any of these choices, because that would kind of give me a negative target sum. So I can't really flesh out this note two anymore. That is there are no valid options for this node. However, some of these other nodes like this for to have valid options. So what I can do is take a three or take a four, and of course, I get a one and zero respectively. And I'll also do this for the three over here, right? For this three node, I can only take a minus three as a choice. Cool. If I look at all of the nodes that I have, now, I like the leaf level, it looks like they all sort of bottom out at a base case, that is there is no further choices we can take. And I also noticed that some of my nodes have a zero in them, if I look at the notes that have a zero in them, they actually are a really nice base case, because I basically have found that I can generate to the original target sum right away, you can kind of understand the base case, when the target sum is zero is you can always generate a sum of zero by taking no elements from the array, right? So these zero nodes are trivially solved. And to me, they should return a true backup to their parent, right. And it's sort of on the flip side for all of my nodes that are not zero. And they also can't break down into any further nodes. Those return false, right? Because they kind of have a leftover. And I know there are no possible options I can take to reduce that further. So all of these other nodes should return false to their parent. And remember what this question is asking, right? It's really asking, Hey, is it possible at all to generate the original target sum? And so the logic is when these values these Boolean values return to their parent, the parent should just check if at least one of them is true. And if at least one of them is true, then that parent should also return true. And if I look at this for note, have a tree above it, and that sort of answers the question, Hey, can I generate a sum of four using elements of the array and you totally can because if you look at the elements of the array, there is exactly one for so I can just take that into my son. So I'll be sure to bubble up, you know all these Boolean values to their parent. And again at the parent will just make sure that at least one of the values that gets back is true. And so if at least one of them is true, then the parent itself will also return true. So very reminiscent of some like Fibonacci bubbling up pattern as well as our grid traveller, except now we're kind of adapting this for some boolean data, but it's really the same structural understanding I have. Cool. So that was an example where we said true, right, there is a way to generate the sum. Let's look at another example. Right? How do we know the flip side. So if we have this example, right, seven, and an array of two, four, in the long run, that's gonna return false, right, there's no possible way you can generate that target sum of seven. So the tree for this example would look like this. So this is a full tree. Notice that all of the leaves get as low as one, but they can't be broken down any further. Like we just said, in our last sketch all of these ones, since they can't be broken down any further, and they haven't reached zero, they're going to return a false up to the parent. And I know that if I bubble up all these false values, of course, a top level call will also return false. Right? So it looks like a key insight we have for this problem is, if we find at least, you know, one base case that returns true, I know that I can sort of stop early and just return true all the way up to my parent, because they're not really asking like, how many ways can you generate the target sum? They're just asking, Hey, yes or no, can you generate the target sum at all. So that's gonna be a really nice way to implement this code. I think we're ready to jump right into the code. So let's do it. Okay, so let's go ahead and implement this can sum function. So since we're going to solve it recursively, I think a good starting place, as always, is to maybe handle some of the base cases. So when we drew up the tree, I noticed that one of our base cases was when the target sum reaches the value zero, right? If our target sum is zero, then we have like, basically trivially solved the problem, because you can definitely always generate the target sum of zero by taking no numbers from the array, right? So I'm going to return true. If ever I reach a zero, then Apart from that, I think let's go ahead and work on the recursive scenario. So I know that I need to establish some logic where I make a recursive call or a branch for every element of the numbers array. So what I'll do is I'll iterate through that array of numbers. So I'll use some for let of syntax we'll say let num of the numbers, right. So if you're unfamiliar with the syntax, all it does is iterate through every element of the array. So for example, let's say I just called the first example for can sum of seven, and an array of two, three, if I just console dot log, the num, I'm just going to see the elements printed out of two, three, right, so let's give this a quick spot check. Nice, so just iterating through every number of the array, no tricks here. But now that I have that number, I need the branching logic. So remember, the logic we use for transitioning from one node of the tree to the next, what we did was subtract our current choice of number from the target sum, and they basically gave us like the new target sum. So I'm going to express that and maybe some variable, so I'll say, Alright, I'm going to generate the remainder by calculating the difference between the target sum and the number, right, so I'm just subtracting the number from my target sum, that gives me my new remainder, which becomes a target sum. Right? So at this point, I think I need to call recursively on can some of our pass in this remaining quantity? It doesn't need a second argument, still, I'm still gonna pass in the same exact numbers array. So I'll still pass in numbers unchanged. I think that you know, the fact that we pass in the same exact numbers, right, it's pretty consistent with the way they stated the problem, because we can totally reuse the numbers of the array as many times as we like. Cool. So now that I'm using our function, again, I'm making the recursive call, I want to think about what this call will return. I know at any point in time, my can sum function is going to return Boolean. And what's great about boolean data is there's only two possibilities, right, either true or false. So I think, based on what we said about the tree was, if this call, if that returns true, then I can just ultimately respond with a true right now. So I'll write it like this. So this is saying, All right, if we figure out that it is possible to generate the remainder now using numbers of the array, then I can return true for this larger problem of target some, right? So if I find at least one way that works out, then I'm going to do an early return true. And the really important pattern here is we don't want to write the LS and then return false. Instead, we'll want to return false after the for loop. And so here's the reason why you want to return false after the for loop. I only know that after I attempted you know all possibilities and I found that none of them worked out Can I actually say that it is impossible to generate the target some Right, so I need to make sure that this for loop tries all possibilities of number. Before I can say false, the target sum cannot be generated. That being said, there's one thing we should add to our code. So if you look at line six, all right, I'm subtracting our choice of num from the target sum. And that means that sometimes the remainder might become negative. So remembering our tree structure, we made sure to sort of return a false whenever we had a sum note, I didn't do any branching. To kind of account for that I can kind of bake that into another base case. But this time, make sure it doesn't return true. So I'll say, you know, if my target sum is negative, if it's less than zero, then you've gone too far. And you can just return false. Here, it's safe to just automatically return false if your target sum hits something less than zero, because you've gone too far. And there's really nothing else you can add from the numbers array to ever fix that negative target sum. Remember, they told us in the problem, that your numbers are always going to be provided as positive numbers or just zero, right? So let's go ahead and give this a go. So I'll try all of these examples. And we'll see what we get. So I expect a true true false, true, false. Cool, so looks like a few of them are working right, it looks like the first four are working totally fine. But looks like on that last example, my program still running. And if you notice, I chose a pretty large value for em. So kind of like we expect this solution has correctness. But it possibly lacks some efficiency, it looks like it just finished, the definitely took way too long. And so it's a very interesting example of a slow input to can some, because for one, the length of the array is pretty sure I only gave two elements here. But it seems like this number as a target sum really affected the runtime. So we'll do, let's head back to the drawing board, and talk about the complexity of this baseline solution. Alright, so we implemented the brute force for our khamsum. But obviously, now we want to make it a little faster. But before we kind of just jump into memorizing the solution, let's at least describe the complexity of our current brute force. So let's try to visualize this example. So I have a target sum of eight. And my choices for my numbers are two, three and five. In the long run, they should return true, the visual for the full tree would look something like this, notice that it's fairly large tree of forbs and relatively small inputs, right, I only have a target sum of eight, and only three choices. So let's try to describe the complexity of this, I'll sort of generalize the shape of it, get rid of those numbers. So this is the overall shape of the tree. And I know I want to describe my complexity in terms of the input to my function, this function has two inputs to the right, I'll say that M is the size of the target sum, and n is the length of the array I'm given, I know that both of these arguments definitely have an effect on the dimensions of this tree. So like we did in all of our other examples, I'll start by maybe analyzing the height of this tree, that is a what would be the maximal distance from the top level call to a base case, or in the structure of the tree, what is the maximal distance from the root of the tree to the farthest leaf. So if you sort of imagine that, we have m as the root of the tree, imagine that along the left hand path, we just did a minus one all the way down, right? So we kept taking a minus one, right? In the worst case, maybe there's a minus one present in our a numbers array. In the worst case, the distance from the root to a base case would be exactly m, because you have to subtract one m times, right. So I can basically say that the height of this tree is M. And it kind of like we said in our other problems, that means that the number of levels is m right. So now that I've identified the height of this tree, the move is now to identify the branching factor. That is, how does the number of nodes change from one level to the next. Remember that initially, we described this particular tree example is having numbers array length of three. And you'll notice that the maximum branching factor is exactly three or in general, N, right? because n is the length of the array. So for example, if I had three numbers in the array, then a node would have at most three children, right? Cuz you have three options to take. We've seen this pattern before I have n levels, and from one level to the next, I would multiply the number of nodes by n, right? This is the same thing as saying, Hey, we take n and multiply it by itself, m times. And this would definitely give us an exponential complexity, right in particular, and to the M time complexity, like we always say another great thing about this type of diagram is we can also derive the space complexity, right? So what would be the space used by the call stack, it would really just be the height of the tree, which we already described as M. So overall, our brute force is looking at an end to the M time complexity, and M space complexity. So now that we actually have you know those concrete numbers for our complexity, let's go ahead and focus on how you can truly improve this So here's again, the drawing for this particular example. It's a fairly large one. And typically, you know, when you're trying to notice where there is room to be optimized, you might have to give yourself a sufficiently large example to see these scenarios. So in this example, I do have overlapping subproblems. And in the context of my tree, that means I have duplicate sub trees. So I can look at possibly this subtree rooted at three, notice that the root of that subtree is trying to answer the question, Hey, can I generate a target sum of three using the array? And of course, once you find that answer, that answer is never going to change for your target sum of three, right? So I know that all three of these sub trees are trying to ask the same problem. And so what I'll do is I'll just cache those results in my memo object like we always do. Let's go ahead and work on that. All right, welcome back to my code editor. And same stuff, different days. Let's go ahead and minimalize. This can sum function, right. Once we've established the brute force through the recursion, then memorization is pretty formulaic. Right? Alright. So we'll start by just baking in our memo object like we usually do. So if someone calls our function without a third argument that is like a top level call, we'll be sure to give them a new object. And I'll, before I forget, be sure to pass down this memo object, I'm going to make sure that every top level call and it's recursive tree shares the same memo object. But now I need to figure out what what should I use to key into the memo object. So here I have two arguments of my original function are I have targets summon numbers, what I want to do is try to notice, you know, which of these arguments is actually going to directly impact the return value. So I know that through the recursive calls like this call over here, the numbers argument doesn't change. And so if it doesn't change, then right now, it doesn't really affect other return value. So I'd be okay to just use the target sum as the key into this memo. So we'll go ahead and do that. So I'll check if my target sum is in the memo, that I've seen that sub problem before, so I can just return the stored value. Cool. And now I have my, you know, memo fetching logic. But now I need to actually store things in the memo. And the trick is, what I want to do is look at all of the return values that were not base cases, and I need to now store them in my memo. So I have to return values here, right, these two lines, lines, 10 and 14. And so I need to store data into the memo for both of those lines, it's as simple as quite literally going into your memo using your key, so target some, and then assigning the value we just returned. So I'll just write it like this. And this is going to be sort of a hard and fast rule, you can always use our for a memorizing a brute force recursive function, right. So I'm just going to take exactly the lines or the expressions that I returned in the recursive scenarios. And now just store them in the memo. Cool. So let's go ahead and give this a shot, this code is looking pretty good. And remember before this last call with an input of target, some 300 took notice will be long. But I think now when I give this a shot, right, it finishes really quick. So this is going to be a nice optimized solution for cancer. Really, most of the work of this problem was done in the brute force. And then afterwards, it's really just a minor adjustment to make it efficient. So to wrap up this problem, let's go ahead back to the drawing board and talk about the improved complexity. So we definitely memorize the heck out of that code. But let's recap by just understanding what the new complexity is. So again, we're saying that M is the target sum, and n is the length of the array. Initially, we said that our brute force solution had an end to the M time complexity, which is exponential. And actually, once we memorized it, we really cut down on that complexity, we brought it down to n m times n type complexity. And so here we say that the memo is complexity is now m times n, because of the memo object, right? We know that the value of the nodes in the tree are just going to be values up to m or their m different possible values we can have in a node. However, it now since we are able to cache values or cache results inside of the memo object, I'm never gonna have to re explore a subtree before M. That being said, I select the branch and times for each of those nodes, right. So overall, I have m times n nodes. So hopefully that can some problem made some sense, because we want to do now is actually carryover a lot of that knowledge to solve this new Howsam problem. So this problem is very similar, it still asks us to take in some target some an array of numbers, but this time around, we want to do is return an array containing a combination that adds up to exactly the target sum. And if there's not any combination that actually leads to the target some, then I should just return null. Along with that, if there are many possible combinations that can reach the target some that that can return any of those. So you probably Already recognizing that this house some problem is very similar in logical structure to the can some problem right? instead of returning a Boolean instead now I want to return exactly the combination of elements of the numbers array that leads to my target sum, so it's a little more involved. That being said, let's take a look at some examples to make sure we're on the same page. So let's say someone asked us to calculate how some and our target was seven, and our array of numbers are 534, and seven. So there are actually a few different combinations that can give you your target sum of seven. One way would be to take three plus four, that's one possible answer, another possible answer would just be returning an array of seven. So no matter which combination array you return, it'll be considered correct in either scenario. Let's say I gave you another example, where your target sum was eight, and your choice of numbers were two, three and five. One possible solution for a combination is two plus two plus two plus two, right? So I returned that in an array, another combination would be just three plus five, notice that no matter what we're always looking to get back in array, if it is possible that our target sum can be generated. But let's look at the flip side. Let's say that we were given this input, so I have a target sum of seven, and my array of numbers is just two and four, the first thing we notice is it is not possible to actually generate the target sum of seven. Like they said, the problem in this scenario, what you want to do is return Nome sort of symbolize that, hey, it's not possible to generate any combination that leads to the target sum. All right, I think it's important that we think about one more scenario. So let's say that I was given a target sum of zero. And we were given really any array of numbers. In this case, I just gave us one, two, and three, we already know that in our previous problem, we had to return a Boolean result, we use the target sum of zero as a base case. And so what I want to think about now is how is the target sum of zero just trivially solved? Well, if I want to know the combination that sums to zero, and that's really just the empty combination, right. So I think the logical result here is to return an empty array, when your target sum is zero. Remember that an array represents a combination. And if I have an empty array, that means I take no elements into my combination, right? If I summed up all the elements in that empty array, it would indeed have a sum of zero. So that's going to be a really important facet that we need to encode into our logical tree that we draw next, as well as our code, right. So let's take a look at how we can structure the tree and try to take a step toward really understanding how we can implement it in some code. So let's say we're stepping through this particular example of target sum of seven, as well as an array of 534, and seven. So the full tree and really the same tree that we drew last time would look something like this. from the get go, I see that I have some scenarios or some nodes where I reach zero, which means that it's definitely possible to generate the target sum. That being said, How can I get back a valid result, right, so I need to return an array. So you sort of reframe this problem in that, all right, all of these base cases that have a target sum of zero, they are trivially solved because they are combination would just be the empty array. Right. So for now, I'll just kind of trace through how one of these base cases would return. So I know that this particular zero is going to return an empty array. And like we always say, when it returns, it's really returning that information to its parent, right. So this array sort of bubbles up to its parent. And now what I want to do is actually manipulate this return value, I want to add something to it. So I want to really put the number that brought me to that zero in the first place. And that would really be the choice of four. So notice along the edge, I have them labeled with a minus four, meaning that, hey, I took a choice of four. And I want to actually add that choice into the current array. So I'll just really push it into the right, of course, I actually don't need a negative sign that was just for the sake of understanding the math here. But now that my call to how some of four is returning an array of four, that array bubbles up to its parent. And of course, now I need to push that edge that I took, which would be that three, so that three gets pushed to the rear as well. And if I look at what I'm seeing right now, it looks like above the seven, we have an array of four, three, which makes sense, because you can totally generate a target sum of seven by doing four plus three, awesome. So four comma three would be a valid answer in this particular problem. But like they said, you could return really any valid combination if one exists. So let's say we retrace through this far right base case, we know that this zero is going to return an empty array, that empty array will be returned to the parent. But we also have to add the value according to the edge, right? So I would press seven in this array. And that makes sense because I could generate the target sum of seven by just summing up a loan seven. So that's still good to go. So I'm feeling pretty good about how we can return a valid combination if one exists about let's say, there are some options that we take that don't work out. So for example, let's say we explored this node first, right? This was the first base case we had and logically in the space of our code, it really would be right. So I know that this node can't really branch out any further. So it's sort of a dead end. This is a node that should work Turn no meaning that, hey, there is no way I can generate a target sum of two using elements in the array. Or if you look at the elements of the array, they're all too big, right? So I know that this base case is going to return null. But along with that, if you look at the next base case, we would hit it would be this one, which also returns Now, if you look at the next base case, to the right, that's actually an affirmative one that should return an empty array. So it's kind of reasoned out how these return values should be considered at their parents. So I know that both of these values, but to the left of the node four would return, right, so it kind of bubbles up a little bit. But for the array on the right hand side, I would also have to push the edge that I took, which was a four in this scenario. So now I'm sort of comparing you know, the nollan, this four, or really, I'm comparing all of the branches that I take from a node. And if at least one of the branches gives me back an array that I know that it's possible, right? So basically, in this scenario, the array of four actually wins out over the null, right, I know it's possible to generate a four. And I'll just continue this process, right, now I can return these two values to its parent. So let's I'm considering them at seven. In the same way, I would have to add the edge I took, which would be a three on this array. And then from there, I know the array would always override the null, right? So I could just ultimately return this four comma three. And a really nice pattern in this code is, as soon as we find a winning path through the tree, or that is we find a combination that creates a target sum, we can actually return early because we don't need to really travel through the rest of this tree. We don't need to explore any other options because they're happy with at least one combination. Right? Awesome. So again, to recap, the punchline is for this tree, I have the information for a combination, encoded as a path through edges of this tree, right, so I've looked at this path I have highlighted in yellow, I see that I took a three followed by four. And that eventually led to a zero. So I know that one valid combination would be four comma three. All right, at this point, I'm feeling pretty good. Let's go ahead and work on the code for this now. All right, here we are back in my code editor, let's work on solving the house implementation. So the codes going to be pretty similar to the cancer problem, it's the really the whole point, we're just kind of going to finesse the return date over here, we're gonna have a very similar base case, like we said, in the tree diagram, whenever we have a target sum, that's zero, we have trivially solved the problem, because I could just return an empty array. And a similar way, what we can do is also have a separate base case, if our target sum ever reaches a negative quantity, then I'll just return null. That's because it's never possible to generate a negative target sum, right? Remember, in this problem, the array of numbers is only going to be positive ones. Cool. We ended up writing very similar base cases for our last problem, except now instead of returning true, we return an empty array. And instead of returning false, we return nil. Nice. So let's go ahead and get the branching logic. Right, how do I want to make my recursive calls, I'm going to need to make a recursive call for every element of the numbers array. So I'll say, let num of numbers, we'll just iterate through every number of this array. And I'll go ahead and do the same logic as last time. So I'll subtract the number from my target sum, they'll give me my remainder, right? So remainder equals target sum minus mine number. Cool. So this remainder is now what I want to find the combination for, right? So here, I would make my recursive call. So how some pass in the remainder. And the second argument will stay exactly the same. I'm going to pass along the same exact numbers array. I don't need to remove anything from the numbers, right? Because they tell us in the problem that we can reuse the elements however we see fit, right. Cool. So now I need to really think about what type how some returns. So this problem is interesting, because we basically have two different types, right, we could get back in array of some elements, if it is possible to generate the remainder, or we can get back know, when it's not possible to generate the remainder. I think no matter what I get back, I'm just going to save into a variable. So I'll say console, this is the result for the remainder. I'll call it remainder result. Cool. So I know that in the context of like how I thought about this logic in terms of the tree, I wanted to basically do an early return if I found a valid combination. And so I'll go ahead and check you know, if the remainder result is not No. So the implication if I enter this if statement, that means that it is possible to generate the remainder. And so what I can do is just return early and I can return of basically almost the same remainder is also the same array. However, I need to make sure I include the element that I took recall from the diagram, whenever we had a recursive call that returned in array, the parent had to also add the number that it took transition to that recursive call in the first place, right. So in the context of the tree, I had to put the labels of the edges into the array here. means I have to put the num into this array. So some syntax I can use for that. So use some spread operator. So I can copy all the elements of the remainder result into this new array. But I'll also add on the number I just took, right. So all I'm doing in this return line is, I'm returning basically the same array that I get back from my recursive call, with the number added to the end of it. Let's say you're unfamiliar with this syntax, I can preview it really quick. It's pretty neat. Let's hop into the, you know, repple. So let's say I had some array, let's say it was this array of just a, b, c, sort of an isolated example, you can use the spread operator, which is the three dots here to like unpack an array. And you can sort of say like, new array equals bracket. So this gives me a new literal array, I can spread out the elements of the old R, right? So if I do that, the new array just has all of those same elements, vi, capitalize that and properly, cool, we can extend that syntax, right. So I still have that array of ABC. Let's say I had another variable, we'll call it other array, what I can do is create a new literal array, copy all the elements from our using the spread operator. And then I can comma, separate any additional things I want to add while I'm here. So let's say I added a z. So if I look at my other array, now, it contains ABC, and also this new element z. So it's all I'm doing on this slide, right? I'm returning the same combination, that my recursive call gives back with the number that I took are added to the end of it. Cool. So what's really nice about this code is it's very reminiscent to our previous code, because we have an early return. So if I find at least one way, to generate the target sum, I can just go ahead and return that first way that I found, right, they say we can return any valid combination here. But let's say this for loop finished, and we never found a valid way to generate the target sum, then after the for loop, you can just return null, because it's not possible not to generate the target sum, apparently. Cool. So this code is looking pretty good. I think let's let's give it a shot. It goes without saying no, sometimes the remainder right will become negative. But we already handled that with this nice base case, right. So let's test this code. So I already have some example outputs over here. So looks like our first few examples are working. Obviously, our last one seems to take quite a long time. So I'll just kill this program early. But before we talk about how to optimize for this larger example over here, if you look at these other examples, what I can see is this array of 322 does add to seven. So that's looking good. This array of four, three, also adds to seven. So that's good. We already saw that this example of seven and two for now, that's your return No, because it's not possible generate that sum. And it looks like the array for the eight example is also good to go. So notice here, there are also some other arrays that we could have returned and would still be considered valid output, the exact combinations that we get back are really just dictated by the order that we happen to iterate in the array. So for example, let's say I switch things around here, like I did 352, it's really the same array, just in a different order, we'll probably get a different answer for that particular one, right? So I'll run this now, notice I get 233. But either case, that still sums to eight, so we're totally good to go. Awesome. They bring this back to the original code. And yeah, it seems that now the limiting factor seems to be the speed of our solution, right? We've been here before. And so let's go ahead and talk about the complexity of it. So we know that the time complexity for this is almost identical to what we did in the cancer problem, which is conveniently over here. But sort of refresh. By now you recognize that all right, the time complexity should be described in terms of like the recursive tree, let's let's lay down the foundation. And we already know that M, we're going to call the target sum. And let's also say that n is the numbers dot length. Cool. And so if I think about the number of recursive calls that I'm going to make, it's really the same as last time, right? So I'm going to say that, hey, the time of this function seems to be Oh, and I have an exponential, the base of the exponent is the branching factor, which is the length of the array. And then the depth of the tree would be the exponent, which is just m, right. So this was the same time complexity as last time for a brute force writes M to the M power. However, it looks like we have some additional time that we consume in this function really coming from this expression, right. So if I look at line nine, in particular, this line creates a copy of an array. So that will actually take sort of a linear number of steps to copy over every element of an array right. So I will have to consider the cost of this operation. It sort of under the hood like iterates through the remaining result, I know that the maximal length of the remainder result I can get back will be at most m, right? Remember that remainder result in the worst case is going to be in array, the longest that array could be is exactly the target sum, right? Imagine that we had the most simple, you know, sort of a combination to generate the target sum. If it was just a bunch of ones, right, my target sum was 50. And I had an array filled with one, then I can just do one plus one plus one plus 150 times to generate the target sum. So in the worst case, this copying operation will take m steps, I need to do that for every recursive call. And so if I have this many number of recursive calls, in addition to that, I do an M operation, you just multiply that M, right. So the time complexity for this brute force so far, is n to the n power times M. Really, the thing we want to optimize away is this exponential part, right? So I really want to focus in on that. But while we're here, let's go ahead and talk about the space of this brute force. So the space, there are things to consider the terms of the stack space, it's going to be the same as last time, so it's going to be o of m. But think about any other space I use up I guess you should also consider the array that you return back, whoever we know that we're going to return back like the first array that we find all the way back up to the top level call. And the combined length of all of those arrays that we return is really just going to be in worst case, M. Right. So I would still say that the space complexity for this function is just m right now. Cool. Obviously, let's work on optimizing this time complexity. So this was the brute force. So brute force. And you guessed it, and there is definitely some people get some problems in this brute force. So we'll just memorize it away. So very formulaic stuff, most of the work is always the brute force. So I'll start with my empty mental object. And like we always say, we'll use the target sum as our key. So if I've seen the target sum before, that is if it's in the memo, then I'll return what I have stored in the Mo. And at this point, what I want to do is make sure I pass along the same memo object to all of the recursive calls. That way, they can benefit from any information or any sub problems that I worked on over here, right in this stack frame. Cool. At this point, I need to make sure that I take all of the return lines that I had before, and I store them in the memo, right? So the trick here is I can say, memo of target. So I'm going to save that array. So notice that now the values in the array, or sorry, the values in the memo object are going to be arrays, because that's the return value of this. It's either going to be an array or not right? So I'm just going to complete this return value by returning what I just put in the memo. And in a similar way, I also want to memorize this late return. So we'll also be memo of target sum equals No, I can just still return No. Cool. So with that change, right, it's very, very straightforward change. Let's see how this does. Now. I know that for this last example of how some 300, it is not possible to generate the sum of 300 using just sevens and fourteens. So should we get an all for that one. So let's give that a shot. Nice. And there, we have a really quick running a know of that very last example. So we definitely had an effect on the runtime here. Let's talk about how we changed up the complexity of this code. So that was a brute force. Let's talk about the memoized version. So it's good with the time over here. So we know that in terms of the time complexity from the number of recursive calls that we make, it's the same story as our last problem, right? We just have to consider any other time operations, which would still be this array copying. So but in just terms of the recursive calls that we make, it's going to be n times m, recursive calls. And then in each recursive call, I need to do this copying pattern, right copying over the contents of an array, and the array will be at most m elements long. Alright, so if I have this many recursive calls, and each recursive call needs to make an M length operation, now I just multiplied by another m term. So right now this looks like n times m times m, which is the same as n times m squared. And I'll talk about the space complexity. So the space complexity will be at least as big as our brute force, so it's going to be at least o of M, above, now we have to consider all the space we use up in the memo object. So I think about the keys of this memo object, the keys are just going to be all unique values of target sum, right? Because I just literally use target sum as a key on all these lines, right? But then everything about the value or the values are going to be Pretty chunky now, because sometimes of value but in the object is going to be an array. And I already said that the maximal length of any of these arrays that I returned is going to be of length M. Right. So to me, it's now the case that your space complexity comes from mostly your mental object, which is going to be of size, m times m, right, because you have m keys. And each key has a value, which is, at worst going to be an array of M elements. So it's m times m, which is m squared. Cool. Notice that we definitely cut down on the brute force implementation, especially in terms of the time complexity, obviously, there was a little trade off that we made here. But now we're able to run our house and function in a reasonable amount of time. So to wrap up this problem, let's go ahead and hop back into the drawing board. Alright, now that we coded the house and prom, let's wrap it up by actually analyzing the complexity of it. So recall that for our house, some problem, we have multiple arguments, right, so I need to describe them. So I'll say that m is my target sum, and n is the length of the array. Remember that the array contains the choices we can take. And our baseline solution, that is our brute force recursion had an exponential time complexity, right? It was kind of in the form of n to the n power times M. And after we actually optimize it using the memorization strategy, we actually reduced it from exponential down to a polynomial time complexity. In particular, our new time complexity was n times m squared. So really important fact about our memoized have complexities, it is no longer exponential. Please recall that when we have m squared, that is not exponential, because the exponent is a constant number. Cool. If I look at the the space complexity for this, I had to actually pay a little more cost in terms of the space because of the memoized object. But that space complexity we use up is still not exponential, right? Still a polynomial or quadratic complexity. So I'm still satisfied with it overall. So we definitely prefer this memo is complexity over the brute force. All right, that was the how some problem but now let's go over one more variation of it. In this problem, I'll still give you a target sum as well as an array of numbers to choose from. But this time, what you want to do is return an array containing the shortest combination of numbers, that adds up to the target sum. And along with that, if there's any ties for the shortest combination, you can return any of those shortest ones. So from this description, you probably recognize that this is similar to our last few problems. But now they're asking for a little bit of an optimization, right? So I want the shortest sums, that means an array containing the least amount of numbers, but it still adds up to the target sum. So we're really just going to add on top of our previous understanding, let's take a look at a few examples. So let's say I gave you this example. So here, my target is seven, and the numbers I choose from are 534, and seven. So there are a few different ways you can generate your target of seven, one way would be to do three plus four, that's one combination. But another way would be just take the seven because notice that seven is actually a member of the array. And here I want to choose the smallest array, which would be just the lone seven. So that would be the expected result for this one. In a similar way, let's see, I gave you a target sum of eight. And your choice of numbers were two, three and five, there are plenty of ways to generate eight, you can do two plus two plus two plus two, or you can do two plus three plus three, or you can just do it three plus five. And here, I want to return always the shortest combination, which would be the three plus five. So that would be the result over here. Cool. So we definitely have some awareness that Alright, this problem is similar in structure to our previous one where I need to sort of generate a way to make the target sum. But now I want to really specify the shortest array, right? So let's try to visualize this as a tree. Like always, let's say we're stepping through this example, with the target sum of eight. So we know that the full tree would look like this. And we've seen this tree before. But now what we're trying to do is come up with a different process for the value your return, right, it's no longer going to be valid to just return the first way that I find to generate the target sum. Now I want the best way. So let's start coming up with the strategy for this. At this point, we know that we can definitely implement some logic in our code where we return an array up our recursive calls, we did that in the last problem. So here, we'll sort of use some abstraction to assume some return values. So let's say that I'm bringing this problem down, my top level problem asked me what's the best way to generate eight. But I see that along that path, I have to find the sub solution for the best way to generate six. So let's say we're rooted at this subtree. And we know that there are a few ways we can make six, right? If I look at this subtree rooted at six, there are two zero base cases downward, right? So if I look at the first one, I get this path, I know that my sub tree over here will be able to know that all right, one way to generate six would be two plus two plus two, right? We kind of implemented that logic in the last problem. But now what I also want to do is consider any other paths in case they end up being shorter. Obviously, I know That this path of three 300 return upward. And now I have to kind of make the decision, you know, between these two options for generating six, I should just prefer the shorter one. So that means it's sort of two to two versus three, three. And obviously, the three, three wins because the array length is shorter. And if I pause right here, and I looked at what the diagram is saying, it does make sense that above the six, we have three comma three, because that is the shortest way we can ever make a six, right, just three plus three. But now I can return this sub array to my parent. But if I do that, I should also include the value of the edge along that path, right? So I also included the two over here. Now this makes sense, because one way I can generate eight is three plus three plus two, right? But we know that's not the optimal way. Let's say we step through this a little further, let's say we took the same sort of process for this other child of five. So I know that to generate five, there are three options according to the subject, right? Because I see three different leaves that are have zero inside of them. And so I know that for these paths, they would work out through their own sub arrays. And here, I really just want to choose the shortest one, right? So obviously, the lone five wins out over here. So that'd be returned all the way up to that five node. But then that five node will return it to its parent, then, of course, I need to include the edge that connects the eight to the five, right? How did I transition to that five in the first place, so I go ahead and just add it in. And now the root node over here has a decision to make, which one does it prefer between 332 or five, three, it'll just choose a shorter one. So the five three should win out over here. And I just need to continue this process for any other branches in my tree, right? I can't return early and this type of problem because I need to find the optimal way. And I can't be sure until I've tried every possibility. Cool. So it looks like our overall logic is we want to explore and find any ways to generate our target sum. But then when we find a way that's shorter than our currently tracked way, we can just replace it, I'll continue that process through the entire tree. If I checked every possibility and keep replacing what I consider the shortest. By the end of us exploring every branch, we would have the absolute shortest. Alright, I think we're ready to hop into the code. Alright, programmers, welcome back to my text editor. Let's work on coding this one out. So here I have some initial examples that we'll use to test our best sum function. I noticed for this last one, right, I get my initial target sum of 100. And the best way to do that is obviously just take a bunch of 20 fives, right for 20 fives. What else I want to bring our attention to is this third example over here. So my target sum is eight. And my choice of numbers are one, four, and five. The optimal answer here is four and four, right? That is the shortest way to generate at a common mistake I see a lot of students make is sort of assume that the best way to generate a target sum always involves taking the largest choice of number as many times as we can, right? That is not true. If you took a five in your your sum, then you would have to take three ones in the long run, which is not going to work out to the shortest answer, right. So it's not the case that just taking the biggest choice of number always yields the target sum in the shortest amount of numbers overall. So don't fall into that trap. Which means that we have to do the full tree exploration, right, we have to do that in exhaustive sort of search using our recursive code. So let's start with that base case, like we always do. So bring this a few times, but still right here. So if the target sum is zero, then I have trivially solved the problem. So I can just return an empty array, right? That is the exact single combination and the shortest combination that can generate the target some cool along with that, let's handle that scenario, if our target sum goes too far downwards, if it's less than zero, so if the target sum is less than zero, I'll just return null, meaning that it's not possible to generate that target sum. And now we'll use our branching logic. So I'm going to iterate through all choice of numbers will say for let num in numbers. And like we always do, I'll go ahead and create my remainder, which I know would be the difference between my target sum, and that choice of number. Right? So what I'm doing is I'm choosing the number that decreases my target sum. At this point, I would make my recursive call right with that remainder, passing in the same numbers array. But now I have to do some thinking, I know that all right, best sum is either going to return to me a combination or an array, or it's going to return null, so I'm gonna have to kind of differentiate between the two. So over here, maybe I'll just save this as a result. I'll call this my remainder. Let's say combination. And if this remainder combination is not null, then it can do some other logic, right? So I'll say if the remainder combination is not equal to No, then do stuff. So if I enter this if statement, then that means it is possible to generate the remainder. And exactly what remainder combination is, is an array containing the short This way, I can generate the remainder. Cool. So if I enter this if statement, then I also have a way to generate the full targets, right, I can just take the remainder combination, copy the elements over from it like we did last time. And also add on the choice of number I just took, right. So this would be now a complete combination for target sum. So I'll just say that as a regular combination. Cool. So so far, this is very reminiscent to our last one. And we need to work in some more logic relevant for this problem, that is a very, very important characteristic that we needed to implement in the tree was, I need to sort of choose the shortest combination, RAMs, I'm going to need to work in that logic. So here's what we can do, I know that I need to compare basically, all of my branches together, all of my recursive calls together, and pick out the shortest combination. So I know that this for loop is a piece of code that sort of iterates and attempts, all of my branches. And so outside of the for loop, I'm going to need some outer variable, I'll call it my shortest combination. And over time, I'll just keep updating this variable if I find a combination shorter than my current shortest combination, right? So I'm gonna initialize this to null. And the reason is, imagine that we set up this shortest combination of variables, no. And then we iterate through the for loop. And there isn't even any way to generate the target sum. So this for a loop finishes, and shortest combination would still be no. And that'd be great to return, right? Because in this problem, they said, if it's still not possible to generate the target sum at all, you should still return null. So this is a good initial default value for shortest combination. But now I need to do my update logic. So here on line 11, I've actually created a combination, that gives me the target sum. But now I need to check, right, so I'll say, if the combination is shorter, then the current shortest that I need to update it. So let's translate that into some code. Just use an if statement. And I'll start by checking if the combination I currently have, which is an array, right? I check if that is less than in length, then the shortest combination variable. And really, it looks like shores combination, it starts as null. So I need to fix this code a little bit. But if I update it with a valid combination, then it's going to be an array, right? So I'm really checking the length of the arrays here. So what I want to do is assign my shortest combination with the combination that is now shorter, good. So this means the shorter combination wins out and gets to stay, right. If I look at this code, it needs a little bit of work, because the first time I find some combination, I know that I be comparing that array length to no dot length, right? Because shortest combination starts as null. And I can't do null dot length in JavaScript. So I'll need like a nice or clause here. So I'll say, if it is the case that your shortest combination is equal to No, then you can go ahead and replace it. Cool. So this sort of check over here is going to make sure that I automatically replace this null value with the first valid combination. Even if right now it may not be the shortest, right later on, I'll compare that combination I have stored to some possibly shorter combinations. Cool. So this code is looking pretty good. Let's go ahead and give it a little test run. Nice. Notice that and on occasions where we call besam, with a remainder that is negative because remember, sometimes we subtract a number that is maybe too large, that's okay. Because that will actually bottom out at a base case and return null, which we sort of check for explicitly in this if statement. Cool. So let's give us code a run node best some. So I get an error over here looks like I get maximum call stack size exceeded, which means that we didn't really hit our base case. So there's some work to be done here. So let's take a look at this code. So if I take a look at this code, it's a very, very small typo. It's kind of unfortunate that it breaks the entire code. But really, I messed up when I iterated in the for loop over here. So here I wrote for letting them in numbers, that would actually give me the indices of the array. So they'd be like 0123. Whereas I want the elements of the array, right? So instead of in any of over here, so that's on me. So with that small change. Let's give it a run now. Yeah, it looks like we're passing these first three examples, we have 735, and four, four, notice that the order among the elements in the combination doesn't really matter too much. But looks like we're definitely a little too slow on this last example, over here, right? So obviously, we know the move is to memorize this because we have the brute force recursion. But before we do that, let's just talk about the complexity of This. So this code is going to be very similar in complexity to our last how sum function, sort of compare the two, it's almost the same code, right? But do these side by side, right, these two functions look very, very similar. So we know in the long run, we probably have the same or close to the same complexity. Let's be methodical. So we know that over here, we always like to say that M is the target sum. And we'll also go ahead and say that n is the numbers dot length. So we just did the brute force. And our brute force shouldn't be the same story as last time. So talk about the time. So the time of the brute force is going to be some sort of exponential, right? If you remember that tree drawing, in general, the exponential number of nodes in a tree will be the branching factor to the height power. So not the branching factor here is n, right? I branch for every choice of number, and then the height of the tree would just be the target sum. So that's n to the M. But along with that, we also have some additional operations, right? If I look at this for loop, so this for loop gives me the branching factor, right. But then I also do this operation on line 11, which is copying over the array of remainder combination. And that array in the worst case will be of length m, right? The largest sort of combination, give me back is a combination that is just filled with a bunch of ones, right? If my target sum was 50, the longest combination possible would be a bunch of ones, so 50 ones in an array. So what I'll do is, I'll say that for each of these n to the M power calls, you also have to do a linear operation in M, right? So like before, it's n to the m times m. Cool. And then the space complexity is sort of interesting in this one, because we're maintaining some values. So look at this. So I talked about the space complexity, just from the stack space, it would just be the heights of like the recursion. In other words, it would be m over here, right? So I know it's going to be at least M. So I'll jot that down. But then we have also like this variable on line five, right? So I know that over time, I'm going to be storing an array inside of this variable. And this array is going to be in the worst case, m in length, right? So what I'm saying is, every recursive call would have to have its own shortest combination variable, right? If the shortest combination variable is going to be an array of length m, that means I have an array of length m, for every recursive call right? Before I bought them out, at like my final base case. And so also, the space complexity here is m times m, which we know is the same thing as m squared, right? And sort of the reasoning is, your maximal stack depth is still m like last time, however, now you need to have those stack frames, you need to store in array, right as you recurse. Nice. So really, the limiting factor for us right now is going to be the time complexity, which is exponential. And so like we always say, let's go ahead and memorize this. So memorization pretty trivial right now, right? You've done it many times. So I'll just begun my initially empty memo object. And I'll check, you know, if my target sum is in the memo, then I should actually return the stored value. So return memo at Target sum. So now that I have my memo checking logic, what I also want to do is add the memo storing logic, right. So I need to just go to my return value and store it in the memo before I return it. Notice that the return value right now, it's no longer inside of the for loop, right in the last problem, it was in the for loop, because I can return early. But this time, we're going to return at the very end, so I'll replace it over here. So I'll say, for here, the memo at Target sum should be stored with the shortest combination, I can still return the shortest combination. And before I forget, let me go ahead and pass down the same memo object by reference. Alright, nothing too much to that. Let's go ahead and try this last example. Now. Give it a shot. Awesome. And there we have it. We have 425 in this last example. And that is the best way to generate a 100 is pretty evident that we cut down on the runtime. So let's talk about the memoized complexity embolized. And so the time is obviously much faster should not be exponential. But if I take a lay of the land, I know that now that every target sum is going to be a key of the memo object. And target some is really just a number, right? So if my target time is 50, then I basically have 50 different keys I can never store in the memo object. So if I have m different keys in my memo object, I know that I won't be exploring any full duplicate sub trees for each of those keys. However, I will slap the branch for every number in the array, right? So overall, I'm looking at an m times n. So so far, I have m times n, but I have some additional work from this array, right? So notice that the n over here that comes from this for loop, I'm iterating through numbers, but then the additional M comes from copying over this array, which would be linear. So it would be m times n times M. And I can just kind of squish these two M's together. So it kind of boils down to m squared, times n, which is really the same thing as last time. And our space complexity would also be the same as it was last time, which was just m squared, mostly coming from the memo, right? And the logic is, your memo keys have m possibilities. But for each of those keys, their value can be an array of length m, right? So just m times m, or m squared. Awesome. So looking at this code, you probably, you know, feeling that Oh, my gosh, like this best some problem is pretty complex. And I think it you know, to be honest, it is, however, what I really want us to focus in on is this, like progression we took, right, I think that everyone could tackle this best some problem, if they warmed up and really understood simpler problems like can some and how some write the code is very, very similar. So to wrap things up, I will let's do some closing words on the drawing board. So in its best some problem, right, we had two inputs, we had m as our targets, and also say that n is the length of the array. So the brute force that we initially implemented it with just some recursion was exponential in time. And after we optimized it, we brought it down to just a polynomial time complexity. And notice that between our brute force and our memulai solution, they actually have the same space complexity. So we definitely prefer this memoized version. So hope you enjoy this series of problems. That is we worked on the can some how some and best some problems, they all had the common frame of us having some target some that we need to generate with some options given in an array. And in particular, if you look at the can some problem it asks us what that task of generating target some Can you do it? So yes or no? The house and problem. So is how will you do it? So what are the exact combination of elements that you'll use? And finally, the best son was the hardest version? And it asked, What is the best way to do it in terms of the least number of elements of the array? So there definitely is a logical progression to these problems. And in particular, they kind of capture a different variation of a dynamic programming problem for our can some problem, right, we had to return Boolean there. That's a type of decision problem, right? Yes or no? Can you accomplish this task? Is it possible? Along with that the house some problem was a combinatoric problem, right? We want to know the exact combination that works out. And the best some problem was a variation of an optimization problem, right? I want the shortest way to generate the target. So we saw that these three problem types definitely have some common ground, but they also have some nuance, depending on exactly the question we're trying to answer. These all fall under the umbrella of dynamic programming. That being said, dynamic programming problems aren't just limited to number inputs. All right, I think it's time to work on another prompt, let's say I gave you this, what I want to do is write a function called can construct that takes in some target string, as well as an array of words in a word bank. My goal is to return a Boolean indicating whether or not I can make the target by concatenate together elements of the word bank. And along with that, we can reuse as many elements as the word bank as we see fit. Alright, notice that in this problem, we're looking for a Boolean response to start, right. So just yes or no. Is it possible to generate the target? So let's take a look at an example here. Let's say I gave you this. So my target is abcdef. And I have a nice long array of some words. And so I'm basically asking, Can you construct abcdef, using elements of the array. So if I kind of take a look at the array, there is exactly just one way to generate the target string, which are just B, A, B, C plus D, F. So the answer here is true, because there is at least one way to make the target, right. Let's take a look at the opposite example. So let's say I had this word of skateboard. And I gave you all of these words in an array. So take a moment to kind of look this one over. And you tell me write Is it possible to generate skateboard here? And the answer here is no. Right? It is not possible to generate skateboard using this array of words. So we should return false, we can get pretty close to making skateboard. But we can never build the full string. Alright, so here are a few ways that we can attempt but they don't work out in the long run, right? This is one series we can take, but we kind of get stuck, as well as this. And also this way, right? Point being there are zero ways we can ever generate skateboard. So we should return false over here. Awesome. So I think let's look at one more example. We should already have the vibe that in general, it's easier to create a shorter string than a longer string, right? To create a longer string, you're probably going to need to use more elements. So if I have that kind of framing in mind, that I know that possibly the easiest string to create would be the empty string, right? So let's say your target was the empty string and I gave you you know, some kind of random array of words, really, the array of words doesn't really matter here. I think no matter what they should return true. Because to generate the empty string, you can just take no zero elements. From the array, and that kind of line of thinking is going to help us really start to solve this problem. So we'll kind of take this example, in stride, what we want to do is return true if our target is empty, and that takes place no matter what array of words we're given, right? Cool, more or less, that kind of sounds like a base case, right? But let's go ahead and start to kind of define some process we can take to explore all of the options, right? So I want to really visualize this in a well ordered way. And that really means a tree. Right? So let's say we're going to trace through this example visually. So my target is abcdef. We already said that, in the long run, we should return or conclude a true about this input, right? So we'll just start with the input argument that is the target string as the root of this tree. And this is sort of a scenario where like, Alright, I have two inputs, how do I know which one to really encode in my drawing? Well, it's about what will actually change. I know from like, one instance of like this problem to the next, the array of words, I can actually reuse as many times as I need. So it's not like I'm taking out elements from the array. So with that in mind, I think it's better or more reasonable to encode the actual target string into the nodes of this picture. Because I'm gonna start with this original target string of my route. Now I have to think about how I transition to the children of this right, so what moves do I take that hopefully shrink the target string, right, I know I need to shrink the target string, because I already have the base case of the empty string in mind. So I need to get closer and closer to a length of zero. We'll talk about one transition we can make. So I know that I need to sort of use the array of words as I transition. And so let's say I took a B as a choice right now, if I take a B, then I guess I could remove, you know, that sequence of characters from my parent node. So if I take a B out of abcdef, then the resulting child is just C, D, F. Cool. So notice that as we transition from one node to the next, looks like we're taking out that substring. In a similar way, I have another substring, that I can take out for my word bank, and that would be ABC, and that would yield in the child d f. And then I can do this for some other string inside of the word bank. And the really important thing to know is, there's a correct logic to doing this. And it's also a common trap I see a lot of students fall into when it comes to this logic. So right now I'll talk about like the common mistake. So it would be ill advised to sort of take out the CD from our root node, if you took out C, D, then the resulting note would be a, b, f. And so what we're doing right now is if we took out C, D, you're taking out something in the middle. And if you take out something in the middle, that means that your resulting string actually creates a new adjacent sequence of characters. In other words, I know that this is kind of suspicious, because look at this little run of characters, A, B, E, those are now like adjacent next to each other. Whereas in the original string of abcdef, A, B, he was not present, right. So if I kind of take out strings in the middle, then I'll have this sort of mistake of creating new adjacencies. Right. And that would actually impact the moves I take later on. So I don't want to do that, right. So the move here is to not take out any characters from the middle of the string. So what would be the correct way of doing it? Well, if you look at our two nodes that we already branched into, a common factor for them is the fact that we actually took a prefix out of our original root node, that is a B as a prefix, and so is ABC or recall that a prefix is just a string, that kind of begins some other larger string. So if I want to transition to a third node over here, there's only one more prefix I can take. And that would just be ABCD. Right? If I take that prefix out, then my resulting child is just E, F. And so the overall logic that we want to use when we build this tree is to only branch to children if we have a matching prefix in the word bank. And of course, the child would be the resulting string after we remove that prefix. So let's keep it rolling. And we'll apply this logic again and again, recursively. So let's say I'm rooted at this CDF node, I'll look inside of the word bank and notice any prefixes that actually match here, I think there's only one and that would just be CD. If I took out the prefix CD, then my result is E, F. In a similar way, I can do that for d f, I find that d f is actually contained in the word bang. So it's technically also a prefix. If I took the prefix def out of that node, then my result would be empty, which is pretty good. At this point, I look at some other nodes like this EF and they have no matching prefixes inside of the word bank. So they sort of bought them out in some sort of base case. In the long run. If I can break down EF any further than I know that it's not possible to construct EF so that can technically return a false to the parent. Mr way, if I ever run into the empty string, that means the job is done. And I can just return true, right? Something we said earlier was, whenever we want to generate the empty string, it is always possible no matter what. And now this pattern should feel a little familiar, right? All I have to do now is kind of bubble up these Boolean values to the parent. And overall, if one of my children return true, then I myself will return true, right? So I'll bubble this up a little bit, bubble up all the way to the top. So our root note chooses basically the true value among these three values it gets from each of its branches. And so the ultimate answer here is just true, right. And it is possible to generate abcdef using words of this array. So you're probably having deja vu right now, right? We almost use the same exact logic for the previous some problem. However, all we did was adjust how we transition from one note to the next, really making a compatible for this string data. But we're going to carry over a lot of knowledge, right? It's really important that we understand like the general like knowledge of dynamic programming and recursive understanding, we can apply that under any circumstances. That being said, let's look at one more example. What I want to do is see an example where this should return false. So let's say I gave you that skateboard example from before. In the long run, this is going to return false. If I tried to break down skateboard, I would always try to transition using any matching prefixes from the array. There are two prefixes at the start that match, and that would be SCA and SK, and those would give me t board and eight board, respectively. And now at this point, if I route myself at the keyboard node, I can only take the T, resulting eboard. In a similar way, if I am at the eight board node, then I can only transition using the eight. And I'm left over with board. At this point, if I look at eboard, that node over there actually hits a dead end, right, there's nothing I can actually take out of Eve or there's no matching prefix, because I basically need something that starts with E, but no words of the word bank start with E. So I guess we have to focus our effort elsewhere. Looking at this board, I can take either a Bo or a Bo AR, and I'm left with art and D respectively. And unfortunately, those two nodes at the leaf level also bottom out, right, I can't transition further and take out any prefixes from those strings. So I know that those will have to return false. And if all of these leaf nodes return false, then my ultimate note at the top that is a skateboard node at the root, that's going to just return a false as well. So it's pretty clear to me that the overall logic we want is if we get back a single true, then we can just return true all the way up to the call stack. But if everything is false, then we'll just go ahead and return a false. With that, I think we're ready to code this one out. All right, let's go ahead and code this one up. So here I have some initial examples we can use to test our code for correctness. Looking at the last example, over here, it's a fairly long one. And notice that the target incident F, whereas no words of the word bank have an F in it. So we know that that should result in a false. So let's go ahead and lay down the base case for this. Like we said, a reasonable base case is to check if your target is empty, right. If you have the empty string, then you can already construct the empty string by taking no words from the word bank. So you can just return true over here. And now I need to make my recursive call in a way where my target string gets progressively smaller and smaller toward this empty string. And so I know that based on the tree I drew, I need to make a choice based on the words in the word bang. So I'm going to iterate through all of the words. So I'll say for let word of word bank. So I'm iterating through every element of the word bank. And now that I have the word element, as I think about when it's okay, to make the recursive call using that word, and we spoke about this, we pointed out that we need to make sure that the word is a prefix of the target. So I can do that, I can just check if the target dot index of word equals zero. So index elf will just give me the index where I can find some substring inside of a larger string. If the index i get back is zero, that means that the word starts at index zero of the target. So if you're unfamiliar with this method in JavaScript really quick, let's say I did potato dot index of pot, that will tell me the index where I can find it, which happens to be at index zero. But if I looked for tayto, I would get the index of the T, the first t rather in potato. So this is a really nice way I can use to check if some substring is a prefix of another string, right, the index should be zero within it. So a way interpret this if statement is if I have a prefix, then I can sort of use it to shrink the target. So I'll create another variable here. I'll call it like the suffix so that's like the string after I remove the prefix. What I can do is slice my target string. Except to target that slice. And what I want to do is start picking up characters. After the length of the words I can say word dot length over here. So it's kind of reasoned out what this logic is doing. So we'll trace through this, let's say that I don't know my word. So I'll open up the note repple. Let's say my word was the string pot. And we'll also say that my current target is potato. So I know when I do target dot index of word, that is going to get back zero. So this if statement would be true. And then what I do is target dot slice of word, dot length. So we're dot length is just the length of the prefix I took, right? So it would be three over here, providing the target that slice starting at index three, that would give me everything after the prefix, right? So I basically have removed pots, and got eight. Oh, cool. So when you use slice, if you pass in a single argument, that's going to be the starting position of what you start grabbing characters, and you'll go all the way through the end. Cool. And that's the logic I definitely want here. So back to the code. Now that I have the suffix, I want to make my recursive call on that suffix. So basically asking, Hey, is it possible? Can I construct the suffix now? And I'll pass along the same choice of word bank. Nice. So here's where I should think recursively. So I'm focused in on what type of data do I get back from can construct I know I get back a Boolean, right? True or false, it tells me whether or not the suffix can be made. And I want to check, you know, if this call returns true, maybe I'll be explicit here. So if the recursive call is true, then I know that the original target can also be made. So what I'll do is return true early here. Cool, I know that if the suffix could be made, and the word that I use to generate the suffix is also in the word bank, then the entire target must also be able to be made. Nice to hear I have my nice early return true. Like you expect, where should I return false, it should be after the for loop, right? Only after I've tried every possible choice of the word and none of them worked out, then can I say, No, the target cannot be created. So I want to do a late return false over here. So this code is looking pretty sharp, I think let's go ahead and test these examples. So I should get true false true, and then false for the last one, give this a go. So I get true, false true. And it looks like the last example over here is taking quite some time to finish. So I think our code has correctness. But it's not efficient enough to you know, reasonably run this last example. So I'm going to kill this program. If you take a look at the example I pass it for this last one, it does sort of describe the worst case here. For one, we know that the answer is going to be false. So you will have to do a full exploration of the tree, meaning you can't do any early return trues, right. And because of like the length of the string, and also the length of the word bank, I have a very big tree, right? Notice that there's going to be a lot of prefixes at work here, right? every element of this array is actually a prefix that could be found over here. And that happens again and again. So I'll tell you what, let's go ahead to the drawing board. And we'll try to visualize what the exact complexity of this function is. Alright, so we implemented looks like the brute force for this solution. Let's talk about how we might want to optimize it, I think any conversation of optimization should begin with actually understanding what the current complexity is. So let's say I gave us this kind of large example to sort of visualize with, so my target string is enter a potent pot, and I gave you a pretty diverse array of words in the word bank. So I'll just go ahead and kind of give us though the full tree in what it would look like, would be this pretty long tree, right, so I follow the same logic that we did in our earlier examples. notice a few things. There are some occasions where I transition using a single character prefix, which is totally fine. And that kind of tends to feel like the worst case scenario, right? If you take single characters out of your target string over time, then you're going to have a very, very tall tree height, or you're gonna have to take many, many steps. So that kind of reminds us of a worst case scenario, like we saw when we ran the code. But that being said, we want to generalize this understanding right for basically some generic size of our input. So let's say we kind of just looked at this tree in terms of its overall shape, and then we'll kind of generalize it. So to me, this tree has this sort of basic structure. And let me start by defining the terms I'll use to describe the complexity right. So I'll say that M is the target string length, and n is the number of words in the word bang. Right, so I'm really trying to use a different variable for each of my inputs, because I have the vibe that both of them contribute to my complexity, possibly in different ways. So you've seen, you know, us analyze trees before in terms of like their structure, right, we're visualizing as a call tree, I know that the time complexity would be the number of nodes in the call tree, right. And so let's start with some familiar territory, I kind of already know that I need to realize the height of this tree, the height of this tree is going to be m, right? Remember that m is our target string. And so imagine, in the worst case that we took, or we had to take a bunch of single character, choice of words to construct our target string, that means that the number of things I would have to take all the way from the root to a base case, or the root to the leaf would be exactly M. Right? If I had to take a bunch of single characters, so we can be confident that the height of this tree would be m. So I talked about the height. Now I need to realize the branching factor, that is from one level of the tree to the next, how does a number of nodes change in the worst case? Well, I know that the branching factor is dictated by how many words I have in the word bank. And so that would be some relation on n. So I know at the root level, I have about one node. But then in the worst case, to get to the next level, I will multiply by n, right? Imagine that, basically, almost every element of the word bank was a matching prefix, right? So you'd multiply by n. And let's say that carried over further, let's say almost every word of the word bank was still a prefix of those resulting nodes, I would keep multiplying by n. And I would do this overall m times, right. So we already know that this is going to be exponential, I need to multiply n by itself, m times over. And so this would give me an N to the M, time complexity. In general, if you visualize your recursion using a nice call tree like this, then the overall time complexity is going to be the branching factor to the height power. So we have a branching factor of n and a height of m. That being said, this will just really consider the number of calls that we make, I want to be super sure and make sure we didn't do any other kind of performance or costly operations inside of our code. So here's our code, same code as we did before, if you look at this code, some things I need to consider are probably line six, right? If I look at line six, that was where I did a target that slice that's like copying over a part of the target string. And to do that operation, I would actually have to iterate through the target string. Right? So that would actually contribute to my complexity here. So in the worst case, would I be slicing? Well, if I'm slicing a target string, that would have like a maximum length of M, right. And so in every call to can construct, I'm going to have to do an additional m operation, if I have n to the M calls, then I can just multiply an additional M over here, right. So I, I added some additional term into our complexity, just multiplying by M. Cool. And so overall, if you look at that time complexity, it was already exponential so that multiplication by m, doesn't really make it that much slower, it's really the N to the M, that makes it unbearably slow in the first place. So that was the time complexity. Now let's look at the space complexity. So if we just refer to the space complexity, due to the call stack, it looks like it's going to be the height of this tree, right? Like we always say, the height of the tree would mean the maximum number of stack frames, we would need on the call stack before we bought them out at the base case, right? Because when we return from a call, we would actually remove something from the call stack, right? The height of this tree is definitely just M. So we'll say the space from the call stack is just o of m. But again, we should also you know, look at our code and see if we created any other like growing structures. So we look at the code right now, I see that on line six, and kind of talking about that slice statement. Again, the slice returns you a new string, right. And that new string is going to tend to be of length M. So on every call to can construct, I'm creating a new string, I actually need to maintain it through the recursion, right, because I actually slice before I return out on line eight. So because of that, I know that each of my M stack frames will have to store a string of length M. So that just means m times m in my space complexity, which is really just m squared. Cool, which isn't too bad of really a space complexity overall. Alright, so it looks like our final complexity for our brute force solution is exponential in time. And it looks like quadratic in space. So obviously, let's work on improving the time complexity over here. And so hopefully, you're seeing where this is heading. Let's say we jump back into my entropion pot example. So this was a huge tree. Take a moment look at this tree, and where can we actually optimize some work away? To we're trying to do is notice any overlapping some problems In the context of our tree, that means I'm looking for any duplicate sub trees. And I see duplicate sub trees right here, right? They follow the same structure all the way down even to their base cases. So I've looked at the sub trees, they're really trying to solve the same problem, right? That is they're both trying to figure out, can we construct the string and teapot, right? And if I saw that once, on the left hand side, the answer is going to be the same on the right hand side. So I can just store that information in a memo, right? So the trick is here to of course, memorize it using our classic strategy. So let's hop to it. All right, here we are back in my code editor. Let's go ahead and just memorize this one. So I'll create my memo object. And here, I'll prefer to use the target as the key to my memo, I know from one call to the next, the word bank doesn't change, right? So I don't need to actually make it a part of the key for my memo object. So I'll just check, hey, if my target is in the memo, then what I'll do is return the stored value of that target. Cool. While I'm here, let me also squish this down to a one liner. So I have the memo checking logic. Now I need to make sure I pass down that memo to all of my recursive calls. And then I need to actually store data in the memo. And the rule is wherever you have your recursive returns. Now, you should also store that return value in your memo before you actually complete the return. Right. So for both of these lines, I'll make sure I store them in the memo. So it's a memo at Target equals that. But I still want to return that same value. So we'll return true and return false over here. Again, notice that I'm using my exact argument targets, right? I don't need to write any memorization logic about the suffix, right? Imagine if we made the call to can construct suffix becomes that frames target. So the memos ation would still work properly. Right? So that looks pretty good. And it wasn't that much code, right? memoization is always this slightly extra layer, we add on top of our brute force solution. So let's try this now can't construct. Nice. And there we have that last example finishing fairly quickly, or we do get the correct answer of false over here. Alright, so here's what we'll do, we're going to head back to the drawing board. And of course, we'll come up with the final complexity analysis for this function. Some classic memorization, right? Let's take a lay of the land over here. So we wrapped up this can construct problem, let's describe its final optimized complexity. So again, like before, we'll say M is the target length, and n is the number of words in the word bank, our original brute force had an exponential time complexity, right, and that was n to the m times m. And when we memorize our solution, we actually improved it from exponential. So now, the time complexity of our solution is n times m squared, right. And the reasoning is, now we don't have to fully explore any duplicate subtree, every time we run into it, instead, we just store the results in the memo. And we can just kind of short circuit and fetch the stored result in the memo. Notice that I have an M squared, right in the memo, I have complexity, that second m really comes from the fact that we still have to do the slice, we have to pay for that cost. So although we added an additional object in our memo II solution, our space complexity would remain in the polynomial class. So overall, we definitely prefer this second solution, because we remove some exponential, you know, complexity. So now it is reasonable to actually run this in an amount of time. I think that wraps up this can construct problem. Now let's work on the counting version of this problem. In particular, I want to work on count construct. So we have the same set up here, I want to take in still a target string and also an array of words in a word bank. But this time I want to return a number, I want to return the total number of ways that the target can be made using words of the word bank. And like before, we can reuse elements of the word bank as many times as we want. So notice that this question asked us to do something slightly different, we don't want just true or false. If it's possible, we want the exact number of ways that we could construct the target string. So let's take a look at some examples. And also, you know, take a high level view of the strategy here. So let's say I gave you this string of abcdef. And this array of words, this particular example we saw before, and it is possible, and there's actually one exact way we can generate abcdef. So we can construct the tree in the same way we did before. And we end up with this. However, this time around, we want to choose a different value to return for our base cases, as well as changing the logic for reconstructing our sub solutions, right? And so if I look at the base cases here, I wanted to kind of just adapt the return value for this new type of data that I need to return which is number. So I think the move is for these scenarios where we can't break down our current target any further. That's your return zero, right? Yeah, f can't be broken down because it doesn't have any more matching prefixes from the array. So that means there are zero ways to breakdown F. And so we should return zero in those scenarios. And then when we have the empty string we know was successful. So we could always, you know, create the empty string. And so that should return one. And we've seen this logic before in problems like Fibonacci, as well as our grid traveller problem, we just want to bubble up these values to their parents, and make sure every parent will add up all the numbers that come back from their children. So it just bubbles up like this. And top level we do zero plus one plus zero, which means that we can generate abcdef in exactly one way. And if you look at the way that actually is symbolized inside the tree, it's exactly the path from this root node all the way down to that lone base case that returned an empty string, right, ABC plus d f. Let's take a look at another example. So here I have the target string purple, and I have some other characters and words inside of the array. So take a moment to look at the input here and predict what number they should return. So the answer here is two, right, there are two ways to create purple. So if you construct the tree, we know we start with the initial unknown of purple. And then we have about three prefixes that match here. And those yield some children, we can follow the same pattern recursively down the tree. So the full tree really looks like this, notice that I have a two base cases that actually end up as the empty string. So I know those are going to turn one up to their parent. And on the right hand side, I have a loan II that can't be broken down any further that returns zero. And like always, right, I just bubble up these values, until at the parent, I actually do one plus one plus zero, which gives me two. All right, there are two distinct ways to create purple using this choice of word bank. All right, I think I'm feeling pretty good about jumping into the code for this one, it's really just a small variation of the last problem we did. So let's hop right in. Alright, programmers back in the code editor, let's go ahead and bang this one out. So I already have some examples that show us what numbers we should return for this count construct, right. So I have some examples here. I'm gonna say even have that big example, with our large inputs, we know that that will probably need to be optimized, let's lay the foundation with a brute force. So we'll start with the same base case, as always, right. So if the target is the empty string, then we have truly solved the problem. Meaning that there is definitely one way to generate the empty string, right, just take no elements from the word bank. Along with that, I need to make my recursive call. So the usual structure, right, I want to iterate through every choice of word. So for let word of word bank. And then for every word of that word bank, I need to check if it's a prefix. So we've done this before, if the target dot index of word bank, or rather index of word, if that is equal to zero, then it must be a prefix. And if it's a prefix, and I can go ahead and take like the rest of the word that is the suffix and call recursively on it. So I'll do this in line now say, count construct, and I'm going to pass in the rest of the word, which would be target dot slice, and then I'll slice starting at word dot length. So this means that the slice contains everything after the word or everything after the prefix. And so Also be sure to pass into the second argument, the same word bank. Nice. And now I need to think about what calc construct returns. So it's returning a number now, right? So in particular, this will be the will say, num ways. I'll say no more ways for the rest, maybe a little wordy of a variable name, but I think it does describe how exactly what this returns here, right. So this is a number of ways that you can generate the suffix with like the rest of the target. What I want to do is keep a running total. So outside this for loop, I need a way to add everything that the for loop iterates through right, so I'll create a total count over here. So I'll say let, I'll say total count. And I'll start it as zero. And now inside of my for loop, whenever I get the number of ways to create the rest of the string, I'll go ahead and just add that into my total count. So it's as simple as total count plus equals the number of ways to generate the rest of the string right after I've removed the word. And after I take the total through the entire for loop, and I can just return the total count. Something really great about this, this programming pattern is let's say that none of the choice of words were a valid prefix. So that means I finished this for loop. And this if statement is never true. If this if statement is never true, that I never add anything into the total count. So if I return total count afterwards, I return a still zero, which makes sense because apparently there are zero ways to make the target right have no initial step to take in the word bank. So let's go ahead and test this one out. So looks like we should get to 104, and then also a zero at the end. Let's give that a go. So 2104. And it looks like the last example is just a little slow for us. So you already know where this one is heading. Let me tell the program, let's just go ahead and just memorize this one off the bat, right? Hopefully, I think through all of these examples, you're feeling really good about memorization. And it's apparent that we basically have drilled into our minds already. So if the target is in the memo, and just return the stored value memo at Target. And then I'll need to adjust my return value, right. So where I return over here is I'll store that total account as the value corresponds to the target. And let's not forget to also pass in the memo to our recursive calls, right? So memo goes right here. Nice. So let's give that a run again. I expect a zero for that last answer, right. So looks like I have an error over here. And of course, I forgot to actually replace my return value. So I can still return the total counts. And addition to that, I'm actually adding it into my memo. So let's try that now. Yeah, these answers look correct, right, I got zero for the last one, notice that the last one is not possible, because it ends in an F and all my words only contain E's. Nice. And if you look at this code, it's a pretty similar to our previous problem of can construct if I kind of split these can construct basically has the same shape. Really, the only difference is this number variable that I add into, and of course, the return values. So we do expect the complexities to be the same for these two functions. Let's wrap this one up on the drawing board. Let's look at the complexity of this. As always, for this calc construct problem, our M is going to be the length of a target string, and our n is going to be the length of the array, right? So that means a number of elements in that array. Our brute force is the same as it was in the can construct problem, right, so it's still exponential. And using memoization. We just brought that down from an exponential to a polynomial time complexity, and the space complexity remain the same. Really, there is no additional cost that we pay in our implementation for this counting version of the problem, right. The only difference is now we're maintaining a number that we just add to, you know, on every iteration of that for loop. But that doesn't really affect the runtime or the space complexity at all. So I think this wraps up this count construct problem. Now I want to do one more variation of the string problem. So what I want to do is write a function called all construct that has the same setup, right, so I'm going to take in a target string, as well as an array of words in a word bank. But this time, what I want to do is return all of the ways that the target can constructed by concatenating elements inside of that word bank. And that means I should return a two dimensional array, a single element of that 2d array is going to represent one of the combinations that can construct the target, right, and I want to return all of the ways within a 2d array. And as always, we can reuse elements of the word bank as many times as we need. So let's make sure we understand this question by looking at some examples and their output. So here's an example using purple. And it's the one we've seen before in the last problem. The last problem I had us return to because there were two ways to create purple. But now I don't want the number, I want the exact ways that actually create purple, that means you need to return a two dimensional array, the outer array, or the outer set of brackets represents the collection of all combinations. Whereas a set of inner brackets or a sub array represents one of the combinations that creates purple. Let's take a look at another example. Let's say I gave you abcdef. And I gave you this long array of word bank, notice that I actually added some elements inside of this word bank array. It's a little more complex of in our previous examples. Because of this, there are actually many ways you can create abcdef. And this contains all of them, right? So there are four ways to create our target string. And again, notice that each sub array represents one of those combinations of words in the word bank that creates the target. Cool. So now that we kind of understand like the shape of this problem in terms of the data we should return, let's take a look at some base scenarios. So let's say I gave you this example. Right? So I have a target of Hello. And my words in the word bank are just Cat Dog and mouse. Obviously, you know, that is not possible to generate Hello. So there are really zero ways to create this. How should we actually return an answer here? Well, I think it'd be reasonable to return an empty array. Remember that we're saying the outer array in this context represents the collection of all of the combinations that can create Hello, since there are zero ways to create Hello, that means that our collection is empty, right? This has a length of zero meaning there are zero ways to create Hello. Now let's say we had another base scenario. Let's say we have to generate the empty string using the same array of word bank. And that scenario, I think it's reasonable to return an array containing an empty array. And again, the reason is if it is possible to To create the target string, then we need to return a 2d array, right where the outer array represents the collection. And if there's one sub array inside of that outer array, that means there's one way to create the empty string. And what does that one way? Well, it's to take no elements from the word bang. So I think this is going to be consistent logic, this is going to be a really important way to think about this problem. That is, when we're given a target that cannot be constructed using the word bank, we should return an empty array, right, a one dimensional empty array, because there are zero ways to create it. On the flip side, if I've ever given the empty string, we know that it's always possible. And so we should return like a two dimensional empty array. Cool. So with that, let's look at some tree examples. Now, let's say I have this large example from before, we know that the overall tree like we've always know created would look something like this. Obviously, now we're just reframing this problem in the return values that we do for our base cases. So you notice that there are four base cases here that kind of reach the empty string. And I know that those represent the four ways to create abcdef. And now I have to adjust the return values here. So I'm already saying that if I have the empty string, then that should return an empty 2d array. So it looks something like this, right? And how I actually reconstruct the entire solution from these very small sub solutions. So let's just stay focused on the left subtree. Right now, I know that these arrays are going to return back to their parents. And when I do that, I also need to make sure that I include the edge that I actually use to transition to the child. In other words, I'm going to be sure to push the label for the edge into each of those sub arrays. So the key insight is this, right? I'm just pushing those edges into their sub arrays. And from here, I continue the pattern up a little further, right. So I know that these arrays return their parent over here, and I still, you know, push the edges that I took to get to those children. So on the left, I'm going to put CD in the front, and on the right, I'm going to put C in the front, resulting in this. And then at this point, notice that this node rooted at C D E F, that is actually going to receive both of these collections, right? And what should this note do? Well, it really needs to just combine a both of these arrays. Recall that, you know, we technically receive a two dimensional arrays, which means that a sub array represents one of the ways to create the actual target. So if I just concatenate these two, two dimensional arrays, that yields this construct, if I do a quick spot check, I'll see if this is compatible with what the question is asking, right? This is a two dimensional array. And I know there are actually two ways that we can create CDF, if I look at what this is saying, The first way is to do C, D plus F. And the other ways to do c plus d f. And so this cell problem is correct in itself. So I'm going to use the same exact logic to trace through the right hand side, I know that these cases return to their parent. And the parent is responsible for adding the edge to each of those sub arrays like this. Now, I'm just gonna reorganize everything at the top just to give us some more room. And maybe I'll even spread out these brackets to make a look more symmetric. But I'm not done here, I still need to consume each of the edges at the very top of the tree. So if I look at my left hand set of arrays, I know that those need to receive AB. In other words, they need to have a B place at the front of each of those. So I'll just do that, right. In the same way, all of the arrays in the middle, which is really just a one sub array, need to add ABC to the front of them, like this. And finally, same thing on the right hand side with ABCD. Cool. Now if I look at what I have, these represent the four ways that we can actually create our original target string. But at this point, the root node just needs to concatenate each of these two dimensional arrays together. So it really just combines them into a single 2d array like this. And if I do a quick sanity check, I know that each of these sub arrays represents one of the paths that I can take to a base case down my tree, which is exactly what we intended at the start. So this type of recursion is definitely more complex than the previous examples. But hopefully, you can recognize what's similar to our last examples, right? Let's do one more together, though, let's go back to this purple example of the full tree would look like this. And we'll trace through this. So on the left hand side, we're going to start with a 2d empty array, right, because that's the base case for the empty string. Meaning that hey, it is possible to create the empty string in one way, and that is to just take nothing. So I do the same logic as our last example, meaning I returned to my parent, but I'm being sure to also include the value of the edge, right. And so the key insight here is, I'm being sure to add the edge label and not the actual node label here. The note and the edge happened to have the same thing le, but I'm really looking forward to adding the edge right? So I add Ellie over here. And likewise, this returns to its parent even further, and so Add that last edge of perp. Cool. Now I need to do everything for this middle path. And so what I'll do is return the empty 2d array at the very bottom. And this really bubbles up all the way to the top. And we just accumulate every edge label as we go, right. So it sort of looks like this. And what's really interesting about this a right hand path, this is actually a base case, that doesn't work out, meaning we kind of hit a dead end at EA. And what we said in our initial examples was, whenever we have a target string, that cannot be created at all using words of the word bank, then we're going to return a one dimensional empty array. And that actually works out in our favor, because if we return this empty array to our parents, our parent is going to try to add the edge into every element of this empty array. But if there are no elements in this array, and it's going to add nothing. In other words, this one dimensional empty array really just bubbles up to the very top. And like always, if I just concatenate all three of these arrays together, I actually end up with my final answer, notice that when I concatenate an empty array to the rest of these, I will actually contribute nothing to my file answer, which makes sense because there are no paths that work out on that right hand side. So our final answer is just this. Like, we know both of those sub arrays represent the two different ways that we can create purple. All right, I'm feeling pretty good about this process. Now, let's go ahead and code it up. Alright, programmers back in the code editor, another problem, another solution. So let's start by laying out the base case over here. Now, like we said, we'll say if the target is the empty string, then we want to return a two dimensional empty array, right? Every single sub array here represents the one way you can create the empty string by taking nothing right I take no words of the word bank. Nice. And then besides that, we need to make our recursive logic. So that's going to be very similar to what we've always done for this style of problem. So I'm going to iterate through every word of the word bank. So I'll say let word of word bank word bank. And I need to still check if there's a prefix, right? If this word is a prefix, so if target dot index of word, if that index is zero, then I know that it must be a prefix, so I can continue some code inside. Nice. And so what I'll do is I'll go ahead and create an extra variable just to store the target after I remove the word which would give me like the suffix so I'll say const suffix equals, and I'll say target dot slice of word dot length. So you've seen this pattern before. But just to refresh, this just gives us everything after the word. So basically, once you move the word, what is the remainder of the string all the way to the end. And now that I have this suffix actually wants to call my function recursively on that remainder, right? Just like this, I'll pass along the same word bank. Cool. And now here's where things get a little intense, right? So we've been doing a lot of problems, you know, using this recursive structure, and then we memorize it in the long run, I think the most important thing to do is when you make your recursive call, you really just assume that your function works, right. So I think about what type I should get back from all construct. So I know that all constructs and give me back an array, right, it's gonna give me back an array containing all of the ways to make the suffix. If there are no ways to make the suffix, then it's going to give me like an empty array, right. And so I'm going to assume that here, so I'll create a variable. And I'll call it, let's say, the suffix waise. And the reason I'm naming it like this is, I really want us to, in our head, think about the return value from this as an array of all the ways to build the suffix. Cool. So that's going to be a two dimensional array, right? There are many ways to do it. Nice. So if this gives me all the ways to make the suffix, how can I get all the ways to make the original target like in this current stack frame? Well, what I need to do is really just take each of those suffix ways, and add my word to the front of it, right? I used this word to even create the suffix in the first place. So what I can do is, I'm going to say my target ways. Right? So now I want to relate how the suffix ways can be used to build my original target, right? All I need to do is really iterate over every sub array over here and add my word to the front of it. Right, that's exactly the process we took in the tree diagram, right? So I can use some nice JavaScript methods here. If I just wanted to basically manipulate every element of the array, I can do a map here. So I'll say suffix, ways dot map. And I know that a single element there's going to be one way he'll say, singular. And then what I want to do is just take that same way, copy it over, meaning I just spread It's elements. But I'm also going to put the word that I took in the front, we've seen this syntax before, although maybe this like map method is maybe new free, maybe you're not super familiar with JavaScript. I think that's fair game. So let's hop into the node repple. Just to demo this, let's say I had some array, I'll just make this array 1234. So there's my array. What I can do in general is use array dot map. And map is a function and you should pass in a callback. So that means another function. And what I'll do is this callbacks gonna take in every element, and it's gonna be an arrow function, what's going to return is really just how I want to modify the elements, let's say I wanted to multiply every element by two, right, the return value of this callback function is going to become a new element of the new array. So notice, I get back 2468, my original array was 1234. And I get back a new array using map. And so essentially, what I'm doing in this example, on line eight, in my actual code, I'm just taking every sub array and just inserting my word at the front of every sub array. So an example that speaks closer to our exact code would might look something like this, like suffix ways. And it's going to be a 2d array. So I'll make a sub array, and we just an array of some strings kind of arbitrary right now. So I'll say x, y, and then z. And then the second sub array will be maybe I don't know, like, a x and then a yz. Right, so that's my original array. And what I want to do is, if I say suffix ways dot map, and let's say for every way, that is every sub array, I just want to copy over that way by spreading out. And then I'll just go ahead and put I don't know, like an A at the front, just like this. So notice what I get back, I basically have every sub array from before, except now I have a as the first elements of both of those. Cool. So that's all I'm doing in this particular chunk of code. Awesome. So we'll leave the node repple. Back to our running solution. And now that I have the target ways, that's good. It's basically just like a piece of the answer I need. However, it now this is really only going to give me all of the ways to make a target that use this word that I'm currently like on in my iteration. However, I know that this for loop gives me multiple branches, right, it's going to use all of the words that are possible to create the target. So I need to kind of gather them all together. So I'll create a variable on the outside, I'll call it result mailstore. Everything right? So across all of these iterations, I just want to add the target ways into the results, I'm going to do result dot push target ways, and I need to actually spread this out that way, I don't nest things too deeply. Right? Remember that this is a one dimensional array, right now, target ways would be two dimensional, I don't want to just push target ways into result. Otherwise, I'd get like a three dimensional array. So I'm going to be sure to spread out target ways, right. So with that, let's actually go ahead and give this run I think all I need to do now is after my for loop, right, after I'm done getting all the ways, then I can just return the results of Mr. Just work recursively. So let's just try maybe the first example for now and see how we do. So I should get, you know, a two dimensional array where I have two ways to make purple. Right? let's give that a go all construct. Nice, and that's looking pretty good. That is a correct answer. Let's try these other ones. Now while we're here. So we should get a nice, large array for that second example. Nice, here it is. And then the third example, skateboard should return just an empty array, because it's not possible. And it looks like that last example is also not possible. So we do get an empty array. And if you kind of notice, to get that last answer, like we did in our other examples, it does noticeably take us some amounts of time. But before we get to that, let's take a look at this code, make sure we really understand it. So the biggest logical leap we made here was really, you know, assuming that we get back valid data from line nine, right? And then it's all about how we can, you know, adjust that sub result to get our full answer, right. So if I get all the ways to make the suffix, and for each of those ways, I can just add my word in front of them. And that would give me all the ways to make my target. Right. And I need to continue that process for every choice of word, which is why I'm maintaining this like a massive results variable outside of the for just pushing all the ways into this result over time. And maybe just to be super clear, you're not familiar with like this push method, especially when you use the spread operator with it as well and really easily step through that. So in isolation, right, this is just using push and spread in general, if I had some, let's say, array, and let's say it was just an array of just some elements 1234 Then I had another array, I'll call it just numbs, let's say in nums, I had elements of seven and eight. So here's my original array. And here's my nums. And if I did array dot push nums, you're actually going to force some nesting in this array. So array dot push returns the new length. But it also actually manipulates or mutates, the argument you call it on. So if I look at the array, now, notice that it actually has another array nested inside. That's because I literally just pushed the entire nums. If you wanted to do something a little different, what you can say is array dot push, and you spread out nums, what's going to do is comma separates the seven and eight, basically removing the outer brackets from it. So have a look at this. Now, look at the array. Notice that with that second push, now I just added a seven and eight without those additional brackets. So that's all I'm doing over here. Like I said, I don't want to add another level of nesting to the result array, right, it should just always remain at most a two dimensional array. Cool. But overall looks like we have solved this one. And let's kind of talk about if there is even a way to make this faster. This is kind of an interesting problem. So this is looking pretty good. So earlier, when we were on the drawing board, we said that, alright, because of what they're asking in this problem, it is basically going to require a full exploration, right? If I want you to return every possible way, to make the target string, you'll just have to kind of do all the work of creating all of those sub arrays, right, so we can't really avoid that stuff over here. If you think about this example, unlike 3334, this is actually not the worst worst case anymore. All right, here, we know that eventually we're going to return just an empty array, because we can never generate the final z in this example. However, if I remove the Z, then the result will be a very, very massive 2d array, because there are a bunch of ways to actually make this target string. Right. So we've kind of changed the direction of what it means to be like worst. On this example, let's say we brought this to use the Z. If you really cared about optimizing, like this particular scenario, you would find some benefit to like memorizing it, although it wouldn't really affect the worst case, because the worst case is actually where you return a two dimensional array and not just an empty array. So maybe just because it's good practice, well memorize this. But when we go to the drawing board, we'll see that this actually doesn't affect the true a bigger worst case. But if you just want to optimize it slightly, let's say we baked in our memo over here, then we check, you know, if our target is in the memo, and if it is, then we return the memo, add the target, we'll be sure to pass along that memo to our recursive calls. And then we just need to make sure that for our return value on line 16, we actually store that inside of our demo using the target as a key, and then we still complete the return by returning result like we once did. Cool. So let's run this now. And I would agree that the last example now runs faster. But it is not the worst case is a really important thing to understand here. Right? The worst case is when you actually have to create a massive sub array, right. And so let's head to the drawing board and see our final conclusion about the complexity here. Like we usually do, well define m to be the length of the target string, and n will be the number of words in the word bank. We know that this problem asks us to return an array containing every combination that generates the target string. And to come up with that answer, we have to visualize it like a tree sort of like this. In our previous drawing, we saw how each leaf of this tree represents one way we could create the target string. In other words, if we can figure out the total number of leaves in a tree like this, then we will basically have the number that represents how many different ways or how many sub arrays we have to generate in this problem. We already know that based on our process, we described that there are dimensions to this tree, we know that the height of this tree is going to be m and we know that from one level to the next, we really just multiply the number of nodes by n. And that means at the base of this tree, we would have entity m number of nodes, right, so we have n to the M leaves, which means that we have n to the M different combinations that generate the target string. And if we have to represent each of these entity m combinations, then we definitely need n to the M sub arrays in our output. And so what we're saying here is no matter what clever implementation we create, we really have to return a result that is exponential in size. And that's really going to drive the complexity of this one. You can't do any better than exponential over here. And its overall will say at the time. complexity of this is n to the M or just exponential. If you want to split hairs here, you can really multiplied by some additional factors of m. However, the exponential nature of this alone really gives us the overall complexity, right, because once something is exponential, there's really no coming back from that. And in a different vein, we can say that this space complexity for this is O of M, like we usually say, it's really just the height of the recursion tree. And an example like this, where output is very large, we usually don't include the size of the output of the size of the result into our space complexity, which case obviously, would also be exponential in space, because the result is exponential. Right. So here, we'll just refer to the additional space we use for the call stack, which would be o of M, right, any solution you come up with for this problem would be just about this fast. Alright, and there, we have our all construct problem. So at this point, we've gone over many dynamic programming problems, and we use that memoization strategy to work out a solution for all of them. However, memoization is only one of the ways we can actually implement a dynamic programming solution. Right now I want to revisit all of those problems, but this time use a different lens of understanding. As always, I want us to ease into things. So we'll warm up with this fib function, this is the same problem as last time. In other words, I want us to return the F number of the Fibonacci sequence. Here, I'll say that the zeroeth number of the sequence is zero. And the first number of the sequence is one, you might notice that this time around, I'm saying that the zeroeth number is zero and the first number is one. Whereas in the first time we did this, we said the first number is one and the second number is one However, no matter how you start the sequence, they will both actually give you the same series of numbers. In other words, down below here, I have some examples for n as well as what the output for our fib function should be. In other words, if I asked you for the six Fibonacci number, the answer there is eight, like we always expect, alright, let's get into the heart of this strategy, right? What does tabulation even mean? Well, it's all about building a table. So what we'll do now is step through a tabulated strategy for calculating fib of six. I know that in the long run, I ought to return the final answer of eight, right? So I hope that I get the answer right by the end of this little trace. So with tabulation we're choosing to do is really think about this dynamic programming problem, still in terms of subproblems. But instead of doing it recursively, we do it iteratively by building a table, really just an array, and I'm going to begin it with roughly the size of the input. In other words, if my input here is the number six, then I basically want an array of length six, notice that if I want the indices of this array, it's kind of line up perfectly with the original input number. And I'm going to have to actually add an extra position. In other words, this array spans indices zero through six, which kind of means that there actually are seven different elements here, right, the length of this array is seven. So something very common that I see when students try to implement a tabulated strategy is they kind of overlook this off by one nature, right? So technically, I'm creating an array that has one greater length than my number input. But any case, if this is my array, what do I want it to represent? Well, in the long run, I want to actually fill out this array in a way where each subproblem corresponds to an elements of this array. So here's how I'm going to begin this table. The move here is to actually initialize every position of this table with zero. And the reason for me initializing zero everywhere in this array is the fact that I know Fibonacci requires me to take a sum, right, and zero is a really great starting value when I need to calculate a running sum, right. But along with that, I need to be sure to seed the starting values within this table. In other words, if I look at the zero position of this table, it already has a zero, which makes sense because the zero number on the Fibonacci sequence is zero. But what I should also do is make index one contain a value of one. And that kind of entails that, hey, the first Fibonacci number is indeed one. And now at this point, once I've seeded those initial values, now I can actually run the general algorithm to fill out this table. So now it's just a matter of iterating through this table, so I'm going to start you know, at the very first position of the table, probably just with a regular for loop. And what I need to do is really remember the definition of Fibonacci, right. So if I have this number, currently in the array zero, that's a zero Fibonacci number, what I can do is just add this current number that I have kind of pointed to in yellow to the next two positions. And the reasoning there is a Fibonacci number is used to contribute to the sum of for the next two Fibonacci numbers. So I'm going to do is take the zero and add it to the next two positions, obviously, since it's zero, actually don't change the values of those next two positions. It's nothing really is calculated on that first iteration. But any case I can continue on to the next iteration, so my current position is always in yellow, and the next few positions are pointed to in blue. At this point, my current position says one is what I should do is take one and add it to both of my next positions. So that means two and three are indices two and three are contained both a one and a one which so far, make some sense because at least for the to the second number of the Fibonacci sequence is indeed one, I can do the same thing, right, my current position contains a one. So I'll add that one into my next two positions. Keep doing this, I add two to my next two positions, at three to my next two positions. And at this point, I sort of reached the end of my array. So I know that I probably shouldn't step out of bounds. So really just want to look one position ahead over here. And at this point, I just add five into my next position, which actually works out to an eight being stored in index six of this array. Which makes sense because logically, the six number of the Fibonacci sequence is a just like we intended. That's all there is to tabulating Fibonacci, the most apparent difference from our previous like recursive strategy was, this is not recursive, right? This really just requires us to iterate through an array. So to fully iterative process. And at this point, the actual complexity of this is really straightforward to kind of foresee, we know that we're just going to iterate through an array of size n, that must mean that our time complexity is just n. In the same way, the only space we use is really just the space of the array, which I know is still going to be sighs and as well. So overall, we're looking at a linear solution for fib. So before we hop into the code, for this one, I just want to draw a really important connection. So although at face value, this iterative strategy looks very different from the recursive solution, a lot of the logic really carries over, I'm still using my overlapping subproblems to solve this one. So for example, I know that every index of this array really corresponds to some number input for fib of n, right? So I can kind of visualize it like this, right? Like we said, the six of bonacci number is eight. So I'm just going to choose a position of this table, let's say I looked at fib of six. So I know that to calculate fib of six, what I did was really add the previous two numbers into this position, right. And if you kind of see that the shape of this, I kind of dropped the table, and even just ignore the rest of the subproblems. This is basically just a tree that can kind of shift things around. And it really looks like this. So overall, you're looking at really the same relationship for calculating Fibonacci. It's just encoded in a table instead of the recursive tree. But overall, if you understand, you know, the recursive solution, you should still find, you know, some comfort inside of this iterative solution. But I think at this point, let's hop into the code for this one. Alright, programmers, let's go ahead and translate that strategy into some code. So I'll start by creating a table, which really just means creating an array, we'll call this table. And I need this array to have certain dimensions that are roughly the size of n. So to do that, in JavaScript, I can say array and call a static method and pass in my desired size. So here, I want to say n plus one. For the reason we did in the sketch, right, I need to make sure that the last index of this array is exactly n, I have to do a plus one here, because of course, indices start at zero. Cool, that should give me an array of that n plus one size. And then at this point, what I want to do is assign particular values into this table. So let's just say I console dot log, what this table looks like, I'll just run this first example of fib of six. So I'll give this a go. So notice that the array that prints out is this, it says like seven empty items, which means that the elements are undefined right now, according to our strategy, but we should do is assign these all to zero. So what I can do is, after I initialize the array over here, I can fill it up with all zeros using the dot fill method, right? It's an array instance method. So with that change, now I should have a seven zeros inside of this array. Cool, so it's looking pretty good. At this point, I also need to fill up a particular value inside of the table, I really need to make sure that the index one contains a value of one, right so I can say table at index one equals one. So we're seeding index one with a value of one to symbolize that the first bonacci number is one, remember that we kind of have like two base scenarios. In Fibonacci, let's just console that log with this table looks like now to make sure it's in the correct state before we actually iterate through it. Cool. So this is a good starting point for our table. So at this point, what I want to do is iterate through the table. So use a regular for loop for that. We're gonna need access to the indices. So I'll say let i equals zero. And I'll go up to and including, and basically give me iterations through the entire table. And I'll do i plus plus, I'm going to hit every position. Cool. According to what we did in our strategy. What I should do is look at the next two positions after I right, so I'll say as I'm gonna look ahead and table and I'll look at positions I plus one, and also i plus two, right? Of course, right now I'm currently situated at position I. And what I did was I added into those two neighboring positions, I added my current value in the table. So what I'll say is, in my next position i plus one, I'm going to increment it by exactly what table I says, right? So I'm incrementing, my next position by the value in my current position. And the same will hold true for the next position two spaces away, right, so they both get plus equals table. I know nice. So this is looking pretty good. After we're done doing this process for the entire table, where we're just going left to right, filling in our next two neighboring positions, what we'll do is just return our answer, which should be table at position n, right? To finish this off, I just want to return the table at index n, because the elements of this array correspond to the Fibonacci number, right? And the index is sort of the input to my function. So let's try these examples. We'll give them all ago. Cool. So I get 813 21. In this very large number, this actually looks correct. Nice. This is a fair implementation of Fibonacci. Notice that because we're already using an iterative strategy, we already satisfy a decent complexity for even this, this last example over here, we already mentioned the time and space complexity of this strategy, right, it gives us a linear time and space. However, you may already have in your head that we could actually optimize a little more here and just use a constant amount of space. However, if you want to kind of subscribe to just a generic tabulation strategy that I know is going to be useful in most problems, I will kind of stick to this overarching pattern right of initializing our table upfront, that is roughly the size of the input, and then iterating through that table. Of course, you can probably finesse this fib function to only track two variables represent your last Fibonacci number, as well as your last last Fibonacci number. But I'll leave that to you because it's not really a classic tabulation, which is a topic want to drill into our heads right now. Let's head back to the drawing board and do another problem. All right, now let's revisit that grid traveller problem using tabulation. So like before, we're going to have a 2d grid, and we start in the top left corner. And our goal is to go to the bottom right corner, we only have two possible moves at any point in time that is to move down or to the right. And how many ways can we actually travel to the goal, if we had a grid of dimensions, m by n. And we want to write our function to actually calculate this. So let's trace through how you can build a table for this one, let's say we're going through grid traveller of a three by three grid, that means that our output should be six in the long run. So like before, our first step in this tabulation recipe, is to create a table that is roughly the size of the input. And so here I have two inputs, and they really represent the number of rows and number of columns. So I can create a 2d array to correspond to that. Like before, if I want the indices to match up here, I'm going to have to make sure that this two dimensional array has dimensions four by four, right? If I give it a four by four array, that means that the bottom right index is really three comma three, which works out nicely, very similar to what we did in that last fib problem. Cool. So I established the dimensions for my table. Now I have to figure out what should I actually begin my table with, right? What are the good seed values to use here? Well, I know this is a counting problem, right? Because they're asking me to count the number of ways to travel through this grid. For these counting problems. Good initial value to choose is usually zero. So I'm going to put zero everywhere in this grid. But at that point, I may need to seed another value in other position in this table, if you recall our previous discussion about this grid traveler problem, we said that it's a really important case that all right grid traveler have a one by one should return one right there is one way to travel the gray one by one grid. And so I'll go ahead and actually take that information and encode it into my table. So I'll make index one one, actually contain the elements of one. And I should give me a nice starting point for this algorithm. So at this point, what we want to do is iterate through this table and come up with some logic that combines the values in this table, right, basically combining different subproblems to solve my larger problem at hand. So let's just begin in the top left corner of this grid. In the long run, we can really implement this kind of iterative pattern through a grid using just some nested for loops, right. And the key insight is I know if I'm at this position, if I see zero at indices, zero comma zero, that means in a zero by zero grid, there's zero ways to move through it, which makes sense, right? Because if any of your dimensions contains zero, then that must mean that your grid is empty, right? So the game isn't even valid. But if we want to have some consistent logic, the move is to use your current position highlighted in yellow right now and add it to your neighbors, right, according to the game, I can only move to the right or downward. So technically, what I'm doing right now is I'm taking my current positions element, and adding it to my down neighbor and my right neighbor. Obviously, if I just have a zero in my current position, then really no arithmetic takes place. But let's go ahead and do these iterations just to make sure we have consistent logic, right, so they keep adding these zeros to their right and down neighbors, nothing changes for now, until I get to the next row, right, this first iteration, next row, still just add zero to its down and write. But once I'm at this point, I'm going to take this one, I'm going to add it to my right and down neighbor. So that means they both turn into a one, I keep following this pattern, right, both of my neighbors turn into a one. And obviously, whenever I kind of reached like the bounds of my grid, I need to probably make sure that I don't do any illegal Array Operations. But that's really an implementation detail for the code. So on to our next row, we contribute zero to both of our neighbors. Here, we contribute one to both of our neighbors. Now we contribute to to both of our neighbors. And here we contribute three to both of our neighbors. And finally, things get interesting in the very, very last row, I contribute zero to my neighbors, I contribute one to my neighbor, I contribute three to my neighbor. And at this point, we've kind of finished iterating through the entire grid. And if you look at this position, it contains a six symbolizing that, hey, how can you actually travel through a three by three grid? Well, there are actually six different ways you could do that. So before we jump into the code for this one, you can probably already foresee the complexity of this. So we know that the complexity is really driven by the dimensions of this table, I know that this table is going to have m rows and n columns. So if I need to iterate through this table, it's just going to take m times n time. And along with that, what is the space that we need? Well, it's really just a space for this 2d array, which is, of course, still m by n. So overall have m times n time and space. Let's jump into the code for this one. Now. All right back into the code editor. Let's work on implementing this grid traveller tabulation strategy. So we'll start by initializing our table, which is going to be a little more complex here compared to our last function, because it needs to be a 2d array. So what I'll do is I'll start by creating, let's say, the correct number of rows. So that will be as simple as calling array. And I want really dimensions m plus one, again, I have to adjust for that off by one error, because I want the max index in this table to actually be exactly right. And I know indices start at zero, so I'm going to up it by one over here. Cool. So I'll give me the correct number of rows. And I want to make sure that the elements inside of this array are also other sub arrays. So a trick I can do for that is we're going to fill this is kind of just a very particular JavaScript pattern, I'm going to call fill on this array I just created. And afterwards, I'm able to map over it. Cool. So maybe I'll spread this out like this. And when I map over this array, what I want to do is make sure that every element of the array is going to be a new array like this, this time of dimensions n plus one though, so I have roughly m rows and n columns. So let's go ahead and just see what the shape of this is. When I print out, let's say the three by two example. So we'll go ahead and run this code. So notice that if I look at the arrays I printed out here, it looks like I have a four by three array, which makes sense because again, I increase my initial dimensions by one that looks good to go. However, what we'll want to do is really understand how what this map is useful for. So a very common mistake that you might be tempted to do is Can't I just, you know, skip this map part, and then do the fill with the array of n plus one. So that will look like it gives you the same thing. So I'll run that code. But if you write it this way, what you're doing is you're filling the entire outer array with a single array instance multiple times, right? So the shortcoming of this code is let's say I kind of change only one position of the table, let's say I just made an X kind of arbitrarily, that would actually look like it changed many positions of the grid, right. And that's because this is technically just one array that's inserted multiple times into the outer array, whereas I need a really unique array references. So that's how we need the map pattern here. I know every time we evaluate the callback function to map, it's actually going to execute this entire function, which means it's going to give us a new inner array instance. Right. So if I use this map pattern I run again, this is really the intended behavior, right? I have a unique array as a sub arrays here. And so let's take out this expert. But that being said, Now that I have the correct shape of my table, I need to insert some good starting values, right? So what are the seed values here, while we're calling in our strategy, we said that basically the entire table could contain mostly zeros. So maybe I'll bake that right into this line. So after I make a new sub array, I'll be sure to fill it with some zeros. So if I run that, now, I should have a nice table, because that's looking pretty good. At this point, I also need to see another value, right? We talked about it in that we should see table at position one, one with exactly one, this is sort of like our base case, when we have a one by one grid, there's definitely only one way to travel through it. So now we have the elements of one in position. One, one. Nice. So at this point, now I need to iterate through my table and fill in other positions. So I'm going to use just nested loops for this. So I'll say let's i equal zero. And I want to make sure that I goes up to and including m right, basically the dimensions of the table, and I'll do i plus plus. And very similar for my inner loop being sure this time to reference j and j is gonna go up to n this time, because we'll just have classic nested loops just iterating through this two dimensional array, right? So as I'm iterating through the table, what was the logic we trace through in our drawing? Well, I what I needed to do was take my current element that I'm at, so maybe I'll call it, let's say, my current. So my current element would be at table ij. So I need to take this current element, and I need to add it into my right neighbor, and my down neighbor, right. So if my row is I and my columns j, that just means I do some arithmetic on i and j. Right. So if I want to increment my, let's say, right neighbor, what I can say is, look at table, a position i, j plus one, that'd be directly to my right. And I'm going to increase it by whatever this current is, if I want to look at my down neighbor, and that'd be i plus one, but I keep j the same. Cool. So this will add to my two neighbors that are to the right of me and below me. And at this point, what we have to be aware of is like what happens at our edges of this table, we know that if we're already at like the last position of a row, if I do j plus one, I'm gonna go out of bounds. And so to make sure I don't step out of bounds on these increment expressions, I'll need some conditional logic. So I'll say if the right position is inbound, so if j plus one is less than or equal to n, then I'll go ahead and increment to the end should be okay. And likewise, if i plus one is less than or equal to m, then I'll also increment right? Notice that when I checked my like j value, that should be using n, right, because that's the number of columns and then I is a number of rows. So it should use M. Cool. So what I have so far as I'm iterating through every position of my table, and I'm going to take the elements at my current position, and I'm going to add it into my right neighbor, as well as my down neighbor, about only if they actually exist. Cool. So once I'm done filling up the table, then my final answer should just be at position m, and right just the very bottom right corner here. So let's give this code a run. So I should get 1336. And then there's a very large number, or run grid traveller. Nice and looks like this solution is totally working. And we are already satisfying our the last example here, right? If you tabulate off the bat, you will have a pretty quick solution. And we already traced through the complexity for this. So take a moment to you know, look at this code, let it sink in, or really try to understand how we took that whiteboard strategy and implemented it in some code, right? We do have to do some like kind of fine implementation details, especially with the bounds checking here. But I think the most important logic, it's about how our current position of the table contributes to our immediate neighbors to the right and downward. With that, I think let's head back into the drawing board. Alright, now that we've seen tabulation in two different problems, hopefully we're noticing some patterns right? Let's give ourselves some rules we can follow to tackle any dynamic programming problem using a general tabulation strategy. With this tabulation recipe, it's going to be pretty different compared to our memoization recipe in that there aren't two main steps when I talked about memorization I said, make sure you implement the brute force first, and then add the memorization app. Towards. Whereas if you just try to tabulate a problem, you're really going to have the most efficient version of your solution all in one swoop. And so what is our first step for tabulation? Well, of course, you have to visualize it as a table. And that really begins with a conversation of what you'd like the size or dimensions of the table V, that should definitely be correlated based on the size of the input. So in the case of fibonacci of n, we made our table roughly, you know, n elements long, sometimes you definitely have to watch out for like an off by one scenario, which we've been seeing lately. In the case of our grid traveler problem, we saw that because our, you know, problem represents a grid, we had to create a two dimensional table at that. So figure out the size of your table based on the inputs to the problem. Cool. And once we do that, we need to initialize some values within the table. To me, this is always about choosing compatible types. In other words, if your problem asks you to return a number, then it'd be wise to initialize the values of your table with numbers right? On the flip side of that, if I was asked to return a Boolean in a problem, Na, consider initializing true or false within the table. Cool. And once I have all of the kind of generic values filled up in the table, then I choose my very important seed value, right? I know that this seed value should capture the scenario, where I have a trivially small instance of the input where I automatically know the answer, right. So in the case of our classic Fibonacci, that means I just seed the N one and the N two values of my table with one and one respectively. And in the case of our grid traveller, we were sure to capture the scenario where we had a one by one grid, so you're gonna need to see those values, because that's the basis upon which we fill the rest of the table. And then we have the hard part, which is really iterating through the table. And you have to come up with some logic, right, as you iterate through the table, you have to design some logic that fills further positions of the table based on the current position. And you really have to, you know, look to the problem to figure out what the logic is. In the case of Fibonacci, it's as simple as if I'm at some position of the table, I look one space ahead and two spaces ahead, very reminiscent of the pattern Fibonacci, in the case of our grid traveler problem was about looking the unit to the right or the unit downward, effectively shrinking the size of our grid. So it's really up to you to figure out the logic, look for it language in the problem. What I always do is focus on what options I have at any point in the problem, right? My options are Do I go rightward? Or do I go downward in that grid traveler and problem. So we'll stick to these rules as we tackle our a tabulation problems, and we'll utilize them right now. Okay, so let's work on tabulating our can some problem now. So recall that in this problem we want to do is take in a target sum as an argument, as well as an array of numbers. And we want to return a Boolean indicating whether or not we could generate the target sum. By adding numbers in the array, we can reuse an element of the array as many times as we want, we can also assume that all the input numbers over here are non negative. So we've solved this problem before, right? recursively using memoization. But now we want to use a tabulation point of view. So let's come up with the strategy here. So let's say I gave you this input of seven and an array of 534. In the long run, the answer is true because it is possible to generate seven, right, you can either do a three plus four or a four plus three, and that would give you your original target. So you know that in the first step for tabulation, all we need to do is create a table. But I guess the question here is, what size table do I create, I have two inputs, I have the target as well as the array of numbers, which of those actually contribute to my initial table. And so the key insight is to think about, you know, what's going to change throughout the problem. In other words, if we can reuse the numbers of the array as many times as we need, then it's not like we're shrinking the size of that array. However, if we think about the target number of the target, some that were given, we do have a goal of actually reaching that target. So now we're going to increase to it over time. And so I'm going to use that as a basis for my table. That is, I want to create an array that is roughly the size of my target sum. So because the target sum is seven, I want to make sure that the indices lined up perfectly. So I'll actually have to create an array with length eight. And in general, I just be creating an array of length target sum plus one, because we have that classic off by one error over here. Alright, now that we have the correct size of table, what do I actually want to store inside? That is what should the eventual elements of this array be? So a really nice rule of thumb you can use for that is to just recognize what type your answer should be. In the long run, right, so here, they're asking for a Boolean answer. So that really tells me that I should probably put some boolean data as the elements of this array. So I'm going to start by initializing every element of this array with a false value. And the reasoning is right now we're kind of assuming before we check anything, that none of these target sums are actually possible to be generated. Cool. But neither think about any particular seed values, I need something inherent to this can some problem is we treat the target sum of zero very special, right sort of the base that we use. In other words, someone gave me a target of zero, I know that that is always possible, no matter what elements I have in the array, because to generate zero, I can just take no elements of the array. So that's going to be a really important seed value here. So I'm going to populate index zero with the element. True. Cool. So now that we have some good seed value, now we want to think about how we can flesh out the other elements of this array, sort of in a linear fashion, right? So I'm gonna have to iterate through this table. So let's just start at the very first index. So right now I'm looking at index zero. And if I'm situated in any position of this array, then the value I'm currently looking at, should basically say, you know, is it possible to generate that index amount? In other words, since I have a true at index zero, then I know that it is possible to generate zero using the coins of the array. But now that I'm at this position, how can I actually transition into the further positions of this table. And so what I want to do now is consider the possible numbers that I can take into my sum. So I'm going to start by looking at this first element of five. I know that if it's possible to generate zero, then if I also have a five in the array, then it's also possible to generate five in the context of my array, that means I should look about five spaces ahead from my current position. So I look at this spot, right, exactly five indices ahead. And right, now I should actually replace this false value that's stored at index five, with a true and again, the reasoning is, if it's possible to generate my current amount, right, right, I have a true in the zero, it's possible to generate my current amount, and I could take a five, that means that the position of five steps later should also be true, right. And I want to actually continue this process for the other values in my numbers array. So I'm just going to look at the next element of three, that's basically telling me that, hey, the next spot three places away should also become a true, so I'll just go ahead and make that a true. And finally, likewise, for the last element of four, right, the element four spaces ahead, should also be turned to true. Cool. And at this point, I think I finished like my very, very first iteration, that is when my current is at index zero. And at this point, I just want to keep iterating through the array, so I'm just gonna shift my point of view, or one space to the right. So now I'm looking at my current as index one. And what does that over here, my current element is actually false. So what that means is, it is not possible to generate one using the numbers of the array, which logically makes sense, right, I only have five, three and four. So there's no way you can ever give me back a one, right. So if I have a false value at my current position, then I should not modify like my further values by looking ahead. So notice that if I look at my edge for five, look, five spaces ahead, I am not going to make that a true because my current position is not true, right. So I'm going to keep iterating. Same thing happens when I'm at index two, because the current value is false, there's nothing to be done here, things get really more interesting on this iteration right? at index three as my current, then I see a true at this position. So that means I need to look ahead to my future positions, and actually assign them to be true. Obviously, you'll notice over here, if I look five positions ahead, when I'm currently at index three, then that would actually be out of bounds. So I don't need to look that far ahead. So we'll kind of drop that connection. But at this point, I should make the space three positions ahead truth, that's almost a space for positions ahead. So that means that indices six and seven are set to true, right? I keep doing this process all the way through the entire table. Notice that as I keep iterating toward the right, these forward looking references kind of stopped being useful because it goes out of bounds. But the point is, by the end of all of these iterations, I do have a true stored at index seven, which means that it is possible not to generate a quantity of seven. And if I do a quick spot check, and I look at just my entire table right now, it has very consistent data. In other words, if I notice where all of the true values are right there at 03456, and seven, that means that all of those quantities for target are actually possible, right? On the flip side, if I look at the only elements and indices that are false, like one and two, that makes sense, because those are the only quantities that are not possible or you can't possibly generate a one and two, using a five, three and four as your option. Adding up, right? So we know by the end of this algorithm, the element that is stored at the last position of our table would be the final answer, right? So if it's true, then it's possible if it's false, it is definitely not possible. All right, so let's wrap up this sketch by actually talking about the complexity of this, as always, we'll define our terms. So we'll say that n is going to be the target sum, which is just a number. And the flip side of that will say that n is going to be the length of the numbers array. And we already have the vibe that, hey, both of these terms definitely affect our complexity, right? If you give me a larger target sum, that's probably a harder thing to calculate. In the same way, if you give me more numbers to choose from, it's even harder to calculate, right. And we can recognize that the algorithm just has us flesh out this table. And I know that the table is of size m, so we're going to have at least M. However, as I iterate through every position of the table, what I had to do was actually look ahead in the table for every single element of the numbers array. So I know that that's going to be accomplished basically, using some nested loops, where I'm going to have a loop to iterate through just the table, and then a nested loop to iterate through every number of the numbers array. So overall, I'm looking at N, N times N, time complexity. And along with that, where does this space here? Well, the space is really just the exact size of this table, which is based solely on M. So I have just m space overall. Nice. So this is already looking like an efficient implementation, especially because we're not dabbling with any exponential complexity over here. I think let's work on the code now. So here we are back in the code editor, let's start like we usually do by creating our table. So I'll store this table, and like we discussed in our drawing, we need to make it roughly the size of our target sum. So I'll create an array, I need to make sure that the indices lineup directly or at one index, that is exactly the target sum. So I'm going to initialize it to target sum plus one, right. So that means if I pass in target sum is seven, when I create an array of length eight, which means that its very last index is seven, which is perfect. Cool. And while we're here, we'll go ahead and actually initialize this array with all false values. So I'll do that. I need to make sure that I seed index zero with a true symbolizing that the target sum is zero, it's always possible to be made, even before I look at, you know what's in the numbers array. So I'll just go ahead and say, table at index zero is equal to true. So let's just console dot log, what this table looks like so far. And I'll just run, let's say the second example here. So I should see my array of length beat, up need to go into this tabulation folder. Now I'm going to run this guy. Cool. So it looks like a good table, right? eight elements. And I do have a true at index zero. Nice. So at this point, now I want to actually lay out the core algorithm by iterating through the table, somebody who's a regular for loop here, let's say let i equals zero, go up to I can even include if I wanted to, up to an including the table dot length is you could also say a target some here some thing based on target some I'll do i plus plus. And here I'm going to choose to iterate using, you know the manual index, because I know I have to do some arithmetic to look ahead on the array. But as I iterate through every element of the array, I want to actually check you know if this position is true, so I'll check if table at index AI is true. Cool, they know inside of this if statement, I need to basically jump ahead or look ahead, based on what's in my numbers array. But I only need to do that if my current position is true, right, I only got to look ahead, if it's even possible to get to this current amount, right, the current amount I'm sort of tracing through would be at index i. So at this point inside of my if statement, what I want to do is now look ahead for each a number of this array, so a nested loop here. And I just want to look at the elements of this array. So I'll say for, let's say, let num of numbers. So I'm grabbing each element this time. And now for this number element, I want to look that many spaces ahead. So I want to look at basically table at index i plus the number. Cool. So imagine that I'm on like the first iteration of this, I know is going to be zero. So that's the very, very first index of my table. And if my num is five, then I'm going to do zero plus five looking five spaces ahead right just like we did in our trace. And for this position, that is like five spaces ahead. I want to assign it to be true, basically saying that, hey, if my current position is reachable, and I can take a step of exactly numb then that must mean position. I plus numb is also reachable write that entire for loop goes inside of this if statement, right? So this code is looking pretty good. So far, I think the last thing we need is just to return our final answer. After we're done iterating through the table. So here I'll just return table at index targets. Cool. So let's, let's go ahead and run these examples. Maybe we'll have to debug some stuff. Looks like I have a probably an infinite loop. Yeah, my program crashed with some massive error. So let's debug this one together. There's something that I'm not considering in my code. But we did consider when we drew it out. So something that is probably breaking right now is having to do with like the bounds checking. Something that's kind of unfortunate in JavaScript is this characteristic, this may have different behavior, depending on the programming language that you're following along in. But I know in JavaScript, I had some array, let's say, array, and I set it equal to just a handful of elements, let's say a, b, and c, if you actually do like array, at some out of bounds index, like index, I don't know 10. And I assign it like x, that will actually change like the effective length of this array. So if I take a look at what happens here, so I'm just running this kind of offhand example, when we run this, notice that we have about seven empty items before we have the X. Basically, when you do this type of weird assignment in JavaScript, it's going to like lengthen your array. That way, it makes sure that this Hey, new element of x is placed at exactly index 10. And so why is that relevant? Well, in my code, here, I don't really check if I'm out of bounds, right, and I just assign to something that's potentially outside of the current length of my array of my table, rather. And if I do that, then this condition is a little suspicious, right? Because if I do this assignment on line nine to an out of bounds position, then the length will get longer. And that's sort of like a circular argument, right? I keep iterating, only to make more stuff out of bounds, only to lengthen the table even more. So that's why I ended up with like an infinite loop over here. One way I can fix that is to just maybe limit this, so I won't make a table outline that's a little too dynamic. I know, I can basically stop once I reach the targets. So that might actually be the better move over here. Pretty, pretty fair bug though. With that, let's go ahead and run this code again. give that a shot. Here we see true true, false, true and false. And that is the correct answer over here. Nice. So here's a nice tabulated solution for our khamsum problem. And we already spoke about this code having n m times n, time complexity. Now it's really obvious, right? Because I can see how these exact the loops. And our space complexity is simply based off of M right. Cool. With that, let's head back into the drawing board for another problem. All right, now let's work on the house some variation of this problem. So like before, I'm gonna take in the target some as well as an array of numbers. But this time around, what I want to do is return an array containing a combination of elements that actually adds up to the target sum. Right? So here, I want to return exactly one way if it's possible. And along with that, let's say that it's actually not possible to return the target sum, then I should just return the null value. And if it's the case that there are actual multiple combinations that can generate the target sum, I can return any one of those. So we expect the strategy for this one to be very similar to our last problem. However, we will need some nuanced logic to return the array now, right? Because I don't want just boolean data once return an array. So let's sketch this one out. Let's see I'm stepping through this example. Or my target is seven and my array is 534. For this input, a reasonable answer to return would be an array of three comma four, right? Because I can add three plus four to get seven. You can also I guess, switch around the order of these elements. So if you also returned four comma three, I think that would also be a valid answer. Point being, I just want to return at least one way that can generate the target. So we know we want to solve this one with tabulation. And so we'll just follow our recipe right? And I need to construct an initialize some table, we already know that the size of this table is going to be based on the target sum, so roughly seven elements. But we of course, have that classic off by one nature, right? Because I want the last index of this table to align with the actual number argument. And in general, I'm going to initialize this array with a length that is target sum plus one. Cool. So now that I have the correct shape of table, what do I actually have to initialize as elements inside of this table? Previously, like the Boolean version of this problem, we initialized everything as false, right. And the reasoning there was, before we actually try all the possibilities, we're going to just assume that it's not possible to generate every quantity. We just want to adapt that for this new type of problem. They tell us that if it's not possible to generate amounts, then we should just return null. So I think null is actually a great initial value to just put everywhere in your table. So at the start, we're going to assume that none of the values or none of the amounts are actually possible to be generated. No is sort of the analog for false in this rendition. Cool. So now that I have most of the values added to the table, now I need to figure out what's the appropriate seed value, I need some actual data to begin and get the ball rolling on this one. So like we expect, it's about having an amount of zero, right, no matter what if you're given an amount, or a target sum of zero, and no matter what array of numbers are given, you can always actually generate that quantity of zero, by just returning the empty array, right, you can take zero elements from the numbers array, and there you have an array whose sum is zero. So of course, that means at index zero of our array, we're going to initialize it with just an empty array. And at this point, what we want to do is, of course, iterate through our table and come up with some logic we can use to fill out further positions to the table based on our current position. So let's say we begin at index zero. So here I am. And I have to figure out which further positions of the table should I be looking at, we've seen this pattern before, I should just base that off of what I have as options in my numbers array. So look at my first number of five, that means I could look exactly five spaces ahead in the table at this position. And so if I know that my current position is possible, but I see that I have an array and my current position, I know it's possible. If this current position is possible, then a position exactly five steps later would also be possible, right. And so what I'll do is I'm just going to copy my current value into my further value. In other words, I want to put an empty array at position five, right. However, I also have to include the number that I'm using right now, right, I took a five to actually get to this position in the first place. So I'm also going to add a five into that array. Cool. If I do a quick spot check, this is a reasonable thing to do. Because if I look at the state of that index five, it contains an array of five, meaning that hey, you can return a sum of five by just taking a single five. And with this, I want to do the same logic for the other numbers of the numbers array, right. So if I have the three, I'll look three positions ahead of my current place. And I'll also start by copying over the array at my current position. So I copy over the empty array. And after I copy that array, I actually want to populate it with the number that I took to get to the spot in the first place. So I just go ahead and put a three in that array. And the same thing happens for before. So if you go to the next iteration, you see that our current element is null, that means that the amount, right in other words, our index amount, is actually not possible, it's not reachable, meaning we can't get an amount of one by adding up any numbers of the given array. So what I should not do is actually manipulate any further values from this position. So I just continue these base iterations. Same thing happens when I'm at index two, things get more interesting when I'm at index three, right, I see that the element here is not null. So it is possible to generate three. And so what I want to do is go ahead and look at my my forward values, and for copy my current array over. So I know I don't need to look five spaces ahead, because that would be out of bounds. So I'm going to look at the position exactly three spaces ahead in the table and copy over my current array like this, what I also need to do is make sure that I include the number that I'm using right now, which is three, right, because I'm looking three spaces ahead. So I end up with three comma three, at index six of my table, which is reasonable, right, I also want to do something very similar for the position four spaces ahead. So I'm going to copy over my current array, but then also include the four that I'm using up right now. So now have three four over here. Cool. At this point, you can kind of see where this is heading right, I've actually already populated index seven of my table, and three comma four is exactly a one way we could create the seven. So we could stop here. However, let's say that you continued the algorithm. So in the next iteration, I know that this space for the indices ahead would be out of bounds, so I don't need to look there. But according to our general logic, I would actually manipulate the spot of the table three moves ahead, right. And so what I'll do is if I, you know, follow the same exact logic, I'm being consistent, not really changing any of the rules here, I would copy over my current array into that position. So I basically overwrite index seven, with just an array of four. But then at this point, I would also be sure to include the number that I'm using right now, which is three, right, because I'm looking three spaces ahead. So I end up with four comma three, which happens to be like another way I can generate these seven. So it doesn't matter if you actually continued this algorithm, right? So if I continued on basically just keep iterating until I hit the very And and what do I know the element at index seven is exactly an array of four, three. Cool. So the point is you actually don't have to like exit early. As you iterate through the table, you'll be okay to just finish all of your iterations. And you'd still have a valid answer, right? Because in this problem, they just want any way to actually make the target. And if I take a quick look at my table, I think all of the stored values here make some logical sense. For example, if I look at index two, there's a null, meaning that it's not possible to make a two by using elements of the array. In a similar way, if I look at index four, there's a sub array of four, meaning you can just take one four, to make a some before. Similarly, you can take two threes to make a sum of six. And of course, you can make a seven by doing four and three. So now that we have a plan of attack, let's look at the complexity for this. So of course, the M will represent the target sum, and our n represent the length of the numbers array. So if we begin with just the time complexity, we know that we're going to at least iterate through the table and the tables of size m, right. So I have at least an M in my time complexity. But for every space in the table, or in every iteration of that loop, I need to also iterate through all of the numbers in my number input array. And so I can multiply by n over here. Nice. But at this point, now, let's say that I actually need to take a valid step here, then what I also did was copy over the array at my current position into that forward looking position, I know that a sub array will be at most of length m, remember that m is our target amount, the worst possible or largest way we can generate the target sum would be to have an array of exactly m ones, right. And so we'll say that in the worst case, we have to copy over an array of an additional length m, that means our time is really m squared times. And now let's come up with the space complexity for this, we know that the initial size of the table is definitely based on M. However, in the long run, we could be storing a sub arrays as elements. So this is going to actually be a 2d table. In a sense, I know that the size of any particular sub array here is also going to be m for the same reason we just said, and so we'd have a m squared space complexity. So overall, we're pretty satisfied with the complexity of this. Because it's polynomial, right? It's not an exponential quantity. So it should run in a reasonable amount of time. All right, I think we're ready to hop into the code for this one. Alright, programmers back in my trusty editor, go ahead and tabulate this one out. So we'll need to create the table to begin, right. So I'll create my table, and it should have roughly targets some positions in it, really, I need to increase it by one. So I'll say array, and I'll pass in target sum plus one, that way the final index aligns, I'm going to go ahead and fill this table up with all of those nulls. Right, exactly what we did in our drawing. What I also need to be sure to do is make sure we populate index zero of our table with an empty array serves as sort of like a base case, in that we know that the target sum of zero can always be generated, right, you can generate the target sum of zero by taking no elements from the numbers array, right? If you took the sum of this array, it would be zero. Nice. This point, we need to iterate through our table. So you've seen this code before. I equals zero, go up to and I won't mess this up this time, I go up to an including target some I'll do i plus plus. So so far, very similar in shape to our last problem. Now that I'm iterating through every element of the table, I need to add some conditional logic. If you remember, from my drawing, we only did like the heavy logic when our current position was not null, right? Because if it is no, you can't even reach this current position in the first place. So I'm going to check if my current amount is attainable. In other words, if table at index i, which is my current amount, if that is not know, if it's not know that I can generate amount I goal. So if I can generate amounts i that i know some other things could also be generated. That is, I want to look forward in my table for every number in the numbers array. So I'll say let num of numbers, so I'm getting the elements of the numbers, right? And I need to look that number of positions ahead right. So if I'm at i right now, that means that a forward looking position would be at Table A plus num. Cool, and I need to assign something over here. So I'm going to do is start by copying over the sub array at position I at my current position, right. I know that if table I is not know the only possibility that it could be is an array, right can be array of zero elements are going to be an array of many elements. So no matter what that array looks like, I'm just gonna copy over that array. So I'm going to spread out all of the elements table I. And so far like this logic is looking pretty good because I'm basing my assignment on table i plus num off of table I, right. But in addition to that, I also need to include this choice of num. So I'm just going to add that into this new array as well using some syntax that we've seen previously in the course. Right. So I'm just copying over all the elements of the sub array at table I, and adding on an addition to that my current number. Nice. So this code is looking pretty good. I think at this point, all we need to do is let all these iterations finish, and then wrap up by returning the last entry of the table, or really the table at index target some cool, so not that much different from our last piece of code. Let's give this a shot. So I'll run how some and here I have a few diverse examples. And notice that some of them should return null because they're not possible. If we check these 32243, no bunch of twos and another No, nice, it looks like this code is totally working. Notice that we already have a very efficient algorithm kind of off the bat kind of comes free with tabulation, we definitely have m iterations in this for loop, we have n iterations on the inner loop. But then we also have to consider this copying over logic, right copying over an array will be at most m. So it's m times n times M, which is just m squared n, exactly how we spoke about it before. It's something I really want to emphasize is, you know, in isolation, if someone just asked you to solve like this house on problem, I don't think it's an easy problem. However, it probably feels almost trivial, especially since we just did cancer, right? There's a really, really straightforward progression to these problems. If you compare the house some and can some functions, they're very, very similar, right, really just changing the type that we store inside of our table, as well as the assignment that we do when we transition into our further elements of that table. Really just adjusting that logic for the data type that they're asking us for. Right? I had like some array answers here. And here, I have some Boolean answers. All right, so we're smashing through all of these tabulation exercises, let's do one more variation of this target some problem, that way, we'll have a little bit of an increase in the difficulty, it's time to revisit the best sum problem. So we're going to take in again, the target sum as well as an array of numbers. But this time we want to do is return an array containing the shortest combination, that adds up to the target sum. And if we have any ties for the shortest combination, we can return any one of those shortest. So you take a look at an example kind of like we did before, we have eight and an array of 235, there are a few different combinations, I can yield eight, right, I can do two plus two plus two plus two, or I can do two plus three plus three, or I can do three plus five. And among these options, the shortest one is obviously the three plus five. So that should be the return value over here. Obviously, if you returned five comma three, that would also be a valid answer. Nice. So let's start tackling this with our classic tabulation recipe. So we need to decide on the size of our table, like we usually say it's going to be based on the target sum and not so much the length of the numbers array. And like we've been saying recently, it would be nice if the last index of a table aligns with the original target. So although the last index of this table is eight, that doesn't mean that its length would be nine, right? Let's start by setting the proper values here. I always want to begin my my value seed with the type that's kind of relevant to this problem. In other words, they want us to always return an array if there is at least one valid way to generate the sum. And I know that trivially if I wanted to calculate the best sum for zero, no matter what array of numbers, I'm given the answer, there's always an empty array. So I'm going to be sure to store that value at index zero of my table. And now for the rest of the values in the array, I can't be sure if I can generate them yet. So I'm going to initialize them as null just like our last problem. At this point, we can begin our general algorithm by iterating through the table. And so what I'll do now is look ahead for my current position based on the numbers of the numbers array. So I'm gonna look two spaces ahead. Right now I see that there's a no inside of that position, which means I haven't quite found a way to create two, but I'm actually finding a way right now, right, I can copy over the array from my current position. But I also need to populate it with how far I'm looking ahead by so just to inside, and do this for the other numbers of the numbers array. So something similar happens for three, as well as for the five. Notice that since I just began the algorithm, this is a really the first time I'm finding any solution for these further values. And so let's keep these iterations going. I'm going to shift my frame of reference by one since my current position, good things and all that kind of entails that one is unreachable, right have no way to generate a sum of one it's actually don't do any logic from this current position. So we move ahead to do, since I do have a non null value, right, I have an array over here, that means I should look ahead to my future values. So I start by looking two spaces ahead, I see that at index four, there's a null, which means that I actually need to replace that now. Alright, I'm going to copy over my current array. But then I also add the value for how far I'm looking ahead by. So now I have two comma two inside of that index of four. So now I look ahead three spaces, which means I'm analyzing index five, I'll notice that index five already has an array there. And if I actually continue my sort of standard process, I would consider this array, I would copy over my current array in yellow, which is an array of two, then I would also add the element I'm looking ahead by which is three. And technically two comma three is one of the ways I can generate five. But it's definitely not the shortest way right, I can basically compare the length of these arrays, and notice that the original array of just five is definitely shorter, so that one should get the stay in this spot. Cool, I'll do something similar for five spaces, I see five spaces ahead for my current position yellow, there's a no right at index seven, which means that I can just go ahead and put this new array inside, so I copy over the array of two, then I add the five, right, because that's how far I'm looking ahead. I keep this process rolling, I'm going to modify two spaces ahead. Notice that this sub array I'm considering would still be longer than what's already stored at index five. So I actually don't use the three comma to write the five gets the same. Now I look three spaces ahead. And this would actually be the first time I found at least a way to generate six. So I copy over my current array, and also add my lookahead value. And now if I look five spaces ahead, there's a Knoll currently in that index eight, which means that I'm actually finding the first way to generate eight, so I copy over my array of three, and then I add my five for my look ahead. Cool. At this point, we've actually already found a way to generate eight. And you can probably foresee that this would be the shortest way in the long run. However, if we kind of continued this algorithm, there would actually be nothing wrong with continuing it right, it's not like we're ever going to put something longer in that spot. So if we continue our algorithm as normal, if I look two spaces ahead for my current position, that would mean I'm considering two comma two comma two, to my array of three, three, and obviously, I just maintain the shorter one, so the three comma three gets to stay. If I look three spots ahead, then I would be comparing two comma two comma three to a two, five. And of course, the two, five is shorter, so it gets to stay. And this process really just continues for the rest of the array. By the end of this algorithm, we do have the correct sub array stored at index eight. So before we jump into the code, let's look at the complexity of this one. So we'll say that MSR target's m, and n is the length of the numbers array. And so we know that in terms of the time complexity, it's going to be the same as last time, right? Still m squared times n, we don't really add any additional like costly logic, all we're really adding into the mix is just some conditional logic, which I know is just a very simple if statement. So that's not going to add any additional time. And the same way, our space complexity is still m squared, right, because I have a table of size m. But I know every element of that table could be an array, really just a two dimensional array. I think with that, let's hop into the code for this. Back in the editor, let's go ahead and code this one out. So we'll start by creating our table. And I'll make this table have roughly size of targets. But really target some plus one that way the final index aligns. And when I actually initialize this table, I want to actually make sure I have no values inside. So by default, all elements will be no basically symbolizing that, hey, these quantities cannot be generated yet, we have to actually prove that there is some combination that can make them. But the really important seed value is to make sure that index zero is an empty array, right? Basically, the one guaranteed way I can make zero is by adding no numbers of the array. This point I need to iterate through every index of my table we always do. I'll go up to the end including why not the target some nice and as I iterate through every position of the table, I only need to do the heavy logic if my current position is not no, right. So if my table and my current position so table I that is not no then I need to do some additional logic. So just to review, why do I need to only do the logic if my current position is not know? Well, let's say that it wasn't All right, if my current position is null, then I don't need to bother considering any further moves from this position if this position isn't reachable in the first place, right? So only if I have a non null position, then I'll actually iterate through my options in the numbers array, or I'll consider possible moves. So let's say let num of numbers. And as I iterate through these additional numbers, and then I need to actually look ahead in the table basically by that number amount. So I need to write some logic about table at index i plus num, right, just looking forward in the table. And we know based on our previous problem, it can some we ended up doing this logic, we just replaced that position with a new array, where we took all the elements from our current position, so table I, we spread out that array. Then we added additionally, this current number that we're using, right, so we have to actually add that into our move list. Cool. However, now we want to actually add some like, min value logic, I want to prefer to always keep the combination array that is shorter, right, I want the best way, not just anyway. And so I think the move is or refactor this code a little bit. I'm going to actually do this step, maybe separately above. So I'm going to save this to a variable. And I'll call this my say, combination. This is like my candidate combination, I'm not sure if this one is actually the shortest right now. And so if I do the assignment over here, then I'll just use combination. But I only want to do this assignment on line nine, if a condition is met, right, so what I want to express is, like, if this current combination is shorter, than what is already stored, right, I only get to replace something if it's shorter. That's pretty straightforward to express and an if statement, right? So I want to check, hey, if. And my condition needs to be if the thing already there. So if the element at index i plus num. If that thing is actually bigger than my current combination, so combination, not length, and I also need to be sure to do that element dot length, right? If my combination is shorter than my current thing being stored in at that future position, then I'll go ahead and replace it. Nice. So this code is looking pretty good. There's one thing that's kind of missing, but I think we'll just run it and we'll kind of debug it together. So let's say at the end, I return my table at index Parkinson. Cool. So compared to our last code, we only added this conditional logic. So if I run this, something's gonna go awry. And kind of foresee it's very an error. And the error message is pretty on the nose over here, it's saying, alright, cannot read property length of null. Right. So it's really about, particularly this expression. So we know that our initial state of the table has a bunch of null values. So when I look ahead to a future value, and for the first time, I actually have a way to make it, then I'm going to check the element, which is no dot length, or I cannot do null dot length. Right. And so I need a way to kind of get around that. If ever, I have like a null value in my table, then I can just go ahead and do the assignment, right, because I found at least one way to make that. So what I'll say is I can put an order here. So I can still do the assignment will say not table, i plus in JavaScript knows a falsie value. So let's say table at index i plus num is no. So this expression is no which is really false. So if I say not false and evaluates to true, so I'll go ahead and still assign that combination to that position, even if it isn't all, another great reason for using this syntaxes. We know toward like the tail end of our algorithm, we're going to potentially look out of bounds, which means that these elements could be undefined and undefined is also a falsie value. Right? So here I'm saying not false, which triggers as true, which means I would still correctly assign those positions. Right. The important thing is I need to be sure to not do this expression. When the element is null or undefined. You can't say no dot length, and you also can't say undefined, not length. So this expression will guard against that. So with that change, let's go ahead and run this. Cool. So 73544 and a bunch of 20 fives nice, especially looking at that last example, like 20 fives is obviously the best way to make 100. And just to be super Sure, should be no different if I even switched around the order of these sort of ways. So what I put like, one, five in two. That should still give me a bunch of 20 fives, right? Because it's really looking for the best way to generate each some, which would mean the smallest summary. So there we have it a pretty interesting tabulation a problem. However, I do want us to recognize how similar it is to our last how some problem, right? So here, I have best some on the left and how some on the right, notice how similar the code is. Really, the only difference is this if statement, right? For the best some problem, I just added this conditional logic. And all this does is prefer to keep the shorter combinations over the longer combinations, right, because that's what I want to return in the long run. Whereas in houssam, we don't really have any preference in either direction. Awesome. So let's go ahead and go back to the drawing board, I think we'll kind of switch things up. Let's switch gears a little bit and revisit those string problems with tabulation. Let's begin with that can construct problem. So we're going to take in a target string as well as an array of words in a word bank. And this version of the problem and I want to do is return a Boolean, right, true or false, indicating whether or not I can make the target by concatenating, any number of elements from the word bank array, and I can reuse elements in the word bank as many times as I want. So we've seen this problem before. But just to refresh, let's take a look at an example. So let's say my target was abcdef. And I gave you this array of some different substrings. In this particular scenario, it is possible to create abcdef using members of the array. So the answer here is true. And one way you can actually create your target string is by just doing ABC plus the fright. Alright, let's start to apply our tabulation to this one. So I'm going to create a table and you probably already recognize that, why I want to base the size of this table off of the target string, right, because that's really where I have some problems, I'm going to be decreasing the size of my target, or in other words, building up to my original target over time. And so I know that I'm going to make it about the target string and not the array of words, because I'm never going to remove a word out of the word bank. So I'm gonna start with this table. And here the sizing is is kind of particular, because we need a way to have this table or represent information of a string, right. Whereas before with our number problems, it's very straightforward to just use the index as the actual number. So we're going to use a little clever have an encoding here. And so what you'll notice is here, I created an array of size seven, right, table dot length is seven right now. And in general, you'll want to size your table by doing target dot length plus one. So we still have that sort of off by one scenario, that's kind of a very characteristic of these tabulation problems. And so what will this table represent? Well, if I just think of the characters of my original target string, I know they have corresponding indices sort of like this, you're probably wondering, then what's the point of having that extra spot at the very end of the array, right? What I need to store index six, if it has no corresponding character. And the reasoning is, we need a way to actually represent information about the empty string. And so here's the pattern we're going to use. If I look at index zero of this table, then I'm actually making a statement about the empty string. So pretend that if you look at a spots of this table, depending on what that index is, you're considering the substring, that starts at index zero, and goes up to but not including your current position. So we'll see how this scales over the rest of the indices here. So imagine that now is at index one, I'm looking at index one, and you're really just looking at the A string, for at index two, then you're looking at a B, if you're at index three, then you're looking at ABC, for index four, then you're going A through D, if you're at index five, you're looking at A through E. And so the really key insight here is if I'm looking at index five of my table, it means I'm making some logical statement about the substring starting from index zero going up to but not including index five, right? So if I wanted to make a statement about the entire target string, then I should actually look at index six, right? Because imagine you start your substring at index zero always, and you go up to but not including your current index. And again, the whole reason for this is I need a way to actually make a statement about the empty string. And so speaking of the empty string, let's go ahead and talk about our C values. We know that a really core you know pattern in this can construct problem is to use the fact that hey, if your target string was empty, no matter what array of words you were given in your word bank, that is always possible. So it's always true. And so that means at index zero of my table, I can go ahead and seed a boolean value. How do I know what type to even store as the elements of my table? Well, if the problem is asking me to return a Boolean, then it's probably a good To actually just begin with some boolean data in your table. So if index zero is going to start as true, I'm going to initialize the rest to be false, right. So I'm going to consider all of these other sub strings or prefixes as not possible until I prove them otherwise. Alright, so let's begin our general algorithm. So we'll begin like we always do by just iterating, from left to right through our table. And just we have some notation to make it easier to kind of translate the information in a table to the statement it makes about our target string, I'm going to have the characters up top, and I'm going to like them up based on the substrings that we're really considering here. So if I begin at index zero, like we said before, that means I'm making a statement about the empty string, right, I see a true inside of this position, which entails that the empty string can be constructed using the word bank. Cool. At this point, I have to figure out how I can look ahead to my further values in the table. And I know that I can do that based on the choice of words in the word bank. So let's look at one of our words like a B. So something great about this option of a B is it actually is something that begins from my current position, right. In other words, if you look at my current position in yellow, the characters below it start with a and follows with me. So taking an AB right now would be a totally legal move, right those characters align. And so what I'll do is, I'll go ahead and look two characters ahead, by look two characters ahead in my table, that just means look two indices ahead. So now I know that this position of the table is actually reachable by just taking a B. So I put a true here. And now at this point, I would do this same logic for the other words that have their characters aligned. In other words, ABC is also some matching word. So I'll go ahead and make that a true. And ABCD would be the only other one that actually aligned its characters. So I'll make that one true. Again, how do I know to actually put this information in the table, it's really just the length of the word right, my current position right now is at index zero. And I'm looking at a word that is four characters long. So that means I just look for characters ahead to store my Boolean. Cool at this point, I'm actually done looking at all the words for this current position. So now I can move my current ahead one. So now I'm at this position. And so what you'll notice is in this current position at index one, we have the value of f, which means that it's false, right? It is not possible to actually generate in a write, there's no way you can ever just give me back an A, because all of your substrings in the word bank are too long, right? So I don't actually need to do any of my heavy logic from this position. However, on the next iteration, when I have something that's true, then I need to do my heavy logic. And if I do a quick spot check, I have a true stored in this position to write if I'm looking at the value stored at index two, that means I'm making some statement about indices zero up to but not including tomb. So really just the substring AB. And I know that if it's true, that means it's possible. And looking at the elements of the array, it's definitely possible, right? You just took a beat even get to this position. But I need to look ahead. So I'm going to look at any words of the word bank that match starting from my current position. Really, the only option here is CD, right. So if you look at my going characters, right now, that means I can take this CD. And so what I'll do is I'll look two characters ahead and place a value at that position. That's true. But the value stored at this position of the table is already true. So nothing really changes here. And there are no other words of the word bank that actually begin with a C. So I'm actually done with this pass. So now I move my current again. And I look for any matching words that begin with like a D, and potentially other characters after that, really, the only one here that works out is D, F. And so looking at my going characters, now I'm basically considering this chunk, right ABC followed by TF. So I'm going to look three positions ahead, because the word I'm considering right now is three characters, right def is just three characters. If I do that, I'd be looking at this position. And I'd be making this one a true now basically saying that it is possible to construct abcdef colon at this point, I would do the same thing for the rest of the table, we know that nothing else is really going to change within this table. And so by the end of the algorithm, we have a true start at index six, which means that our entire string is possible to be constructed. Right? And so if we do a quick sanity check, if I look at the table at index six, that means I'm making some statement about the substring that starts at zero and goes up to but not including index six, which would really be the entire A through F. I know that it's true. So that substring is possible to be generated, right, which was our final answer fairly elsewhere. Like maybe at index five, there's a false here, which means that I'm making some statement about indices zero through four, right, so ABS C, D E. And since there's a false at this position, that means that it's not possible to generate this string. And looking at just the elements of the word bank, it's definitely not possible to ever get a, b, c, d, e. Cool. So the data within our table a does look consistent with that, let's actually talk about the complexity. So we know that m is going to be our target string, whereas n is going to be the number of words in the word bank. And so we just consider the time complexity right now, we know that we're at least iterating through every element of our table, and our table was roughly size M, right? However, once we were at some current position of a table, we had to look ahead based on all of the words in the word bank. And so that would be an N operation for every iteration of m. And even still, as always, considering the words coming from my current position. And to make sure that their characters aligned, right, I can't just take arbitrary words, from the word bank, I have to choose the ones that have their characters matching right now, I know that the maximum length of any word or the maximal length of a match, I would have to do would basically just be in terms of M, so give me an additional m term. So overall, I'm looking at m squared n for the time complexity of this one. And besides that, we already know that the space complexity comes from the table, which is just a array of Boolean, so just m space overall. And so from a bird's eye view, it looks like this strategy would be an efficient one, right, we have a polynomial time complexity, but more importantly, a non exponential complexity. So let's code this one up now. Alright, let's code up this can construct problem. So we'll start by creating our table. And so we'll say create a new array of size target dot length, plus one just like we spoke about in our drawing, then from there, I need to give some initial values to my table. So I'm going to go ahead and fill this one up with all falses. So we're going to assume that every like prefix of our target cannot be constructed until we prove it otherwise, then I need to actually also initialize table index zero to be true, symbolizing that the empty string can always be generated, right? Then from there, I need to iterate through every index of my table. So I'll say i equals zero, just go up to the entire table. And here just to be safe, I'll say is less than or equal to target dot length. Nice. And as we iterate, we actually need to check our current positions value. Recall that in our strategy, we said we only need to do like the heavy logic, if our current position is reachable, so our current position should be true. So I'm gonna go ahead and check Hey, if my table at index i is true, then I'll do some additional logic. If I enter this if statement that I need to consider any further choice of words I can use to progress to other spots in the array, right? So over here in this if statement, I want to iterate through every word of the word bank, right? So I'll say four letter word of word bank. Nice. And so what I want to do, as I iterate through all my possible words is I need to make sure that these words I iterate through actually match their characters to my current spot that I'm at. In other words, if I am at my very first index of a over here, then when I iterate through all the options of words, I need to make sure that I only use a valid words that match like a, b, or ABC, or ABCD. Right? There's no point of me trying to use def right now. Right? I need to actually kind of narrow down my words, based on whether or not they match characters, right. So to do that, I can check pay if. And what I want to express is, you know, if the word matches the characters, starting at position, I position I goal, it's not going to translate that into some code. Well, checking if a string matches to something straightforward, just about using equality, right? But I have to figure out how can I look at the correct you know, substring of characters within my original target, right? It's pretty straightforward. All I need to do is say target dot slice. I'm going to slice starting at index i. And how far do I want to go? I basically want to go like the length of my word. So the move here is to say go from I up to i plus word dot length. And so let's kind of break down this logic. So what it's trying to do is check if your word matches your characters starting at position I. So let's check an easiest scenario. Let's say that my current position is zero, right? So right now is zero. That means I'm at this position like this ABCD f example. Let's say right now that my word is ABC. So if I look at this slice, it's going to slice starting at index zero, up to and not including zero plus three, right, the length of my word is three, zero plus three is just three, that would actually look at this run of ABC. Good. And let's say we were somewhere further in the string. Let's say we were actually at this D character over here. And so if I'm at this D character, that means my value for AI is going to be three Wragge, oh 0123. And the current word I'm considering, let's say is this def, the length of this word is three. So this ending index is going to be three plus three, which is six. And so I would actually slice starting at this character, three, going all the way through the end, right? 345. And I always exclude the ending index of six here. So this logic is good, right? All it does is check if my current word matches the characters starting at index i, goal. So if I enter this if statement, then I have a successful word. So I should actually set a true in that forward looking position. So I can just set table. And when I look ahead, how far do I need to look? Well, you need to look exactly this far ahead, right, i plus word dot length, matching me we're at index zero of our target string. If I take ABC, then I end up word dot length characters later, three characters later in this example, right. So I'm saying table of i plus word dot length, that should be set to true. Cool, notice how similar This is to our previous number, like our target some problems, except now we have to adapt this four string data. So now I jump ahead based on the length of the word I'm choosing. So this code is looking pretty good. Let's go ahead and do our final return value. So I want to return the table at Target dot length, right? Basically, if I'm at the last position, or at Target length, a position of my table, that means I have the information for the entire target range starting at index zero all the way through the end of the target. So here are some examples. Let's give this a run. So I get true, false, true, false. Nice, and there we have a nice working solution. So take a moment to let this code sink. And really the most tedious logic here is probably this particular slice. So really try to make sure you kind of understand how this comment is implemented using this code. If you remember our previous recursive immobilization strategy for this problem, we needed to actually have logic to make sure our prefixes match. This is sort of the analog for that, right. So we already spoke about the time complexity for this one, but it's already in plain sight, right, we have m iterations here, we have n iterations here. And then the additional work we do is this slice, right, I'm slicing from my target, which is also M. So overall, I have m times n times m, or m squared, N. And of course, the table just takes up definitely m space. Cool. So with that, let's head back into the drawing board. All right, now let's revisit the count construct problem. So we're still going to take in a target string as well as an array of words in a word bank. And this time, we want to return a number, right, we want to turn the number of ways that the target can be created by concatenating elements of that word bank, we can reuse elements of the word bank as many times as we want. So let's take a look at an example of this. So let's say I had this input, so I have the target of purple, and I have an array with some substrings. The return value here should be two because they are exactly two ways to create purple. You can do perp plus le, or you can do p plus u r plus p plus le ran, those are two distinct ways. So notice in this problem, we really just want to return to the numbers. So we'll just keep that in mind. So let's follow that classic tabulation recipe, we'll initialize our table with roughly the size of the input that changes, which is really going to be the target string, right, we want to actually use overlapping subproblems on the target string. And like we always say, we're going to have a little off by one scenario here. Although the number of characters in my purple string is six, I want to initialize a table of length seven. In general, you want to make sure your table has target dot length plus one a number of elements inside. That way, I have a way to represent the empty string, just like our last tabulation. And so we'll start by thinking about what are some proper seed values for this problem. I know that when I have an empty string, empty target strings, my input, no matter what array I'm given, you can always generate the empty string right by taking no elements of the word bank. So the answer for the target string should be one, right, there's one way to make the target string and that is to concatenate nothing together. So I'm going to store the value of one at index zero of my table. Now I have to decide how to initialize the other values in my table. Well, I know that it should be number data, right? It's really important that you try your best to make your table contain a consistent data type. If I remember my similar counting problems like Fibonacci, or even the grid traveller of the move here is actually start those other spots in the array at zero, basically saying, you know, at the start, I have zero ways to generate these different prefixes of purple. And then as to actually find distinct ways, I'll go ahead and increment those spots in the table. But we'll get to that. So we'll iterate through our table from left to right, of course, beginning at index zero of the table. And as we do this, I'll also up top show the characters that we're considering within our table. Remember, the really important pattern here is when I'm at some position of my table, that means I'm encoding some information about the substring that starts index zero, up to but not including my current index, right. So if I have some information stored at index zero, that information is about the empty string. From my current position. Now I need to consider any words of the word bank that I can take right now to give me another spot within the table. In other words, since I'm at index zero, I need words that start with P. So let's say we looked at this first word of perp. And so if I looked at that word, and I took it, that would bring me a somewhere else within the table, right have to look forward like we usually do. So they'll end up at this position index four. Again, notice that we have the kind of off by one scenario here. So although I'm going to write information into index four, that informations about the substring, that spans from zero, up through index three, right, and what I need to do now is take my current value in yellow, actually add that into my future position. So I just add one into the zero. And now index four contains a one, right basically saying that so far, we found a one way to construct the string perp. And then from there, I look at any other words that the word bank that match right, so I'll just look at the next word of P. Again, just moving left to right through our word bank, right now, I'm still at my current position. And what I do if I use this P is I only look one character ahead, right, because p dot length is only one. What I do in this future position is of course, just take my current value in yellow, and I added into this position, so zero plus one is just a one. There is one more word I can use right now from my current position, that's purple without the E. And if I look at that corresponding position, they'll bring me all the way over here. And again, I add the one into that position. Nice, I think that ends are a very first pass. And now we move our current position to the right. So now my current is at index one. So now I need to consider words of the word bank that begin with a you and kind of match characters throughout. The only option here is really just you are in the word bank, if I took a u r, that would bring me two characters ahead, right, so bring me over here, what I should do like always, is just take my current value and add it into that position. Now I have a one over here. There are actually no other words of the word bank that match from my current location. So I can just move ahead to the next spot in the table. If we're at index two, it looks like we need something that starts with an R, there's actually no words of the word bank that we can use from this position. And what we also notice is there's also a zero at this opposition. So this won't really do anything in our table anyway. So we can progress just the next spot in the table. Here's where things start to get interesting, right? So right now my current is at index three. So I need to grab words from the word bank that start with P and potentially have an L enemy. So what I can do here is just take this loan p, which will bring me one character ahead. If I look one character ahead, what I do now is I take my current value, right circled in yellow, and I add it into my look at value. So I increment that to two now. Nice. And then from there, I need to continue on my logic by just moving rightward in my table, right shift my current point of view. At this point, what I do is I again, look at any words that start with an le, and there's really only one that matches perfectly, I look two characters head because the length of Le is two. And what I do is I take my current value in yellow, and add it into that position, right, my logic throughout this entire algorithm is very consistent. And there we have a two in the last spot, you can probably already foresee that we have our final answer. But if we continue the algorithm as normal, or really, we've noticed that if we move to the next position, there's no word of the word that starts with E. So I don't do anything here. And once we hit the very end, we're actually done with our algorithm. Right? And we see that, hey, we have a two in the last spot a retailer which is consistent with our expected answer. Something I think is really important to do is not only recognize that our final answer is in the last position of the table, but really any element of this table should make some logical statement about our target string, right? So let's say I look at index for over here. I see that there's a two in that spot in the array. So what does that really saying? Well, if I am looking at the value stored at index four, that means I'm considering the substring That starts at zero and goes up to including four. So that really means I'm looking at purp, right. And if there's a two inside of the spot in the array, that means that there are apparently two ways to create perp. And if I do a quick check in my word bank, that's exactly true, right, you can just take the straight up perk, because that's actually a literal word of your word bank. Or you can do p plus u r plus P. Cool. That's some consistent information. And then from there, if I actually consider now what I can do from that position at index four, well, you could have taken an le, that's basically what we did when we traced through the algorithm. If you take an le, what are you really doing? Why is that value in the index six, also two? Well, for one, Ellie has a length of two. So that's why I'm looking just two spaces ahead in my table. And if I think about what like logical operation I'm doing, if I were to take an L e, and add it to all of the ways that I have in blue Rhino, then I would end up with all of the ways to generate purple right in my last spot, literally just taking the Le as an additional move, right? Pretty elegant logic. Let's talk about the time complexity for this though. So we know that, hey, r m is going to be the target and n is going to be the number of words in the word bank, this is really going to be about the same complexity as our last rendition, right? You can't construct. So we're gonna have m squared times n time, right, we definitely have m time for just iterating through every spot of the array. And we also have an additional n, because we have to actually look forward for every element of the word bank. And so where does that final m come from? Like we've been saying lately, it comes from actually performing the match of the check for matching characters. In a similar way, the space complexity is just going to be m, really just based off of the size of the table, we're only ever storing just some numbers within this table. So our space just stays at linear o of M space. All right, I'm feeling good about this plan, time to code it up. Alright, let's code this one out. Let's start by creating my table, it's gonna begin as an array with roughly target length, size, really target length plus one, that way the index aligns, right, I'm going to fill this entire table with zeros for now, because I know in the long run, I'm going to add into it. So zero is a great starting value. That being said, I have a really important c value, I need to make sure that my table at index zero actually starts as one, right, because there is one way to make the empty string right. Now from here, I can start my core algorithm by just iterating through every position of the table. So set i equal to zero, go up to an including target dot length, and hit every index, right. And so what I'll do is iterate through every word of the word bank. So let's word of word bank. And as I iterate through every word, I need to check if the word matches my current location right there has aligning characters. So you've seen this pattern before, I'll check if target that slice I'm gonna slice some characters out of my target. Starting index i right, this is my current position, I need to look at the next couple of characters based on the length of my word, I'm going to slice i to i plus word dot length. goal. So my current position is I, this will give me the next word dot length number of characters starting at position, I will check if that little segment is equal to the word I'm considering right now. And if it is equal, then I have a word that I can actually take, I can modify some further position of the table. In particular, what I should do is need to modify my table at index i plus word dot length. So I'm looking to head remember that our current position is I so I'm looking ahead in the table based on my current words length, what I want to do is actually increase that by some value, want to increase it by the number stored in my current position, right? That's what we did at every point of the algorithm. Nice. A common mistake that I see students make is, usually they have a tendency to just increment by one here, right? That's not the move Do you want to do is increment by exactly the number that's in your current position, right. Now, at this point, I think all we need to do is just return our final answer, which like we always know, is basically table at Target dot length. Cool, so I have some examples below. With their expected results, let's give this a shot count construct. So I get to 104 and zero, that's that we have a fairly large example, which should return zero, you can't possibly generate this final f here, because your word bank only contains a bunch of E's, and it ran fairly quick. So this solution is looking pretty good. Notice how small of a variation it is from our last problem. Hopefully you're really feeling the progression. So let's kind of quickly just take a look at That that was our can construct problem. So let me split these guys. Very, very trivial difference, right, instead of assigning a true value. Now I actually just increment because I have numbers within my table. That was short and sweet. Let's head back into the drawing board. Let's do one more problem together, let's revisit that all construct problem. So we're still taking in our target string, as well as an array of words in a word bank. But this time, you want to return all of the ways that you could construct the target by concatenating elements of the word bank. So here, you need to return a 2d array, that means a single element of the array represents a one combination that makes it a target. And like usual, we can reuse elements of the word bank as many times as we see fit. So we take a look at an example, let's say I gave you the target of abcdef, I gave you this array of words, you actually have many ways to create the target string, there are four ways over here. So each sub array represents one combination, where if you concatenate it together, you get the target. And so notice that I want to return every single combination as an element of the outer array over here, right. So if there are many ways to create something, then I want to return a 2d array. That being said, there are a few examples that we should keep really in mind having to do with our seed values that we kind of foresee. Right. So let's say I gave you this example, where your target is the empty string, and I gave you just some words in the word bank. The point is, since your target string is empty, your result should really be a 2d, empty array. So recall that the outer array here represents like the collection of multiple combinations. And here, there's only one combination that makes the empty string, right, so the inner array represents like that empty combination, we take no words of the word bank. And this is a reasonable thing to do, because they'll take give you this example, if your target was birds, and I gave you an array of cat and dog, then the result, there should be just a one dimensional empty array, right? The array over here represents the collection of combinations. And there are no elements here, right? There are no combinations at every yield of birds. So we'll keep those sort of seed value base case scenarios in mind. So let's trace through this example of abcdef. I'll start by initializing our table, we're gonna need a lot of room in this one. And so we know that the size of this table is going to be based on the target string, right, just like we usually do. And then we have to figure out what our seed values are, we kind of just spoke about them, we know that if I have the empty string, then I need to have a 2d empty array. And so we'll initialize that in our table, I know we're going to need a lot of room. So I'm going to spread out these outer brackets. Now for all of the other elements of our table, we want to make them just one dimensional empty arrays. And here's the reason why I write for these further values in the table, I have to actually check for ways to generate them. So for now, there are no ways that I found right, that's why I just have an array with zero combinations inside, except for index zero, right? index zero has one combination inside, that happens to be the empty combination. Cool. So let's start with our general algorithm. A lot of this logic will feel very similar to our last few problems. Now we're just adjusting it have to generate these combinations, right. So we know that we're going to try to visualize how we look at our string in terms of some prefixes, right. I know that if I'm highlighting some spot in my table, then that grabs the prefix or an index zero all the way up to midnight, including my current index of the table. And I'll light up those characters at the top over here. So what I need to do now is since I'm currently at index zero of my table, I consider words of my word bank that have matching characters from this position. So I can look at this a B First, if I took this A B move, then I should look two characters ahead, right, because a B has a length of two. So that brings me to this position in light blue. And what I should do is take all of the combinations in my current location in yellow, and actually copy them over into this future position. But when I do that, I also need to be sure to include in those combinations, the move of the word I just took, right, so that would just be adding an AB inside. Right. So again, kind of repeating that step, I copy over the contents of my current position in yellow. But then I have to make sure I add my word that I'm consuming right now into each of those combinations I just added. Cool. And now we would actually keep our current position here because there are still some other words of the word bank that have matching characters, right, I can take an ABC from this position, and something very similar happens write a copy over my current array. And then I go ahead and add ABC to it. This happens one more time for ABCD Awesome. At this point, I've considered all of the words that match from this current position. So I can move forward a little bit. At this point, though, I need something that starts with A, B, right? Because my current position has to be on it. And I actually don't find any words of the word bank, that kind of match from this position. So I actually moved my current once more. What about this position, I have to consider words of the word bank that have a seat, right, and there are a few that can use here, I can use CD, right. And if I do that, I look two characters ahead or two indices ahead for my current position. So I'm looking at this index. So if I look at index four, I have a really interesting situation, right? I already have a some combination stored inside of index four. And that represents one way we could make ABCD, right by just taking that loanword ABCD. But what I can also do is, I should take all of the combinations from my current position and just add them into that index for right, so I'm taking the A B from index two, just adding it to index four. But then I also need to add to that new combination of the word I'm using right now, which was CD. Cool. And then from here, you already noticed that, hey, there are so far, two ways to create ABCD, you can just take that loan word ABCD. Or you can do a B plus CD. So this logic is taking some some nice shape here. For my current position, there's actually one more word that matches, it's really just a single character see, so like I do, I look one character head or one index head in my table just next door, and I copy over the combinations from my current location in yellow. But then I also append the word I'm consuming, which is just an additional C, right. So so far, I have two ways to make ABC. At this point, I can progress my current spot and table. And I look for any matching words that start with a D, right? So that would be the D f over here. d f has a length of three. So I look three spots ahead in my table. And I need to add some information here, I copy over my current combinations in yellow. And then I have to append at the end of all of them the D F word that I'm using right now. So it looks like this. At this point, I can iterate my current position. So now I'm at index four as my current. And I look at any words that start with any, and there's actually just one over here just as EF. And so I look to character's head or to indices ahead for my current position. And again, this is a scenario where I need to append new combinations to this existing list, right. So if I look at my current position at index four in yellow, I'm going to take all of those combinations and copy them over into my light blue position at index six. And then from there, I also need to make sure that I append to them the word I'm using right now, which is an E, F. So those two combinations also get an F at the end of them. Nice. At this point, you see our final results at the last spot on the table, we continue the general algorithm, we would just move our current position to index five, which is actually a no words of x star with an F over here. So nothing happens on this iteration. And there we have our algorithm, right, that index six of the table contains all of those four ways we described how we can use to create our original target string, take a moment to look at this table. And notice how every position of the table should make some logical statement about the target string. Right, I can see that if I chose some position, let's say index three, that's giving me some information about the substring ABC, right, that prefix of my original target. And what saying is, there are two ways to make ABC, you can just take ABC, that's actually a word of my word bank. Or you can do a B plus C, right? That's the only other way that applies for all positions within my table. If I look somewhere else, like at index five, that means I'm reading some information about A through E. And if there's no combinations at this position, means there's no way you can ever generate ABCDE. And if you take a quick glance at the words in the word bank, that is correct, you can't possibly just generate ABCD. Overall, the information in our table and our logic that we did was very consistent. Before we hop into the code for this one, let's do the analysis. So we know that m is going to be the length of our target string. And we'll define n to be the number of words in the word bank. So like we described, when we did the memorization and recursive solution for this problem, we really can't do better than a sort of brute force, right? Because they're asking us to return literally every single way to generate our target string here. So really, the time complexity is going to be the shape of an exponential, right, so about n to the M, again, it will have some extra like multiplication terms of M in it. But overall, we're looking at an exponential time complexity. And what's the reason behind that? Well, we know there's going to be an exponent The number of actual combinations that we need to return. And I have to actually construct all of those combinations piece by piece. And so I'm looking at at least exponential time. And really, once something gets exponential, that's really the limiting factor, right. And similarly, we can say that the space complexity for this is also going to be exponential. Again, roughly, we know that we're going to have at least m space just from the straightforward sides of the table. But notice that every element of the table itself is going to be a 2d array. So in a sense, we're really dealing with like a 3d table in this instance. And so we know that at any particular position of our table, we're going to have potentially an exponential number of things, right, if you consider the very last spot in our table, just the last spot that already has an exponential number of combinations inside of it, but then I would actually have that throughout my entire table, so it'd be really a little more than just n to the M. But overall, I'll say it's roughly exponential complexity. And you can't really do any better here, right, especially in terms of the time, I'm really asking you to just do this in a brute force way, I think we'll actually be able to see that once we create the solution. Let's hop into it. Alright, for one last time, let's solve this all construct problem. So I'll begin by creating my table. By now we're tabulation masters, this is very, very formulaic. Create my table with roughly target size really target dot length plus one. And here, I need to initialize the elements of my table as arrays. So here, I need to be very particular and make sure I get a unique array in every spot of the array. So I'm going to do a fill using this index before, then I'll map over it that way I can generate a new array for every element here. Cool, let me just style this up. And so this should give me an array as every element of my table, right? Notice that the elements right now are just one dimensional empty arrays, right? These arrays represent the collection of combinations. At the start, they're empty, meaning that I have no combinations for most of these elements, except for one, right, we already spoke about table at index zero, right? That should be a 2d, empty array. And the reason was, still, the outer array here represents the collection of combinations. For the empty string, there is one combination that you can use to make it right, that is the empty combination, where you take no words to the word bank. Cool. So now I can begin my general algorithm by just iterating through my target, and you've done this many times before. So I'm iterating through my table. And what I'll also do is look at all of the words, right, so in every iteration, I'm going to consider some words of the word bank. So I'll say that word of word bank. And like before I send you to check that, alright, these words better have matching characters based on my current position. So I'll use that classic logic, right, seen it many times by now. And I'll slice a segment out of my target string, I'll say target dot slice starting index i, and grabbing the next word, dot length, number of characters. Cool. I'm going to compare that chunk to the current word that I'm iterating through. Nice. So using this statement, I'm sort of filtering down all the words, and only going to do some logic on the words that match based on my current position. Nice. And so we've seen you know, this nested for loop an if statement before, right, we're now we're adding our new logic within this if statement. Cool, all I need to do is really understand what types of data I'm dealing with here. All right, so let's say I just wrote this expression table at index i, well, that's going to do is just reference my current spot in the table. And if I think about what data it's going to be, it's definitely gonna be an array, right? It's either going to be like an empty array, meaning there are no ways to make this current target. Or it could be a multi dimensional array, where every sub array represents one way I can make the current target. So if this is all of the combinations that generate my current, then what I'll do is I'm going to map over them. Remember, in our drawing, what we did was kind of copy over all of our current combinations. But then make sure we also add our word that we're using right now to the end of each of those combinations. So that's what I'm doing right here, and a map over every summary. And what I'll do is, I'll just copy over the elements of that summary using that spread syntax that we've seen before. And then also be sure to add on at the end my word. Nice. And then that will give me let's say, our new combinations. So I'll just say that to a variable maybe. So new combinations is going to be a 2d array, right where every sub array represents some combination. And I want to take these combinations and add them into that further spots inside of my table. So I know I need to kind of look at in my table. So I can look at table at index i plus word dot length. I know that all positions of the table are going to be arrays, right? So this is going to be an array, there's going to be some data that might already exist at this position, right? So what I don't want to do is just assign you combinations, because then that would overwrite any previous existing combinations that I already found for this position. Instead, I need to make sure that I just add these new combinations to that list, right. So what I'll do here is I'll push to that array, I'll push all of the elements of my new combinations right here, I can spread this out, I want to spread it out. That way, I don't add any additional nesting over here, right. So this is looking pretty good. I think let's go ahead and return the table at Target dot length. Here, I have some examples down below. Let's give this a shot. Nice. And these answers look correct. Right, notice that these last two examples should return empty arrays because they are impossible to generate. Cool, let's hover over this code for a moment. Probably the most tedious logic here would be these two lines, right? Really try to understand how like these two lines translate into the logic we did in our drawing. So let's break down these lines again, right? line 11 is going to take all of the combinations in my current position in my drawing that was like my yellow box. And it's going to add our current word to each of those combinations. And then we're going to take each of those new arrays and add them to the list add our further position, right being sure not to overwrite, we already spoke about the complexity of this one, it's definitely going to be exponential, probably a little more than exponential. Right? If you imagine all the work we have to do using these maps and like spreading, we have to actually construct our result array, which is going to contain an exponential number of things. Right? So when I look at this last example, let's say that I made it a little longer, right? So let's say actually pull up the size of that input. It's going to be very, very slow might even crash, but let's just do it. So let's say I had a bunch of A's here, just for fun. So if we try this example. Yeah, it looks like our program still crashes, right? This is going to return a really, really a massive array probably can't even fit into memory over here, right? So we'll get like a stack size exceeded, even though this isn't even recursive, right? Just calling functions way too much over here. So no matter what we do, we really expect this problem to sort of demand an exponential solution. With that, let's head back into the drawing board where you can wrap up this course. Alright, programmers, by now we solve many different dynamic programming problems together. And I want to leave us with some final advice. So whenever you're starting to attack any dynamic programming problem, start by really taking a moment and noticing any overlapping subproblems. And then from there really focus on like the input to the problem, in particular its type. And from there, you shouldn't be able to recognize what is the trivial smallest input? So in our string problems, usually that means the empty string in our array problems, it's the empty array, or in our number problems, it's usually a number like zero or one, right? Is there some input wherever you don't really have to do any extra work to know the answer? The answer just sort of is what it is. Once you recognize this trivial scenario, or the base case, really, then you'll want to realize how you can relate this base case toward your larger inputs. And then you have two options, right? You can either think recursively, and use memorization, or you can think more iteratively and build a table using our tabulation formula. I think as you practice dynamic programming problems, I would actually always solve problems in two ways, one using memoization and one using tabulation. And of course, from that point on, it's okay, if you have like a favorite strategy. For me, personally, that's memorization. But that being said, it really helps to have options when you're in an interview. And once you've made that decision on, you know which route you're going to take, definitely, you know, slow it down and draw a strategy first. This is I think, the most important thing about just data structures and algorithms in general. Usually, when we draw our pseudocode, or draw our process on the whiteboard, we can recognize all of these edge scenarios. And it's much harder to do when we just write like the code out of the gate. And so I promise if you slow it down and take a moment to draw a visual, you're gonna find your work a lot more productive, as well as more intuitive when you explain it to someone else. And of course, through all of that, you're gonna have a lot more fun, you know, dealing with these quite hard topics. Alright, programmers, that's all I got for this course. Hopefully, you had fun, you know, learning this new topic. I definitely had fun teaching it to all of you. So what you want to do now is head down to the links in description and head to coder byte comm where you can actually practice this new topic. Thanks for watching, and I'll see you in the next one.