-
Notifications
You must be signed in to change notification settings - Fork 0
learn: solve Longest Increasing Subsequence (#300) via guided TDD #241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
e6b37f3
learn: cycle 1 - initialize dp array and implement nested loop with m…
pertrai1 5da0d9f
learn: cycle 2 - verify example 2 with mixed decreasing and increasin…
pertrai1 ce95465
learn: cycle 3 - verify identical elements return LIS of 1
pertrai1 815e469
learn: cycle 4 - verify single element array returns 1
pertrai1 ba71dae
learn: cycle 5 - verify fully sorted array returns n (learner-authore…
pertrai1 34108dd
learn: cycle 6 - verify fully descending array returns 1 (learner-aut…
pertrai1 3876fdf
learn: solve Longest Increasing Subsequence (#300) via guided TDD
pertrai1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
214 changes: 214 additions & 0 deletions
214
leetcode/medium/0300-longest-increasing-subsequence/POST_MORTEM.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| # Interview Debrief — Longest Increasing Subsequence | ||
|
|
||
| **Date:** 2026-02-17 | ||
| **Problem:** [Longest Increasing Subsequence](https://leetcode.com/problems/longest-increasing-subsequence/) — Medium | ||
| **Duration:** ~90 minutes | ||
| **Mode:** Guided TDD (Coach Mode) | ||
|
|
||
| --- | ||
|
|
||
| ## Session Overview | ||
|
|
||
| The candidate worked through LIS using a guided TDD approach across 6 cycles. The | ||
| core algorithm was identified and implemented correctly in cycle 1 with no hints | ||
| needed — a strong signal. The main gap was in articulating invariants precisely: | ||
| the candidate's first invariant statement described loop bounds rather than the DP | ||
| state property. After two rounds of follow-up questions they landed on the correct | ||
| statement. Test ownership progressed from L1 through L3 with growing confidence, | ||
| and the candidate authored two full `it()` blocks in cycles 5-6 with sound reasoning. | ||
|
|
||
| --- | ||
|
|
||
| ## Problem Solving — Score: 4/5 | ||
|
|
||
| **Strengths:** | ||
|
|
||
| - Immediately identified Dynamic Programming as the right pattern and correctly | ||
| named `dp[i]` as the key state variable (LIS length ending at index i) without | ||
| any hints. | ||
| - Translated the recurrence `dp[i] = max(dp[i], dp[j] + 1)` into working nested | ||
| loop code on the first attempt — no false starts or approach pivots needed. | ||
|
|
||
| **Areas for Improvement:** | ||
|
|
||
| - The initial invariant statement — "we are still in bounds of iterating over the | ||
| array" — described a loop guard, not the DP property. Needed two follow-up | ||
| questions before arriving at "`dp[i]` always equals the length of the longest | ||
| strictly increasing subsequence ending at `nums[i]`, with a minimum of 1." | ||
| Being able to state this upfront, unprompted, is the mark of strong DP fluency. | ||
|
|
||
| **Help Needed:** Minimal — no algorithm hints required. One L1 checkpoint | ||
| clarification on the invariant definition. | ||
|
|
||
| --- | ||
|
|
||
| ## Coding — Score: 4/5 | ||
|
|
||
| **Strengths:** | ||
|
|
||
| - Clean, idiomatic TypeScript. `const dp: number[] = new Array(length).fill(1)` | ||
| initialises the base case correctly in one line. | ||
| - The nested loop structure (`for i`, inner `for j < i`) with the `Math.max` | ||
| accumulation is textbook and readable. | ||
| - `return Math.max(...dp)` is concise and correct. | ||
|
|
||
| **Areas for Improvement:** | ||
|
|
||
| - Minor: `expect` lines in authored tests were missing semicolons in cycles 5 and 6. Consistent punctuation matters in real codebases and under review. | ||
|
|
||
| **Debugging Process:** | ||
|
|
||
| - Not tested in this session — the implementation was correct on the first | ||
| attempt. Would want to see a debugging cycle in a future session to evaluate | ||
| systematic fault-finding. | ||
|
|
||
| --- | ||
|
|
||
| ## Verification — Score: 3/5 | ||
|
|
||
| **Strengths:** | ||
|
|
||
| - Correctly anticipated that a fully descending array returns 1 and explained why: | ||
| "each time the input is not increasing so the value is 1 since it is the only | ||
| element that is increasing." Shows understanding of how the strict inequality | ||
| gates the recurrence. | ||
| - Proposed the single-element and already-sorted cases without prompting — good | ||
| boundary instinct. | ||
|
|
||
| **Areas for Improvement:** | ||
|
|
||
| - Did not propose a negative-numbers test case. The constraints allow | ||
| `-10^4 <= nums[i] <= 10^4` and the algorithm handles negatives correctly, but | ||
| explicitly testing it would show awareness of the full input domain. | ||
| - Did not consider a two-element case (e.g. `[1, 2]` → 2, `[2, 1]` → 1) as a | ||
| minimal non-trivial input — useful for catching off-by-one errors in early | ||
| implementations. | ||
|
|
||
| --- | ||
|
|
||
| ## Communication — Score: 3/5 | ||
|
|
||
| **Strengths:** | ||
|
|
||
| - Gave a coherent high-level description of the pattern on the first checkpoint: | ||
| "problems can build off of sub problems that were previously solved" — correct | ||
| framing of DP. | ||
| - Explained the complexity reasoning clearly when asked: named O(n²) time and | ||
| O(n) space with a correct, if informal, justification. | ||
|
|
||
| **Areas for Improvement:** | ||
|
|
||
| - The initial invariant response was vague ("we are still in bounds") and required | ||
| prompting. In a real interview, the interviewer would not ask follow-up | ||
| questions; the candidate would be expected to state the invariant precisely | ||
| before coding. | ||
| - Complexity explanation said "iterates over i twice" rather than describing the | ||
| triangular scan. Aim for: "for each of n elements, we scan up to i previous | ||
| elements, giving n(n-1)/2 comparisons — O(n²)." | ||
|
|
||
| --- | ||
|
|
||
| ## Complexity Analysis — Score: 3/5 | ||
|
|
||
| **Strengths:** | ||
|
|
||
| - Correctly identified O(n²) time and O(n) space without prompting. | ||
| - Named the right variables (dp array → space, nested loop → time). | ||
|
|
||
| **Areas for Improvement:** | ||
|
|
||
| - The "why" for time complexity was slightly imprecise: "iterates over i twice" | ||
| suggests two independent passes rather than a nested triangular scan. The | ||
| distinction matters when reasoning about amortised or tighter bounds. | ||
| - Did not mention the O(n log n) follow-up variant (patience sorting / binary | ||
| search). Awareness of the optimal approach and when it matters is a | ||
| differentiator at senior levels. | ||
|
|
||
| --- | ||
|
|
||
| ## Pattern Recognition | ||
|
|
||
| - **Pattern Used:** 1D Dynamic Programming (bottom-up, tabulation) | ||
| - **Key Insight:** Every element is both a valid single-element subsequence (base | ||
| case: `dp[i] = 1`) and a potential extension of any shorter increasing | ||
| subsequence ending before it. The recurrence `dp[i] = max(dp[i], dp[j] + 1)` | ||
| for all `j < i` where `nums[j] < nums[i]` captures all possibilities in O(n²). | ||
| - **Pattern Checkpoint Results:** 1/1 correct after two follow-up questions (PASS | ||
| with guidance) | ||
| - **Related Problems to Practice:** | ||
| - [300 follow-up — O(n log n) LIS](https://leetcode.com/problems/longest-increasing-subsequence/) | ||
| — Same problem, patience sorting approach; builds deeper understanding of the | ||
| same pattern with a different data structure (tails array + binary search) | ||
| - [1143 — Longest Common Subsequence](https://leetcode.com/problems/longest-common-subsequence/) | ||
| — 2D DP extension of the same "extend or reset" recurrence thinking; a natural | ||
| next step | ||
| - [354 — Russian Doll Envelopes](https://leetcode.com/problems/russian-doll-envelopes/) | ||
| — LIS applied to 2D pairs after sorting; harder variant that tests whether you | ||
| understand the pattern deeply enough to apply it in disguise | ||
|
|
||
| --- | ||
|
|
||
| ## Session Progression | ||
|
|
||
| | Cycle | What Was Tested | Ownership | Hints | Checkpoint | Notes | | ||
| | ----- | ---------------------------- | --------- | ----- | -------------------- | ------------------------------------------------- | | ||
| | 1 | Example 1: mixed array → 4 | L1 | 0 | PASS (with guidance) | Implemented full algorithm in one cycle; no hints | | ||
| | 2 | Example 2: mixed array → 4 | L1 | 0 | SKIP | Passed immediately; noted over-implementation | | ||
| | 3 | All identical elements → 1 | L2 | 0 | SKIP | Proposed case correctly; missed assertion sketch | | ||
| | 4 | Single element → 1 | L2 | 0 | SKIP | Full assertion provided; clean L2 contribution | | ||
| | 5 | Already sorted ascending → n | L3 | 0 | SKIP | First full it() block; minor missing semicolon | | ||
| | 6 | Fully descending → 1 | L3 | 0 | SKIP | Correct reasoning explained unprompted; good L3 | | ||
|
|
||
| --- | ||
|
|
||
| ## No-Hire Trigger Check | ||
|
|
||
| - **Critical Triggers Observed:** none | ||
| - **Recovery Evidence:** n/a | ||
| - **Guardrail Applied:** none | ||
|
|
||
| --- | ||
|
|
||
| ## Overall Assessment | ||
|
|
||
| **Interview Recommendation:** Lean Recommend | ||
| **Hiring Decision:** Lean Hire | ||
| **Would Move Forward to Next Round:** Yes | ||
| **Confidence:** Medium | ||
|
|
||
| **Summary:** | ||
| The candidate demonstrated a solid foundation in dynamic programming — pattern | ||
| identification was immediate, implementation was clean and correct, and the test | ||
| suite grew purposefully through the session. The primary gap is in invariant | ||
| articulation: arriving at the right answer through guiding questions is different | ||
| from stating it fluently before touching the keyboard, which is what a real | ||
| interview expects. With deliberate practice on explaining DP state definitions | ||
| precisely, this candidate is well-positioned for a strong performance. | ||
|
|
||
| **Strengths to Build On:** | ||
|
|
||
| - Fast, correct pattern identification with no algorithm hints needed | ||
| - Clean, readable TypeScript that translates intent directly into code | ||
| - Good boundary awareness (sorted, descending, single element) in test design | ||
|
|
||
| **Priority Areas for Growth:** | ||
|
|
||
| 1. **Invariant articulation** — Before coding any DP solution, practice stating | ||
| in one sentence: "At all times, `dp[i]` equals \_\_\_." Do this before opening | ||
| the editor. | ||
| 2. **Complexity precision** — Practise explaining O(n²) as "a triangular scan: n | ||
| elements, each looking back at up to i predecessors" rather than "nested loop." | ||
| 3. **Full input domain testing** — Habit-build checking negative numbers, mixed | ||
| signs, and two-element arrays as instinctive edge cases. | ||
|
|
||
| **Recommended Next Steps:** | ||
|
|
||
| - Solve [1143 — Longest Common Subsequence](https://leetcode.com/problems/longest-common-subsequence/) | ||
| next — it forces 2D DP state definition and is a direct extension of this | ||
| session's recurrence thinking | ||
| - Attempt the O(n log n) LIS variant to deepen understanding of the patience | ||
| sorting insight | ||
| - Before each DP problem, write out the state definition and invariant as a | ||
| comment before writing any code — treat it as a mandatory first step | ||
|
|
||
| --- |
44 changes: 44 additions & 0 deletions
44
leetcode/medium/0300-longest-increasing-subsequence/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # 300. Longest Increasing Subsequence | ||
|
|
||
| [](https://leetcode.com/problems/longest-increasing-subsequence/) | ||
|
|
||
| [LeetCode Problem](https://leetcode.com/problems/longest-increasing-subsequence/) | ||
|
|
||
| ## Problem | ||
|
|
||
| Given an integer array `nums`, return the length of the **longest strictly increasing subsequence**. | ||
|
|
||
| A **subsequence** is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. | ||
|
|
||
| ## Examples | ||
|
|
||
| **Example 1:** | ||
|
|
||
| ```text | ||
| Input: nums = [10,9,2,5,3,7,101,18] | ||
| Output: 4 | ||
| Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. | ||
| ``` | ||
|
|
||
| **Example 2:** | ||
|
|
||
| ```text | ||
| Input: nums = [0,1,0,3,2,3] | ||
| Output: 4 | ||
| ``` | ||
|
|
||
| **Example 3:** | ||
|
|
||
| ```text | ||
| Input: nums = [7,7,7,7,7,7,7] | ||
| Output: 1 | ||
| ``` | ||
|
|
||
| ## Constraints | ||
|
|
||
| - `1 <= nums.length <= 2500` | ||
| - `-10^4 <= nums[i] <= 10^4` | ||
|
|
||
| ## Follow-up | ||
|
|
||
| Can you come up with an algorithm that runs in O(n log n) time complexity? |
28 changes: 28 additions & 0 deletions
28
leetcode/medium/0300-longest-increasing-subsequence/longest-increasing-subsequence.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { lengthOfLIS } from './longest-increasing-subsequence'; | ||
|
|
||
| describe('Longest Increasing Subsequence', () => { | ||
| it('returns 4 for [10,9,2,5,3,7,101,18] (example 1)', () => { | ||
| expect(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18])).toBe(4); | ||
| }); | ||
|
|
||
| it('returns 4 for [0,1,0,3,2,3] (example 2)', () => { | ||
| expect(lengthOfLIS([0, 1, 0, 3, 2, 3])).toBe(4); | ||
| }); | ||
|
|
||
| it('returns 1 when all elements are identical', () => { | ||
| expect(lengthOfLIS([1, 1, 1, 1, 1, 1])).toBe(1); | ||
| }); | ||
|
|
||
| it('returns 1 for a single element array', () => { | ||
| expect(lengthOfLIS([42])).toBe(1); | ||
| }); | ||
|
|
||
| it('returns the length of the input array when already sorted', () => { | ||
| expect(lengthOfLIS([1, 2, 3, 4, 5])).toBe(5); | ||
| }); | ||
|
|
||
| it('should return 1 when the input is fully descending', () => { | ||
| expect(lengthOfLIS([5, 4, 3, 2, 1])).toBe(1); | ||
| }); | ||
| }); |
14 changes: 14 additions & 0 deletions
14
leetcode/medium/0300-longest-increasing-subsequence/longest-increasing-subsequence.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export function lengthOfLIS(nums: number[]): number { | ||
| const length = nums.length; | ||
| const dp: number[] = new Array(length).fill(1); | ||
|
|
||
| for (let i = 0; i < length; i++) { | ||
| for (let j = 0; j < i; j++) { | ||
| if (nums[j] < nums[i]) { | ||
| dp[i] = Math.max(dp[i], dp[j] + 1); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return Math.max(...dp); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
numsis empty,dpis also empty andMath.max(...dp)returns-Infinity, which is not a valid LIS length and can propagate incorrect results for callers outside LeetCode’s constrained input set. Adding an early return (typically0) forlength === 0prevents this incorrect sentinel value from escaping.Useful? React with 👍 / 👎.