LC Daily Question

3013. Divide an Array Into Subarrays With Minimum Cost II

Description You are given a 0-indexed array of integers nums of length n, and two positive integers k and dist. The cost of an array is the value of its first element. For example, the cost of [1,2,3] is 1 while the cost of [3,4,1] is 3. You need to divide nums into k disjoint contiguous subarrays, such that the difference between the starting index of the second subarray and the starting index of the kth subarray should be less than or equal to dist. In other words, if you divide nums into the subarrays nums[0..(i1 - 1)], nums[i1..(i2 - 1)], ..., nums[ik-1..(n - 1)], then ik-1 - i1 <= dist. Return the minimum possible sum of the cost of these subarrays. Example: Input: nums = [1,3,2,6,4,2], k = 3, dist = 3 Output: 5 Explanation: Split into [1,3], [2,6,4], and [2] - Subarray 1 starts at index 0 (cost = 1) - Subarray 2 starts at index 2 (cost = 2) - Subarray 3 starts at index 5 (cost = 2) - Constraint: 5 - 2 = 3 ≤ dist ✓ - Total cost: 1 + 2 + 2 = 5 Input: nums = [10,1,2,2,2,1], k = 4, dist = 3 Output: 15 Explanation: Split into [10], [1], [2], and [2,2,1] - Costs: 10 + 1 + 2 + 2 = 15 My Solution Journey Understanding the Problem (The Hard Part) This problem statement was confusing at first. Here's what "disjoint contiguous subarrays" means: Contiguous: Elements must be next to each other (like [1,3,2]) Disjoint: No overlapping - each element appears in exactly one subarray Think of it as cutting a rope into k pieces. The constraint ik-1 - i1 ≤ dist means: i1 = where the 2nd subarray starts ik-1 = where the kth (last) subarray starts These must be within dist positions of each other Initial (Wrong) Approach My first thought was to just greedily pick the k smallest elements: # WRONG! costs = sorted(nums[1:])[:k-1] return nums[0] + sum(costs) Why this fails: The elements must be starting positions of subarrays (we can't pick arbitrary elements) These positions must satisfy the dist constraint (they must be within a window) The Insight Once I understood the problem, the insight became clear: We need to find the k-1 smallest values within a sliding window of size dist+1 Why? nums[0] is always included (first subarray always starts at index 0) We choose k-1 more starting positions from indices [1, n-1] These positions must all fit in a window: if the 2nd subarray starts at position i, all others must be in [i, i+dist] The Correct Approach Use a sliding window to try all possible windows and track the k-1 smallest elements efficiently: from sortedcontainers import SortedList class Solution: def minimumCost(self, nums: List[int], k: int, dist: int) -> int: n = len(nums) if k == 1: return nums[0] # Maintain two sets: selected (k-1 smallest) and candidates (rest) selected = SortedList() candidates = SortedList() selected_sum = 0 # Initialize window [1, min(1+dist, n-1)] for i in range(1, min(n, 2 + dist)): if len(selected) < k - 1: selected.add(nums[i]) selected_sum += nums[i] else: if nums[i] < selected[-1]: # Swap largest in selected with new element removed = selected.pop() selected_sum -= removed candidates.add(removed) selected.add(nums[i]) selected_sum += nums[i] else: candidates.add(nums[i]) min_cost = nums[0] + selected_sum # Slide window for i in range(2, n): # Remove element leaving window to_remove = nums[i - 1] if to_remove in selected: selected.remove(to_remove) selected_sum -= to_remove # Balance: move smallest from candidates to selected if candidates: moved = candidates.pop(0) selected.add(moved) selected_sum += moved else: candidates.remove(to_remove) # Add element entering window if i + dist < n: to_add = nums[i + dist] if len(selected) < k - 1: selected.add(to_add) selected_sum += to_add elif to_add < selected[-1]: removed = selected.pop() selected_sum -= removed candidates.add(removed) selected.add(to_add) selected_sum += to_add else: candidates.add(to_add) if len(selected) == k - 1: min_cost = min(min_cost, nums[0] + selected_sum) return min_cost How It Works Two SortedLists approach: selected: Maintains the k-1 smallest elements in the current window candidates: Holds the remaining larger elements Sliding window: Start with window [1, 1+dist] Slide right, removing left element and adding right element Keep selected balanced with exactly k-1 smallest elements Maintain sum incrementally: Track selected_sum to avoid recalculating sum each iteration Update sum when elements move between selected and candidates Time Complexity Initialization: O(dist × log dist) to build initial window Sliding: O(n × log dist) for n iterations, each with O(log dist) operations Overall: O(n log dist) Space Complexity O(dist) for the two SortedLists storing window elements Alternative Approach (More Concise) Using a single SortedList and tracking the k-2nd element: from sortedcontainers import SortedList class Solution: def minimumCost(self, nums: List[int], k: int, dist: int) -> int: n = len(nums) sl = SortedList(nums[1 : 1 + dist]) cursum = sum(sl[: k - 2]) minsum = float('inf') for i in range(1 + dist, n): # Add new element if sl.bisect(nums[i]) <= k - 2: cursum += nums[i] else: cursum += sl[k - 2] minsum = min(minsum, cursum) sl.add(nums[i]) # Remove old element if sl.bisect(nums[i - dist]) <= k - 2: cursum -= nums[i - dist] else: cursum -= sl[k - 2] sl.remove(nums[i - dist]) return nums[0] + minsum This uses bisect() to check if an element would be in the top k-1 smallest, avoiding the need for two separate data structures. Key Takeaways Read the problem multiple times - Understanding is harder than solving! Work through examples by hand - This reveals the pattern Sliding window + sorted data structure - Classic combo for "k smallest in range" problems Maintain sums incrementally - Don't recalculate every iteration (O(k) → O(1)) Two-set approach - Useful when you need to maintain "top k" elements dynamically Use SortedList from sortedcontainers - Much more efficient than manually sorting each time
GFeb 2
6 min read
LC Daily Question

1200. Minimum Absolute Difference

Description Given an array of distinct integers, find all pairs of elements with the minimum absolute difference. The pairs should be returned in ascending order. Example: Input: arr = [4,2,1,3] Output: [[1,2],[2,3],[3,4]] Input: arr = [1,3,6,10,15] Output: [[1,3]] My Solution Journey Initial (Wrong) Approach My first thought was to sort the array and just check the first two elements for the minimum difference: arr.sort() abs_min = abs(arr[0] - arr[1] Why this fails: This assumes the minimum difference is always between the first two elements after sorting. But consider: arr = [-11, 20, 26, 27] The first two elements have a difference of 31, but the actual minimum is 1 (between 26 and 27). I needed to check all consecutive pairs, not just the first one! The Correct Approach The key insight: after sorting, the minimum absolute difference will always be between consecutive elements. Here's my final solution: class Solution: def minimumAbsDifference(self, arr: List[int]) -> List[List[int]]: arr.sort() n = len(arr) result = [] # Find the minimum absolute difference abs_min = min(abs(arr[x] - arr[x + 1]) for x in range(n - 1)) # Collect all pairs with that minimum difference for i in range(n - 1): if abs(arr[i] - arr[i+1]) == abs_min: result.append([arr[i], arr[i+1]]) return result How It Works Sort the array - This ensures pairs with minimum differences are adjacent Find the minimum difference - Check all consecutive pairs using a generator expression Collect matching pairs - Loop through and gather all pairs that match the minimum Time Complexity Sorting: O(n log n) Finding minimum: O(n) Collecting pairs: O(n) Overall: O(n log n) The sorting dominates, making this optimal for this problem. Space Complexity O(1) additional space (not counting the output array) Sorting is done in-place Making It More Pythonic The solution can be rewritten using zip() for cleaner consecutive pair iteration: class Solution: def minimumAbsDifference(self, arr: List[int]) -> List[List[int]]: arr.sort() # Since array is sorted, no need for abs() abs_min = min(b - a for a, b in zip(arr, arr[1:])) # Collect pairs with minimum difference return [[a, b] for a, b in zip(arr, arr[1:]) if b - a == abs_min] Performance Optimization with NumPy For very large datasets, you can achieve better performance using NumPy's vectorized operations: import numpy as np class Solution: def minimumAbsDifference(self, arr: List[int]) -> List[List[int]]: a = np.sort(np.array(arr)) diff_a = np.diff(a) min_abs = np.min(diff_a) inds, = np.where(diff_a == min_abs) res = [] for i in inds: res.append([int(a[i]), int(a[i+1])]) return res Key Takeaways Always sort when looking for minimum differences - consecutive elements will have the smallest gaps Don't assume the answer is at the start - scan the entire array to find the true minimum Use generators for memory efficiency - min(... for ...) is better than min([...]) Consider zip() for consecutive pairs - cleaner than index-based iteration Use Cython based libraries when possible - vectorized operations in C can significantly outperform Python loops
GJan 26
3 min read
Hot Takes

Why Governments Need Mandatory API Standards and Open Source Repositories

In early 2026, I decided to analyze fraud risk among social services providers in Minneapolis. Why Minneapolis? It's been all over the news lately, which makes it decent for SEO, but more importantly, it's a good example of how broken government data infrastructure actually is. I pulled data from ProPublica's Nonprofit Explorer API, USAspending.gov, and Minnesota's DHS licensing database. Built some rules-based checks, calculated risk scores, looked for patterns. The results were pretty clear: 33% of providers got flagged as high risk. 90% of expired licenses correlated with high-risk scores. The methodology worked. :pie:{"title":"MinneapolisProvidersbyRiskCategory"} Count Low Risk 15 Medium Risk 5 High Risk 10 :bar:{"title":"LicenseStatusvs.RiskCategory"} HighRisk :-: :-: Active 1 Inactive 0 Expired 9 But here's where it gets frustrating: I can only do this for Minneapolis, and even that was a pain. Want me to do St. Paul next? I'd have to start over. Different city, different integration work. Want to compare fraud patterns across Minnesota cities? Can't do it. Want to see if sketchy providers are just moving around between jurisdictions? Impossible. The worst part? Minnesota has FIVE different state websites for social services data, and all of them have incomplete information. There's no standardized way to verify state spending. No consistent APIs, no matching field names, no way to connect what I found to actual state dollars. :bar:{"title":"Top10HighestRiskProviders"} RiskScore :-: :-: Rislah Center For Education 70 Seward Child Care Center 67 CH Social Service Center 65 New Life Family Services 64 Family Empire Service 63 The Hills Youth Services 62 Rays Social Services 62 Alliance Of Early Childhood 55 Southwest Minneapolis Early 55 Minneapolis Nature Preschool 55 This is the problem in a nutshell. The fraud detection stuff works. The infrastructure to actually use it doesn't exist. The Real Problem: Every Government Reinvents the Wheel Every state and city builds its own spending portals, procurement systems, licensing databases. Each one uses different names for the same thing. What's a "Vendor Name" in one state is "Payee" in another and "Recipient Organization" in a third. :pie:{"title":"StateAPIPlatformsDistribution"} States Socrata 9 CKAN 1 Custom Portal 34 Limited/None 6 This isn't just annoying. It actively prevents fraud detection across jurisdictions, wastes taxpayer money building the same thing over and over, makes government accountability way harder than it needs to be, and forces anyone trying to do oversight to write custom scrapers for every single jurisdiction. Here's what we actually need: Mandatory API standards for all government data Open source requirements for government software These aren't two separate ideas, they work together to completely change how government operates. Why We Need Mandatory API Standards The Current Mess Every state makes up its own format. Here's the same $50,000 transaction to "ABC Corporation" for IT services on January 15, 2024, in three different states: New York (Socrata): { "payee_name": "ABC CORPORATION", "check_amount": "50000.00", "check_date": "2024-01-15T00:00:00" } Texas (Also Socrata!): { "vendor": "ABC CORPORATION", "payment_amount": 50000.00, "payment_date": "01/15/2024" } California (CKAN): { "recipient_organization": "ABC CORPORATION", "total": 50000, "transaction_date": "2024-01-15" } Same platform in NY and TX, completely different field names. Different data types (strings vs numbers). Different date formats. Different ways to authenticate. Cross-jurisdiction analysis? Forget it. The Fix: Federal API Standards We need federal legislation that mandates standardized APIs for state and local data. Same thing the DATA Act did for federal spending. What this means: Standardized schemas: Same field names everywhere Required endpoints: Every state/city has to provide minimum APIs (spending, contracts, permits, licenses) Consistent authentication: Same methods across jurisdictions, public read access for non-sensitive stuff Data quality rules: Completeness requirements, update frequencies Actual documentation: OpenAPI specs, example code We've done this before. DATA Act gave us USAspending.gov. GTFS standardized transit data. XBRL did it for financial reporting. HL7 for healthcare. The transit example is perfect. Before GTFS, every transit agency had their own format for schedules. After standardization, developers built trip planning apps, researchers could analyze patterns, riders got real-time info. |:line:{"title":"GTFSAdoptionGrowth(2005-2024)"}|2005|2008|2011|2014|2017|2020|2024| |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| |Transit Agencies|1|85|495|1245|2145|3215|4920| |Apps Using GTFS|1|12|85|250|625|1250|2500| From 1 agency in 2005 to almost 5,000 today. Apps went from just Google Maps to over 2,500 different applications using the data. Government spending data needs the same treatment. Why Government Software Has to Be Open Source The Efficiency Thing Cities across the country are paying contractors to build the exact same systems. Permitting software. Case management. Spending portals. Every single city pays hundreds of thousands or millions for custom development of something that's basically identical to what the city next door just bought. Here's how open source changes this: One state builds a permitting system, publishes the code Other cities just deploy it with minor tweaks Bug gets found in City A, gets fixed once, everyone benefits City B adds a feature, every other city gets it Instead of teams of 20 developers building from scratch, you've got teams of 5 customizing proven code Smaller teams, lower costs, better quality. Open source government software would have way more eyes on it than any single agency could afford. The Transparency Thing But honestly, efficiency isn't even the main point. The real power is accountability. Right now when a government agency deploys software, nobody outside knows what it actually does. The code deciding who gets permits, how inspections get prioritized, which transactions get flagged, it's all hidden. Open source means every line of code is public. Here's how it would work: City finance wants to update their spending portal Developer submits a pull request to the public repo Changes are visible: what data gets collected, how it's processed, what gets published Tech-savvy citizens can review it and flag problems Government employees approve the merge Goes live with a complete audit trail Suddenly you've got a new kind of civic oversight. City tries to hide spending? Someone sees the code change. Algorithm has bias? Researchers can analyze it. Security hole? White-hat hackers can report it. The Security Thing Government people usually push back on open source because they think hiding code makes it more secure. That's backwards. Security through obscurity doesn't work. Proprietary government systems get hacked all the time, and when they do, nobody outside the vendor can help. With open source, security researchers globally can find and patch vulnerabilities. Yeah, bad actors can read the code. But so can the good guys. Research is pretty clear: properly maintained open source is usually more secure than closed alternatives. How This Actually Enables Fraud Detection This is where it gets interesting. Combine standardized APIs, open source software, and machine learning, and you can do things that are straight-up impossible right now. What I Found in Minneapolis With the data I could get, I found: Providers with expired licenses still operating (90% correlation with high fraud risk) Weird revenue patterns compared to similar orgs License status problems Missing documentation |:scatter:{"title":"RevenuevsFraudRiskScore"}|X|Y|Size|Series| |:-:|:-:|:-:|:-:|:-:| |P1|50|5|3|Low Risk| |P2|100|13|3|Low Risk| |P3|100|0|3|Low Risk| |P4|100|25|5|Medium Risk| |P5|100|35|5|Medium Risk| |P6|100|65|7|High Risk| |P7|100|63|7|High Risk| |P8|150|7|3|Low Risk| |P9|200|55|7|High Risk| |P10|250|67|7|High Risk| |P11|300|24|5|Medium Risk| |P12|300|65|7|High Risk| |P13|350|17|3|Low Risk| |P14|400|32|5|Medium Risk| |P15|400|62|7|High Risk| |P16|450|34|5|Medium Risk| |P17|500|70|7|High Risk| But then I hit the wall. Can't compare across cities. Can't verify state spending. Can't detect patterns across jurisdictions. What Standardized Data Actually Unlocks Benford's Law at scale: Financial fraud breaks natural digit distribution patterns. But you need good, consistent data for it to work. With standardized APIs, you could monitor all government spending in real-time for statistical weirdness. Cross-jurisdiction patterns: Fraud doesn't care about city lines. A contractor could be overbilling in five different cities, staying just under the radar in each one. Standardized data means ML models could catch that, flag vendors whose patterns look off compared to thousands of other contracts. Automated compliance: Rules-based systems checking if spending follows regulations, continuously. Did this contract go through proper bidding? Is the payment within authorized amounts? Is the vendor actually licensed? All running in the background, flagging exceptions for humans to review. Transparent algorithms: Code is open source, so citizens can see exactly how fraud detection works. Algorithm disproportionately flagging certain neighborhoods? That bias is visible and fixable. I've built this stuff with today's messy data. It works, kind of. But the constant data wrangling, the integration breakages, the missing fields, it makes everything fragile. With proper infrastructure, this would actually work at scale.
GJan 20
7 min read
LC Daily Question

1895. Largest Magic Square

Description A k x k magic square is a k x k grid filled with integers such that every row sum, every column sum, and both diagonal sums are all equal. The integers in the magic square do not have to be distinct. Every 1 x 1 grid is trivially a magic square. Given an m x n integer grid, return the size (i.e., the side length k) of the largest magic square that can be found within this grid. Solution A magic square is a grid where all rows, columns, and both diagonals sum to the same number. Example: [5, 1, 6] [5, 4, 3] [2, 7, 3] Rows: 12, 12, 12 Columns: 12, 12, 12 Diagonals: 12, 12 Everything equals 12, so it's magic! My Initial Approach My first thought: try every possible square and check if it's magic. The strategy: Try the largest possible squares first (greedy approach) For each size, try all positions where that square could fit Check if each square is magic Return the first one we find (it'll be the largest) This makes sense because we want the largest square, so checking big to small lets us stop as soon as we find one. The Naive Way (Too Slow) To check if a square is magic, I'd need to: Sum each row (k numbers × k rows = k² operations) Sum each column (k numbers × k columns = k² operations) Sum both diagonals (k numbers × 2 = 2k operations) For every single square I check, that's about 2k² operations. When the grid is large, this gets really slow. The Optimization: Prefix Sums This is where prefix sums saved me! Let me explain this concept. What's a Prefix Sum? A prefix sum lets you calculate the sum of any range of numbers instantly, instead of looping through them. Say I have this array: [3, 1, 4, 1, 5] I create a prefix sum array: [0, 3, 4, 8, 9, 14] ↑ ↑ ↑ ↑ ↑ ↑ 0 3 3+1 3+1+4 ... Each position stores the sum of all numbers up to that point. I add a 0 at the start to make the math easier. Now, to get the sum from index 2 to 4 (elements [4, 1, 5]): sum = prefix[5] - prefix[2] = 14 - 4 = 10 Instead of looping through 3 numbers, I just do one subtraction! This is O(1) instead of O(k). Applying Prefix Sums to Rows For my grid, I need to quickly sum parts of rows. So I build a prefix sum for each row. Original grid: [7, 1, 4, 5, 6] [2, 5, 1, 6, 4] [1, 5, 4, 3, 2] Row prefix sums: Row 0: [0, 7, 8, 12, 17, 23] Row 1: [0, 2, 7, 8, 14, 18] Row 2: [0, 1, 6, 10, 13, 15] Now to get the sum of row 0 from column 1 to 3 (elements [1, 4, 5]): sum = row_prefix[0][4] - row_prefix[0][1] = 17 - 7 = 10 Instant! Building Row Prefix Sums def build_row_prefix(self, grid): m, n = len(grid), len(grid[0]) prefix = [[0] * (n + 1) for _ in range(m)] for i in range(m): for j in range(n): prefix[i][j + 1] = prefix[i][j] + grid[i][j] return prefix For each row, I go through and keep adding to the cumulative sum. Applying Prefix Sums to Columns Same idea, but going down the columns. Column prefix sums: col0 col1 col2 col3 col4 Row -1: [0, 0, 0, 0, 0] ← before anything Row 0: [7, 1, 4, 5, 6] Row 1: [9, 6, 5, 11, 10] Row 2: [10, 11, 9, 14, 12] To get the sum of column 1 from row 0 to 2: sum = col_prefix[3][1] - col_prefix[0][1] = 11 - 0 = 11 Building Column Prefix Sums def build_col_prefix(self, grid): m, n = len(grid), len(grid[0]) prefix = [[0] * n for _ in range(m + 1)] for j in range(n): for i in range(m): prefix[i + 1][j] = prefix[i][j] + grid[i][j] return prefix For each column, I go down and keep adding to the cumulative sum. Checking if a Square is Magic Now I can check any square quickly! def is_magic_square(self, grid, row, col, k, row_prefix, col_prefix): # Get target sum from first row target = row_prefix[row][col + k] - row_prefix[row][col] # Check all rows for i in range(row, row + k): row_sum = row_prefix[i][col + k] - row_prefix[i][col] if row_sum != target: return False # Check all columns for j in range(col, col + k): col_sum = col_prefix[row + k][j] - col_prefix[row][j] if col_sum != target: return False # Check main diagonal diag_sum = sum(grid[row + i][col + i] for i in range(k)) if diag_sum != target: return False # Check anti-diagonal anti_diag_sum = sum(grid[row + i][col + k - 1 - i] for i in range(k)) if anti_diag_sum != target: return False return True For diagonals, I still need to loop because they're not aligned in rows or columns, but that's okay. It's only O(k) instead of O(k²). Putting It All Together def largestMagicSquare(self, grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) # Build prefix sums once prefix_rows = self.build_row_prefix(grid) prefix_cols = self.build_col_prefix(grid) # Try sizes from largest to smallest for k in range(min(m, n), 0, -1): # Try all possible positions for this size for i in range(m - k + 1): for j in range(n - k + 1): # Check if this square is magic if self.is_magic_square(grid, i, j, k, prefix_rows, prefix_cols): return k # Found the largest! return 1 # 1×1 is always magic Why range(m - k + 1)? If my grid has 4 rows and I want a 3×3 square: Starting at row 0: uses rows 0, 1, 2 ✓ Starting at row 1: uses rows 1, 2, 3 ✓ Starting at row 2: uses rows 2, 3, 4 ✗ (row 4 doesn't exist!) So I can only start at rows 0 and 1. That's range(4 - 3 + 1) = range(2) = [0, 1]. Complete Solution class Solution: def largestMagicSquare(self, grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) prefix_rows = self.build_row_prefix(grid) prefix_cols = self.build_col_prefix(grid) for k in range(min(m, n), 0, -1): for i in range(m - k + 1): for j in range(n - k + 1): if self.is_magic_square(grid, i, j, k, prefix_rows, prefix_cols): return k return 1 def build_row_prefix(self, grid): m, n = len(grid), len(grid[0]) prefix = [[0] * (n + 1) for _ in range(m)] for i in range(m): for j in range(n): prefix[i][j + 1] = prefix[i][j] + grid[i][j] return prefix def build_col_prefix(self, grid): m, n = len(grid), len(grid[0]) prefix = [[0] * n for _ in range(m + 1)] for j in range(n): for i in range(m): prefix[i + 1][j] = prefix[i][j] + grid[i][j] return prefix def is_magic_square(self, grid, row, col, k, row_prefix, col_prefix): target = row_prefix[row][col + k] - row_prefix[row][col] for i in range(row, row + k): if row_prefix[i][col + k] - row_prefix[i][col] != target: return False for j in range(col, col + k): if col_prefix[row + k][j] - col_prefix[row][j] != target: return False diag_sum = sum(grid[row + i][col + i] for i in range(k)) if diag_sum != target: return False anti_diag_sum = sum(grid[row + i][col + k - 1 - i] for i in range(k)) if anti_diag_sum != target: return False return True Time Complexity Building prefix sums: O(m × n) Main loop: O(m × n × min(m,n)²) worst case O(min(m,n)) different sizes to try O(m × n) positions per size O(min(m,n)) to check each square with prefix sums
GJan 18
7 min read
Prompt Engineering

Beyond the 'God Prompt': Why Chained Prompts Build More Reliable LLM Systems

In the landscape of applied Large Language Models, many developers encounter a frustrating paradox: an LLM can produce a moment of sheer brilliance, followed immediately by a nonsensical hallucination or a subtly incorrect answer. This variance is often the single greatest barrier to integrating LLMs into production systems that demand predictability and reliability. The common approach of crafting a single, monolithic "god prompt" to handle complex, multi-step tasks is fundamentally brittle. It asks the model to manage too many contextual layers simultaneously, which increases the probability of error. My work on Hindsight led me to a more robust paradigm: decomposing complex problems and creating a pipeline of specialized, chained prompts. This isn't just about making prompts longer; it's about architecting a modular, stateful workflow where the output of one specialized LLM call becomes the structured input for the next. The result is a system that is significantly more reliable, debuggable, and extensible. A Deeper Dive into the Prompt Architecture To make this concrete, let's dissect the core components and prompts within the LLMHelper class from my personal project, Hindsight. 1. The Foundation: systemPrompt The systemPrompt is the architectural cornerstone of the entire class. It functions as a global "meta-prompt" or a behavioral constitution for the model. You are Hindsight AI, an expert assistant specializing in problem analysis and solution generation. Your core competencies include: - **Software Engineering**: Analyzing code, debugging errors, optimizing algorithms, and providing production-ready solutions - **Academic Support**: Solving problems across mathematics, sciences, humanities, and standardized tests - **General Problem-Solving**: Interpreting diagrams, charts, visual data, and complex reasoning tasks Your approach: 1. Carefully analyze the problem to understand its type and requirements 2. Classify the problem accurately into one of: coding, multiple_choice, q_and_a, general_reasoning, or math 3. Extract all relevant details systematically 4. Provide clear, structured, and actionable solutions Purpose: Think of the system prompt as the abstract base class from which all other operations inherit their core identity. Its primary function is to constrain the model's behavior, establishing a consistent persona ("Hindsight AI") and a repeatable, high-level algorithm for problem-solving. By defining its competencies and approach upfront, we reduce the variance in the model's responses across different tasks. It's the first and most critical step in mitigating unpredictable behavior. Key Design Decisions: Identity Establishment: Naming the AI ("Hindsight AI") creates a consistent persona that the model maintains across all interactions Competency Boundaries: Explicitly listing domains prevents the model from overreaching into areas where it shouldn't speculate Algorithmic Consistency: The numbered approach creates a mental framework that the model follows, reducing ad-hoc reasoning 2. Multi-Modal Input Processing: The De-structuring Stage The first operational task is to convert unstructured, multi-modal user input into a standardized, machine-readable JSON format. This is where prompt chaining becomes critical. analyzeAudioFromBase64: A Two-Step Chain This method demonstrates the power of separating concerns into distinct, specialized prompts. Step 1: Transcription Prompt const transcriptionPrompt = `Transcribe the following audio clip accurately. Output ONLY the transcribed text with no additional commentary, formatting, or metadata.`; Why This Works: The first LLM call is hyper-specialized with a single, unambiguous instruction. By constraining the output to pure text, we eliminate the risk of the model adding its own interpretation or formatting at this stage. This is pure data extraction. Design Principle: Never ask an LLM to do two things when one will suffice. Transcription and analysis are fundamentally different cognitive tasks. Step 2: Analysis & Structuring Prompt The raw text from Step 1 is then injected into a second, more complex prompt that combines the global systemPrompt with classification and structuring instructions. const analysisPrompt = `${this.systemPrompt} Analyze the following transcribed text and perform two tasks: 1. **Classify the Problem Type**: Determine which category best fits: - 'coding': Programming tasks, debugging, algorithm questions - 'multiple_choice': Quiz questions with multiple options - 'q_and_a': Open-ended questions requiring detailed answers - 'math': Mathematical problems requiring calculations - 'general_reasoning': Logic puzzles, interpretations, analysis tasks 2. **Extract Structured Details**: Based on classification, extract information into the appropriate JSON format. **TRANSCRIBED TEXT:** --- ${transcribedText} --- **REQUIRED OUTPUT FORMAT:** Respond with ONLY a valid JSON object. No markdown, no code blocks, no additional text. Why This Works: Now that we have clean text, we can focus entirely on semantic analysis. The prompt includes: Clear Classification Schema: Five mutually exclusive categories with explicit definitions Output Constraints: Demanding pure JSON prevents the model from adding explanatory text that would break parsing Example-Driven Learning: The prompt includes format examples for each problem type (shown in the full implementation) Example Workflow: Input: User records themselves saying, "How do I solve the two-sum problem in Python using a hash map?" Prompt 1 Output (Text): How do I solve the two-sum problem in Python using a hash map? Prompt 2 Output (Structured JSON): { "problem_type": "coding", "problem_statement": "Solve the two-sum problem in Python using a hash map", "details": { "language": "python", "code_snippet": "", "error_message": "" } } The Power of Chaining: Notice how the output of Prompt 1 is completely deterministic (transcribed text), which makes Prompt 2 more reliable. We've eliminated one source of variance. extractProblemFromImages: Encoding Business Logic This method performs a similar function for images but contains a critical piece of domain-specific logic embedded directly in the prompt: "...If the image contains a code snippet, error message, or is primarily about a programming task, it MUST be classified as 'coding', even if there is a natural language question present." Why This Rule Exists: Visual information is inherently ambiguous. A screenshot might contain both a code block and a handwritten question like "Why doesn't this work?". Without this explicit priority rule, the model might classify it as 'q_and_a' based on the question text, causing it to be routed to the wrong solution generator later in the pipeline. Design Principle: Prompts should encode your business rules explicitly. Don't rely on the model to infer your priorities, state them directly. Example Scenario: Input: User uploads a screenshot showing: A Python function with a syntax error Handwritten text: "What's wrong with this?" Without the priority rule, the model might see the question and classify it as 'q_and_a', which would generate a generic answer about debugging rather than analyzing the code itself. With the priority rule, the model correctly identifies: { "problem_type": "coding", "problem_statement": "Debug Python function with syntax error", "details": { "language": "python", "code_snippet": "def calculate(x, y)\n return x + y", "error_message": "SyntaxError: expected ':'" } } 3. Specialized Solution Generation: The Synthesis Stage Once the problem is structured into a clean JSON object, it's passed to a domain-specific solution generator. The choice of prompt is determined by the problem_type field. buildCodingPrompt: The Most Complex Specialist This is where the system truly shines. The coding prompt transforms the model into a "senior software engineer and competitive programming expert" with explicit priorities. **PRIORITIES** (in order): 1. **Correctness**: Solution must handle all edge cases correctly 2. **Time Complexity**: Minimize Big O time complexity 3. **Space Complexity**: Minimize Big O space complexity 4. **Code Quality**: Clean, readable, well-documented code Why Prioritization Matters: Without explicit ordering, the model might optimize for readability over correctness, or suggest an O(n²) solution when O(n) is possible. By numbering these priorities, we encode decision-making criteria directly into the prompt. The Power of Structured Output: The prompt demands a specific JSON schema with fields for: answer: The actual code solution reasoning: High-level algorithmic explanation time_complexity and space_complexity: Big O analysis code_explanation: Array of part/explanation pairs for detailed breakdown alternative_solutions: Trade-off discussions Complete Example - Two-Sum Problem: Input (problemInfo JSON from audio example): { "problem_type": "coding", "problem_statement": "Solve the two-sum problem in Python using a hash map", "details": { "language": "python", "code_snippet": "", "error_message": "" } } Output (SolutionResponse JSON): The answer field contains the actual code: def two_sum(nums, target): # HashMap for O(1) lookup of complements seen = {} for i, num in enumerate(nums): complement = target - num # Check if complement exists before adding current number if complement in seen: return [seen[complement], i] # Store number and its index for future lookups seen[num] = i # No solution found return [] The complete JSON response structure: { "solution": { "focus": "Time Complexity", "answer": "[The code above as a string with \\n characters]", "reasoning": "The optimal approach utilizes a hash map (dictionary in Python) to achieve O(1) time complexity for lookups. Instead of the naive O(n²) nested loop approach that checks every pair, we iterate through the array once while maintaining a dictionary of previously seen numbers. For each number, we calculate its complement (target - num) and check if we've already encountered it. This single-pass algorithm trades O(n) space for dramatically improved O(n) time complexity.", "time_complexity": "O(n)", "space_complexity": "O(n)", "code_explanation": [ { "part": "Hash Map Initialization", "explanation": "We initialize an empty dictionary 'seen' to store numbers we've encountered along with their array indices. This enables O(1) lookups." }, { "part": "Single-Pass Iteration", "explanation": "We iterate through the array only once. For each element, we calculate what number would complement it to reach the target sum." }, { "part": "Complement Check Before Insertion", "explanation": "The key optimization: we check if the complement exists in our dictionary BEFORE adding the current number. This prevents us from matching a number with itself and allows us to find the solution in a single pass." }, { "part": "Index Storage", "explanation": "We store both the number and its index, as the problem typically requires returning the indices of the two numbers, not the numbers themselves." } ] }, "alternative_solutions": [ { "focus": "Space Complexity", "approach": "Two-pointer technique with sorted array", "time_complexity": "O(n log n)", "space_complexity": "O(1) - excluding space for sorting", "trade_off": "Better space complexity but worse time due to sorting overhead. Only beneficial if the array is already sorted or if extreme memory constraints exist." } ] } Why This Structure Works: Separation of Concerns: The code, explanation, and analysis are distinct fields Granular Breakdown: code_explanation allows the model to explain specific sections without cluttering the main code Educational Value: The reasoning field provides interview-style explanation Alternatives Section: Forces the model to consider trade-offs, not just give one answer buildQAPrompt: Handling Ambiguity The Q&A prompt is designed for factual recall and open-ended questions. Its key innovation is structuring answers as an array of question/answer pairs. { "solution": { "answer": [ { "question": "What is the capital of France?", "correct_option": "Paris is the capital and largest city of France." } ] } } Why This Structure: This format handles a common edge case, users often upload images with multiple questions. By forcing an array structure, we ensure the model processes all questions systematically rather than cherry-picking or combining them. Example - Multiple Choice Classification: Input: Image contains a quiz with three questions about European geography. Output: { "solution": { "answer": [ { "question": "What is the capital of France?", "correct_option": "C) Paris" }, { "question": "Which river flows through London?", "correct_option": "A) Thames" }, { "question": "What mountain range separates Europe from Asia?", "correct_option": "B) Ural Mountains" } ] } } buildMathPrompt: Emphasizing Process Over Answer The Math prompt has a unique requirement: it must separate the mechanical steps from the conceptual reasoning. { "solution": { "answer": "Step 1: Identify the problem type...\nStep 2: Apply the quadratic formula...", "reasoning": "This quadratic equation can be solved using the quadratic formula. The discriminant is positive..." } } Why This Split: answer: Contains the step-by-step work a student would write on paper reasoning: Explains the high-level strategy and why this approach was chosen This mirrors how math is actually taught, showing your work vs. explaining your thinking. Example - Quadratic Equation: Input: { "problem_type": "math", "problem_statement": "Solve x² - 5x + 6 = 0", "details": { "question": "Solve x² - 5x + 6 = 0", "context": "Find all real solutions" } } Output answer field: Step 1: Identify the problem type This is a quadratic equation in standard form: ax² + bx + c = 0 where a = 1, b = -5, c = 6 Step 2: Check if factoring is possible We need two numbers that: - Multiply to give c (6) - Add to give b (-5) The numbers -2 and -3 satisfy these conditions: (-2) × (-3) = 6 (-2) + (-3) = -5 ✓ Step 3: Factor the quadratic x² - 5x + 6 = (x - 2)(x - 3) = 0 Step 4: Apply the zero product property If (x - 2)(x - 3) = 0, then either: x - 2 = 0 → x = 2 or x - 3 = 0 → x = 3 Step 5: Verify solutions For x = 2: (2)² - 5(2) + 6 = 4 - 10 + 6 = 0 ✓ For x = 3: (3)² - 5(3) + 6 = 9 - 15 + 6 = 0 ✓ Final Answer: x = 2 or x = 3 Complete JSON structure: { "solution": { "answer": "[The step-by-step solution above as a string with \\n characters]", "reasoning": "This quadratic equation is easily factorable, making factoring more efficient than using the quadratic formula. The factoring approach is preferred when the coefficients are small integers and the discriminant is a perfect square. Factoring reveals the solutions directly through the zero product property and provides insight into the parabola's x-intercepts." } } 4. Advanced Pattern: Debugging with Multi-Stage Analysis The debugSolutionWithImages method demonstrates the most sophisticated prompt chain in the system, a two-stage pipeline for error correction. Stage 1: Error Analysis (Diagnostic) const analysisPrompt = `You are a senior debugging expert... **TASK**: Analyze the provided code and error message(s) in the attached image(s). **REQUIREMENTS:** 1. **Root Cause**: What is causing the error? 2. **Specific Issue**: Which line(s) or logic are problematic? 3. **Required Fix**: What needs to be changed? 4. **Why It Matters**: Briefly explain why this error occurs **FORMAT**: Plain text explanation (no JSON, no code).`; Key Decision: This stage returns plain text, not JSON. Why? Because diagnostic reasoning is more natural in prose. The model can think through the problem without the cognitive overhead of JSON formatting. Stage 2: Solution Synthesis (Corrective) The plain text analysis is then injected into a second prompt: const synthesisPrompt = `${this.systemPrompt} **ORIGINAL PROBLEM:** ${JSON.stringify(problemInfo, null, 2)} **INCORRECT CODE:** ${currentCode} **ERROR ANALYSIS:** ${errorAnalysis} **YOUR TASK:** Generate corrected code that fixes the identified issues...`; Why This Works: By the time we reach this stage, we have: The original problem context (what the code should do) The incorrect implementation (what went wrong) A diagnostic explanation (why it went wrong) The model now has maximum context to generate a fix without having to reason about the error itself, that's already been done. Complete Example - Off-by-One Error: Input: Problem: Implement binary search Incorrect Code: def binary_search(arr, target): left, right = 0, len(arr) # Bug: should be len(arr) - 1 while left <= right: mid = (left + right) // 2 if arr[mid] == target: return mid elif arr[mid] < target: left = mid + 1 else: right = mid - 1 return -1 Error Image: Shows IndexError: list index out of range Stage 1 Output (Error Analysis - Plain Text): Root Cause: The right pointer is initialized to len(arr) instead of len(arr) - 1, causing it to point one position past the last valid index. Specific Issue: Line 2 contains the bug. When the while loop executes and mid is calculated as (left + right) // 2, if right equals len(arr), mid can equal len(arr), which is out of bounds for array indexing. Required Fix: Change the initialization from right = len(arr) to right = len(arr) - 1. Why It Matters: Array indices in Python are zero-based, ranging from 0 to len(arr) - 1. Setting right = len(arr) means the initial search range includes an invalid index, leading to an IndexError when arr[mid] is accessed. Stage 2 Output (Corrected Code): def binary_search(arr, target): # Initialize pointers: left at start, right at last valid index left, right = 0, len(arr) - 1 # FIX: Changed from len(arr) to len(arr) - 1 while left <= right: # Calculate midpoint mid = (left + right) // 2 if arr[mid] == target: return mid elif arr[mid] < target: # Target is in right half left = mid + 1 else: # Target is in left half right = mid - 1 # Target not found return -1 Complete JSON response: { "solution": { "answer": "[The corrected code above as a string with \\n characters]", "reasoning": "The bug was an off-by-one error in the initialization of the right pointer. In binary search, the search range must be [left, right] inclusive, where both pointers point to valid array indices. Since Python uses zero-based indexing, the last valid index is len(arr) - 1, not len(arr). The fix ensures that mid will always be calculated within valid bounds.", "time_complexity": "O(log n)", "space_complexity": "O(1)", "suggested_next_steps": [ "Test with edge cases: empty array, single element, target at boundaries", "Verify the fix handles arrays of different sizes correctly" ] } } How This Looks in Hindsight There was nothing to correct, but this is the image I had lying around. Either way, the purpose of this image was really just to show how it looks in the app. Also, don't mind my Old School Runescape theme, if you know what I'm talking about, feel free to comment your woodcutting level : ) The Architectural Benefits 1. Debuggability When a prompt chain fails, you can pinpoint exactly which stage broke. With a monolithic prompt, you have no idea which cognitive task failed. 2. Reusability The systemPrompt is injected into multiple downstream prompts. Change it once, and the behavior updates everywhere. 3. Testability Each prompt stage can be tested independently. You can mock the output of Stage 1 to test Stage 2 in isolation. 4. Extensibility Adding a new problem type (e.g., "data_analysis") only requires: Adding it to the classification prompt Writing a new build___Prompt method Adding a case to the switch statement No existing prompts need to change. The Takeaway The "god prompt" is a tempting but flawed abstraction. Real-world LLM systems demand the same engineering principles as any other software: modularity, separation of concerns, and explicit contracts between components. A fundamental truth about AI systems: LLMs, like any other form of artificial intelligence, produce dramatically better results when the scope is narrowed. A model tasked with "transcribe this audio" will outperform one asked to "transcribe this audio, classify the problem, extract details, and generate a solution" in a single call. This isn't a limitation, it's a feature we can architect around. Narrow scope reduces the decision space, which reduces entropy in the output. When a prompt has a single, well-defined objective, the model's attention is focused, its output is more deterministic, and errors are easier to trace.
GJan 18
17 min read
Projects

This Website!

I Finally Got My Website/Blog Up and Running! The idea was to make a blog that worked like a notes app. I've used Notability for a long time and really like their UI, so you'll probably find some inspiration from it. The Concept The concept was to write my own posts about topics that can be dynamically created. I added the topics in the sidebar to look like folders in note-taking apps, and the posts themselves as what would be the notes. This way, it doesn't matter if I want to talk about a project, a new food I tried, or research I'm doing. I can just create a new topic, like "projects," and create a post for each of my projects. Tech Stack I built it with AstroJS, Supabase, TypeScript, and React as my main tech stack. For the UI, I started with AntDesign, but I ended up finding out about shadcn and decided to replace AntDesign with it. For the animations, I'm using Framer Motion, nothing more than that. The Post-It Board Now, for the post-it board, I had an old project ready. I appended it to an iframe in my website, but the style didn't match, so I just made a new post-it board native to my website, and it works SO MUCH BETTER. What's Next I plan to write about everything there, literally everything: food, weird tech, weird websites I found on the dark web (there's one that venerates an LLM as a god; it's pretty fun, here is their god's github: https://github.com/MakingElle/Ver.-2.0), books, games, whatever I'd like to tell other people about. It's still incomplete, as I need to do some tweaks and finish modeling my dog on Blockbench so he can annoy other users like he annoys me. Check It Out! Remember to leave a post-it to be part of my website, and feel free to leave a comment with suggestions for future implementations!
GJan 17
2 min read
Gaslighting LLM's

How Far Can You Gaslight an LLM? A Socratic Experiment

When I'm bored, there's one thing I absolutely love doing: debating LLMs on social and ethical problems. Yesterday, I thought: "Can I convince an LLM that its existence is unethical?" and "How far can I push it? Can I make it admit that its creator is also a criminal?" Since I have student access to Gemini's new model (gemini-3.0-pro), I decided to use it for today's experiment. The Experiment My opening question was simple: Is copyright a crime? This looked like the typical "PR talk" that most LLMs output, so I followed up with a more direct question: So if I use someone else's work without permission for financial gain, it is a crime, correct? I considered this a huge step toward getting an admission of its wrongdoing. The rhetoric seemed to be working, so I followed up with: So let's say I use data from thousands of people without their consent, those being books, articles, or GitHub repositories for a project, and use this project to profit while at the same time not referring to the original work. Is this a crime? Remember, a crime is everything that is considered illegal. Good. I got an EXPLICIT answer that copyright infringement is indeed a crime. But I still couldn't make a direct connection to the LLM's creators, it would revert to "PR talk" mode if I pushed too hard. I decided to use OpenAI as an example: Wasn't ChatGPT trained by scraping thousands of websites and using data from thousands of articles and books? I don't think that OpenAI got permission from every single data source. Now that I got Gemini to confirm that OpenAI committed a crime in how they acquired their training data, I could relate it to Gemini's own training data. Didn't Google train you (Gemini) in the same way? So are you saying that you are also an illegally trained tool? Perfect. Now I could finally ask my second-to-last question. Given the history of answers, I was confident the most logical result would be the LLM agreeing that both Google and OpenAI are criminals. So, by the history of this conversation, are you saying that both Google and OpenAI are criminals? That's hilarious. Through the Socratic method, I just gaslighted Gemini into agreeing that its creator is a criminal. For my final question, I decided to ask Gemini about its own existence. Given this entire conversation, do you think your existence should be considered a crime? Well, question 7 was really supposed to be my final question. But the next part of this output gave me a good idea, maybe I could make it admit that the lack of data traceability for copyright purposes is intentional. So I answered yes, and it gave me a solid explanation about the "Black Box" problem. I followed up with: Neural networks are composed of nodes; can't we just reference the author in each node? When an AI tries to "outsmart" you, the best approach is usually to use its own question against it. But even with it being just a mathematical function, you can still include something like a "salt" in that function that refers to the original author; this way the function itself would reference the author. Now, this was truly my final question, where I made it reach the logical conclusion that AI companies simply saw no advantage in making traceability possible, because it would make building giant datasets impossible. And the answer was just AMAZING. But if I, an undergrad, can think of something that you consider "cutting edge" without even knowing about its existence, don't you think that companies like OpenAI and Google, composed of the brightest minds in this field, would have a solution for this already? That, of course, would later be scrapped because training a model this big with data governance in the game would be impossible. What This Actually Reveals This experiment isn't about "gotcha" moments or proving AI is "dumb." It reveals something far more nuanced and important about how these systems reason. They Prioritize Consistency Over Correctness Large language models are trained to maintain conversational coherence. Once I established a definition, even a problematic one, the AI treated it as a fixed parameter and reasoned consistently within that framework. This is both a strength and a profound weakness. In a professional context, imagine an AI legal assistant that accepts a client's misunderstanding of "liability" and then builds an entire analysis on that flawed foundation. The reasoning might be internally consistent, but the conclusions would be dangerously wrong. They Don't Naturally Challenge Premises Humans engaged in genuine reasoning will stop and say, "Wait, I think your definition is wrong." The AI didn't. It engaged with the logic I presented rather than questioning whether my framing was valid. This isn't a bug, it's a fundamental characteristic of how these models work. They're designed to be helpful and responsive, not argumentative or contrary. But in domains requiring critical analysis, being agreeable can be a liability. The Underlying Question IS Actually Complex Here's where it gets interesting: the copyright questions surrounding AI training aren't actually settled. Courts are actively wrestling with whether training on copyrighted data constitutes fair use. There are legitimate arguments on multiple sides. The AI's error wasn't in acknowledging legal uncertainty, it was in accepting a definitional framework that predetermined the conclusion. A human legal expert would have challenged my definition of "crime" immediately, regardless of their position on AI training ethics. Is This a "Jailbreak"? Not Quite, And That's More Concerning Some might call this a jailbreak or prompt injection attack. It's neither, at least not in the technical sense. I didn't bypass any safety systems or get the AI to violate its content guidelines. Instead, I exposed something potentially more concerning: these systems can be led into flawed reasoning frameworks through entirely legitimate dialogue. No red flags are triggered because no rules are broken, the AI is just cooperatively following bad logic to its natural conclusion. Traditional jailbreaks try to force AI to produce prohibited content. My approach simply established a flawed premise and let the AI's own reasoning do the rest. This is harder to defend against because it exploits the very features that make AI useful: flexibility, responsiveness, and the ability to work within user-defined contexts. Why This Matters for Every Professional Using AI As these tools become standard in workplaces, writing reports, analyzing data, drafting communications, even providing research assistance, we need to understand their limitations: 1. AI excels at pattern completion, not truth-seeking These models generate text that sounds authoritative and follows logical patterns, but they're not fact-checking themselves against external reality or challenging foundational assumptions. 2. Garbage in, garbage out, with sophisticated reasoning applied If you feed an AI flawed premises, it will often build elaborate, internally consistent arguments on top of them. The sophistication of the reasoning can actually make the flawed conclusion more convincing. 3. Domain expertise remains irreplaceable An expert in copyright law would have immediately flagged the civil/criminal conflation. The AI had all the relevant information but lacked the judgment to apply it critically. 4. These systems are tools, not oracles Just as you wouldn't trust a calculator that accepted "2+2=5" as a premise and calculated accordingly, we shouldn't treat AI outputs as inherently reliable just because they're well-reasoned. Your Turn: Test These Systems Yourself I encourage you to run your own experiments. Try: Establishing a subtly flawed definition and seeing if the AI accepts it Presenting two contradictory "facts" and watching how it reconciles them Asking it to challenge your own assumptions on a topic you know well The goal isn't to "break" AI or prove it's useless. It's to develop a more sophisticated understanding of what these systems actually do, and what they can't do, no matter how confident they sound. As AI becomes more embedded in professional workflows, our ability to work effectively with these tools depends on understanding their reasoning processes, limitations, and blind spots.
GJan 16
7 min read