{"id":4709,"date":"2024-10-21T12:11:45","date_gmt":"2024-10-21T12:11:45","guid":{"rendered":"https:\/\/algocademy.com\/blog\/maximum-product-subarray-a-comprehensive-guide-to-solving-this-classic-algorithm-problem\/"},"modified":"2026-10-15T12:51:16","modified_gmt":"2026-10-15T12:51:16","slug":"maximum-product-subarray-a-comprehensive-guide-to-solving-this-classic-algorithm-problem","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/maximum-product-subarray-a-comprehensive-guide-to-solving-this-classic-algorithm-problem\/","title":{"rendered":"Maximum Product Subarray: Why You Track Both Max and Min"},"content":{"rendered":"<p>To find the maximum product subarray in O(n) time and O(1) space, scan the array once and keep two values at each position: the largest and the smallest product of any subarray ending there. You need the smallest one because multiplying by a negative number turns the most negative product into the most positive one. The answer is the largest &#8220;max ending here&#8221; you see.<\/p>\n<h2>The problem<\/h2>\n<p>Given an integer array <code>nums<\/code>, find a contiguous, non-empty subarray with the largest product and return that product (not the subarray). This is LeetCode 152. The array can hold positives, negatives and zeros. Examples:<\/p>\n<ul>\n<li><code>[2, 3, -2, 4]<\/code> returns <code>6<\/code> (the subarray <code>[2, 3]<\/code>).<\/li>\n<li><code>[-2, 0, -1]<\/code> returns <code>0<\/code>. Neither <code>-2<\/code> nor <code>-1<\/code> beats it, and you cannot skip the zero to combine them because the subarray must be contiguous.<\/li>\n<li><code>[-2, 3, -4]<\/code> returns <code>24<\/code>, the whole array: two negatives cancel out.<\/li>\n<\/ul>\n<h2>Brute force: O(n^2)<\/h2>\n<p>Start at every index, extend to the right, and keep a running product. Track the best value seen.<\/p>\n<pre><code class=\"language-python\">def max_product_brute(nums):\n    best = nums[0]\n    for i in range(len(nums)):\n        product = 1\n        for j in range(i, len(nums)):\n            product *= nums[j]\n            best = max(best, product)\n    return best\n<\/code><\/pre>\n<p>This is correct, uses O(1) extra space, and takes O(n^2) time. It is a fine first answer in an interview because it is easy to verify, and it gives you a reference to test the faster version against.<\/p>\n<h2>Why the maximum product alone is not enough<\/h2>\n<p>The maximum subarray sum problem (Kadane&#8217;s algorithm) keeps one number: the best sum ending at the current index. If it is negative, you drop it and start fresh. Products behave differently. Look at <code>[-2, 3, -4]<\/code>:<\/p>\n<ul>\n<li>The best product ending at <code>3<\/code> is <code>3<\/code>, because <code>-2 * 3 = -6<\/code> is worse than starting over.<\/li>\n<li>If you only kept that <code>3<\/code>, you would compute <code>3 * -4 = -12<\/code> and conclude the answer is <code>3<\/code>.<\/li>\n<li>The real answer is <code>(-2) * 3 * (-4) = 24<\/code>. The bad-looking <code>-6<\/code> was worth keeping, because the next negative number flips it to <code>24<\/code>.<\/li>\n<\/ul>\n<p>So at every index you remember two things about the subarrays that end there: the largest product and the smallest (most negative) product. A negative number swaps their roles: the old minimum times a negative becomes the new maximum.<\/p>\n<h2>The O(n) solution<\/h2>\n<p>For each new number <code>x<\/code>, the best subarray ending at <code>x<\/code> is one of three things:<\/p>\n<ol>\n<li><code>x<\/code> alone (start a new subarray here, which also handles zeros),<\/li>\n<li><code>x<\/code> times the previous maximum,<\/li>\n<li><code>x<\/code> times the previous minimum.<\/li>\n<\/ol>\n<p>The new maximum is the largest of the three and the new minimum is the smallest. Compute both from the same old values before overwriting either one.<\/p>\n<pre><code class=\"language-python\">def max_product(nums):\n    cur_max = cur_min = result = nums[0]\n    for x in nums[1:]:\n        candidates = (x, cur_max * x, cur_min * x)\n        cur_max, cur_min = max(candidates), min(candidates)\n        result = max(result, cur_max)\n    return result\n\nprint(max_product([2, 3, -2, 4]))   # 6\nprint(max_product([-2, 0, -1]))     # 0\nprint(max_product([-2, 3, -4]))     # 24\nprint(max_product([-2]))            # -2\n<\/code><\/pre>\n<p>Building a tuple of candidates first avoids the classic bug of updating <code>cur_max<\/code> and then using the already-changed value to compute <code>cur_min<\/code>. Time is O(n), space is O(1).<\/p>\n<h3>A worked trace<\/h3>\n<p>Take <code>nums = [2, 3, -2, 4, -1]<\/code>. Both values start at <code>2<\/code>. Each row shows the state after processing <code>x<\/code>:<\/p>\n<table>\n<thead>\n<tr>\n<th>x<\/th>\n<th>candidates (x, max*x, min*x)<\/th>\n<th>cur_max<\/th>\n<th>cur_min<\/th>\n<th>result<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>2 (start)<\/td>\n<td>&#8211;<\/td>\n<td>2<\/td>\n<td>2<\/td>\n<td>2<\/td>\n<\/tr>\n<tr>\n<td>3<\/td>\n<td>3, 6, 6<\/td>\n<td>6<\/td>\n<td>3<\/td>\n<td>6<\/td>\n<\/tr>\n<tr>\n<td>-2<\/td>\n<td>-2, -12, -6<\/td>\n<td>-2<\/td>\n<td>-12<\/td>\n<td>6<\/td>\n<\/tr>\n<tr>\n<td>4<\/td>\n<td>4, -8, -48<\/td>\n<td>4<\/td>\n<td>-48<\/td>\n<td>6<\/td>\n<\/tr>\n<tr>\n<td>-1<\/td>\n<td>-1, -4, 48<\/td>\n<td>48<\/td>\n<td>-4<\/td>\n<td>48<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Watch the last row. After <code>4<\/code>, the maximum is only <code>4<\/code>, but the minimum <code>-48<\/code> is the product of the whole prefix <code>2 * 3 * -2 * 4<\/code>. The final <code>-1<\/code> flips it into <code>48<\/code>, the answer. A solution that tracked only the maximum would return <code>6<\/code>.<\/p>\n<p>Now a zero. For <code>[-2, 0, -1]<\/code>: start at <code>-2<\/code>. At <code>0<\/code> every candidate is <code>0<\/code>, so max and min both become <code>0<\/code> and the old product is wiped out. At <code>-1<\/code> the candidates are <code>-1, 0, 0<\/code>, so max is <code>0<\/code>, min is <code>-1<\/code>, and the result stays <code>0<\/code>. No special zero handling is needed because &#8220;x alone&#8221; is always a candidate.<\/p>\n<h2>Variant: swap instead of computing three candidates<\/h2>\n<p>Many write-ups use an equivalent form. If <code>x<\/code> is negative, swap the max and min first, then multiply:<\/p>\n<pre><code class=\"language-python\">def max_product_swap(nums):\n    cur_max = cur_min = result = nums[0]\n    for x in nums[1:]:\n        if x &lt; 0:\n            cur_max, cur_min = cur_min, cur_max\n        cur_max = max(x, cur_max * x)\n        cur_min = min(x, cur_min * x)\n        result = max(result, cur_max)\n    return result\n<\/code><\/pre>\n<p>It does the same thing: after the swap, the old minimum sits in <code>cur_max<\/code>, so multiplying by a negative <code>x<\/code> gives the larger value. The three-candidate version is harder to get wrong, so it is the safer one to write under pressure.<\/p>\n<h2>Another way: prefix and suffix products<\/h2>\n<p>There is a second O(n) idea. Within a stretch of the array without zeros, the best subarray is either the whole stretch (an even number of negatives) or it drops a prefix or a suffix up to the first or last negative number. So the best product is always a prefix product or a suffix product of some zero-free stretch. Walk the array from both ends at once, reset the running product to 1 after a zero, and take the maximum over everything you see:<\/p>\n<pre><code class=\"language-python\">def max_product_prefix_suffix(nums):\n    best = nums[0]\n    prefix = suffix = 1\n    for i in range(len(nums)):\n        prefix = (prefix or 1) * nums[i]\n        suffix = (suffix or 1) * nums[-1 - i]\n        best = max(best, prefix, suffix)\n    return best\n<\/code><\/pre>\n<p>The <code>(prefix or 1)<\/code> trick restarts the product after a zero. This version is short, but the reasoning behind it is harder to explain, so the max\/min version is usually the better choice in an interview.<\/p>\n<h2>Test it against the brute force<\/h2>\n<p>All four functions above pass these cases, plus 20,000 random arrays of length 1 to 9 with values from -4 to 4, and they agree with the brute force. Good hand-written cases to include:<\/p>\n<pre><code class=\"language-python\">cases = [\n    ([2, 3, -2, 4], 6),\n    ([-2, 0, -1], 0),\n    ([-2, 3, -4], 24),\n    ([0, 2], 2),\n    ([-2, -3, -4], 12),\n    ([-3], -3),               # single negative element\n    ([-1, -1], 1),\n    ([0, 0], 0),\n    ([1, 0, -1, 2, 3, -5, -2], 60),\n]\nfor nums, expected in cases:\n    assert max_product(nums) == expected, nums\nprint(\"all passed\")\n<\/code><\/pre>\n<h2>Common mistakes<\/h2>\n<ul>\n<li><strong>Tracking only the maximum.<\/strong> It fails on <code>[-2, 3, -4]<\/code>, where the answer depends on a negative product you would have thrown away.<\/li>\n<li><strong>Overwriting before using.<\/strong> Setting <code>cur_max<\/code> and then computing <code>cur_min<\/code> from the new <code>cur_max<\/code> gives wrong results. Compute both from the old values.<\/li>\n<li><strong>Starting from 1 or 0.<\/strong> Initialize with <code>nums[0]<\/code>. If you start <code>result<\/code> at 0, an input like <code>[-3]<\/code> returns <code>0<\/code> instead of <code>-3<\/code>.<\/li>\n<li><strong>Forgetting &#8220;x alone&#8221;.<\/strong> Without it, a zero (or a fresh start after one) never resets the chain properly.<\/li>\n<li><strong>Dividing to slide a window.<\/strong> Some solutions try to update a product by dividing out the element that leaves. That breaks as soon as a zero is in the window. Fixed-length windows need a different approach (rebuild the product, or split the array at zeros).<\/li>\n<li><strong>Overflow.<\/strong> In C++ or Java the intermediate products can overflow. The LeetCode statement guarantees that the answer fits in a 32-bit integer, but intermediate values such as the running minimum can still be large, so use 64-bit integers there. Python integers do not overflow.<\/li>\n<\/ul>\n<h2>What an interviewer may ask next<\/h2>\n<ul>\n<li><strong>Why is it correct?<\/strong> Any subarray ending at index <code>i<\/code> is a subarray ending at <code>i - 1<\/code> extended by <code>nums[i]<\/code>, or just <code>nums[i]<\/code>. The product of the extended one is monotone in the old product: if <code>nums[i]<\/code> is positive, the largest old product gives the largest new one; if negative, the smallest does. So only the extremes matter.<\/li>\n<li><strong>What about the maximum sum version?<\/strong> That is Kadane&#8217;s algorithm with a single value. You can practice it in the <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=maximum-sum-subarray&amp;solution=1\">Maximum Sum Subarray<\/a> lesson, and the <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=maximum-sum-subarray&amp;solution=2\">second version<\/a> takes it further with partial sums.<\/li>\n<li><strong>Can you return the subarray itself?<\/strong> Yes. Remember where the current candidate started (reset the start index whenever &#8220;x alone&#8221; wins, and update the best start and end when <code>result<\/code> improves). The extra state does not change the complexity.<\/li>\n<li><strong>Complexity?<\/strong> O(n) time, O(1) extra space. You cannot do better than O(n) because every element can change the answer.<\/li>\n<li><strong>What if the array is empty?<\/strong> The LeetCode problem guarantees at least one element. In an interview, ask, and agree on what to return before you code.<\/li>\n<\/ul>\n<p>If you want more array problems with step-by-step hints, you can try them in the <a href=\"https:\/\/app.algocademy.com\/\">AlgoCademy app<\/a>. The same two-state idea (keep the best and the worst ending at each index) shows up in other dynamic programming problems, so it is worth being able to explain it without notes.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Solve Maximum Product Subarray (LeetCode 152) in O(n): why a negative number flips max and min, a traced example, tested Python code, and follow-ups.<\/p>\n","protected":false},"author":1,"featured_media":4708,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-4709","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4709"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=4709"}],"version-history":[{"count":2,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4709\/revisions"}],"predecessor-version":[{"id":8930,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4709\/revisions\/8930"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/4708"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=4709"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=4709"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=4709"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}