<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Avinash Tyagi</title>
    <description>The latest articles on DEV Community by Avinash Tyagi (@avinash_tyagi_98).</description>
    <link>https://dev.to/avinash_tyagi_98</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3897290%2F417790ad-95fd-4c33-b67a-1389c58f57ac.png</url>
      <title>DEV Community: Avinash Tyagi</title>
      <link>https://dev.to/avinash_tyagi_98</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/avinash_tyagi_98"/>
    <language>en</language>
    <item>
      <title>I Couldn't Optimize Max Sum Min-Product Until I Flipped the Question</title>
      <dc:creator>Avinash Tyagi</dc:creator>
      <pubDate>Mon, 11 May 2026 03:01:14 +0000</pubDate>
      <link>https://dev.to/avinash_tyagi_98/i-couldnt-optimize-max-sum-min-product-until-i-flipped-the-question-4eli</link>
      <guid>https://dev.to/avinash_tyagi_98/i-couldnt-optimize-max-sum-min-product-until-i-flipped-the-question-4eli</guid>
      <description>&lt;p&gt;This is part of a series where I break down coding concepts I understand when reading about them but struggle to reuse when I actually need them. Not tutorials. Personal learning journeys that break down the "why does this actually work" part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem that tripped me up
&lt;/h2&gt;

&lt;p&gt;LeetCode 1856: Maximum Subarray Min-Product. The formula is straightforward. For any subarray, multiply the minimum element by the sum of the subarray. Find the maximum across all possible subarrays.&lt;/p&gt;

&lt;p&gt;I could see the O(n^2) solution clearly. Try every subarray, track the running min and running sum, update the max. But dropping to O(n)? That's where I got stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where my intuition went wrong
&lt;/h2&gt;

&lt;p&gt;My first instinct was a sliding window approach. Add an element to the range, check if it improves the answer, otherwise store the max and reset. It felt right because you're building ranges and checking them.&lt;/p&gt;

&lt;p&gt;But here's why it fails: the decision to "keep going or reset" depends on both the sum AND the minimum changing at the same time. Adding a large number helps the sum but might not help if the min stays low. Adding a small number hurts the min but you don't know yet whether the sum makes up for it. There's no clean greedy condition to check.&lt;/p&gt;

&lt;p&gt;I was stuck because I was asking the wrong question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe that made it click
&lt;/h2&gt;

&lt;p&gt;Instead of asking "what's the best range?", flip it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For each element, what's the widest subarray where THAT element is the minimum?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why does this cover every possible answer? Because whatever the optimal subarray is, it has some minimum element. That minimum is one of the values in the array. So if you check every element as a potential minimum and find its best subarray, you're guaranteed to find the answer.&lt;/p&gt;

&lt;p&gt;This changes the problem from "search all subarrays" to "for each element, find its boundaries." And finding boundaries is exactly what a monotonic stack does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwqkpykjb73x0imv4cnlm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwqkpykjb73x0imv4cnlm.png" alt="Diagram: An element finding its left and right boundaries" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the monotonic stack finds boundaries
&lt;/h2&gt;

&lt;p&gt;You maintain a stack of indices where the values are strictly increasing from bottom to top. As you iterate through the array, when you encounter a value smaller than or equal to the stack top, you pop.&lt;/p&gt;

&lt;p&gt;The magic: when an element gets popped, it has found both its boundaries in that single moment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Right boundary&lt;/strong&gt;: the current element (the one causing the pop). It's smaller, so the popped element can't be the minimum past this point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Left boundary&lt;/strong&gt;: whatever is now on top of the stack after popping. It's the nearest smaller element to the left.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The subarray where the popped element is the minimum goes from &lt;code&gt;left_boundary + 1&lt;/code&gt; to &lt;code&gt;right_boundary - 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let me trace through &lt;code&gt;[3, 1, 6, 4, 5, 2]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;i=0, val=3: push 0          stack: [0]
i=1, val=1: pop 0, push 1   stack: [1]
  → popped 3: right=1, left=-1 (empty stack), subarray=[0..0]
i=2, val=6: push 2          stack: [1, 2]
i=3, val=4: pop 2, push 3   stack: [1, 3]
  → popped 6: right=3, left=1, subarray=[2..2]
i=4, val=5: push 4          stack: [1, 3, 4]
i=5, val=2: pop 4           stack: [1, 3]
  → popped 5: right=5, left=3, subarray=[4..4]
         pop 3              stack: [1]
  → popped 4: right=5, left=1, subarray=[2..4]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the stack is empty after popping, the left boundary is -1 (meaning "nothing smaller exists to the left, extend all the way to index 0"). After processing all elements, anything still in the stack has no right boundary, so it extends all the way to the end of the array (right boundary = n).&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefix sums for O(1) range sums
&lt;/h2&gt;

&lt;p&gt;Once you have boundaries for each element, you need the sum of that subarray. Building a prefix sum array with a leading zero handles this cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# sum of nums[a..b] = prefix[b+1] - prefix[a]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leading zero is important. Without it, getting the sum starting from index 0 requires special-case handling that leads to off-by-one bugs (I know because I made this exact mistake).&lt;/p&gt;

&lt;h2&gt;
  
  
  The full solution
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;maxSumMinProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;popped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;popped&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# drain remaining elements
&lt;/span&gt;    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;popped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;popped&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mistakes I actually made
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;gt;=&lt;/code&gt; in the while condition.&lt;/strong&gt; With just &lt;code&gt;&amp;gt;&lt;/code&gt;, equal elements don't get popped. This means when duplicates exist, a later equal element might compute an incorrect left boundary because its twin is still sitting in the stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using the index as the value.&lt;/strong&gt; I wrote &lt;code&gt;result = max(result, popped * total)&lt;/code&gt; where &lt;code&gt;popped&lt;/code&gt; is the index I popped from the stack. Should have been &lt;code&gt;nums[popped]&lt;/code&gt;. The index is just a position. The value at that position is what you multiply by.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefix sum without the leading zero.&lt;/strong&gt; My first attempt used &lt;code&gt;pref_arr[0] = nums[0]&lt;/code&gt;, which made the formula for getting sums starting from index 0 a special case. Adding the leading zero makes every range use the same formula: &lt;code&gt;prefix[right] - prefix[left + 1]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to practice next
&lt;/h2&gt;

&lt;p&gt;These all use the same "for each element, find its boundaries" pattern with a monotonic stack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Largest Rectangle in Histogram&lt;/strong&gt; (LeetCode 84). The classic. For each bar, find the widest rectangle where it's the shortest bar. Almost identical logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sum of Subarray Minimums&lt;/strong&gt; (LeetCode 907). Instead of max(min * sum), you're summing all the min-products. Same boundary finding, different aggregation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sum of Subarray Ranges&lt;/strong&gt; (LeetCode 2104). Find both the min-contribution and max-contribution of each element using two monotonic stacks (one increasing, one decreasing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trapping Rain Water&lt;/strong&gt; (LeetCode 42). Uses a monotonic decreasing stack. When you pop, the boundaries tell you the width and height of water you can trap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next Greater Element II&lt;/strong&gt; (LeetCode 503). Simpler. Good warm-up for understanding the pop mechanics without the prefix sum layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'd recommend doing them in order: 84 first (it's the closest to what we just solved), then 907, then the rest.&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://levelop.dev" rel="noopener noreferrer"&gt;Levelop&lt;/a&gt; to help people get better at problems like these. If you want more breakdowns like this, check it out.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>devjournal</category>
      <category>learning</category>
      <category>leetcode</category>
    </item>
    <item>
      <title>Remove Duplicate Letters: I Knew Monotonic Stacks, But This Problem Still Got Me</title>
      <dc:creator>Avinash Tyagi</dc:creator>
      <pubDate>Sat, 09 May 2026 03:26:08 +0000</pubDate>
      <link>https://dev.to/avinash_tyagi_98/remove-duplicate-letters-i-knew-monotonic-stacks-but-this-problem-still-got-me-4ie3</link>
      <guid>https://dev.to/avinash_tyagi_98/remove-duplicate-letters-i-knew-monotonic-stacks-but-this-problem-still-got-me-4ie3</guid>
      <description>&lt;p&gt;I've been working through coding problems that I can understand when reading solutions but struggle to solve on my own. This series is about breaking down the "why does this actually work" part. Not tutorials. Just honest breakdowns of where my thinking got stuck and what finally unstuck it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem that looked familiar
&lt;/h2&gt;

&lt;p&gt;LeetCode 316: Remove Duplicate Letters. Given a string like &lt;code&gt;"cbacdcbc"&lt;/code&gt;, remove duplicate characters so every letter appears exactly once, and the result is the smallest possible in lexicographical order. The answer here is &lt;code&gt;"acdb"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'd just finished learning monotonic stacks for Sum of Subarray Minimums. Greedy popping, maintaining order, comparing against the stack top. I recognized the pattern immediately: scan the string, and if the current character is smaller than what's on the stack and the stack-top character appears later in the string, pop it. Classic monotonic stack.&lt;/p&gt;

&lt;p&gt;So I coded it up, and it was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I got stuck
&lt;/h2&gt;

&lt;p&gt;My first solution treated uniqueness as a cleanup step. I let duplicates pile up in the stack, then deduplicated at the end by reading from the bottom and skipping repeats. The stack logic would handle ordering, and the dedup pass would handle uniqueness. Two separate concerns, handled separately.&lt;/p&gt;

&lt;p&gt;That's the thinking that broke it. In this problem, ordering and uniqueness aren't separate. They're coupled. The greedy decisions the stack makes (should I pop this character or keep it?) depend on knowing exactly which characters are already committed to the result. A duplicate sitting in the stack corrupts those decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplicates poison the greedy logic
&lt;/h2&gt;

&lt;p&gt;Here's the specific failure. Take &lt;code&gt;"abacb"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With post-hoc deduplication, my stack processed it like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Char&lt;/th&gt;
&lt;th&gt;Stack action&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;push&lt;/td&gt;
&lt;td&gt;[a]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;a &amp;lt; b, push&lt;/td&gt;
&lt;td&gt;[a, b]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;b &amp;gt; a, pop b. a = a, push&lt;/td&gt;
&lt;td&gt;[a, a]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;a &amp;lt; c, push&lt;/td&gt;
&lt;td&gt;[a, a, c]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;c &amp;gt; b but c has freq 1, push&lt;/td&gt;
&lt;td&gt;[a, a, c, b]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dedup from the left: &lt;code&gt;"acb"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The correct answer is &lt;code&gt;"abc"&lt;/code&gt; (a at index 0, b at index 1, c at index 3).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83jszucxhsx8slrghqy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83jszucxhsx8slrghqy8.png" alt="Why post-hoc deduplication fails" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What went wrong? Step 3 pushed a second &lt;code&gt;a&lt;/code&gt; into the stack. That second &lt;code&gt;a&lt;/code&gt; caused &lt;code&gt;b&lt;/code&gt; to get popped unnecessarily. With &lt;code&gt;b&lt;/code&gt; gone, it could only re-enter at the end, after &lt;code&gt;c&lt;/code&gt;. The duplicate &lt;code&gt;a&lt;/code&gt; sitting in the stack made the greedy logic produce a worse ordering.&lt;/p&gt;

&lt;p&gt;If I'd skipped the second &lt;code&gt;a&lt;/code&gt; entirely (it's already in the result, no need to process it), &lt;code&gt;b&lt;/code&gt; would have stayed in the stack, and the final answer would have been &lt;code&gt;"abc"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seen-set: a co-pilot for the stack
&lt;/h2&gt;

&lt;p&gt;The fix is a set that tracks what's currently in the stack. Before doing any work for a character, check the set. If the character is already there, skip it. Decrement its frequency (you've consumed this occurrence) and move on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;in_stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;        &lt;span class="c1"&gt;# this occurrence is consumed
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;in_stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;# already in the result
&lt;/span&gt;        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="c1"&gt;# ... stack logic ...
&lt;/span&gt;    &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;in_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't just a performance optimization. It fundamentally changes what the stack sees. Without it, the stack is making ordering decisions with ghost characters that shouldn't be there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Popping means evicting, and the set needs to know
&lt;/h2&gt;

&lt;p&gt;Here's the second thing that got me. I added the set, but when I popped a character from the stack, I forgot to remove it from the set.&lt;/p&gt;

&lt;p&gt;Think about what popping means in this problem. You're looking at the stack-top character and saying: "You're bigger than what I've got now, and you appear again later in the string. I'll use a later copy of you instead." That character is leaving your result. It's no longer committed. So when you encounter it again later, it needs to be allowed back in.&lt;/p&gt;

&lt;p&gt;If the set still thinks it's in the stack, the later occurrence gets skipped. The character ends up nowhere.&lt;/p&gt;

&lt;p&gt;Trace &lt;code&gt;"cbacdcbc"&lt;/code&gt; with a stale set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Char&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Set&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;push&lt;/td&gt;
&lt;td&gt;[c]&lt;/td&gt;
&lt;td&gt;{c}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;pop c (freq 3&amp;gt;1), push b&lt;/td&gt;
&lt;td&gt;[b]&lt;/td&gt;
&lt;td&gt;{c, b}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;pop b (freq 2&amp;gt;1), push a&lt;/td&gt;
&lt;td&gt;[a]&lt;/td&gt;
&lt;td&gt;{c, b, a}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;skipped&lt;/strong&gt; (set says c is here)&lt;/td&gt;
&lt;td&gt;[a]&lt;/td&gt;
&lt;td&gt;{c, b, a}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;d&lt;/td&gt;
&lt;td&gt;push&lt;/td&gt;
&lt;td&gt;[a, d]&lt;/td&gt;
&lt;td&gt;{c, b, a, d}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6-8&lt;/td&gt;
&lt;td&gt;c,b,c&lt;/td&gt;
&lt;td&gt;all skipped&lt;/td&gt;
&lt;td&gt;[a, d]&lt;/td&gt;
&lt;td&gt;{c, b, a, d}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Result: &lt;code&gt;"ad"&lt;/code&gt;. The actual answer is &lt;code&gt;"acdb"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Characters &lt;code&gt;c&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; were popped from the stack but never removed from the set. Every future occurrence got skipped. They vanished from the result entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqj6ny4hprqvx4aysvbj7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqj6ny4hprqvx4aysvbj7.png" alt="Why the seen-set must sync with the stack" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix: when popping, remove from the set before (or during) the pop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;in_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The complete solution
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;removeDuplicateLetters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;freq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;in_stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;in_stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;in_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;in_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things work together: the stack maintains lexicographic order, the frequency map answers "is it safe to evict this character?", and the set tracks what's currently committed. Remove any one piece and the solution breaks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1xv5p8n5t90srzpz5p9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1xv5p8n5t90srzpz5p9.png" alt="Three pieces working together" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern underneath
&lt;/h2&gt;

&lt;p&gt;Standard monotonic stack problems (next greater element, daily temperatures, histogram) only care about ordering. Every element eventually enters the stack exactly once. The stack just controls when elements leave and what order they end up in.&lt;/p&gt;

&lt;p&gt;Remove Duplicate Letters adds a constraint: each character can appear in the result at most once. That single constraint means the stack can't operate alone anymore. It needs a partner that tracks membership, and that partner has to stay perfectly in sync. Every push updates the set. Every pop updates the set. If they drift, the greedy logic makes decisions based on a state that doesn't match reality.&lt;/p&gt;

&lt;p&gt;This "stack + membership set" pattern shows up in a few other places. Anytime a monotonic stack problem adds a uniqueness or "already used" constraint, look for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to practice next
&lt;/h2&gt;

&lt;p&gt;These are ordered roughly by how they connect to the ideas above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LC 1081, Smallest Subsequence of Distinct Characters&lt;/strong&gt; (medium). Literally the same problem with a different name. Good for confirming you actually get it and didn't just memorize the solution for 316.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 402, Remove K Digits&lt;/strong&gt; (medium). Monotonic stack with a removal budget instead of a uniqueness constraint. Tests whether you understand the greedy popping logic on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 321, Create Maximum Number&lt;/strong&gt; (hard). Combines monotonic stack selection from two arrays. The merging step is tricky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 907, Sum of Subarray Minimums&lt;/strong&gt; (medium). Classic monotonic stack without the uniqueness layer. If you haven't done this one, it's the best way to solidify the base pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 84, Largest Rectangle in Histogram&lt;/strong&gt; (hard). Another pure monotonic stack problem. Boundary finding in both directions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm building &lt;a href="https://levelop.dev" rel="noopener noreferrer"&gt;Levelop&lt;/a&gt; to help people get better at problems like these. If you want more breakdowns like this, check it out.&lt;/p&gt;

</description>
      <category>leetcode</category>
      <category>dsa</category>
      <category>coding</category>
      <category>python</category>
    </item>
    <item>
      <title>I Understood Monotonic Stacks When I Stopped Thinking About Stacks</title>
      <dc:creator>Avinash Tyagi</dc:creator>
      <pubDate>Thu, 30 Apr 2026 20:43:41 +0000</pubDate>
      <link>https://dev.to/avinash_tyagi_98/i-understood-monotonic-stacks-when-i-stopped-thinking-about-stacks-471p</link>
      <guid>https://dev.to/avinash_tyagi_98/i-understood-monotonic-stacks-when-i-stopped-thinking-about-stacks-471p</guid>
      <description>&lt;p&gt;I've been working through coding problems that I can understand when reading solutions but struggle to solve on my own. This series is about breaking down the "why does this actually work" part. Not tutorials. Just honest breakdowns of where my thinking got stuck and what finally unstuck it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;LeetCode 907: Sum of Subarray Minimums. Given an array, find the minimum of every contiguous subarray, and return the sum of all those minimums.&lt;/p&gt;

&lt;p&gt;I could solve this in O(n²) almost immediately. Two nested loops, track the running minimum as you extend each subarray, add it to the total. Simple. But the optimal O(n) solution uses a monotonic stack, and I could not figure out why a stack had anything to do with finding minimums across subarrays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flipping the question
&lt;/h2&gt;

&lt;p&gt;The brute force iterates over every subarray and asks "what's the minimum here?" The O(n) approach asks a completely different question: "for each element, how many subarrays is it the minimum of?"&lt;/p&gt;

&lt;p&gt;These two questions give the same answer. If element &lt;code&gt;3&lt;/code&gt; is the minimum of 5 subarrays, it contributes &lt;code&gt;3 × 5 = 15&lt;/code&gt; to the total sum. Add up every element's contribution and you get the same result as checking every subarray individually.&lt;/p&gt;

&lt;p&gt;This reframing is the entire foundation. Everything else builds on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting subarrays with the multiplication principle
&lt;/h2&gt;

&lt;p&gt;Once you're asking "how many subarrays is arr[i] the minimum of?", it becomes a counting problem. For arr[i] to be the minimum of a subarray, that subarray can't contain anything smaller than arr[i]. So you look left and right from index i and find the boundaries: the nearest smaller element on each side.&lt;/p&gt;

&lt;p&gt;Take &lt;code&gt;[3, 1, 2, 4]&lt;/code&gt; and look at the element &lt;code&gt;2&lt;/code&gt; at index 2.&lt;/p&gt;

&lt;p&gt;To the left: index 1 has value &lt;code&gt;1&lt;/code&gt;, which is smaller. Can't extend past it.&lt;br&gt;
To the right: index 3 has value &lt;code&gt;4&lt;/code&gt;, which is fine. Then we hit the end.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;2&lt;/code&gt; is the minimum of subarrays &lt;code&gt;[2]&lt;/code&gt; and &lt;code&gt;[2, 4]&lt;/code&gt;. Two subarrays.&lt;/p&gt;

&lt;p&gt;But you don't need to list them out. You just need two numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;L = how many positions you can include to the left (including the element itself). Here, L = 1.&lt;/li&gt;
&lt;li&gt;R = how many positions you can include to the right (including the element itself). Here, R = 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total subarrays = L × R = 1 × 2 = 2.&lt;/p&gt;

&lt;p&gt;This is the multiplication principle. You're picking a left endpoint (L choices) and a right endpoint (R choices) independently. Every combination gives a valid subarray where arr[i] is the minimum. Same math as "3 shirts and 4 pants = 12 outfits."&lt;/p&gt;

&lt;h2&gt;
  
  
  The bargain hunter in the mall
&lt;/h2&gt;

&lt;p&gt;So the whole problem reduces to: for each element, find L and R. Which means finding the nearest smaller element to the left and right. The brute force way to do this is to scan in both directions from every index. That's O(n²), which defeats the purpose.&lt;/p&gt;

&lt;p&gt;This is where the monotonic stack comes in. And the analogy that made it click for me was a bargain hunter walking through a mall.&lt;/p&gt;

&lt;p&gt;Picture a long corridor of shops, all selling the same item at different prices. You're walking left to right. At each shop, you want to know: what's the nearest shop behind me that's cheaper?&lt;/p&gt;

&lt;p&gt;You could turn around and check every shop you've passed. Or you could be smart about what you remember.&lt;/p&gt;

&lt;p&gt;As you walk, you keep a mental list of shops worth remembering. The rule: when you see a new price, forget every remembered shop that's more expensive than it. Why? If shop A sells for $5 and shop B (closer to you, further down the corridor) sells for $3, shop A is dead forever. For any shop you visit in the future, you'd hit shop B first, and it's cheaper. Shop A can never be anyone's answer again.&lt;/p&gt;

&lt;p&gt;What survives in your memory is always a list of prices in increasing order. The most recent one is the nearest cheaper option for wherever you're standing.&lt;/p&gt;

&lt;p&gt;That mental list is the monotonic stack. You walk through the array once, and for each element, the stack tells you the nearest smaller element to the left. One pass, O(n).&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;[3, 1, 2, 4]&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At index 0 (val 3): List empty. Nothing cheaper. L = 1. Remember $3.&lt;/li&gt;
&lt;li&gt;At index 1 (val 1): $3 is more expensive than $1. Forget $3. List empty. Nothing cheaper. L = 2. Remember $1.&lt;/li&gt;
&lt;li&gt;At index 2 (val 2): $1 is cheaper than $2. Keep it. Nearest cheaper = index 1. L = 2 - 1 = 1. Remember $2.&lt;/li&gt;
&lt;li&gt;At index 3 (val 4): $2 is cheaper than $4. Keep it. Nearest cheaper = index 2. L = 3 - 2 = 1. Remember $4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One left-to-right pass gives all L values. A right-to-left pass with the same logic gives all R values. Two passes, O(n) total.&lt;/p&gt;

&lt;h2&gt;
  
  
  The duplicate problem and the asymmetric fix
&lt;/h2&gt;

&lt;p&gt;There's one tricky edge case. What happens when two elements have the same value?&lt;/p&gt;

&lt;p&gt;Take &lt;code&gt;[3, 1, 2, 1]&lt;/code&gt;. Both &lt;code&gt;1&lt;/code&gt;s could claim to be the minimum of the subarray &lt;code&gt;[1, 2, 1]&lt;/code&gt;. If both claim it, you double-count.&lt;/p&gt;

&lt;p&gt;The fix feels weird at first: make one side strict and the other inclusive. On the left pass, pop elements that are greater than &lt;em&gt;or equal&lt;/em&gt; to the current value (so equal values don't block you). On the right pass, only pop elements that are &lt;em&gt;strictly greater&lt;/em&gt; (so equal values do block you).&lt;/p&gt;

&lt;p&gt;This means the left &lt;code&gt;1&lt;/code&gt; can't extend past the right &lt;code&gt;1&lt;/code&gt; on its right side, because equal counts as a boundary there. The right &lt;code&gt;1&lt;/code&gt; can extend past the left &lt;code&gt;1&lt;/code&gt; on its left side, because equal doesn't count as a boundary there. They split the territory cleanly.&lt;/p&gt;

&lt;p&gt;I initially thought this was a hack. Two identical elements should split ownership evenly, right? But think of it like HR assigning projects to identical twins at a company. It doesn't matter who gets each project, as long as every project goes to exactly one person. No double billing, no gaps. The asymmetric rule is just a consistent tiebreaker.&lt;/p&gt;

&lt;p&gt;This pattern shows up everywhere in competitive programming. The &lt;code&gt;i &amp;lt; j&lt;/code&gt; constraint when counting pairs (the handshake problem), skipping duplicates in two-pointer solutions for 3Sum, &lt;code&gt;lower_bound&lt;/code&gt; vs &lt;code&gt;upper_bound&lt;/code&gt; in binary search. They're all the same idea: when multiple candidates could claim the same thing, pick a consistent tiebreaker so exactly one wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing when (and when not) to reach for a monotonic stack
&lt;/h2&gt;

&lt;p&gt;The core signal: "for each element, find the nearest element that beats it in some direction." If you can rephrase a problem as "nearest greater" or "nearest smaller" to the left or right, it's monotonic stack territory.&lt;/p&gt;

&lt;p&gt;It shows up in several disguises. Stock span problems ("how many consecutive days had a lower price?"), temperature problems ("how many days until a warmer day?"), histogram problems ("how wide can this bar extend?"). They all reduce to finding boundaries.&lt;/p&gt;

&lt;p&gt;Where it doesn't apply, even when it looks like it might:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sliding window maximum&lt;/strong&gt; uses a monotonic &lt;em&gt;deque&lt;/em&gt;, not a stack. You need to remove from both ends. The "from both ends" requirement is the giveaway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Min Stack&lt;/strong&gt; has "min" and "stack" in the name, but it's a data structure design problem. You're not popping based on value comparisons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Longest Valid Parentheses&lt;/strong&gt; uses a stack, but not a monotonic one. Elements are pushed and popped based on matching rules, not because one element is "bigger" or "smaller" than another.&lt;/p&gt;

&lt;p&gt;The quick filter: does the popping logic depend on comparing values for ordering? If yes, monotonic stack. If the popping is driven by something else (window boundaries, bracket matching, design constraints), it's a different tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to practice next
&lt;/h2&gt;

&lt;p&gt;These are ordered by difficulty, with notes on how each connects to what's covered above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LC 496, Next Greater Element I&lt;/strong&gt; (easy). Direct "next greater" application. Good warm-up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 739, Daily Temperatures&lt;/strong&gt; (medium). Next greater element with distance calculation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 901, Stock Span Problem&lt;/strong&gt; (medium). Previous greater element. Tests if you can flip the direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 84, Largest Rectangle in Histogram&lt;/strong&gt; (hard). Same L/R boundary finding, applied to area. Very close to Sum of Subarray Minimums.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 2104, Sum of Subarray Ranges&lt;/strong&gt; (medium). Four monotonic stack passes: L and R for minimums, L and R for maximums.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 503, Next Greater Element II&lt;/strong&gt; (medium). Circular array twist. Handle it by looping through the array twice using &lt;code&gt;i % n&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LC 42, Trapping Rain Water&lt;/strong&gt; (hard). Try solving it with a monotonic stack after the others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I've been working through these on &lt;a href="https://levelop.dev" rel="noopener noreferrer"&gt;Levelop&lt;/a&gt; and this one really forced me to understand the "why" behind the pattern, not just the template.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>leetcode</category>
      <category>code</category>
      <category>algorithms</category>
      <category>datastructures</category>
    </item>
    <item>
      <title>I Tried Two Pointers on a DP Problem and Learned What "Subset" Actually Means</title>
      <dc:creator>Avinash Tyagi</dc:creator>
      <pubDate>Wed, 29 Apr 2026 12:46:13 +0000</pubDate>
      <link>https://dev.to/avinash_tyagi_98/i-tried-two-pointers-on-a-dp-problem-and-learned-what-subset-actually-means-4goe</link>
      <guid>https://dev.to/avinash_tyagi_98/i-tried-two-pointers-on-a-dp-problem-and-learned-what-subset-actually-means-4goe</guid>
      <description>&lt;p&gt;I've been working through coding problems that I can understand when reading solutions but struggle to solve on my own. This series is about breaking down the "why does this actually work" part. Not tutorials. Just honest breakdowns of where my thinking got stuck and what finally unstuck it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;LeetCode 416: Partition Equal Subset Sum. Given an array of positive integers, determine if you can split it into two subsets with equal sum.&lt;/p&gt;

&lt;p&gt;My first instinct: use two pointers. One starting from the left, one from the right. Grow each window. Whichever side has the smaller sum, expand that pointer. When they meet, check if the sums are equal.&lt;/p&gt;

&lt;p&gt;I even thought about sorting first to make the approach work better. It felt clean. It felt efficient. It was completely wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why two pointers don't work here
&lt;/h2&gt;

&lt;p&gt;Take &lt;code&gt;[2, 3, 5, 7, 1, 6]&lt;/code&gt;. Total is 24, so each subset needs to sum to 12. The answer is true: &lt;code&gt;{5, 7}&lt;/code&gt; and &lt;code&gt;{2, 3, 1, 6}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sorted: &lt;code&gt;[1, 2, 3, 5, 6, 7]&lt;/code&gt;. Running two pointers from both ends, the sums land at 11 and 13. They never balance. But the valid partition exists. It just requires picking the 5 and 7 from the middle while leaving everything else.&lt;/p&gt;

&lt;p&gt;Two pointers assume you're splitting at some dividing line. Everything left goes in one group, everything right goes in the other. But "subset" means you can cherry-pick from anywhere. The 5 and the 7 aren't adjacent in the sorted array, and they definitely aren't on the same side of any split point.&lt;/p&gt;

&lt;p&gt;That distinction between "split at a point" and "pick from anywhere" is what I missed entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real question hiding inside this problem
&lt;/h2&gt;

&lt;p&gt;Once the subset framing clicked, the problem simplified. If the total sum is S, each subset needs to be S/2. And if S is odd, you can stop immediately. You can't split an odd number into two equal integers.&lt;/p&gt;

&lt;p&gt;So the problem reduces to: can I find any subset of &lt;code&gt;nums&lt;/code&gt; that adds up to exactly S/2?&lt;/p&gt;

&lt;p&gt;That's subset sum. And subset sum is a DP problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with plain recursion
&lt;/h2&gt;

&lt;p&gt;Before any DP, I wrote the simplest version. For each element, you have two choices: include it in the subset or skip it. Include means the remaining target shrinks by that element's value. Skip means the target stays the same.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canReach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canReach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canReach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;or&lt;/code&gt; is because we only need one path to work. If including element 3 eventually leads to a valid subset, we don't care what skipping it would have done.&lt;/p&gt;

&lt;p&gt;This works. It's also slow. For &lt;code&gt;[1, 5, 5, 11]&lt;/code&gt; with target 11, the recursion tree branches twice at every element. That's 2^n paths. But more importantly, some of those paths ask the same question. "Can I reach target 5 starting from index 3?" shows up from multiple branches. Pure recursion answers it every time from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding memoization
&lt;/h2&gt;

&lt;p&gt;The fix is three lines. Before computing, check if you've seen this (index, target) pair before. After computing, store the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canReach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canReach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;canReach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same recursion. Same logic. Just remembering answers. This is the top-down DP approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom-up version (and where I got confused)
&lt;/h2&gt;

&lt;p&gt;The array-based DP flips the approach. Instead of recursing and caching, you build a boolean array where &lt;code&gt;dp[s]&lt;/code&gt; answers: "can I reach sum s with the elements I've processed so far?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I understood what the code does. I could trace through it. But the line &lt;code&gt;dp[s - num]&lt;/code&gt; felt disconnected from anything I could reason about. In my previous DP problem (decode ways), &lt;code&gt;dp[i]&lt;/code&gt; meant "position i in the string." The index was a location. Here, the index IS the sum. That shift messed with my mental model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cashier analogy
&lt;/h2&gt;

&lt;p&gt;What made it click was thinking about making exact change.&lt;/p&gt;

&lt;p&gt;You're a cashier. A customer owes you $11. You have coins worth $1, $5, $5, and $11. You can only use each coin once. Can you make exactly $11?&lt;/p&gt;

&lt;p&gt;You start knowing you can make $0 (by using nothing). Then you pick up each coin one at a time and ask: "what new amounts can I make now?"&lt;/p&gt;

&lt;p&gt;Before any coins: &lt;code&gt;{0}&lt;/code&gt;&lt;br&gt;
Pick up the $1 coin: everything I could make before, plus everything-plus-1. So &lt;code&gt;{0, 1}&lt;/code&gt;.&lt;br&gt;
Pick up the first $5 coin: &lt;code&gt;{0, 1, 5, 6}&lt;/code&gt;.&lt;br&gt;
Pick up the second $5 coin: &lt;code&gt;{0, 1, 5, 6, 10, 11}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;11 is in the set. Done.&lt;/p&gt;

&lt;p&gt;Each time you pick up a coin, the old amounts stay (that's skipping the coin) and new amounts appear (that's including it). The same two choices as the recursion, just expressed as a growing set of reachable values.&lt;/p&gt;

&lt;p&gt;Now &lt;code&gt;dp[s - num]&lt;/code&gt; has a natural reading: "I want to make $s. I'm holding a $num coin. Can I cover the rest?" If &lt;code&gt;dp[s - num]&lt;/code&gt; is true, then yes, adding this coin gets me to $s.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backwards loop
&lt;/h2&gt;

&lt;p&gt;This was the other thing that felt arbitrary until I traced through what goes wrong without it.&lt;/p&gt;

&lt;p&gt;Say &lt;code&gt;dp = [True, False, False, ...]&lt;/code&gt; and you process the $1 coin. If you loop forwards (small to large):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;s=1&lt;/code&gt;: &lt;code&gt;dp[0]&lt;/code&gt; is True, so &lt;code&gt;dp[1] = True&lt;/code&gt;. Fine.&lt;br&gt;
&lt;code&gt;s=2&lt;/code&gt;: &lt;code&gt;dp[1]&lt;/code&gt; is True (you just set it), so &lt;code&gt;dp[2] = True&lt;/code&gt;. Wait.&lt;br&gt;
&lt;code&gt;s=3&lt;/code&gt;: &lt;code&gt;dp[2]&lt;/code&gt; is True (just set again), so &lt;code&gt;dp[3] = True&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You used the $1 coin three times. You only have one.&lt;/p&gt;

&lt;p&gt;The problem is you're reading values you wrote during the same round. Each "yes" you write feeds the next check, and the coin keeps getting reused.&lt;/p&gt;

&lt;p&gt;Going backwards (large to small): when you set &lt;code&gt;dp[s] = True&lt;/code&gt;, the values you read from (&lt;code&gt;dp[s - num]&lt;/code&gt;) are at smaller indices. You already passed those indices. You haven't touched them this round. So you're always reading "yesterday's answers," not the ones you just wrote.&lt;/p&gt;

&lt;p&gt;The mental image that stuck: backwards means each coin goes back in the drawer after you check it. You can't accidentally grab it again. Forwards means the coin stays on the counter, and you keep picking it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheatsheet
&lt;/h2&gt;

&lt;p&gt;This generalizes to a simple rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can each item be used only once?&lt;/strong&gt; Loop backwards. This is 0/1 knapsack, partition subset sum, target sum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can items be reused?&lt;/strong&gt; Loop forwards. This is coin change, unbounded knapsack, combination sum.&lt;/p&gt;

&lt;p&gt;One question. One answer. That's the entire decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete solution
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canPartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;dp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                    &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I got wrong along the way
&lt;/h2&gt;

&lt;p&gt;My first attempt at the DP had &lt;code&gt;dp[target - i]&lt;/code&gt; instead of &lt;code&gt;dp[i - num]&lt;/code&gt;. The difference matters. &lt;code&gt;dp[i - num]&lt;/code&gt; asks "was the sum that's &lt;code&gt;num&lt;/code&gt; less than &lt;code&gt;i&lt;/code&gt; reachable?" That's the include question: if I can reach 1, and I have a 5-coin, I can reach 6. My wrong version was asking a completely unrelated question about a completely unrelated index.&lt;/p&gt;

&lt;p&gt;The fix came from going back to the cashier framing. "I want $6. I have $5. Can I cover the remaining $1?" That's &lt;code&gt;dp[6 - 5]&lt;/code&gt;. Not &lt;code&gt;dp[11 - 6]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to practice next
&lt;/h2&gt;

&lt;p&gt;These all use the same "items toward a target" DP pattern, ordered by difficulty:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 494 (Target Sum)&lt;/strong&gt;: Same include/skip structure. Each element is either added or subtracted. The target just shifts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 322 (Coin Change)&lt;/strong&gt;: Unlimited reuse, so the loop goes forwards. Good for locking in the direction rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 474 (Ones and Zeroes)&lt;/strong&gt;: 0/1 knapsack but with two dimensions (count of 0s and 1s). Same backwards loop, just a 2D array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 518 (Coin Change II)&lt;/strong&gt;: Counting combinations instead of just true/false. Forwards loop, addition instead of boolean OR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'm building &lt;a href="https://levelop.dev" rel="noopener noreferrer"&gt;Levelop&lt;/a&gt; to help people get better at problems like these. If you want more breakdowns like this, check it out.&lt;/p&gt;

</description>
      <category>leetcode</category>
      <category>algorithms</category>
      <category>datastructures</category>
      <category>coding</category>
    </item>
    <item>
      <title>I Couldn't See Why dp[i] = dp[i+1] + dp[i+2] Actually Counts Anything</title>
      <dc:creator>Avinash Tyagi</dc:creator>
      <pubDate>Tue, 28 Apr 2026 20:03:27 +0000</pubDate>
      <link>https://dev.to/avinash_tyagi_98/i-couldnt-see-why-dpi-dpi1-dpi2-actually-counts-anything-3f00</link>
      <guid>https://dev.to/avinash_tyagi_98/i-couldnt-see-why-dpi-dpi1-dpi2-actually-counts-anything-3f00</guid>
      <description>&lt;p&gt;I've been working through coding problems that I can understand when reading solutions but struggle to solve on my own. This series is about breaking down the "why does this actually work" part. Not tutorials. Just honest breakdowns of where my thinking got stuck and what finally unstuck it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;LeetCode 91: Decode Ways. You get a string of digits. Each digit or pair of digits maps to a letter (1 = A, 2 = B, ... 26 = Z). Count how many ways you can decode the entire string.&lt;/p&gt;

&lt;p&gt;When I first read this, I thought: find all the different permutations and combinations of the input string, filter out the ones that are one or two digits, count them. If it starts with 0, return 0.&lt;/p&gt;

&lt;p&gt;That's wrong in a very specific way. The string order is fixed. You never rearrange anything. You're deciding where to place the splits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partitioning, not permutations
&lt;/h2&gt;

&lt;p&gt;Take &lt;code&gt;"226"&lt;/code&gt;. You're not shuffling characters around. You're choosing how to chop the string into consecutive pieces, left to right, where each piece is one or two digits between 1 and 26.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;2 | 2 | 6&lt;/code&gt; → B, B, F&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;22 | 6&lt;/code&gt; → V, F&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2 | 26&lt;/code&gt; → B, Z&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three ways. Every digit is used exactly once. Every piece is consumed in order.&lt;/p&gt;

&lt;p&gt;This seems obvious once you see it, but my first instinct was to think about substrings and combinations. The shift to "I'm just deciding where to put dividers in a fixed sequence" changed how I thought about the whole problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The binary choice at each position
&lt;/h2&gt;

&lt;p&gt;Once partitioning clicks, the structure gets simple. At every position in the string, you have exactly two options: take one digit or take two digits. That's it. Then you repeat from wherever you land.&lt;/p&gt;

&lt;p&gt;At position 0 of &lt;code&gt;"226"&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Take &lt;code&gt;"2"&lt;/code&gt; (one digit) → now you're at position 1 with &lt;code&gt;"26"&lt;/code&gt; remaining&lt;/li&gt;
&lt;li&gt;Take &lt;code&gt;"22"&lt;/code&gt; (two digits) → now you're at position 2 with &lt;code&gt;"6"&lt;/code&gt; remaining&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those remaining strings has its own set of choices. If you draw this out, it forms a binary tree. Every path from the root to a leaf where you've consumed the entire string is one valid partition.&lt;/p&gt;

&lt;p&gt;I could see this. I could draw the tree. I could count partitions by hand. What I could not figure out was why the DP formula &lt;code&gt;dp[i] = dp[i+1] + dp[i+2]&lt;/code&gt; actually produces the right count. It looked like it should work. I could verify it on examples. But I couldn't explain &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where I got stuck
&lt;/h2&gt;

&lt;p&gt;The DP says: &lt;code&gt;dp[i]&lt;/code&gt; is the number of ways to decode the string starting from position &lt;code&gt;i&lt;/code&gt;. Fill right to left. Base case: &lt;code&gt;dp[n] = 1&lt;/code&gt; (you've consumed everything, that counts as one completed partition).&lt;/p&gt;

&lt;p&gt;The recurrence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# take 1 digit
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;–&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# take 2 digits
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could fill in the array mechanically. For &lt;code&gt;"1226"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dp[4] = 1
dp[3] = 1
dp[2] = 2
dp[1] = 3
dp[0] = 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correct answer: 5. But &lt;em&gt;why&lt;/em&gt; does adding two values from the right give you the count of all partitions? I stared at visual walkthroughs and formula breakdowns and nothing was landing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually made it click: the addition principle
&lt;/h2&gt;

&lt;p&gt;The breakthrough had nothing to do with DP specifically. It came from a much more basic idea.&lt;/p&gt;

&lt;p&gt;You have 3 red shirts and 2 blue shirts. How many shirts can you pick? You say 3 + 2 = 5. This works because no shirt is both red and blue. The groups don't overlap.&lt;/p&gt;

&lt;p&gt;That's the addition principle. If you can split a counting problem into non-overlapping groups, the total count is the sum of the group counts.&lt;/p&gt;

&lt;p&gt;Now, at position &lt;code&gt;i&lt;/code&gt; in the string, every partition that passes through this position does exactly one of two things: it takes one digit or it takes two digits. Not both. These are two non-overlapping groups.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Group A (took one digit): all partitions where the first chunk starting at &lt;code&gt;i&lt;/code&gt; is a single digit. How many partitions are in this group? However many ways you can finish the rest of the string from position &lt;code&gt;i+1&lt;/code&gt;. That's &lt;code&gt;dp[i+1]&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Group B (took two digits): all partitions where the first chunk starting at &lt;code&gt;i&lt;/code&gt; is a two-digit number. How many? However many ways to finish from position &lt;code&gt;i+2&lt;/code&gt;. That's &lt;code&gt;dp[i+2]&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every partition is in exactly one group. No overlap, no gaps. So the total is the sum: &lt;code&gt;dp[i] = dp[i+1] + dp[i+2]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's it. The addition isn't some DP trick. It's the same logic as counting shirts. You're splitting the partitions into two non-overlapping groups based on the choice made at this position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Addition vs. multiplication (and why this matters for other DP problems)
&lt;/h2&gt;

&lt;p&gt;While working through this, I realized the addition principle has a sibling that shows up in different DP problems: the multiplication principle.&lt;/p&gt;

&lt;p&gt;Addition: you're choosing between alternatives. Red shirt OR blue shirt. Take 1 digit OR take 2 digits. The groups compete for one slot. You add.&lt;/p&gt;

&lt;p&gt;Multiplication: you're combining independent sequential choices. Pick a shirt AND pick pants. 3 shirts × 4 pants = 12 outfits. The choices don't compete. They stack.&lt;/p&gt;

&lt;p&gt;The question to ask yourself at each step of a DP recurrence: "Am I looking at alternatives (OR) or combinations (AND)?"&lt;/p&gt;

&lt;p&gt;Decode ways is pure OR. At each position, you do one thing or the other. So the formula is a sum.&lt;/p&gt;

&lt;p&gt;Something like "how many n-character PINs can you make with 5 vowels for position 1 and 10 digits for position 2?" is pure AND. You need both positions filled, every vowel pairs with every digit. So it's a product: &lt;code&gt;dp[i] = dp[i-1] × choices[i]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The paint fence problem (no three consecutive posts the same color) uses both. At each post, the total splits into two groups (same as previous OR different from previous, that's addition). Within the "different" group, each valid history combines with each color choice (that's multiplication). So the recurrence has both: &lt;code&gt;diff[i] = (same[i-1] + diff[i-1]) × (k-1)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Recognizing which principle applies at each piece of a recurrence is what turns "I can verify it works" into "I understand why it works."&lt;/p&gt;

&lt;h2&gt;
  
  
  The base case makes sense now too
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dp[n] = 1&lt;/code&gt; confused me initially. You're past the end of the string. There's nothing left. Why is that 1 and not 0?&lt;/p&gt;

&lt;p&gt;Because it's the "I made it" signal. If a path of choices led you all the way past the last digit, that path consumed everything and represents one complete partition. If &lt;code&gt;dp[n]&lt;/code&gt; were 0, every path would add up to 0 and nothing would ever get counted.&lt;/p&gt;

&lt;p&gt;Think of it like the road fork analogy. If you take a path and it leads to an exit, that exit counts as 1. The exit existing is what makes the path worth counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The direction question
&lt;/h2&gt;

&lt;p&gt;One more thing that wasn't obvious: why fill right to left?&lt;/p&gt;

&lt;p&gt;It depends on how you define &lt;code&gt;dp[i]&lt;/code&gt;. I defined it as "ways to decode from position &lt;code&gt;i&lt;/code&gt; to the end." That means &lt;code&gt;dp[i]&lt;/code&gt; depends on &lt;code&gt;dp[i+1]&lt;/code&gt; and &lt;code&gt;dp[i+2]&lt;/code&gt;, both of which are to the right. So those need to exist before you compute &lt;code&gt;dp[i]&lt;/code&gt;. Right to left.&lt;/p&gt;

&lt;p&gt;But you could define it the other way: &lt;code&gt;dp[i]&lt;/code&gt; = ways to decode from the start up to position &lt;code&gt;i&lt;/code&gt;. Now your answer at &lt;code&gt;i&lt;/code&gt; depends on &lt;code&gt;dp[i-1]&lt;/code&gt; and &lt;code&gt;dp[i-2]&lt;/code&gt;, both to the left. Fill left to right. Same final answer, different direction.&lt;/p&gt;

&lt;p&gt;The direction isn't a property of the problem. It's a property of how you phrase the question &lt;code&gt;dp[i]&lt;/code&gt; is answering. "How many ways to get here?" looks backward. "How many ways from here to the end?" looks forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;numDecodings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;two_digit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;two_digit&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practice problems
&lt;/h2&gt;

&lt;p&gt;In order of difficulty, all using similar DP thinking:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 70 (Climbing Stairs)&lt;/strong&gt;: Same recurrence structure as decode ways but without validity checks. Pure &lt;code&gt;dp[i] = dp[i-1] + dp[i-2]&lt;/code&gt;. Good for building muscle memory on the pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 198 (House Robber)&lt;/strong&gt;: 1D DP with "take or skip" choices at each position. The OR/addition principle applies the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 256 (Paint House)&lt;/strong&gt;: Tracking states (which color was last used) similar to the paint fence same/diff tracking. Uses both addition and multiplication thinking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 639 (Decode Ways II)&lt;/strong&gt;: Decode ways but with wildcard characters. Same core logic, more validity branches to handle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://levelop.dev" rel="noopener noreferrer"&gt;Levelop&lt;/a&gt; to help people get better at problems like these. If you want more breakdowns like this, check it out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>leetcode</category>
      <category>code</category>
      <category>datastructures</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>I Knew BFS Existed but Couldn't Wire It Into a Grid Problem</title>
      <dc:creator>Avinash Tyagi</dc:creator>
      <pubDate>Mon, 27 Apr 2026 12:57:54 +0000</pubDate>
      <link>https://dev.to/avinash_tyagi_98/i-knew-bfs-existed-but-couldnt-wire-it-into-a-grid-problem-1l65</link>
      <guid>https://dev.to/avinash_tyagi_98/i-knew-bfs-existed-but-couldnt-wire-it-into-a-grid-problem-1l65</guid>
      <description>&lt;p&gt;I've been working through coding problems that I can understand when reading solutions but struggle to solve on my own. This series is about breaking down the "why does this actually work" part. Not tutorials. Just honest breakdowns of where my thinking got stuck and what finally unstuck it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;LeetCode 1091: Shortest Path in Binary Matrix. You get an n×n grid of 0s and 1s. Find the shortest path from the top-left corner to the bottom-right corner, moving through cells with value 0. You can move in all 8 directions (up, down, left, right, and all four diagonals). Return the number of cells in the path, or -1 if no path exists.&lt;/p&gt;

&lt;p&gt;I looked at this and thought: okay, at every cell that's a 0, I check the connected neighbors. Skip anything out of bounds or already visited. Straightforward enough.&lt;/p&gt;

&lt;p&gt;But when I actually sat down to write it, two things tripped me up. I didn't know how to pick BFS over DFS for this. And even after choosing BFS, I had no idea how to track the path length.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Shortest" is doing the work
&lt;/h2&gt;

&lt;p&gt;My first instinct was just "explore the neighbors." I hadn't committed to BFS or DFS. Both would find &lt;em&gt;a&lt;/em&gt; path. But the problem says &lt;em&gt;shortest&lt;/em&gt;, and that word changes everything.&lt;/p&gt;

&lt;p&gt;DFS goes deep. It picks one direction and follows it as far as possible before backtracking. It'll find a path, but it might be a terrible one. To guarantee the shortest, you'd have to explore every possible path and compare. That's expensive.&lt;/p&gt;

&lt;p&gt;BFS explores in waves. Wave 1 is everything 1 step from the start. Wave 2 is everything 2 steps away. Wave 3, everything 3 steps. The first time you reach the destination, you got there in the fewest steps possible. You never need to compare alternatives because you tried all shorter routes first.&lt;/p&gt;

&lt;p&gt;That's the core insight: BFS gives you shortest path for free. You don't need extra logic to compare paths or track the minimum. The structure of the algorithm handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "How do I track the distance though?"
&lt;/h2&gt;

&lt;p&gt;This was my actual sticking point. I had a queue. I was popping cells and pushing neighbors. The BFS loop worked. But I couldn't figure out where the path length came from.&lt;/p&gt;

&lt;p&gt;My queue held coordinates like &lt;code&gt;[i, j]&lt;/code&gt;. When I reached the bottom-right corner, I had no way to know how many steps it took to get there.&lt;/p&gt;

&lt;p&gt;The fix is simple once you see it: store the distance &lt;em&gt;with each item&lt;/em&gt; in the queue. Instead of &lt;code&gt;[i, j]&lt;/code&gt;, push &lt;code&gt;[i, j, distance]&lt;/code&gt;. The start cell goes in as &lt;code&gt;[0, 0, 1]&lt;/code&gt; (distance 1 because the problem counts the start cell). When you process a cell at distance &lt;code&gt;d&lt;/code&gt; and add its neighbors, each neighbor gets &lt;code&gt;d + 1&lt;/code&gt;. When you pop the destination cell, its distance is your answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# row, col, distance
&lt;/span&gt;
&lt;span class="c1"&gt;# later, when adding neighbors:
&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;curr_distance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The distance piggybacks on the BFS traversal. No separate tracking needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full solution
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shortestPathBinaryMatrix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# mark visited
&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;curr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dj&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dj&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;continue&lt;/span&gt;
                    &lt;span class="n"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;dj&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ni&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ni&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;nj&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;nj&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;nj&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                            &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                            &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;nj&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walk through a small grid to see the waves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0, 0, 1]
[0, 1, 0]
[0, 0, 0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wave 1 (distance 1): &lt;code&gt;(0,0)&lt;/code&gt;.&lt;br&gt;
Wave 2 (distance 2): &lt;code&gt;(0,1)&lt;/code&gt;, &lt;code&gt;(1,0)&lt;/code&gt;.&lt;br&gt;
Wave 3 (distance 3): &lt;code&gt;(2,0)&lt;/code&gt;, &lt;code&gt;(2,1)&lt;/code&gt;.&lt;br&gt;
Wave 4 (distance 4): &lt;code&gt;(2,2)&lt;/code&gt;, &lt;code&gt;(1,2)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;BFS reaches &lt;code&gt;(2,2)&lt;/code&gt; at distance 4. That's the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes I made that are probably common
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Counting edges instead of cells.&lt;/strong&gt; I initially set the starting distance to 0. For the grid above, that gives 3 instead of 4. The LeetCode problem counts cells visited (both endpoints), not hops between them. This is the kind of off-by-one that makes you fail test cases and stare at correct-looking code for ten minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marking cells as visited when popping, not when enqueueing.&lt;/strong&gt; This is subtle. Say cell &lt;code&gt;(1,1)&lt;/code&gt; is a neighbor of both &lt;code&gt;(0,0)&lt;/code&gt; and &lt;code&gt;(0,1)&lt;/code&gt;. If you process &lt;code&gt;(0,0)&lt;/code&gt; first and add &lt;code&gt;(1,1)&lt;/code&gt; to the queue but don't mark it visited yet, then when you process &lt;code&gt;(0,1)&lt;/code&gt;, you add &lt;code&gt;(1,1)&lt;/code&gt; again. Now it's in the queue twice, and all its neighbors get processed twice too. The fix: mark a cell visited the moment you add it to the queue, not when you pop it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Off-by-one on the destination check.&lt;/strong&gt; If the grid is 5×5, the bottom-right is &lt;code&gt;(4,4)&lt;/code&gt;, not &lt;code&gt;(5,5)&lt;/code&gt;. Checking &lt;code&gt;i == len(grid)&lt;/code&gt; instead of &lt;code&gt;i == len(grid) - 1&lt;/code&gt; means you never find the destination. Obvious in hindsight. Not obvious at 11pm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;list.pop(0)&lt;/code&gt; instead of &lt;code&gt;collections.deque&lt;/code&gt;.&lt;/strong&gt; A regular Python list pops from the front in O(n) because it shifts every remaining element. On a big grid, this quietly turns your O(n) BFS into O(n²). &lt;code&gt;deque.popleft()&lt;/code&gt; is O(1). Small change, big difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  When BFS works (and when it doesn't)
&lt;/h2&gt;

&lt;p&gt;BFS gives shortest path when all edges have the same weight. In a grid where every step costs 1, that holds. If some steps cost more than others (weighted graph), BFS won't work. You'd need Dijkstra's algorithm instead.&lt;/p&gt;

&lt;p&gt;The BFS template itself stays remarkably stable across problems. Once you have the loop, the only things that change are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where you start.&lt;/strong&gt; One cell? Multiple cells at once? Every cell of a certain type? (Multi-source BFS seeds the queue with all starting points at distance 0.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where you stop.&lt;/strong&gt; Reached a specific cell? All targets converted? Queue is empty?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which directions.&lt;/strong&gt; 4-directional (up/down/left/right) or 8-directional (including diagonals)?&lt;/p&gt;

&lt;p&gt;The loop, the queue, the visited tracking, the distance propagation: all identical every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice problems
&lt;/h2&gt;

&lt;p&gt;In order of difficulty, all using BFS on grids:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 994 (Rotting Oranges)&lt;/strong&gt;: Multi-source BFS. All rotten oranges start spreading simultaneously. Same wave-by-wave expansion, but you seed the queue with every rotten cell instead of one corner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 542 (01 Matrix)&lt;/strong&gt;: Find the shortest distance from every cell to the nearest 0. Multi-source BFS starting from all 0-cells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 1162 (As Far from Land as Possible)&lt;/strong&gt;: Multi-source BFS from all land cells. The answer is the last wave number. Same pattern, different framing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 752 (Open the Lock)&lt;/strong&gt;: BFS on a non-grid graph. Each "node" is a 4-digit lock combination. Neighbors are one-digit turns. Tests whether you see that BFS works on any graph, not just grids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 127 (Word Ladder)&lt;/strong&gt;: BFS on words where neighbors differ by one letter. Harder to see the graph structure, but the BFS mechanics are identical.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://levelop.dev" rel="noopener noreferrer"&gt;Levelop&lt;/a&gt; to help people get better at problems like these. If you want more breakdowns like this, check it out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>dsa</category>
      <category>coding</category>
      <category>leetcode</category>
    </item>
    <item>
      <title>Why I Stopped Trying to Count "Exactly Right" and Started Subtracting</title>
      <dc:creator>Avinash Tyagi</dc:creator>
      <pubDate>Sun, 26 Apr 2026 10:21:20 +0000</pubDate>
      <link>https://dev.to/avinash_tyagi_98/why-i-stopped-trying-to-count-exactly-right-and-started-subtracting-19c3</link>
      <guid>https://dev.to/avinash_tyagi_98/why-i-stopped-trying-to-count-exactly-right-and-started-subtracting-19c3</guid>
      <description>&lt;p&gt;I've been working through coding problems that I can understand when reading solutions but struggle to solve on my own. This series is about breaking down the "why does this actually work" part. Not tutorials. Just honest breakdowns of where my thinking got stuck and what finally unstuck it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem that broke my brain
&lt;/h2&gt;

&lt;p&gt;LeetCode 795: Number of Subarrays with Bounded Maximum. You get an array of integers and a range &lt;code&gt;[left, right]&lt;/code&gt;. Count how many contiguous subarrays have their maximum element within that range.&lt;/p&gt;

&lt;p&gt;I understood what the problem was asking. I could verify answers by hand. But when I sat down to code it, I kept getting tangled. My sliding window logic was a mess because every element fell into one of three buckets: too small (below &lt;code&gt;left&lt;/code&gt;), just right (in the range), or too big (above &lt;code&gt;right&lt;/code&gt;). Each bucket needed different handling, and my code reflected that confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three zones, two problems
&lt;/h2&gt;

&lt;p&gt;Here's what made this problem feel harder than it should be. When an element is bigger than &lt;code&gt;right&lt;/code&gt;, it's a hard wall. No valid subarray can include it. You reset everything.&lt;/p&gt;

&lt;p&gt;But when an element is smaller than &lt;code&gt;left&lt;/code&gt;? It's fine. It just can't be the maximum. A subarray like &lt;code&gt;[2, 1, 1]&lt;/code&gt; with &lt;code&gt;left=2, right=3&lt;/code&gt; is perfectly valid because the max is 2. The small elements are just passengers.&lt;/p&gt;

&lt;p&gt;So "not in range" actually means two completely different things depending on which side you fall on. Trying to handle both cases in one pass meant tracking which elements were carrying the subarray (in-range elements) separately from which elements were just tagging along (too-small elements). Every &lt;code&gt;if/else&lt;/code&gt; branch got another condition. The logic kept growing.&lt;/p&gt;

&lt;p&gt;That's when I realized: maybe counting "exactly in range" directly is the wrong approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if you only had one boundary?
&lt;/h2&gt;

&lt;p&gt;Consider a simpler question: how many subarrays have their maximum ≤ some value &lt;code&gt;k&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;This is dramatically easier. There's only one type of violation. If an element is ≤ &lt;code&gt;k&lt;/code&gt;, it extends your current valid window. If it's &amp;gt; &lt;code&gt;k&lt;/code&gt;, the window breaks. Two states, one condition.&lt;/p&gt;

&lt;p&gt;And here's the counting trick that makes it fast. At each position &lt;code&gt;i&lt;/code&gt;, the number of valid subarrays &lt;em&gt;ending at that position&lt;/em&gt; equals the current window length. If you've had 4 consecutive elements all ≤ &lt;code&gt;k&lt;/code&gt;, there are exactly 4 subarrays ending at position 3: the single element, the pair ending here, the triple, and all four together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_at_most&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire helper. Walk through &lt;code&gt;nums = [2, 1, 1, 3]&lt;/code&gt; with &lt;code&gt;k = 3&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Position&lt;/th&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;≤ 3?&lt;/th&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Subarrays ending here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[2]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[1], [2,1]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[1], [1,1], [2,1,1]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[3], [1,3], [1,1,3], [2,1,1,3]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total: 1 + 2 + 3 + 4 = 10. Every subarray in the entire array has max ≤ 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subtraction
&lt;/h2&gt;

&lt;p&gt;Now the key move. "Max is in &lt;code&gt;[left, right]&lt;/code&gt;" means "max ≤ right" AND "max ≥ left." That second condition is the annoying one. But you can rewrite it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;subarrays where max is in [left, right] = subarrays where max ≤ right &lt;strong&gt;minus&lt;/strong&gt; subarrays where max ≤ left - 1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first count includes everything up to &lt;code&gt;right&lt;/code&gt;. The second count captures the ones that are too small (max below &lt;code&gt;left&lt;/code&gt;). Subtracting removes exactly those.&lt;/p&gt;

&lt;p&gt;Same array, &lt;code&gt;left = 2, right = 3&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;atMost(3)&lt;/code&gt; = 10 (computed above)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;atMost(1)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Position&lt;/th&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;≤ 1?&lt;/th&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total: 3.&lt;/p&gt;

&lt;p&gt;Answer: 10 - 3 = &lt;strong&gt;7&lt;/strong&gt;. The valid subarrays are &lt;code&gt;[2], [2,1], [2,1,1], [2,1,1,3], [1,1,3], [1,3], [3]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The full solution is five lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;numSubarrayBoundedMax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_at_most&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;count_at_most&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;count_at_most&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where else this works (and where it doesn't)
&lt;/h2&gt;

&lt;p&gt;This decomposition isn't specific to "bounded max." It works anywhere you need to count subarrays defined by a range, as long as the "at most" version behaves nicely with a sliding window.&lt;/p&gt;

&lt;p&gt;LeetCode 992 (Subarrays with Exactly K Distinct Integers) is the same idea wearing different clothes. Counting "exactly K distinct" directly is painful because valid subarrays form a band in the middle of your window. Some shorter subarrays ending at the same position might have fewer than K distinct elements. Some longer ones might have more. You'd need two left pointers to track both edges of that band.&lt;/p&gt;

&lt;p&gt;But "at most K distinct" is a clean sliding window. One left pointer, shrink when you exceed K. So: &lt;code&gt;exactly(K) = atMost(K) - atMost(K - 1)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;LeetCode 1248 (Count Number of Nice Subarrays) is the same pattern again, just counting odd numbers instead of distinct elements.&lt;/p&gt;

&lt;p&gt;The pattern works because of a property called monotonicity: if a window satisfies "max ≤ k," every sub-window inside it also satisfies "max ≤ k." Same for "distinct count ≤ k." Shrinking the window from the left can never violate the property if it already holds.&lt;/p&gt;

&lt;p&gt;This breaks for something like "median ≤ k." Removing an element from the left of a window can increase or decrease the median unpredictably. You can't maintain a valid window by just shrinking, so the sliding window approach falls apart, and the subtraction decomposition with it.&lt;/p&gt;

&lt;p&gt;The general rule: if a range constraint is making your counting hard, check whether you can split it into two one-sided constraints. If each one-sided version has the monotonicity property (valid window implies all sub-windows are valid), the decomposition works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice problems
&lt;/h2&gt;

&lt;p&gt;In order of difficulty, all using this pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 713 (Subarray Product Less Than K)&lt;/strong&gt;: Single-sided, products instead of max. The "subarrays ending here = window size" insight is the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 2110 (Number of Smooth Descent Periods)&lt;/strong&gt;: Run-length counting, almost identical to the &lt;code&gt;count_at_most&lt;/code&gt; helper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 992 (Subarrays with Exactly K Distinct Integers)&lt;/strong&gt;: The classic atMost(K) - atMost(K-1). Needs a hashmap for frequencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 1248 (Count Number of Nice Subarrays)&lt;/strong&gt;: Same decomposition, counting odd numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 2444 (Count Subarrays With Fixed Bounds)&lt;/strong&gt;: Harder. Two simultaneous constraints without the subtraction shortcut. Tests whether you can extend the thinking.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://levelop.dev" rel="noopener noreferrer"&gt;Levelop&lt;/a&gt; to help people get better at problems like these. If you want more breakdowns like this, check it out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>leetcode</category>
      <category>dsa</category>
    </item>
    <item>
      <title>I Couldn't Count Subarrays Until I Thought About Handshakes</title>
      <dc:creator>Avinash Tyagi</dc:creator>
      <pubDate>Sat, 25 Apr 2026 10:04:37 +0000</pubDate>
      <link>https://dev.to/avinash_tyagi_98/i-couldnt-count-subarrays-until-i-thought-about-handshakes-2ee</link>
      <guid>https://dev.to/avinash_tyagi_98/i-couldnt-count-subarrays-until-i-thought-about-handshakes-2ee</guid>
      <description>&lt;p&gt;Hi everyone, I'm starting a series where I break down the core intuition behind coding patterns I've struggled to truly understand or reuse confidently. Not just the "how" but the "why does this actually work" part. Starting with one that tripped me up longer than I'd like to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Couldn't Count Subarrays Until I Thought About Handshakes
&lt;/h2&gt;

&lt;p&gt;I understood the &lt;em&gt;concept&lt;/em&gt; behind "Subarrays Divisible by K" for a while. Prefix sums, mod k, check for matching remainders. Cool. But every time I tried to write the actual counting logic, I'd freeze. Why does &lt;code&gt;count += map[remainder]&lt;/code&gt; work? Why not &lt;code&gt;count += 1&lt;/code&gt;? Where does the formula come from?&lt;/p&gt;

&lt;p&gt;It finally clicked when I stopped thinking about arrays and started thinking about people in a room.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap in my understanding
&lt;/h2&gt;

&lt;p&gt;Here's what I already knew: if two prefix sums have the same remainder when divided by k, the elements between them sum to a multiple of k. Standard prefix sum trick.&lt;/p&gt;

&lt;p&gt;What I &lt;em&gt;didn't&lt;/em&gt; get was the leap from "find matching remainders" to "count all valid subarrays." I could identify &lt;em&gt;that&lt;/em&gt; a valid subarray existed, but not how to count &lt;em&gt;all&lt;/em&gt; of them efficiently.&lt;/p&gt;

&lt;p&gt;Turns out I was missing some pretty basic math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: The Handshake Problem
&lt;/h2&gt;

&lt;p&gt;5 people in a room. Everyone shakes hands with everyone else exactly once. How many handshakes?&lt;/p&gt;

&lt;p&gt;The formula is &lt;code&gt;n * (n-1) / 2&lt;/code&gt;. For 5 people, that's 10. But the formula alone wasn't what helped me. What helped was seeing it built &lt;strong&gt;incrementally&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Person A enters. Nobody there. &lt;strong&gt;0 handshakes.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Person B enters. Shakes hands with A. &lt;strong&gt;+1.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Person C enters. Shakes hands with A and B. &lt;strong&gt;+2.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Person D enters. Shakes with A, B, C. &lt;strong&gt;+3.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Person E enters. Shakes with A, B, C, D. &lt;strong&gt;+4.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: 0 + 1 + 2 + 3 + 4 = &lt;strong&gt;10.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the same as 5×4/2 = 10. The incremental version and the formula give the same answer, just computed differently.&lt;/p&gt;

&lt;p&gt;This is exactly what the hashmap does. When you write &lt;code&gt;count += map[remainder]&lt;/code&gt;, you're saying: "this new prefix sum just walked into the room. How many prefix sums with the same remainder are already here? Shake hands with all of them."&lt;/p&gt;

&lt;p&gt;You don't add 1. You add however many are already waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: Modular Arithmetic (Remainder Buckets)
&lt;/h2&gt;

&lt;p&gt;When you divide any integer by k, the remainder is always between 0 and k-1. So every number falls into one of k "buckets."&lt;/p&gt;

&lt;p&gt;The rule that matters: &lt;strong&gt;if two numbers have the same remainder mod k, their difference is always divisible by k.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;14 % 5 = 4
 9 % 5 = 4
14 - 9 = 5   → divisible by 5 ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works every time, not just with convenient examples. If &lt;code&gt;a % k == b % k&lt;/code&gt;, then &lt;code&gt;(a - b) % k == 0&lt;/code&gt;. Always.&lt;/p&gt;

&lt;p&gt;This is why we care about prefix sum remainders. The subarray sum between index i and j is &lt;code&gt;prefix[j] - prefix[i]&lt;/code&gt;. If both prefix sums land in the same remainder bucket, their difference (the subarray sum) is divisible by k.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;So the algorithm becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compute prefix sums and their remainders (that's the mod arithmetic)&lt;/li&gt;
&lt;li&gt;Group them into remainder buckets&lt;/li&gt;
&lt;li&gt;Count pairs within each bucket (that's the handshake problem)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hashmap approach does step 3 incrementally. Here's a concrete walkthrough with &lt;code&gt;arr = [4, 5, 0, -2, -3, 1]&lt;/code&gt;, &lt;code&gt;k = 5&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start: map = {0: 1}   (prefix[-1] = 0, remainder 0)

i=0: prefix=4,  rem=4, map[4]=0  → count += 0, map={0:1, 4:1}
i=1: prefix=9,  rem=4, map[4]=1  → count += 1, map={0:1, 4:2}
i=2: prefix=9,  rem=4, map[4]=2  → count += 2, map={0:1, 4:3}
i=3: prefix=7,  rem=2, map[2]=0  → count += 0, map={0:1, 4:3, 2:1}
i=4: prefix=4,  rem=4, map[4]=3  → count += 3, map={0:1, 4:4, 2:1}
i=5: prefix=5,  rem=0, map[0]=1  → count += 1, map={0:2, 4:4, 2:1}

Total: 0 + 1 + 2 + 0 + 3 + 1 = 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at remainder 4. It appeared 4 times total (at prefix indices 0, 1, 2, 4). The incremental additions for that bucket alone were 0, 1, 2, 3 which sums to 6. And 4×3/2 = 6. Same answer. The handshake formula, built up one step at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The {0: 1} Seed
&lt;/h2&gt;

&lt;p&gt;One thing that confused me initially: why do we start the map with &lt;code&gt;{0: 1}&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Because there's a "virtual" prefix sum before index 0, which equals 0. If any prefix sum itself is divisible by k (remainder 0), it needs something to pair with. That virtual prefix[-1] = 0 is the pairing partner. Without it, you'd miss all subarrays that start from index 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending to "Subarray Sum Equals K" (LeetCode 560)
&lt;/h2&gt;

&lt;p&gt;Once you get the divisible-by-k version, this one is a small twist. Instead of "same remainder," you look for a specific difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Divisible by K: prefix[j] % k == prefix[i] % k  →  look up same remainder
Sum Equals K:   prefix[j] - prefix[i] == k      →  look up (prefix[j] - k)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subarraySum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;freq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;
        &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;freq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three ingredients: prefix sums give you the "what to track," the lookup condition tells you "what to search for," and the frequency map handles "how to count" (handshakes, not just existence).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; use a frequency dict, not a set. I made this mistake. A set only tells you "yes, it exists." A dict tells you "it exists 3 times," which means 3 valid subarrays, not 1. That's the handshake lesson all over again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math Foundation I Was Missing
&lt;/h2&gt;

&lt;p&gt;If you're stuck on these problems like I was, here's the learning order that worked for me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pair counting (n choose 2):&lt;/strong&gt; Get comfortable with why "n people, count handshakes" = n(n-1)/2, and why building it as 0+1+2+...+(n-1) gives the same answer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modular arithmetic:&lt;/strong&gt; Drill the rule "same remainder means difference is divisible by k" until it's automatic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connecting the two:&lt;/strong&gt; Subarray problems ask you to count pairs of prefix sums that satisfy some condition. Once you see that, the hashmap is just the efficient way to count those pairs on the fly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Problems to Practice
&lt;/h2&gt;

&lt;p&gt;These all use the same pattern, roughly in order of difficulty:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 560&lt;/strong&gt; - Subarray Sum Equals K (the classic)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 974&lt;/strong&gt; - Subarrays Sums Divisible by K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 525&lt;/strong&gt; - Contiguous Array (convert 0s to -1s, then it's sum equals 0)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 1248&lt;/strong&gt; - Count Number of Nice Subarrays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 1010&lt;/strong&gt; - Pairs of Songs Divisible by 60 (pure remainder + pairs, no prefix)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeetCode 437&lt;/strong&gt; - Path Sum III (same technique, but on a tree)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying skill isn't memorizing the prefix sum trick. It's recognizing when a subarray condition can be rewritten as a pairwise condition on prefix values, and then counting those pairs with a hashmap.&lt;/p&gt;

&lt;p&gt;That recognition is what these problems are really testing.&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://levelop.dev" rel="noopener noreferrer"&gt;Levelop&lt;/a&gt; to help people get better at problems like these. If you want more breakdowns like this, check it out.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>coding</category>
      <category>100daysofcode</category>
    </item>
  </channel>
</rss>
