<?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: Xinyang Wu</title>
    <description>The latest articles on DEV Community by Xinyang Wu (@xinyangwuethz).</description>
    <link>https://dev.to/xinyangwuethz</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3971050%2F40c7cbce-7957-4642-a778-9b8f0329c053.png</url>
      <title>DEV Community: Xinyang Wu</title>
      <link>https://dev.to/xinyangwuethz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xinyangwuethz"/>
    <language>en</language>
    <item>
      <title>Your SSH Key Isn’t Always the Problem: A Layer-by-Layer Debugging Guide</title>
      <dc:creator>Xinyang Wu</dc:creator>
      <pubDate>Sun, 19 Jul 2026 06:55:41 +0000</pubDate>
      <link>https://dev.to/xinyangwuethz/your-ssh-key-isnt-always-the-problem-a-layer-by-layer-debugging-guide-281p</link>
      <guid>https://dev.to/xinyangwuethz/your-ssh-key-isnt-always-the-problem-a-layer-by-layer-debugging-guide-281p</guid>
      <description>&lt;p&gt;I once ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh demo-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and immediately suspected my SSH key.&lt;/p&gt;

&lt;p&gt;The actual error was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh: Could not resolve hostname demo-server: Name or service not known
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My key was innocent. SSH had not opened a TCP connection, exchanged a single protocol message, checked the server's identity, or attempted user authentication. It did not even know which IP address to contact.&lt;/p&gt;

&lt;p&gt;That failure taught me a more useful way to debug SSH: &lt;strong&gt;treat every error as a timestamp&lt;/strong&gt;. It tells you how far the connection progressed before it stopped.&lt;/p&gt;

&lt;p&gt;Instead of changing keys, restarting services, and deleting &lt;code&gt;known_hosts&lt;/code&gt; entries at random, ask one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which layer did I fail to reach?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the model I now use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The connection has seven checkpoints
&lt;/h2&gt;

&lt;p&gt;When you type &lt;code&gt;ssh demo-server&lt;/code&gt;, the useful mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;configuration
    → name resolution
    → TCP connection
    → SSH transport and key exchange
    → server verification
    → user authentication
    → channel creation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage depends on the previous one. A failure in name resolution cannot be fixed by rotating an authentication key, because the authentication stage never happened.&lt;/p&gt;

&lt;p&gt;These are diagnostic checkpoints, not seven separate wire-protocol layers. In the SSH protocol, server host-key authentication is part of transport key exchange; I separate verification here because it produces its own recognizable failures and fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 0: SSH resolves its configuration
&lt;/h2&gt;

&lt;p&gt;Before contacting the network, the client combines command-line options with SSH configuration, usually from &lt;code&gt;~/.ssh/config&lt;/code&gt; and the system configuration.&lt;/p&gt;

&lt;p&gt;Imagine this entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host lab-*
    User dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;ssh lab-gpu&lt;/code&gt; matches the pattern and sets the remote user to &lt;code&gt;dev&lt;/code&gt;. It does &lt;strong&gt;not&lt;/strong&gt; give &lt;code&gt;lab-gpu&lt;/code&gt; an IP address. Without a &lt;code&gt;HostName&lt;/code&gt;, the destination is still literally &lt;code&gt;lab-gpu&lt;/code&gt;, so the operating system must resolve that name later.&lt;/p&gt;

&lt;p&gt;A complete alias might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host demo-server
    HostName 192.0.2.10
    User dev
    Port 22
    IdentityFile ~/.ssh/id_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fastest way to inspect what SSH actually derived is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; demo-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a compact view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; demo-server | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(hostname|user|port|identityfile) '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is safer than reasoning from one config block by eye. Multiple matching blocks may contribute settings, and for most options SSH uses the first value it obtains. “The most specific block wins” is therefore a misleading model.&lt;/p&gt;

&lt;p&gt;If the resolved hostname, user, port, or identity file is wrong, stay at this stage. The network is not your problem yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: The operating system resolves the hostname
&lt;/h2&gt;

&lt;p&gt;SSH next needs to turn the resolved hostname into an address. Depending on the machine, that may involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/hosts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a VPN or overlay network's DNS&lt;/li&gt;
&lt;li&gt;other system name-service mechanisms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One subtle trap is confusing a deployment tool's inventory with the operating system's resolver. An inventory might contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;lab-gpu&lt;/span&gt; &lt;span class="py"&gt;ansible_host&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;192.0.2.10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ansible understands that mapping. OpenSSH does not automatically read Ansible's inventory. Unless &lt;code&gt;lab-gpu&lt;/code&gt; is also defined through SSH configuration or a system resolver, &lt;code&gt;ssh lab-gpu&lt;/code&gt; can still fail with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Could not resolve hostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message is unusually precise: configuration completed, but the client could not obtain an address. No packet reached an SSH server.&lt;/p&gt;

&lt;p&gt;First use &lt;code&gt;ssh -G&lt;/code&gt; to find the effective &lt;code&gt;hostname&lt;/code&gt;; resolver tools do not read &lt;code&gt;~/.ssh/config&lt;/code&gt; aliases. If that effective value is a name such as &lt;code&gt;server.example.com&lt;/code&gt;, check the Linux system name-service path with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;getent hosts server.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, the equivalent system lookup can be inspected with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dscacheutil &lt;span class="nt"&gt;-q&lt;/span&gt; host &lt;span class="nt"&gt;-a&lt;/span&gt; name server.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To query DNS specifically rather than the full system resolver path, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short server.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check whether the name only exists while a company VPN, mesh VPN, or private DNS service is active.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: The client opens a TCP connection
&lt;/h2&gt;

&lt;p&gt;Once the client has an address, it tries to connect to the configured port—normally port 22.&lt;/p&gt;

&lt;p&gt;At this point, similar-looking messages describe different conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection timed out&lt;/strong&gt;: packets may be dropped by a firewall, the route may be broken, the VPN may be missing, or the host may be unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No route to host&lt;/strong&gt;: the local networking stack cannot find a usable path, or an intermediate device reports that the destination is unreachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection refused&lt;/strong&gt;: the client received an active rejection, usually because the destination has nothing listening on that port. An intermediary can also reject it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small TCP probe against the effective address and port from &lt;code&gt;ssh -G&lt;/code&gt; helps isolate this layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-vz&lt;/span&gt; 192.0.2.10 22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not prove that SSH authentication will work. It only answers a narrower and very useful question: can I establish direct TCP connectivity to this host and port? If SSH uses &lt;code&gt;ProxyJump&lt;/code&gt; or &lt;code&gt;ProxyCommand&lt;/code&gt;, a probe from your machine does not test the same path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: SSH negotiates a secure transport
&lt;/h2&gt;

&lt;p&gt;After TCP succeeds, the client and server exchange SSH protocol versions and negotiate algorithms. They perform a key exchange to derive fresh session keys, and the server proves control of its host key by signing data from the exchange.&lt;/p&gt;

&lt;p&gt;A successful exchange creates the encrypted, integrity-protected transport used by the rest of the session. Modern cipher suites often provide authenticated encryption directly, so it is better to think in terms of confidentiality and integrity than to assume every connection uses a separate MAC algorithm.&lt;/p&gt;

&lt;p&gt;At the wire-protocol level, the host-key proof belongs to this exchange. The next checkpoint is the client's decision about whether to trust that key.&lt;/p&gt;

&lt;p&gt;Failures here look different from network failures. Examples include protocol banners that never arrive, an early connection reset, or messages such as “no matching key exchange method found.” These often point to incompatible algorithm policies, a non-SSH service on the port, an intermediary, or a server-side SSH problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: The client verifies the server
&lt;/h2&gt;

&lt;p&gt;Encryption is not enough if you encrypt a session to the wrong machine. The client therefore checks the server's host key against &lt;code&gt;~/.ssh/known_hosts&lt;/code&gt; or another configured host-key database.&lt;/p&gt;

&lt;p&gt;On a first connection, many setups use trust on first use: you verify and accept the fingerprint, and the client remembers it. On later connections, the key should match.&lt;/p&gt;

&lt;p&gt;If you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REMOTE HOST IDENTIFICATION HAS CHANGED!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the connection has already passed name resolution, TCP, and enough of the SSH handshake to receive a host key. The warning could mean a legitimate rebuild or address reassignment—but it could also indicate misrouting or an active attack.&lt;/p&gt;

&lt;p&gt;Do not make “delete the &lt;code&gt;known_hosts&lt;/code&gt; entry” your automatic response. First verify the new fingerprint through a trusted, independent channel. Remove or replace the old entry only after you understand why it changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: The server authenticates the user
&lt;/h2&gt;

&lt;p&gt;Only now does your personal SSH key enter the story.&lt;/p&gt;

&lt;p&gt;With public-key authentication:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the private key remains on the client;&lt;/li&gt;
&lt;li&gt;the corresponding public key is normally listed in the remote account's &lt;code&gt;authorized_keys&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the client signs session-bound authentication data to prove that it controls the private key;&lt;/li&gt;
&lt;li&gt;the server verifies that signature with the public key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The private key is not uploaded to the server.&lt;/p&gt;

&lt;p&gt;Two similarly named files have completely different jobs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Usually lives on&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;known_hosts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;client&lt;/td&gt;
&lt;td&gt;Verifies the server's identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;authorized_keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;server&lt;/td&gt;
&lt;td&gt;Lists public keys allowed to authenticate as a user&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This distinction explains why the following error is actually evidence of partial success:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Permission denied (publickey,password)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client resolved a destination, connected over TCP, negotiated SSH, and accepted the server identity. It then failed to authenticate the requested user.&lt;/p&gt;

&lt;p&gt;Now—and only now—it makes sense to inspect the resolved username, offered identities, file permissions, account policy, SSH agent, and the remote &lt;code&gt;authorized_keys&lt;/code&gt; entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 6: SSH opens a channel
&lt;/h2&gt;

&lt;p&gt;A successful authentication does not necessarily mean you will receive an interactive shell. SSH can open different channels for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an interactive shell&lt;/li&gt;
&lt;li&gt;a single remote command&lt;/li&gt;
&lt;li&gt;SFTP&lt;/li&gt;
&lt;li&gt;local, remote, or dynamic port forwarding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;ExitOnForwardFailure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-L&lt;/span&gt; 127.0.0.1:8080:127.0.0.1:3000 demo-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;asks the client to listen only on the local loopback interface at port &lt;code&gt;8080&lt;/code&gt;, without running a remote command. A connection to that port travels through the encrypted SSH session, then the remote SSH server connects to &lt;code&gt;127.0.0.1:3000&lt;/code&gt; from its own network context. &lt;code&gt;ExitOnForwardFailure&lt;/code&gt; catches failure to establish the requested listener, although it cannot guarantee that the target service will accept a later forwarded connection.&lt;/p&gt;

&lt;p&gt;If authentication succeeds but forwarding reports “administratively prohibited” or “open failed,” the problem is at the channel or destination-policy layer—not with DNS or your key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the error as a stage marker
&lt;/h2&gt;

&lt;p&gt;Here is the compact version I wish I had earlier:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error or symptom&lt;/th&gt;
&lt;th&gt;Last relevant stage&lt;/th&gt;
&lt;th&gt;Check next&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Could not resolve hostname&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;name resolution&lt;/td&gt;
&lt;td&gt;effective &lt;code&gt;HostName&lt;/code&gt;, DNS, hosts file, VPN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;No route to host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;network path&lt;/td&gt;
&lt;td&gt;routes, VPN, interface, gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Connection timed out&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP path&lt;/td&gt;
&lt;td&gt;reachability, firewall, correct address and port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Connection refused&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP destination&lt;/td&gt;
&lt;td&gt;SSH service and listening port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no matching key-exchange algorithm&lt;/td&gt;
&lt;td&gt;SSH transport&lt;/td&gt;
&lt;td&gt;client/server algorithm policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;host identification changed&lt;/td&gt;
&lt;td&gt;server verification&lt;/td&gt;
&lt;td&gt;independently verify the new fingerprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Permission denied (publickey)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;user authentication&lt;/td&gt;
&lt;td&gt;resolved user, offered key, &lt;code&gt;authorized_keys&lt;/code&gt;, account policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forwarding is prohibited or open fails&lt;/td&gt;
&lt;td&gt;channel creation&lt;/td&gt;
&lt;td&gt;SSH server policy and target reachability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is not a replacement for logs, but it prevents category errors. It tells you where to begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  My three-step first response
&lt;/h2&gt;

&lt;p&gt;When an SSH connection fails, I now start with these steps.&lt;/p&gt;

&lt;p&gt;First, inspect the effective configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; demo-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, on a Unix-like server, get a stage-by-stage trace without password prompts and make a successful probe exit immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-vvv&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;BatchMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;ConnectTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5 demo-server &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verbose output usually makes the boundary visible: configuration applied, address resolved, connection attempted, host key checked, identities offered, or channel rejected.&lt;/p&gt;

&lt;p&gt;Be careful before sharing that output. Sanitize hostnames, usernames, addresses, paths, key fingerprints, and other infrastructure details.&lt;/p&gt;

&lt;p&gt;Third, test the suspected boundary independently. Resolve the effective hostname if resolution failed; probe the effective address and port if they exist; inspect offered keys only if authentication was actually reached. Remember that resolver and TCP tools do not apply your SSH alias or proxy configuration for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debug the earliest failure, not the loudest theory
&lt;/h2&gt;

&lt;p&gt;SSH combines configuration, naming, networking, cryptography, identity, and multiplexed channels behind one short command. That makes it feel mysterious—but it also makes its errors surprisingly informative.&lt;/p&gt;

&lt;p&gt;The next time &lt;code&gt;ssh&lt;/code&gt; fails, read the error as a timestamp, identify the earliest incomplete checkpoint, and test that boundary directly. A DNS problem does not need a new key. A refused TCP connection does not need a cleared &lt;code&gt;known_hosts&lt;/code&gt;. And an authentication error is proof that several lower layers already worked.&lt;/p&gt;

&lt;p&gt;That one mental model turns SSH debugging from guesswork into a sequence of small, testable questions.&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>networking</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>10 Cross-Topic Lessons from a 123/138 LeetCode Sprint</title>
      <dc:creator>Xinyang Wu</dc:creator>
      <pubDate>Sat, 06 Jun 2026 10:02:16 +0000</pubDate>
      <link>https://dev.to/xinyangwuethz/6-cross-topic-lessons-from-grinding-46-leetcode-problems-1bh2</link>
      <guid>https://dev.to/xinyangwuethz/6-cross-topic-lessons-from-grinding-46-leetcode-problems-1bh2</guid>
      <description>&lt;p&gt;I first published this retrospective 46 problems into a 12-week DSA sprint. My tracker is now at &lt;strong&gt;123 / 138 slots&lt;/strong&gt;: I have worked through Week 11 and am &lt;strong&gt;2 / 12 into Week 12&lt;/strong&gt;. The tracker deliberately repeats a few problems across topics, so 123 is a progress metric—not a claim of 123 unique accepted problems.&lt;/p&gt;

&lt;p&gt;The expanded tracker did not produce dozens of unrelated tricks. It made the same failures show up in new disguises. A bad state definition in dynamic programming felt a lot like an under-specified sliding window. Marking a BFS node too late felt like inserting into a hash map too early. A wrong heap invariant looked suspiciously like a wrong binary-search invariant.&lt;/p&gt;

&lt;p&gt;These are the ten lessons that survived that repetition. The bugs below are not hypothetical warnings. They are mistakes I wrote, plus the smallest counterexamples that finally made the mistake obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. If I cannot define the state in one sentence, I am not ready to update it
&lt;/h2&gt;

&lt;p&gt;My first House Robber state was internally inconsistent:&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="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;nums&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;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="nf"&gt;max&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;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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initialization says &lt;code&gt;dp[1]&lt;/code&gt; means “money from robbing house 1.” The recurrence needs it to mean “the best result using houses 0 through 1.” Those are different contracts.&lt;/p&gt;

&lt;p&gt;The counterexample is &lt;code&gt;[2, 1, 1, 2]&lt;/code&gt;. Initializing &lt;code&gt;dp[1] = 1&lt;/code&gt; permanently loses the valid choice of taking the first house. The recurrence returns 3 instead of 4. I added a separate &lt;code&gt;vmax&lt;/code&gt; to patch the output, but the wrong state had already contaminated later states. A downstream maximum could not repair an upstream definition.&lt;/p&gt;

&lt;p&gt;The correct base case has the same meaning as 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;prev2&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;prev1&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;nums&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;nums&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;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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
    &lt;span class="n"&gt;prev2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prev1&lt;/span&gt;&lt;span class="p"&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;prev1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same lesson appeared outside DP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In Fruit Into Baskets, I stored only the kinds of fruit in a deque. That could not answer “when has this kind completely left the window?” The input &lt;code&gt;[1,1,1,2,3,3]&lt;/code&gt; exposed it: my window kept three kinds and reported 5 instead of 4. The state needed counts, not just names.&lt;/li&gt;
&lt;li&gt;In Maximum Product Subarray, keeping only the largest product ending here loses a negative value that may become the next maximum. For &lt;code&gt;[2,-5,-2,-4,3]&lt;/code&gt;, the state must retain both the current maximum and minimum to recover the answer 24.&lt;/li&gt;
&lt;li&gt;In Longest Palindromic Substring, one left endpoint per right endpoint is not enough. Palindromicity depends on both boundaries, so the state naturally becomes &lt;code&gt;dp[left][right]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My current rule is simple: finish the sentence &lt;strong&gt;“this state contains exactly…”&lt;/strong&gt; before writing an update. If I need a patch variable later, I first suspect that sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The empty state is data, not a special case
&lt;/h2&gt;

&lt;p&gt;Prefix sums taught me to put &lt;code&gt;prefix[0] = 0&lt;/code&gt; in front. Later topics made the idea more general: the empty state should be represented by the identity that cooperates with the operation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Empty-state value&lt;/th&gt;
&lt;th&gt;Why it works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sum&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x + 0 = x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x * 1 = x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimum&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a real candidate always beats it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-inf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a real candidate always beats it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counting-DP empty choice&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;there is exactly one way to choose nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unreachable count&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no valid construction has reached this state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefix-frequency count&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{0: 1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one empty prefix exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Earliest prefix index&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{0: -1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the empty prefix ends before index 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One of my earliest versions used an inclusive prefix array, appended a zero at the &lt;em&gt;end&lt;/em&gt;, and relied on Python's negative indexing:&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;return&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="c1"&gt;# left == 0 reads prefix[-1]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It passed, but only because &lt;code&gt;prefix[-1]&lt;/code&gt; wrapped around to the hidden zero. The same code is an out-of-bounds bug in languages without negative indexing. The honest form is:&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;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;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;x&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;prefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;right&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;prefix&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coin Change exposed the other half of the rule. I used &lt;code&gt;-1&lt;/code&gt; for “unreachable” while minimizing. Then unreachable states won every &lt;code&gt;min&lt;/code&gt;, and &lt;code&gt;-1 + 1&lt;/code&gt; created fake zero-cost solutions. With &lt;code&gt;coins = [2,3]&lt;/code&gt; and &lt;code&gt;amount = 4&lt;/code&gt;, that version returned 0 instead of 2.&lt;/p&gt;

&lt;p&gt;Internally, &lt;code&gt;inf&lt;/code&gt; is the right value because it composes safely:&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="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inf&lt;/span&gt;&lt;span class="sh"&gt;"&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;amount&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="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&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;for&lt;/span&gt; &lt;span class="n"&gt;coin&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coins&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;coin&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;a&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;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&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;a&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;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;coin&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;return&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;dp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&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;amount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The external API may require &lt;code&gt;-1&lt;/code&gt;. That does not mean &lt;code&gt;-1&lt;/code&gt; belongs inside the recurrence. I now separate the algorithm's internal algebra from the return-value protocol, and translate only at the exit.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Update order is part of the algorithm
&lt;/h2&gt;

&lt;p&gt;Two adjacent lines are not interchangeable just because both eventually run.&lt;/p&gt;

&lt;p&gt;In prefix-sum counting, the map is a record of the past. I must query it before the current prefix joins that past:&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="n"&gt;x&lt;/span&gt;
&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;seen&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="c1"&gt;# query history
&lt;/span&gt;&lt;span class="n"&gt;seen&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;seen&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="c1"&gt;# then become history
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I insert first, the current prefix can match itself. For Subarray Sum Equals K with &lt;code&gt;nums = [1,-1]&lt;/code&gt; and &lt;code&gt;k = 0&lt;/code&gt;, the correct answer is 1; inserting first counts two zero-length “subarrays” as well and returns 3.&lt;/p&gt;

&lt;p&gt;The same ordering constraint reappeared in three different tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BFS:&lt;/strong&gt; mark a node when it enters the queue, not when it leaves. Otherwise several parents can enqueue the same node before its first dequeue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Union-Find cycle detection:&lt;/strong&gt; ask whether &lt;code&gt;find(u) == find(v)&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; union. If I union first, the condition is true for every edge. On &lt;code&gt;[[1,2],[2,3],[3,1],[3,4]]&lt;/code&gt;, the redundant edge is &lt;code&gt;[3,1]&lt;/code&gt;, not the first or last edge by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compressed 0/1 knapsack:&lt;/strong&gt; iterate capacity backward. With one item &lt;code&gt;2&lt;/code&gt; and target &lt;code&gt;4&lt;/code&gt;, a forward scan sets &lt;code&gt;dp[2]&lt;/code&gt; and then reuses that freshly written value to set &lt;code&gt;dp[4]&lt;/code&gt;—using the same item twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all temporal invariants. “Seen” means seen &lt;strong&gt;before now&lt;/strong&gt;. “Already connected” means connected &lt;strong&gt;before this edge&lt;/strong&gt;. “Previous row” means the state &lt;strong&gt;before this item&lt;/strong&gt;. Moving a line changes that meaning, even when the variables have the same names.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Every destructive move needs a proof
&lt;/h2&gt;

&lt;p&gt;Popping a stack entry, advancing a pointer, pruning a branch, and moving a binary-search boundary all destroy candidates. Before doing any of them, I now ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What fact proves this candidate can never be the answer?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My rotated-array minimum passed tests with two overlapping safety nets: an early “peek at the previous element” return, plus a final fallback. The loop itself used &lt;code&gt;right = mid - 1&lt;/code&gt; even when &lt;code&gt;mid&lt;/code&gt; could still be the minimum. Delete either safety net and the code quietly breaks.&lt;/p&gt;

&lt;p&gt;The clean invariant keeps &lt;code&gt;mid&lt;/code&gt; whenever it may still be the 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;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right&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="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="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&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;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;if&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;mid&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;right&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;mid&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;# mid is provably not the minimum
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&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;mid&lt;/span&gt;      &lt;span class="c1"&gt;# mid may be the minimum, so keep it
&lt;/span&gt;&lt;span class="k"&gt;return&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;left&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Container With Most Water gave me the same lesson in greedy form. I tried moving both ends when neither immediate next move improved the current area. On &lt;code&gt;[5,1,100,50]&lt;/code&gt;, that jumps past the optimal pair &lt;code&gt;(100,50)&lt;/code&gt; with area 50.&lt;/p&gt;

&lt;p&gt;The valid greedy move is not “take the next thing that looks better.” It is “discard the shorter wall because every narrower container that keeps it is capped by the same short wall.” That is a domination proof.&lt;/p&gt;

&lt;p&gt;Longest Increasing Subsequence exposed the distinction between &lt;strong&gt;pop&lt;/strong&gt; and &lt;strong&gt;replace&lt;/strong&gt;. Treating its &lt;code&gt;tails&lt;/code&gt; array like a monotonic stack and popping larger endings destroys the historical fact that a subsequence of that length has existed. For &lt;code&gt;[2,3,1,4]&lt;/code&gt;, popping gives length 2 instead of 3. The correct operation replaces exactly one threshold—the first value greater than or equal to the new number—without erasing longer records.&lt;/p&gt;

&lt;p&gt;The syntax is tiny; the proof is the algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Constraints are algorithm instructions
&lt;/h2&gt;

&lt;p&gt;I used to read constraints after understanding the problem. Now I read them as a list of approaches the problem setter is trying to kill.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;“O(log n) required”&lt;/strong&gt; killed my “binary search, then expand left and right” solution for finding a target range. On &lt;code&gt;[8,8,8,8,8]&lt;/code&gt;, the expansion is linear. The answer needs two boundary searches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“The array may contain negatives”&lt;/strong&gt; kills a sum-based sliding window because the window sum is no longer monotone. Prefix sum plus a hash map survives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“O(n) required”&lt;/strong&gt; in Longest Consecutive Sequence kills &lt;code&gt;sorted(set(nums))&lt;/code&gt;. Sorting gets the right result in &lt;code&gt;O(n log n)&lt;/code&gt; but misses the point; scanning only from values whose predecessor is absent gives linear total work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinates up to &lt;code&gt;10^9&lt;/code&gt; or &lt;code&gt;2^31 - 1&lt;/code&gt;&lt;/strong&gt; kill per-coordinate arrays and loops. I tried a difference array for Interval List Intersections and a coordinate-by-coordinate loop for Skyline. One wants impossible memory; the other can run for billions of empty positions. The right iteration domain is intervals or event points, not the numeric axis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This question has become part of my pre-code checklist:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which obvious solution is this constraint designed to exclude?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It catches complexity bugs before a correct-looking implementation makes them harder to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. When many searches share a destination, reverse the search
&lt;/h2&gt;

&lt;p&gt;Several graph problems became simple only after I stopped searching from every unknown point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Surrounded Regions is awkward as “which regions are enclosed?” It becomes easy as “which &lt;code&gt;O&lt;/code&gt; cells can the boundary reach?” Mark those safe cells, then flip the rest.&lt;/li&gt;
&lt;li&gt;Pacific Atlantic Water Flow is expensive as “can this cell flow to each ocean?” Reverse the edges: start from both oceans and climb to cells of equal or greater height. The answer is the intersection of the two reachable sets.&lt;/li&gt;
&lt;li&gt;01 Matrix asks every cell for its nearest zero. Put &lt;strong&gt;all zeros&lt;/strong&gt; into one queue at distance 0 and expand once.&lt;/li&gt;
&lt;li&gt;Rotting Oranges asks how simultaneous infection spreads. Put &lt;strong&gt;all rotten oranges&lt;/strong&gt; into the initial queue; one BFS layer is one minute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shared template is small:&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="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;all_states_with_known_answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;mark_all_as_seen&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="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;state&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;nxt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;reverse_or_outward_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&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;nxt&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nxt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;state&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;seen&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;nxt&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;nxt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running one BFS per source repeats the same regions. A multi-source BFS is not merely an optimization; it models simultaneous expansion correctly.&lt;/p&gt;

&lt;p&gt;I now look for phrases such as “nearest source,” “eventually reaches a boundary,” or “all sources spread at once.” They often mean: &lt;strong&gt;start from the states whose answers are already known and propagate outward&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The lifetime of &lt;code&gt;visited&lt;/code&gt; depends on the question
&lt;/h2&gt;

&lt;p&gt;I once treated “mark visited” as a generic graph rule. Backtracking showed why that is incomplete.&lt;/p&gt;

&lt;p&gt;For flood fill, the question is about nodes: “which cells belong to this component?” Once a cell is processed, visiting it again has no value. The mark is permanent.&lt;/p&gt;

&lt;p&gt;For Word Search or permutations, the question is about paths: “which choices form this particular solution?” A cell or item may be used by a different sibling path. The mark must live only for the current recursive frame:&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;path&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;choice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;used&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="bp"&gt;True&lt;/span&gt;

&lt;span class="nf"&gt;backtrack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;used&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="bp"&gt;False&lt;/span&gt;
&lt;span class="n"&gt;path&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;p&gt;Leaving out the restoration does not just leak state; it permanently blocks legal sibling branches and creates false negatives.&lt;/p&gt;

&lt;p&gt;I hit the companion bug in Subsets:&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;answers&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;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# stores the same mutable list object
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After recursion unwound, every entry referred to the same now-empty list. The fix is a snapshot:&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;answers&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;path&lt;/span&gt;&lt;span class="p"&gt;[:])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also explains why “generate everything, then deduplicate” is usually a smell. My first subset approach generated both &lt;code&gt;[1,2]&lt;/code&gt; and &lt;code&gt;[2,1]&lt;/code&gt; and tried to filter afterward. A &lt;code&gt;start&lt;/code&gt; index makes the invalid ordering impossible to generate. For duplicate values, sorting plus same-level pruning gives each result one canonical path.&lt;/p&gt;

&lt;p&gt;The rule I keep now is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enumerating &lt;strong&gt;nodes&lt;/strong&gt;: mark permanently.&lt;/li&gt;
&lt;li&gt;Enumerating &lt;strong&gt;paths&lt;/strong&gt;: mark on entry, restore on exit.&lt;/li&gt;
&lt;li&gt;Saving a mutable path: copy at the moment it becomes a result.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. A recursive function can return one value and build another answer
&lt;/h2&gt;

&lt;p&gt;Tree diameter initially tempted me into two recursive functions: compute a node's height, then recursively compute diameters and call height again at every node. The logic is correct; on a skewed tree the repeated height work makes it &lt;code&gt;O(n^2)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One postorder traversal can produce two different quantities:&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;best&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;def&lt;/span&gt; &lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&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="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&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="n"&gt;best&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;best&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;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# answer using both branches
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&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;left&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="c1"&gt;# value one parent can extend
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction is structural. A path that continues to the parent can use only one child branch. A path whose highest point is the current node can join both.&lt;/p&gt;

&lt;p&gt;Maximum Path Sum uses the same skeleton:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return the best one-sided gain to the parent;&lt;/li&gt;
&lt;li&gt;update a global best with left + node + right;&lt;/li&gt;
&lt;li&gt;clamp negative child gains to zero.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its smallest counterexample is also its most important initialization test: a one-node tree &lt;code&gt;[-3]&lt;/code&gt;. Initializing the global answer to 0 returns a path that does not exist. It must start from a real node value or &lt;code&gt;-inf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Minimum Depth delivered a related warning about base cases. Replacing &lt;code&gt;max&lt;/code&gt; with &lt;code&gt;min&lt;/code&gt; in the maximum-depth recurrence fails on &lt;code&gt;[1,None,2]&lt;/code&gt;: the nonexistent left branch contributes 0 and wins, producing depth 1 instead of 2. An identity that is harmless under one aggregation can poison another.&lt;/p&gt;

&lt;p&gt;I now write down two contracts for recursive tree problems: &lt;strong&gt;what this call returns upward&lt;/strong&gt;, and &lt;strong&gt;what candidate answer this node contributes globally&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Graph modeling happens before DFS, BFS, or Union-Find
&lt;/h2&gt;

&lt;p&gt;The hardest part of several graph problems was choosing what a node meant.&lt;/p&gt;

&lt;p&gt;In Bus Routes, counting station-to-station edges answers the wrong question. The cost is buses boarded, so a BFS layer must represent one additional route. The useful index is &lt;code&gt;stop -&amp;gt; routes containing that stop&lt;/code&gt;, with separate visited sets for stops and routes.&lt;/p&gt;

&lt;p&gt;In Accounts Merge, my account-to-account model was correct but expensive: compare every pair of accounts for shared email, then union matching accounts. That is &lt;code&gt;O(n^2)&lt;/code&gt; before considering set intersections.&lt;/p&gt;

&lt;p&gt;The cleaner model makes &lt;strong&gt;emails&lt;/strong&gt; the nodes. Union all emails within each account. If an email appears in two accounts, it is literally the shared node that joins the components. The pairwise comparison disappears.&lt;/p&gt;

&lt;p&gt;The same modeling question showed up elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Word Ladder nodes are words; edges are one-character changes.&lt;/li&gt;
&lt;li&gt;Open the Lock nodes are four-digit strings; edges are one wheel turn.&lt;/li&gt;
&lt;li&gt;Clone Graph is primarily an &lt;code&gt;old_node -&amp;gt; new_node&lt;/code&gt; mapping problem; DFS versus BFS is secondary.&lt;/li&gt;
&lt;li&gt;In grid Union-Find, &lt;code&gt;(row, col)&lt;/code&gt; becomes &lt;code&gt;row * width + col&lt;/code&gt;, but boundary checks must still happen in two dimensions. Checking only the flattened id lets the right edge wrap into the next row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before choosing a traversal, I now ask three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What exactly is a node?&lt;/li&gt;
&lt;li&gt;What operation creates an edge?&lt;/li&gt;
&lt;li&gt;What does one unit of cost or one BFS layer mean?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Getting those right often makes the algorithm routine. Getting them wrong can make a correct traversal solve a different problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Keep the unresolved frontier, not the whole history
&lt;/h2&gt;

&lt;p&gt;My best heap improvements came from asking what must be eligible &lt;strong&gt;right now&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For Merge K Sorted Lists, I first pushed every node and used the list index as a tuple tie-breaker. One list containing &lt;code&gt;[1,1,2]&lt;/code&gt; breaks it: equal values from the same list also share the same list index, so Python eventually tries to compare two &lt;code&gt;ListNode&lt;/code&gt; objects and raises &lt;code&gt;TypeError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The better invariant is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The heap contains at most one node from each list: that list's smallest unresolved node.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pop one node, then push its successor. Now the list index is unique within the heap, the heap size stays at most &lt;code&gt;K&lt;/code&gt;, and the complexity becomes &lt;code&gt;O(N log K)&lt;/code&gt; instead of sorting all &lt;code&gt;N&lt;/code&gt; nodes.&lt;/p&gt;

&lt;p&gt;IPO exposed the same frontier bug in a different form. I rescanned every project on every round and reinserted affordable projects, so the same project could be completed twice. With &lt;code&gt;k = 2&lt;/code&gt;, &lt;code&gt;w = 0&lt;/code&gt;, profits &lt;code&gt;[1,2]&lt;/code&gt;, and capital &lt;code&gt;[0,3]&lt;/code&gt;, my version earned the first project's profit twice and returned 2; the correct answer is 1.&lt;/p&gt;

&lt;p&gt;Sorting projects by capital and advancing a pointer fixes the lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each newly affordable project enters the profit heap once;&lt;/li&gt;
&lt;li&gt;unchosen affordable projects stay eligible in the heap;&lt;/li&gt;
&lt;li&gt;the chosen project leaves once;&lt;/li&gt;
&lt;li&gt;the pointer never moves backward because capital never decreases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same skeleton later powered offline interval queries and Skyline: sort by the condition that unlocks a candidate, advance one-way, keep active candidates in a heap, and lazily remove candidates only when they can affect the top.&lt;/p&gt;

&lt;p&gt;A heap is not a bag of everything I have seen. A monotonic stack is not a bag of previous indices. They are compressed representations of the &lt;strong&gt;unresolved frontier&lt;/strong&gt;. If I cannot state exactly why every stored item is still eligible, I probably do not yet have the invariant.&lt;/p&gt;




&lt;h2&gt;
  
  
  The meta-lesson
&lt;/h2&gt;

&lt;p&gt;At 46 problems, I thought my bugs lived in edge cases around otherwise-correct algorithms. At 123 tracker slots, I think that distinction is mostly false. The “edge” details are the algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What does the state mean?&lt;/li&gt;
&lt;li&gt;How is the empty state represented?&lt;/li&gt;
&lt;li&gt;In what order do reads, writes, marks, and moves happen?&lt;/li&gt;
&lt;li&gt;What proof allows a candidate to be discarded?&lt;/li&gt;
&lt;li&gt;Which approach does a constraint rule out?&lt;/li&gt;
&lt;li&gt;Can the search run backward from known answers?&lt;/li&gt;
&lt;li&gt;How long should a visited mark live?&lt;/li&gt;
&lt;li&gt;What returns to the parent, and what updates the global answer?&lt;/li&gt;
&lt;li&gt;What are the graph's actual nodes and edges?&lt;/li&gt;
&lt;li&gt;What exactly belongs in the unresolved frontier?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most useful part of my notes is still the same as it was in the first version: every problem gets a “bug I actually wrote” and a counterexample. The fix tells me how to pass one test. The counterexample tells me which assumption was false—and that is the part that transfers to the next topic.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What mistake has followed you across the largest number of seemingly unrelated problems?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>leetcode</category>
      <category>python</category>
      <category>interview</category>
    </item>
  </channel>
</rss>
