Make sure to study dynamic programming and matrix/grid problems. Leetcode medium only. Some of the questions were on algomonster.
DFS and matrixes
This repository documents my solutions and progress through the LeetCode 75 study plan. My goal is to master the key patterns needed for technical assessments and to become a more proficient problem-solver.
Status: ✅ Completed Link: LeetCode Problem
Click to view problem description
You are given two strings
word1
andword2
. Merge the strings by adding letters in alternating order, starting withword1
. If a string is longer than the other, append the additional letters onto the end of the merged string.Return the merged string.
Example 1:
Input: word1 = "abc", word2 = "pqr" Output: "apbqcr"
Example 2:
Input: word1 = "ab", word2 = "pqrs" Output: "apbqrs"
Example 3:
Input: word1 = "abcd", word2 = "pq" Output: "apbqcd"
/**
* @param {string} word1
* @param {string} word2
* @return {string}
*/
var mergeAlternately = function (word1, word2) {
let res = "";
let minLength = Math.min(word1.length, word2.length);
// Iterate up to the length of the shorter string, merging characters.
for (let i = 0; i < minLength; i++) {
res += word1[i];
res += word2[i];
}
// Check which string is longer and append its remaining part.
if (word1.length > word2.length) {
res += word1.slice(minLength);
} else {
res += word2.slice(minLength);
}
return res;
};
Status: ✅ Completed Link: LeetCode Problem 1071
Click to view problem description
For two strings
s
andt
, we say "t
dividess
" if and only ifs = t + t + ... + t
(i.e.,t
is concatenated with itself one or more times).Given two strings
str1
andstr2
, return the largest stringx
such thatx
divides bothstr1
andstr2
.Example 1:
Input: str1 = "ABCABC", str2 = "ABC" Output: "ABC"
Example 2:
Input: str1 = "ABABAB", str2 = "ABAB" Output: "AB"
Example 3:
Input: str1 = "LEET", str2 = "CODE" Output: ""
/**
* @param {string} str1
* @param {string} str2
* @return {string}
*/
var gcdOfStrings = function (str1, str2) {
let smaller;
let larger;
let match = "";
// Determine the smaller and larger strings to set loop bounds.
if (str1.length > str2.length) {
larger = str1;
smaller = str2;
} else {
larger = str2;
smaller = str1;
}
// Iterate through all possible prefixes of the smaller string.
for (let i = 1; i <= smaller.length; i++) {
let candidate = smaller.slice(0, i);
// Check if the candidate prefix can form both strings by repeating.
if (
candidate.repeat(str1.length / candidate.length) === str1 &&
candidate.repeat(str2.length / candidate.length) === str2
) {
// If it can, it's a valid common divisor. We keep the longest one found.
match = candidate;
}
}
return match;
};
-
Time Complexity
: O(min(N, M) * (N + M)), where N and M are the lengths of str1 and str2. The loop runs min(N, M) times. Inside the loop, repeat() and the string comparison can take up to O(N + M) time in each iteration, leading to a polynomial time complexity which can be slow for long strings. -
Space Complexity
: O(N + M). The repeat() method can create new strings that are as long as the input strings, consuming significant space.
Status: ✅ Completed Link: LeetCode Problem 1431
Click to view problem description
There are
n
kids with candies. You are given an integer arraycandies
, where eachcandies[i]
represents the number of candies thei
th kid has, and an integerextraCandies
, denoting the number of extra candies that you have.Return a boolean array
result
of lengthn
, whereresult[i]
istrue
if, after giving thei
th kid all theextraCandies
, they will have the greatest number of candies among all the kids, orfalse
otherwise.Note that multiple kids can have the greatest number of candies.
Example 1:
Input: candies = [2,3,5,1,3], extraCandies = 3 Output: [true,true,true,false,true]
Example 2:
Input: candies = [4,2,1,1,2], extraCandies = 1 Output: [true,false,false,false,false]
Example 3:
Input: candies = [12,1,12], extraCandies = 10 Output: [true,false,true]
/**
* @param {number[]} candies
* @param {number} extraCandies
* @return {boolean[]}
*/
var kidsWithCandies = function (candies, extraCandies) {
let result = [];
// Find the greatest number of candies any kid currently has.
let largest = Math.max(...candies);
// Iterate through each kid's candy count.
for (let i = 0; i < candies.length; i++) {
// Check if this kid can have the greatest number of candies.
if (candies[i] + extraCandies >= largest) {
result.push(true);
} else {
result.push(false);
}
}
return result;
};
Status: ✅ Completed Link: LeetCode Problem 605
Click to view problem description
You have a long flowerbed in which some of the plots are planted, and some are not. However, flowers cannot be planted in adjacent plots.
Given an integer array
flowerbed
containing0
s and1
s, where0
means empty and1
means not empty, and an integern
, returntrue
ifn
new flowers can be planted in theflowerbed
without violating the no-adjacent-flowers rule andfalse
otherwise.Example 1:
Input: flowerbed = [1,0,0,0,1], n = 1 Output: true
Example 2:
Input: flowerbed = [1,0,0,0,1], n = 2 Output: false
/**
* @param {number[]} flowerbed
* @param {number} n
* @return {boolean}
*/
var canPlaceFlowers = function (flowerbed, n) {
let count = 0;
// Iterate through the flowerbed to find empty plots.
for (let i = 0; i < flowerbed.length; i++) {
// Check if the current plot is empty.
if (flowerbed[i] === 0) {
// Check if the previous plot is empty (or if it's the beginning of the array).
const isLeftEmpty = i === 0 || flowerbed[i - 1] === 0;
// Check if the next plot is empty (or if it's the end of the array).
const isRightEmpty = i === flowerbed.length - 1 || flowerbed[i + 1] === 0;
// If both adjacent plots are empty, we can plant a flower here.
if (isLeftEmpty && isRightEmpty) {
flowerbed[i] = 1; // Plant the flower.
count++; // Increment the count of newly planted flowers.
}
}
}
// Return true if we were able to plant at least n flowers.
return count >= n;
};
Status: ✅ Completed Link: LeetCode Problem 443
Click to view problem description
Given an array of characters
chars
, compress it using the following algorithm:Begin with an empty string
s
. For each group of consecutive repeating characters inchars
:
- If the group's length is 1, append the character to
s
.- Otherwise, append the character followed by the group's length.
The compressed string
s
should not be returned separately, but instead, be stored in the input character arraychars
. Note that group lengths that are 10 or longer will be split into multiple characters inchars
.After you are done modifying the input array, return the new length of the array.
You must write an algorithm that uses only constant extra space.
Example 1:
Input: chars = ["a","a","b","b","c","c","c"] Output: Return 6, and the first 6 characters of the input array should be: ["a","2","b","2","c","3"]
Example 2:
Input: chars = ["a"] Output: Return 1, and the first character of the input array should be: ["a"]
Example 3:
Input: chars = ["a","b","b","b","b","b","b","b","b","b","b","b","b"] Output: Return 4, and the first 4 characters of the input array should be: ["a","b","1","2"]
/**
* @param {character[]} chars
* @return {number}
*/
var compress = function (chars) {
// 'read' pointer scans the original array.
let read = 0;
// 'write' pointer indicates the position to write the compressed character/count to.
let write = 0;
while (read < chars.length) {
let currentChar = chars[read];
let count = 0;
// Count consecutive occurrences of currentChar.
while (read < chars.length && chars[read] === currentChar) {
read++;
count++;
}
// Write the character itself.
chars[write] = currentChar;
write++;
// If the count is greater than 1, write the count as characters.
if (count > 1) {
let countString = count.toString();
for (let i = 0; i < countString.length; i++) {
chars[write] = countString[i];
write++;
}
}
}
// 'write' is now the new length of the compressed array.
return write;
};
Note on the if(count > 1) check: This check is crucial. Without it, the function would incorrectly write the number '1' for single-character groups. This can cause a "runaway" issue where the
write
pointer exceeds the original bounds of relevant data, potentially overwriting characters that theread
pointer hasn't processed yet, or in JavaScript, expanding the array.Example without the check: If
chars = ["a", "b", "c"]
:
- It processes "a",
count
is 1. It writes "a" at index 0, then writes "1" at index 1.chars
becomes["a", "1", "c"]
.- The
read
pointer is now at index 1 (which is now "1"), but it should be looking at the original "b". This leads to incorrect behavior.The
if (count > 1)
guard ensures that only counts for groups larger than one are written, which is the correct logic according to the problem description.
-
Time Complexity
: O(N), where N is the length of thechars
array. Both theread
andwrite
pointers traverse the array at most once from beginning to end. -
Space Complexity
: O(1). The compression is done in-place. The space used for variables likecount
andcountString
is mostly constant. For numbers like the index values and count, In most programming languages, a number variable uses a fixed amount of memory (e.g., 64 bits) regardless of whether it holds the value 5 or 5,000,000.
However, since countString is a string, its space complexity is proportional to its number of characters. This growth is logarithmic relative to the value of the count. For example:
If count is 9, countString is "9", which is 1 character long. If count is 1000, countString is "1000", which is 4 characters long. The space required grows with the number of digits, which is proportional to log10(count). In the worst-case scenario where count is roughly equal to N, the space complexity for this single variable is O(log N).
Dynamic Programming (DP) is a powerful problem-solving technique used in computer science for optimization problems. The core idea is to break down a complex problem into simpler, overlapping subproblems. You solve each subproblem just once, store its solution, and then use these stored solutions to solve the larger problem.
It's different from simple recursion because it avoids re-calculating the same work over and over. It's different from a "greedy" approach because it considers the overall best path, not just the best immediate choice.
This method is especially useful for optimization problems (like finding the maximum or minimum value) or counting problems (finding the number of ways to do something).
This means that the optimal solution to the main problem can be constructed from the optimal solutions of its subproblems.
This means that a simple recursive solution would end up solving the exact same subproblem multiple times. DP avoids this by storing the results. This storage is often called memoization (top-down approach) or tabulation (bottom-up approach).
Look for problems that have these clues:
- The problem asks for a maximum/minimum value, the number of ways to do something, or if something is possible.
- You have to make a sequence of decisions at each step.
- A decision at one step affects the available choices at a later step.
- A "greedy" approach (always making the best local choice) fails.
Status: ✅ Completed Link: LeetCode Problem 198
Click to view problem description
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing all of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given an integer array
nums
representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.Example 1:
Input: nums = [1,2,3,1] Output: 4 Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3). Total amount you can rob = 1 + 3 = 4.
Example 2:
Input: nums = [2,7,9,3,1] Output: 12 Explanation: Rob house 1 (money = 2), house 3 (money = 9) and house 5 (money = 1). Total amount you can rob = 2 + 9 + 1 = 12.
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function (nums) {
// This variable will hold the final answer for arrays of length 3+
let max;
// --- Guard Clauses for Edge Cases ---
// If there are no houses, we can't rob anything :(
if (nums.length === 0 || !nums) {
return 0;
// If there is one house, we must rob it. >:D
} else if (nums.length === 1) {
return nums[0];
// If there are two houses, we rob the richer one. $$
} else if (nums.length === 2) {
return Math.max(nums[0], nums[1]);
} else {
// --- DP Initialization ---
// For the second house, the max profit is either robbing it or the first one.
// This sets up our DP table correctly.
nums[1] = Math.max(nums[0], nums[1]);
// --- Main DP Loop ---
// Iterate from the third house to the end.
for (let i = 2; i < nums.length; i++) {
// At each house, we have two choices:
// 1. Don't rob house `i`: The max profit is the same as the max profit up to `i-1`.
// 2. Rob house `i`: The profit is `money in house i` + `max profit up to house i-2`.
max = Math.max(nums[i - 1], nums[i] + nums[i - 2]);
// Store the result of our choice back into the array.
// `nums[i]` now represents the max possible profit robbing houses from 0 to `i`.
nums[i] = max;
}
}
// The final element holds the max profit for the entire street.
return max;
};
Status: ✅ Completed Link: LeetCode Problem 790
The key to this problem is a systematic, step-by-step counting method. Instead of trying to find all tilings at once, we calculate the number of ways to tile a 2 x i
board by using the known counts for smaller boards (2 x (i-1)
, 2 x (i-2)
, etc.). This is the essence of Dynamic Programming.
To build our solution, we must identify all possible shapes of the tiled area's rightmost boundary. At any column i
, there are only three possible states.
-
full[i]
: The number of ways to have a perfectly tiled, rectangular2 x i
board.Column i ↓ +-------+ | X X X | +-------+ | X X X | +-------+
-
topEmpty[i]
: The number of ways to tile a2 x i
area, leaving the top-right cell empty.Column i ↓ +-------+ | X X X | <-- Empty +-------+---+ | X X X | X | +-------+---+
-
bottomEmpty[i]
: The number of ways to tile a2 x i
area, leaving the bottom-right cell empty.Column i ↓ +-------+---+ | X X X | X | +-------+---+ | X X X | <-- Empty +-------+
These three state variables will store the counts we need to solve the problem. Our final goal is the value of full[n]
.
These are the formulas that describe how to calculate the counts for a state at column i
using the known counts from previous columns.
To count the ways to fill a full 2 × i
board, look at the last tile(s) placed.
There are four possibilities:
Step | Last action | Description | Contribution |
---|---|---|---|
A | Vertical domino | We can create a full[i] perfect 2 x i board by placing one vertical domino to a full[i-1] 2 × (i − 1) board. Thus we must consider the full[i-1] permutation. |
full[i‑1] |
B | Two horizontal dominoes | We can create a full[i] perfect 2 x i board by placing two horizontal dominoes to a perfect full[i-2] 2 × (i − 2) board. Thus we must consider the full[i-2] permutation. |
full[i‑2] |
C | Tromino in a topEmpty gap |
We can create a full[i] perfect 2 x i board by placing a tromino block on a topEmpty[i-1] board with an L‑shaped tromino. |
topEmpty[i‑1] |
D | Tromino in a bottomEmpty gap |
We can create a full[i] perfect 2 x i board by placing a tromino block on bottomEmpty[i‑1] board the same way. |
bottomEmpty[i‑1] |
full[i] = full[i-1] + full[i-2] + topEmpty[i-1] + bottomEmpty[i-1]
/**
* @param {number} n The width of the 2 x n board to be tiled.
* @return {number} The number of ways to tile the board, modulo 10^9 + 7.
*/
var numTilings = function (n) {
// Define a modulo constant to prevent integer overflow for large results.
const MOD = 10 ** 9 + 7;
// DP state definitions:
// full[i]: The number of ways to completely tile a 2 x i board.
// topfill[i]: The number of ways to tile a 2 x i board with the top-right cell empty.
// bottomfill[i]: The number of ways to tile a 2 x i board with the bottom-right cell empty.
let full = new Array(n + 1).fill(0);
let topfill = new Array(n + 1).fill(0);
let bottomfill = new Array(n + 1).fill(0);
// Handle trivial cases
if (n === 0) {
return 1; // One way to tile a 2x0 board: the empty tiling.
}
if (n === 1) {
return 1; // One way to tile a 2x1 board: one vertical domino.
}
// --- Base Cases for the DP ---
// For a 2x0 board, there's one way to tile it (do nothing).
full[0] = 1;
// For a 2x1 board:
full[1] = 1; // One vertical domino.
topfill[1] = 0; // Impossible to leave a top gap.
bottomfill[1] = 0; // Impossible to leave a bottom gap.
// Iterate from i = 2 up to n, building the solution from the base cases.
for (let i = 2; i <= n; i++) {
// Calculate the number of ways to fully tile a 2 x i board.
full[i] =
(full[i - 1] + full[i - 2] + topfill[i - 1] + bottomfill[i - 1]) % MOD;
// Calculate the number of ways to have a top-right gap on a 2 x i board.
topfill[i] = (full[i - 2] + bottomfill[i - 1]) % MOD;
// Calculate the number of ways to have a bottom-right gap on a 2 x i board.
bottomfill[i] = (full[i - 2] + topfill[i - 1]) % MOD;
}
// The final answer is the number of ways to fully tile the 2 x n board.
return full[n];
};
Status: ✅ Completed Link: LeetCode Problem 62
Click to view problem description
There is a robot on an m x n grid. The robot is initially located at the top-left corner (i.e., grid[0][0]). The robot tries to move to the bottom-right corner (i.e., grid[m - 1][n - 1]). The robot can only move either down or right at any point in time.
Given the two integers m and n, return the number of possible unique paths that the robot can take to reach the bottom-right corner.
The test cases are generated so that the answer will be less than or equal to 2 * 109.
Example 1:
Input: m = 3, n = 7 Output: 28
Example 2:
Input: m = 3, n = 2 Output: 3
My approach to solving this problem was to identify a pattern and build the solution from the ground up. This is a classic Bottom-Up Dynamic Programming strategy.
-
Core Insight: The key constraint is that the robot can only move Down or Right. This immediately told me that to reach any given cell
(r, c)
, the robot must have arrived from either the cell directly above,(r-1, c)
, or the cell directly to the left,(r, c-1)
. -
The Recurrence Relation: Based on that insight, I formulated the central rule for this problem: The total number of unique paths to any cell is the sum of the paths to the cell above and the cell to the left.
paths(r, c) = paths(r-1, c) + paths(r, c-1)
-
Identifying the Base Cases: I then considered the "easiest" parts of the grid. For any cell in the very first row, there is only one way to get there: moving right continuously from the start. The same is true for the very first column, where the only way is to move down continuously. This means every cell in the top row and left column represents exactly 1 unique path.
-
The DP Matrix Plan: With the rule and the base cases defined, I planned my algorithm:
- Create an
m x n
matrix (a 2D array,dp
) to store the number of paths to each cell. - Initialize the matrix to handle the base cases. A simple way to do this is to set all values in the first row and first column to
1
. - Iterate through the rest of the matrix, starting from
dp[1][1]
. - Apply my recurrence relation (
dp[i][j] = dp[i-1][j] + dp[i][j-1]
) to fill in each cell based on the values I've already computed. - The final answer is the value stored in the bottom-right corner,
dp[m-1][n-1]
.
- Create an
This code implements the bottom-up DP strategy.
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
// Edge case: If the grid is just a single row or column, there's only one path.
if (m === 1 || n === 1) {
return 1;
}
// Step 1: Create the DP matrix.
// I'm initializing the entire m x n grid with 1s. This is a clean way to
// set up the base cases, since every cell in the first row and first
// column should have a value of 1.
const dp = Array.from({ length: m }, () => Array(n).fill(1));
// Step 2: Fill the rest of the grid using the recurrence relation.
// Start iterating from (1, 1) since the 0-th row and 0-th column are
// already correctly initialized as our base cases.
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
// Apply the core rule: paths to here = paths from above + paths from left.
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
// Step 3: The final answer is stored in the bottom-right cell.
return dp[m - 1][n - 1];
};
Status: ✅ Completed Link: LeetCode Problem 1143
Click to view problem description
Given two strings
text1
andtext2
, return the length of their longest common subsequence. If there is no common subsequence, return 0.A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.
- For example,
"ace"
is a subsequence of"abcde"
.A common subsequence of two strings is a subsequence that is common to both strings.
Example 1:
Input: text1 = "abcde", text2 = "ace" Output: 3 Explanation: The longest common subsequence is "ace" and its length is 3.
Example 2:
Input: text1 = "abc", text2 = "abc" Output: 3
Example 3:
Input: text1 = "abc", text2 = "def" Output: 0
This problem is a quintessential Bottom-Up Dynamic Programming problem. A brute-force approach of generating all subsequences would be far too slow (exponential time). DP allows us to build the solution efficiently.
-
Core Insight: The problem can be broken down into smaller, overlapping subproblems. The LCS of two long strings depends on the LCS of shorter versions of those same strings. This structure is a perfect fit for a DP matrix (or 2D array).
-
The DP Matrix Plan: My central idea was to create a 2D grid,
dp
, wheredp[i][j]
stores the length of the Longest Common Subsequence for the prefixestext1.substring(0, i)
andtext2.substring(0, j)
. The grid dimensions would be(text1.length + 1) x (text2.length + 1)
to account for empty string prefixes. -
The Recurrence Relation (The Two Golden Rules): I then determined how to fill each cell
dp[i][j]
by looking only at its neighbors. This involved comparing the characterstext1[i-1]
andtext2[j-1]
:- Case 1: The characters match. This is the "happy path." We've found a character that can extend our subsequence. The length is
1
plus the length of the LCS of the strings before these characters. This pre-computed answer is always stored in the diagonal cell:dp[i-1][j-1]
.- Rule:
dp[i][j] = 1 + dp[i-1][j-1]
- Rule:
- Case 2: The characters do NOT match. We can't extend the subsequence with the current characters. The best we can do is carry forward the longest subsequence found so far. This means taking the maximum length from either the subproblem without
text1
's current character (the cell to the left,dp[i][j-1]
) or the subproblem withouttext2
's current character (the cell above,dp[i-1][j]
). This is the "messenger propagation" rule.- Rule:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- Rule:
- Case 1: The characters match. This is the "happy path." We've found a character that can extend our subsequence. The length is
-
Identifying the Base Cases: The "easiest" subproblem is finding the LCS between a string and an empty string. The answer is always 0. By creating a grid one size larger than the strings and initializing it with zeros, the entire first row and first column automatically represent these base cases correctly.
-
Final Answer: After applying these rules iteratively to every cell in the grid, the problem for the full strings will be solved. The final answer will be stored in the bottom-right corner of the grid,
dp[text1.length][text2.length]
.
This code implements the bottom-up DP strategy discussed above.
/**
* @param {string} text1
* @param {string} text2
* @return {number}
*/
var longestCommonSubsequence = function(text1, text2) {
const m = text1.length;
const n = text2.length;
// Step 1: Create the DP matrix with +1 size for the empty string base cases.
// It is initialized with all 0s, which correctly handles the base cases
// where either string is empty.
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
/*
'' a b a text1 = aba
'' 0 0 0 0
text2 = dbab d 0 0 0 0
b 0 0 1 0
a 0 0 1 1
b 0 1 2 2
*/
// Step 2: Iterate through the grid, starting from (1, 1).
// The loops fill the grid based on the two rules.
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
// Get the characters corresponding to the current subproblem.
// We use i-1 and j-1 because strings are 0-indexed.
const char1 = text1[i - 1];
const char2 = text2[j - 1];
// Step 3: Apply the recurrence relation.
if (char1 === char2) {
// Rule 1 (Match): Add 1 to the result from the diagonal.
// The diagonal is the largest subsequence at a given substr
dp[i][j] = 1 + dp[i - 1][j - 1];
} else {
// Rule 2 (No Match): Propagate the best result from above or left. This means u aren't extending the largest subsequence.
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// Step 4: The final answer is in the bottom-right corner.
return dp[m][n];
};
- The largest subsequence at each point is at the bottom right. For example, largest subsequence of ab and dbab is 2.
- likewise, the largest subsequence of aba and dbab is 2 as well. since you dont match on the last letter, just propogate 2 to the right, which propogates largest subsequence from the last 2 substr comparisons.
Status: ✅ Completed Link: LeetCode Problem 714
Click to view problem description
You are given an array
prices
whereprices[i]
is the price of a given stock on thei
th day, and an integerfee
representing a transaction fee.Find the maximum profit you can achieve. You may complete as many transactions as you like, but you need to pay the transaction fee for each transaction.
Note:
- You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).
- The transaction fee is only charged once for each stock purchase and sale.
Example 1:
Input: prices = [1,3,2,8,4,9], fee = 2 Output: 8 Explanation: The maximum profit can be achieved by: - Buying at prices[0] = 1 - Selling at prices[3] = 8 - Buying at prices[4] = 4 - Selling at prices[5] = 9 The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
Example 2:
Input: prices = [1,3,7,5,10,3], fee = 3 Output: 6
This problem is a classic application of State Machine Dynamic Programming. A simple greedy approach like "buy lowest, sell highest" fails because it doesn't account for multiple profitable "waves" or transaction fees invalidating small gains. The key is to realize that on any given day, we can only be in one of two states.
-
Core Insight: At the end of any given day, my financial status can be described in one of two ways: I am either holding a stock, or I have cash on hand (no stock). The goal is to maximize my net worth in both of these states every single day.
-
The State Variables: My strategy revolves around tracking two variables as I iterate through the prices:
hold
: Represents the maximum possible net worth if I end the day holding one share of stock. This value carries the "memory" of my past transactions.cash
: Represents the maximum possible profit if I end the day not holding a stock. This is my realized, spendable profit.
-
The Recurrence Relation (The Two Golden Rules): To find the values for the current day, I only need the values from the previous day. This leads to two simple rules for updating my state:
- Rule 1: Calculating the new
cash
value. To have cash today, I could have either (a) carried over my cash from yesterday, or (b) sold the stock I was holding yesterday. I choose the more profitable option.new_cash = max( yesterday's_cash, yesterday's_hold + today's_price - fee )
- Rule 2: Calculating the new
hold
value. To be holding a stock today, I could have either (a) kept holding the stock from yesterday, or (b) used my cash from yesterday to buy a stock today. I choose the more profitable option.new_hold = max( yesterday's_hold, yesterday's_cash - today's_price )
- Rule 1: Calculating the new
-
Identifying the Base Cases: Before the first day, my net worth is 0 and I hold no stock. So, to start the process for Day 0:
- Initial
cash
=0
- Initial
hold
=-prices[0]
(I used my 0 cash to buy the first stock).
- Initial
-
Final Answer: After iterating through all the days, the final answer must be the
cash
value. I cannot end with a stock in hand, as profit is only realized upon selling.
// At the final state, you can either be holding stock or not
// max profit holding stock
// 1) carried over stock from yesterday
// 2) i bought stock
//max profit holding no stock
//1) sold my stock today
//2) carried over my cash
//Day 1 stock[0] = 1
// 1) - net worth hold stock: Max( yesterdays hold value, hold value when buying stock with yesterday's cash )
// - Max( -infinity , 0 - 1 ) => Max(-infinity, -1) => -1
// 2) - net worth no stock: Max(yesterdays cash value, cash value selling yesterdays stock)
// - Max( 0 , -infinity + 1 - 2 ) => Max(0, -infinity) => 0
//Day 2 stock[1] = 3
// 1) - net worth hold stock: Max( yesterdays hold value, hold value when buying stock with yesterday's cash )
// - Max(-1, 0-3 = -3) => -1
//
// 2) - net worth no stock: Max(yesterdays cash value, cash value selling yesterdays stock)
// - Max( 0, 3-1-2 = 0) = 0
//Day 3 stock[2] = 2
// 1) - net worth hold stock: Max( yesterdays hold value, hold value when buying stock with yesterday's cash )
// - Max(-1, 0 - 2 = -2) => -1
// We propogate the lowest buying price of a stock.
//
// 2) - net worth no stock: Max(yesterdays cash value, cash value selling yesterdays stock)
// - Max( 0, (2 - 1) - 2) => 0
// We propogate the best selling price, or just carry cash over
// Day 4 stock[3] = 8
// 1) - net worth hold stock: Max( yesterdays hold value, hold value when buying stock with yesterday's cash )
// - Max( -1, 0-8 = -8)=> -1
// 2) - net worth no stock: Max(yesterdays cash value, cash value selling yesterdays stock)
// - Max(0, (8-1)-2) => 5! new max.
//
// So we propogate the lowest buying price of a stock, which is still -1
// However, we found a new global max of holding cash, its best to sell yesterdays stock rather than hold onto yesterdays cash.
// Day 5 stock[4] = 4
// 1) - net worth hold stock: Max( yesterdays hold value, hold value when buying stock with yesterday's cash )
// - Max( -1, 5-4 = 1)=> 1
// Key moment, better to hold new stock (implying sold first stock for profit earlier. Now we have positive net worth holding a stock from previous sell at 8). Reinvesting our profits for potential future gains.
// 2) - net worth no stock: Max(yesterdays cash value, cash value selling yesterdays stock)
// - Max(5, (4-1)-2) => 5. We propogate the global gainz of selling at 8 (bought at 1).
// Day 6 stock[5] = 9
// 1) - net worth hold stock: Max( yesterdays hold value, hold value when buying stock with yesterday's cash )
// - Max(1, 5-9 = -4)=> 1
// We propogate the highest net worth of holding a stock, which is still 1 (bought at 4, sold at 5.)
// 2) - net worth no stock: Max(yesterdays cash value, cash value selling yesterdays stock)
// - Max(5, (9+1)-2) => 8. New global max.
// we realized best to buy at 1, sell at 8, then buy at 4, sell at 9. Rather than just keep profit at 5 (buy 1 sell at 8)
/**
* @param {number[]} prices
* @param {number} fee
* @return {number}
*/
var maxProfit = function(prices, fee) {
let cashNetWorth = Array.from(prices.length).fill(0)
let holdStockNetWorth = Array.from(prices.length).fill(0)
holdStockNetWorth[0] = -Infinity
cashNetWorth[0] = 0;
for(let i = 0; i < prices.length; i++){
holdStockNetWorth[i+1] = Math.max(holdStockNetWorth[i],(cashNetWorth[i]-prices[i]))
cashNetWorth[i+1] = Math.max(cashNetWorth[i], (prices[i] + holdStockNetWorth[i])-fee)
}
return cashNetWorth[cashNetWorth.length-1]
};
maxProfit([1,3,2,8,4,9], 2)