Complete Reference · File 07 of 07
C++ Programming

The Logic Builder
How to Think Before You Code

Every programmer — beginner or senior — has been stuck staring at a blank editor, knowing what the answer should be but not knowing how to get there. This file fixes that. Not by giving you code to copy, but by teaching you how to see every problem: the pattern, the constraint, the trick. Problem → Think → Visualize → Code. Every time.

Ch 1 · How to Think About Problems Ch 2 · Number Logic Ch 3 · Pattern Printing Ch 4 · Array Logic Ch 5 · String Logic Ch 6 · Recursion Thinking Ch 7 · Searching & Sorting Logic Ch 8 · Mathematical Logic Ch 9 · Two Pointer & Sliding Window Ch 10 · Bit Logic Ch 11 · Advanced Logic Patterns Ch 12 · Logic Master Reference
Chapter 01 · The Foundation
How to Think About Any Problem

1.1   The 5-Step Problem Framework Read This First

The difference between a programmer who gets stuck and one who doesn't isn't intelligence — it's process. Every problem, no matter how hard, yields to the same five questions asked in the same order.

  1. What am I given? List all inputs. Their type, their range, their constraints. Never skip this. "n ≤ 10⁵" tells you everything about what algorithm is even possible.
  2. What do I need to produce? State the output in one sentence. If you can't, you don't understand the problem yet.
  3. What's the simplest case? Try with n=1, n=2, n=0. Draw it out by hand. The pattern is always visible in small cases.
  4. Is there a mathematical relationship? Can I express the answer as a formula? Can I see it as: "for each X, I need to find Y"? What changes with each step?
  5. What data structure/algorithm fits? Match the problem shape to what you know. Sorted data → binary search. Extremes → heap. Connectivity → graph. Subproblems → DP.
Applying the 5 Steps: "Find the Missing Number"

Problem: Given an array containing n distinct numbers taken from 0, 1, 2, ..., n, find the one that is missing from the array. Constraints: n ≤ 10⁴.

1. Given: Array of size n, distinct elements ranging [0, n].

2. Produce: A single integer missing from the sequence.

3. Simplest Case: `arr = [0, 1]` (n=2, should have 0,1,2). Missing is 2. `arr = [3, 0, 1]` (n=3). Missing is 2.

4. Math Relationship: The array *should* contain every number from 0 to n. If I sum the ideal range [0..n] and subtract the sum of the actual array, the difference mathematically must be the missing number.

5. Algorithm: Math formula for sum `n*(n+1)/2`, minus loop sum. O(n) time, O(1) space.

The Golden Rule of Logic Building

Solve by hand on paper first. Always. Pick 3–4 small examples. Trace what you do with your hands and eyes. Your code is just translating that manual process into instructions. If you can't solve it manually, you can't code it. If you can, coding it is mechanical.

1.2   Reading Constraints — They Tell You the Answer Critical Insight

Constraint on nWhat it means for your algorithmTypical Pattern
n ≤ 10Brute force everythingTry all permutations, all subsets
n ≤ 20Bitmask or meet-in-middle2ⁿ is around 10⁶ — barely ok
n ≤ 10²O(n²) or O(n³) fineNested loops, all pairs
n ≤ 10³O(n²) ok, O(n³) riskyBubble/insertion sort, simple DP
n ≤ 10⁵O(n log n) requiredSort, binary search, segment tree
n ≤ 10⁶O(n) onlySingle pass, hash map, sieve
n ≤ 10¹⁸O(log n) or O(1)Math formula, binary search on answer
If you're stuck — go brute force first

Write the O(n²) or O(2ⁿ) brute force first. Always. It gives you correct answers to verify against, it helps you understand the structure of the problem, and sometimes — especially in interviews — it's all you need. Optimize after you have correctness.

1.3   Pattern Recognition — What Kind of Problem Is This? Before Every Problem

Count Something

Look for: math formula, combinatorics, prefix sums, DP. Ask: can I compute it without enumerating all cases?

Find Min / Max

Look for: binary search on answer, greedy, DP. Ask: is there a monotone property? If I fix one thing, can I minimize the other?

Yes / No (Exists?)

Look for: DFS/BFS for reachability, two pointers, hash set membership. Often easier than finding the actual answer.

Construct / Build

Look for: greedy construction, sort then assign, modular building. Start with what's forced, then fill in the rest.

Optimize a Value

Look for: DP with state, greedy with proof, binary search on the answer. The key: define the state precisely first.

Transform / Convert

Look for: model as graph, reduce to known problem. Ask: is this secretly a shortest path? A matching? A flow problem?

↑ back to top
Chapter 02 · Number Logic
Number Logic — Primes, Armstrong, Strong, Perfect & Friends

2.1   Prime Number Check Classic

Given a number n, determine if it is prime. A prime has exactly two divisors: 1 and itself.

The 5-Step Logic

1. Given: An integer n. 2. Produce: True if prime, False otherwise.

3. Simplest Case: n=2 (True), n=4 (False: 2x2), n=9 (False: 3x3).

4. Math Relationship: A number n is prime if no integer from 2 to √n divides it evenly. Why √n? Because if n = a × b and a ≤ b, then a must be ≤ √n. If no factor exists up to √n, none exist beyond it either.

5. Algorithm: Iterate i from 2 to √n. If n % i == 0, return false. Skip evens early to optimize.

Trace: Is 29 prime? √29 ≈ 5.38 → check divisors: 2, 3, 4, 5 29 ÷ 2 = 14.5 → not divisible 29 ÷ 3 = 9.67 → not divisible 29 ÷ 4 = 7.25 → not divisible 29 ÷ 5 = 5.8 → not divisible → No divisor found → 29 IS PRIME ✓ Trace: Is 36 prime? √36 = 6 → check: 2 36 ÷ 2 = 18 → divisible! → 36 is NOT prime ✗ (stop immediately)
#include<bits/stdc++.h> using namespace std; bool isPrime(int n) { if (n < 2) return false; // 0, 1 → not prime if (n == 2) return true; // 2 → prime if (n % 2 == 0) return false; // Even > 2 → not prime for (int i = 3; i * i <= n; i += 2) // Check odd divisors up to √n if (n % i == 0) return false; return true; } int main() { int n; cin >> n; cout << (isPrime(n) ? "Prime" : "Not Prime") << "\n"; }
Extension: Print all primes up to n (Sieve of Eratosthenes)

Mark all multiples of each prime as composite, starting from 2. What remains unmarked is prime. Time: O(n log log n). For n = 10⁶, this runs in milliseconds. See File 06 Ch17 for the full linear sieve.

2.2   Armstrong Number Classic

A number is Armstrong (narcissistic) if the sum of its digits each raised to the power of the number of digits equals the number itself. Example: 153 = 1³ + 5³ + 3³.

The 5-Step Logic

1. Given: An integer n. 2. Produce: True if Armstrong, False otherwise.

3. Simplest Case: n=153. Digits are 1,5,3. 1³ + 5³ + 3³ = 1 + 125 + 27 = 153. True.

4. Math Relationship: The answer requires distinct mathematical operations: counting total digits, extracting individual digits sequentially, and summing their digit-wise powers.

5. Algorithm: Convert to string to count digits. Use a `while(n > 0)` loop with `% 10` (extract) and `/ 10` (remove) to compute sum. Compare sum to original.

Trace: Is 1634 Armstrong? n = 1634, digits = 4 Extract digits and compute: 1634 % 10 = 4 → 4⁴ = 256 | 1634 / 10 = 163 163 % 10 = 3 → 3⁴ = 81 | 163 / 10 = 16 16 % 10 = 6 → 6⁴ = 1296 | 16 / 10 = 1 1 % 10 = 1 → 1⁴ = 1 | 1 / 10 = 0 Sum = 256 + 81 + 1296 + 1 = 1634 Sum == n → 1634 IS Armstrong ✓
bool isArmstrong(int n) { int original = n, sum = 0; int digits = to_string(n).length(); // Count digits while (n > 0) { int d = n % 10; // Extract last digit sum += pow(d, digits); // Add d^(num_digits) n /= 10; // Remove last digit } return sum == original; } // Armstrong numbers: 1, 2, 3, ..., 9, 153, 370, 371, 407, 1634, 8208, 9474

2.3   Perfect Number & Strong Number Classic

Perfect Number — Sum of proper divisors equals itself

A number is perfect if the sum of all its proper divisors (divisors excluding itself) equals the number. Example: 28 = 1 + 2 + 4 + 7 + 14.

The Logic

Find all divisors of n from 1 to √n. For each divisor d, both d and n/d are divisors (add both, but don't double-count when d == n/d). Sum them all. Compare with n.

bool isPerfect(int n) { if (n <= 1) return false; int sum = 1; // 1 is always a proper divisor for (int i = 2; i * i <= n; i++) { if (n % i == 0) { sum += i; if (i != n / i) sum += n / i; // Don't add twice if i == sqrt(n) } } return sum == n; } // Perfect numbers: 6, 28, 496, 8128, 33550336...

Strong Number — Sum of factorial of digits equals itself

A number is strong if the sum of factorials of its digits equals the number itself. Example: 145 = 1! + 4! + 5! = 1 + 24 + 120.

Trace: Is 145 a Strong Number? Precompute factorials: 0!=1, 1!=1, 2!=2, 3!=6, 4!=24, 5!=120, 6!=720, 7!=5040, 8!=40320, 9!=362880 145 → digits: 1, 4, 5 1! = 1 4! = 24 5! = 120 ─── Sum = 145 == original → Strong Number ✓
bool isStrong(int n) { int fact[10] = {1,1,2,6,24,120,720,5040,40320,362880}; // Precomputed factorials 0..9 int sum = 0, tmp = n; while (tmp > 0) { sum += fact[tmp % 10]; // Add factorial of last digit tmp /= 10; } return sum == n; } // Strong numbers: 1, 2, 145, 40585

2.4   Palindrome, Automorphic, Spy & Harshad Numbers More Classics

// Palindrome Number — reads same forwards and backwards bool isPalindromeNum(int n) { int original = n, rev = 0; while (n > 0) { rev = rev * 10 + n % 10; n /= 10; } return rev == original; } // 121, 1331, 12321 are palindromes // Automorphic Number — square ends with the number itself bool isAutomorphic(int n) { int sq = (long long)n * n; int digits = to_string(n).length(); int mod = 1; for (int i = 0; i < digits; i++) mod *= 10; return sq % mod == n; } // 5² = 25 (ends in 5) ✓ 25² = 625 (ends in 25) ✓ 76² = 5776 (ends in 76) ✓ // Spy Number — sum of digits equals product of digits bool isSpy(int n) { int sum = 0, prod = 1; while (n > 0) { int d = n % 10; sum += d; prod *= d; n /= 10; } return sum == prod; } // 123: sum=6, prod=6 ✓ 1124: sum=8, prod=8 ✓ // Harshad (Niven) Number — divisible by sum of its digits bool isHarshad(int n) { int sum = 0, tmp = n; while (tmp > 0) { sum += tmp % 10; tmp /= 10; } return n % sum == 0; } // 18: digitSum=9, 18%9=0 ✓ 21: digitSum=3, 21%3=0 ✓
↑ back to top
Chapter 03 · Patterns
Pattern Printing — The Formula Hidden in Every Shape

3.1   The Universal Pattern Formula Master Key

How to Solve ANY Pattern Problem

Draw the pattern. Label each row with i (1 to n). For each row i, ask: How many spaces? How many stars/numbers? What changes? The answer is always a formula in i and n. Once you have those three formulas, the code writes itself with two nested loops.

The 3-Question Method

For any pattern, look at row i (1-indexed, n rows total):

Q1: Leading spaces? — Often n-i or i-1 or 0.

Q2: Characters printed? — Often i, 2i-1, n-i+1, or something in between.

Q3: What is each character? — Star, digit (i or j), cumulative count?

Analysis: Right-angle Triangle (n=5) Row 1: * → 0 spaces, 1 star (i=1) Row 2: * * → 0 spaces, 2 stars (i=2) Row 3: * * * → 0 spaces, 3 stars (i=3) Row 4: * * * * → 0 spaces, 4 stars (i=4) Row 5: * * * * * → 0 spaces, 5 stars (i=5) Pattern → spaces=0, stars=i → nested loop: j from 1 to i
Analysis: Pyramid (n=5) Row 1: * → 4 spaces, 1 star (n-i=4, 2i-1=1) Row 2: * * * → 3 spaces, 3 stars (n-i=3, 2i-1=3) Row 3: * * * * * → 2 spaces, 5 stars (n-i=2, 2i-1=5) Row 4: * * * * * * * → 1 space, 7 stars (n-i=1, 2i-1=7) Row 5: * * * * * * * *→ 0 spaces, 9 stars (n-i=0, 2i-1=9) Pattern → spaces=n-i, stars=2i-1
// Right-angle triangle void rightTriangle(int n) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) cout << "* "; cout << "\n"; } } // Pyramid void pyramid(int n) { for (int i = 1; i <= n; i++) { for (int sp = 1; sp <= n-i; sp++) cout << " "; for (int j = 1; j <= 2*i-1; j++) cout << "*"; cout << "\n"; } } // Diamond = Pyramid + Inverted Pyramid void diamond(int n) { // Upper half (including middle) for (int i = 1; i <= n; i++) { for (int sp = 1; sp <= n-i; sp++) cout << " "; for (int j = 1; j <= 2*i-1; j++) cout << "*"; cout << "\n"; } // Lower half for (int i = n-1; i >= 1; i--) { for (int sp = 1; sp <= n-i; sp++) cout << " "; for (int j = 1; j <= 2*i-1; j++) cout << "*"; cout << "\n"; } }

3.2   Number Patterns — Floyd's, Pascal's & More Intermediate

Floyd's Triangle Pattern 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Logic: keep a running counter, print it j times in row i
Pascal's Triangle (n=5) 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 Each element = C(row, col) = prev row's adjacent sum Row i (0-indexed): element j = C(i,j) = i! / (j! * (i-j)!)
// Floyd's Triangle void floyds(int n) { int num = 1; for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) cout << num++ << " "; cout << "\n"; } } // Pascal's Triangle void pascals(int n) { for (int i = 0; i < n; i++) { long long val = 1; for (int j = 0; j <= i; j++) { cout << val << " "; val = val * (i - j) / (j + 1); // Compute next C(i,j+1) from C(i,j) } cout << "\n"; } } // Butterfly Pattern (n=4) // * * Row 1: i stars, (2n-2i) spaces, i stars // ** ** Row 2: ... // *** *** // ******** void butterfly(int n) { auto row = [&](int i) { for(int j=0;j<i;j++) cout<<"*"; for(int j=0;j<2*(n-i);j++) cout<<" "; for(int j=0;j<i;j++) cout<<"*"; cout<<"\n"; }; for(int i=1;i<=n;i++) row(i); for(int i=n;i>=1;i--) row(i); }
↑ back to top
Chapter 04 · Arrays
Array Logic — The Thinking Behind Every Array Problem

4.1   Rotate, Reverse & Shift — The Three Fundamentals Core Logic

Reverse = the most powerful array tool

You can solve rotation with three reverses. This is a technique worth memorizing because it works in O(n) time and O(1) space — no extra array needed.

Rotate left by k: Reverse [0..k-1], reverse [k..n-1], reverse entire array.

Rotate right by k: Reverse entire array, reverse [0..k-1], reverse [k..n-1].

Left Rotate by 2: [1,2,3,4,5] → [3,4,5,1,2] Original: [ 1 2 | 3 4 5] Step 1 — Reverse first k=2 elements: [ 2 1 | 3 4 5] Step 2 — Reverse remaining n-k=3 elements: [2 1 | 5 4 3] Step 3 — Reverse entire array: [ 3 4 5 1 2] ✓
// Rotate array left by k positions — O(n) time, O(1) space void rotateLeft(vector<int>& a, int k) { int n = a.size(); k %= n; // Handle k > n reverse(a.begin(), a.begin() + k); // Reverse [0..k-1] reverse(a.begin() + k, a.end()); // Reverse [k..n-1] reverse(a.begin(), a.end()); // Reverse entire array } // Find second largest — O(n), single pass int secondLargest(vector<int>& a) { int first = INT_MIN, second = INT_MIN; for (int x : a) { if (x > first) { second = first; first = x; } else if (x > second && x != first) second = x; } return second; } // Move all zeros to end — order of non-zeros preserved void moveZerosToEnd(vector<int>& a) { int insertPos = 0; for (int i = 0; i < a.size(); i++) if (a[i] != 0) a[insertPos++] = a[i]; // Write non-zeros to front while (insertPos < a.size()) a[insertPos++] = 0; } // Separate even and odd — evens before odds void evenOdd(vector<int>& a) { int lo = 0, hi = a.size() - 1; while (lo < hi) { while (lo < hi && a[lo] % 2 == 0) lo++; // Skip evens at front while (lo < hi && a[hi] % 2 != 0) hi--; // Skip odds at end if (lo < hi) swap(a[lo], a[hi]); } }

4.2   Subarray Problems — The Classic Patterns Must Master

Given array, find the maximum sum contiguous subarray (Kadane's algorithm). This is the most famous array problem and the logic behind it teaches you half of all array problems.

Kadane's Key Insight

At each position, you have exactly two choices: extend the existing subarray (add current element to running sum), or start fresh at the current element.

Start fresh when? When the running sum becomes negative — because a negative prefix only drags you down. Any subarray starting after the negative part will be better.

Track the maximum sum seen at any point. That's your answer.

Kadane's Trace: [-2, 1, -3, 4, -1, 2, 1, -5, 4] idx: 0 1 2 3 4 5 6 7 8 arr: -2 1 -3 4 -1 2 1 -5 4 curSum: -2 → start fresh? yes (neg) → 0 -2+1= -1 → start fresh? yes (neg) → 0 ... Actually: i=0: curSum = max(-2, 0+-2) = -2 → reset to 0 if neg: so start fresh Better: curSum = max(arr[i], curSum+arr[i]) i=0: max(-2, -2) = -2 maxSum=-2 i=1: max( 1, -2+1 ) = 1 maxSum= 1 i=2: max(-3, 1-3 ) = -2 maxSum= 1 i=3: max( 4, -2+4 ) = 4 maxSum= 4 i=4: max(-1, 4-1 ) = 3 maxSum= 4 i=5: max( 2, 3+2 ) = 5 maxSum= 5 i=6: max( 1, 5+1 ) = 6 maxSum= 6 i=7: max(-5, 6-5 ) = 1 maxSum= 6 i=8: max( 4, 1+4 ) = 5 maxSum= 6 Answer: 6 (subarray [4,-1,2,1])
// Kadane's Algorithm — O(n) time, O(1) space int maxSubarraySum(vector<int>& a) { int maxSum = a[0], curSum = a[0]; for (int i = 1; i < a.size(); i++) { curSum = max(a[i], curSum + a[i]); // Extend or start fresh maxSum = max(maxSum, curSum); } return maxSum; } // Track start and end indices too tuple<int,int,int> maxSubarrayWithIndices(vector<int>& a) { int maxSum=a[0], curSum=a[0], start=0, end=0, tempStart=0; for(int i=1;i<a.size();i++){ if(a[i] > curSum+a[i]){ curSum=a[i]; tempStart=i; } else curSum+=a[i]; if(curSum>maxSum){ maxSum=curSum; start=tempStart; end=i; } } return {maxSum, start, end}; } // Maximum product subarray — track both max and min (min*negative = new max) int maxProductSubarray(vector<int>& a) { int maxP=a[0], minP=a[0], result=a[0]; for(int i=1;i<a.size();i++){ if(a[i]<0) swap(maxP,minP); // Negative flips max and min maxP = max(a[i], maxP*a[i]); minP = min(a[i], minP*a[i]); result = max(result, maxP); } return result; } // Dutch National Flag — sort array of 0s, 1s, 2s in O(n) one pass void sort012(vector<int>& a) { int lo=0, mid=0, hi=a.size()-1; while(mid <= hi) { if (a[mid]==0){ swap(a[lo++],a[mid++]); } else if(a[mid]==1){ mid++; } else { swap(a[mid],a[hi--]); } } }

4.3   Matrix Logic — Spiral, Transpose & Rotate Intermediate

Matrix Rotation Logic

Rotate 90° clockwise: First transpose (swap a[i][j] with a[j][i]), then reverse each row. This is a two-step O(n²) in-place operation. No extra matrix needed.

Transpose rule: element at (i,j) goes to (j,i). Only swap upper triangle to avoid double-swapping.

// Matrix Transpose (in-place) void transpose(vector<vector<int>>& m) { int n = m.size(); for(int i=0;i<n;i++) for(int j=i+1;j<n;j++) // Only upper triangle swap(m[i][j], m[j][i]); } // Rotate matrix 90° clockwise = Transpose + Reverse each row void rotate90(vector<vector<int>>& m) { transpose(m); for(auto& row : m) reverse(row.begin(), row.end()); } // Print matrix in spiral order vector<int> spiralOrder(vector<vector<int>>& m) { vector<int> res; int top=0, bottom=m.size()-1, left=0, right=m[0].size()-1; while(top<=bottom && left<=right) { for(int i=left;i<=right;i++) res.push_back(m[top][i]); top++; for(int i=top;i<=bottom;i++) res.push_back(m[i][right]); right--; if(top<=bottom) for(int i=right;i>=left;i--) res.push_back(m[bottom][i]);bottom--; if(left<=right) for(int i=bottom;i>=top;i--) res.push_back(m[i][left]); left++; } return res; } // Set matrix zeros — if m[i][j]==0, set entire row and column to 0 void setZeroes(vector<vector<int>>& m) { bool row0=false, col0=false; int R=m.size(), C=m[0].size(); for(int j=0;j<C;j++) if(!m[0][j]) row0=true; for(int i=0;i<R;i++) if(!m[i][0]) col0=true; for(int i=1;i<R;i++) for(int j=1;j<C;j++) if(!m[i][j]){ m[i][0]=0; m[0][j]=0; } // Use first row/col as markers for(int i=1;i<R;i++) for(int j=1;j<C;j++) if(!m[i][0]||!m[0][j]) m[i][j]=0; if(row0) for(int j=0;j<C;j++) m[0][j]=0; if(col0) for(int i=0;i<R;i++) m[i][0]=0; }
↑ back to top
Chapter 05 · Strings
String Logic — Palindromes, Anagrams & Classic Problems

5.1   Palindrome — Three Ways to Check Core

Determine if a string reads the same forwards and backwards. Ignore case and non-alphanumeric characters for the "clean" version.

Approach 1

Reverse & Compare

Create reversed copy. Compare with original. Simple, clear.

Time: O(n) · Space: O(n)
Approach 2

Two Pointers

Left pointer at start, right at end. Move inward. Compare characters. Stop when mismatch or pointers cross.

Time: O(n) · Space: O(1) ✓
Best Practice

Two Pointers + Filters

Skip non-alphanumeric. Lowercase before comparing. Works for "A man, a plan, a canal: Panama".

Time: O(n) · Space: O(1) ✓
// Two-pointer palindrome check — O(n) time, O(1) space bool isPalindrome(string s) { int l = 0, r = s.size() - 1; while (l < r) { if (s[l] != s[r]) return false; l++; r--; } return true; } // Ignore non-alphanumeric, case-insensitive version bool isPalindromeClean(string s) { int l=0, r=s.size()-1; while(l < r) { while(l<r && !isalnum(s[l])) l++; while(l<r && !isalnum(s[r])) r--; if(tolower(s[l++]) != tolower(s[r--])) return false; } return true; }

5.2   Anagram & Frequency Map Logic Core

Two strings are anagrams if one is a rearrangement of the other. "listen" and "silent" are anagrams. The fundamental tool here is the frequency map.

The Frequency Map — Your String Swiss Army Knife

Any time a string problem involves counting characters, comparing character sets, or finding missing/extra characters, a frequency map (array[26] for lowercase letters, or unordered_map for general) is your first instinct. Build it in O(n), query it in O(1).

// Anagram check — O(n) time, O(1) space bool isAnagram(string a, string b) { if(a.size() != b.size()) return false; int freq[26] = {0}; for(char c : a) freq[c-'a']++; for(char c : b) freq[c-'a']--; for(int x : freq) if(x != 0) return false; return true; } // First non-repeating character char firstUnique(string s) { int freq[26] = {0}; for(char c : s) freq[c-'a']++; for(char c : s) if(freq[c-'a']==1) return c; return '#'; // Sentinel: none found } // Check if string is isomorphic to another // "egg" and "add" → e→a, g→d ✓ "foo" and "bar" → f→b, o→a, o→r ✗ bool isIsomorphic(string s, string t) { int mapS[256]={0}, mapT[256]={0}; for(int i=0;i<s.size();i++){ if(mapS[s[i]] != mapT[t[i]]) return false; mapS[s[i]] = mapT[t[i]] = i+1; // Record last seen position } return true; } // Roman to Integer — table + one logic rule // If current value < next value → subtract (IV=4, IX=9) // Otherwise add int romanToInt(string s) { map<char,int> v = {{'I',1},{'V',5},{'X',10},{'L',50}, {'C',100},{'D',500},{'M',1000}}; int res = 0; for(int i=0;i<s.size();i++) { if(i+1<s.size() && v[s[i]]<v[s[i+1]]) res -= v[s[i]]; else res += v[s[i]]; } return res; }
↑ back to top
Chapter 06 · Recursion
Recursion Thinking — See It, Trust It, Don't Fear It

6.1   The Recursion Mental Model — How to Think About It Fundamental

The One Thought That Unlocks Recursion (Base vs. State)

1. The Base Case (The Anchor): This is the simplest un-splittable unit. When n=0 or the array is empty, what's the answer? Stop here. DO NOT overthink it.

2. The Recursive State (The Leap of Faith): Assume your function already works flawlessly for n-1. Do not trace it down! Just take the imaginary result of f(n-1) and figure out the math to turn it into f(n). You trust the smaller version works, and just write one extra step.

Three Questions for Every Recursive Problem

Q1: Base case — When do I stop? (n=0, n=1, empty, single element)

Q2: Recursive case — How does the answer for n depend on the answer for n-1 (or smaller)?

Q3: Combine — What do I do with the recursive result to get the full answer?

Tower of Hanoi — Mental Model (3 disks) Problem: Move 3 disks from A to C using B as helper. Think recursively: 1. "Assume I can move 2 disks from A to B" (trust the recursion) 2. Move disk 3 (largest) from A to C 3. "Assume I can move 2 disks from B to C" (trust again) That's it. The recursion handles the 2-disk problem: Move 2 disks = Move 1 disk → Move disk 2 → Move 1 disk back Execution trace (3 disks A→C via B): Move 2 disks A→B (via C): Move 1 disk A→C Move disk 2: A→B Move disk 1: C→B Move disk 3: A→C Move 2 disks B→C (via A): Move disk 1: B→A Move disk 2: B→C Move disk 1: A→C Total moves for n disks = 2ⁿ - 1
// Tower of Hanoi void hanoi(int n, char from, char to, char via) { if (n == 0) return; // Base case: nothing to move hanoi(n-1, from, via, to); // Step 1: move n-1 disks to helper cout << "Move disk " << n << ": " << from << " → " << to << "\n"; hanoi(n-1, via, to, from); // Step 3: move n-1 from helper to dest } // Call: hanoi(3, 'A', 'C', 'B') // Time: O(2ⁿ), Moves: 2ⁿ - 1 // Fibonacci — Recursion made efficient with memoization int memo[100] = {}; int fib(int n) { if (n <= 1) return n; if (memo[n]) return memo[n]; // Already computed! return memo[n] = fib(n-1) + fib(n-2); // Store before returning } // Without memo: O(2ⁿ) | With memo: O(n) — that's the power of memoization // Power function — recursive fast exponentiation long long power(long long base, int exp) { if (exp == 0) return 1; if (exp % 2 == 0) { long long half = power(base, exp/2); return half * half; // b^(2k) = (b^k)² } return base * power(base, exp-1); // b^n = b * b^(n-1) } // O(log n) — each call halves the exponent // Count ways to climb n stairs (1 or 2 steps at a time) int climbStairs(int n) { if(n<=1) return 1; // Base: 0 or 1 step → 1 way return climbStairs(n-1) + climbStairs(n-2); // Last step was 1 or 2 } // Pattern: same as Fibonacci! f(n) = f(n-1) + f(n-2)

6.2   Recursive Tree Logic — Generate All Possibilities Advanced

Recursion Tree = Decision Tree

At each recursive call, you branch based on choices. For "generate all subsets", at each element you branch: include it OR exclude it. For "generate all permutations", you branch by which element you place next. The recursion tree has choices^depth leaves — understand this to predict time complexity.

Subset Generation Tree for [1,2,3] [] / \ [1] [] / \ / \ [1,2] [1] [2] [] / \ / \ / \ / \ [1,2,3][1,2][1,3][1] [2,3][2] [3] [] 8 leaves = 2³ subsets At each level, left = include element, right = exclude element
// Generate all subsets — include/exclude decision at each index void subsets(vector<int>& nums, int idx, vector<int>& curr, vector<vector<int>>& res) { if(idx == nums.size()) { res.push_back(curr); return; } curr.push_back(nums[idx]); // Include nums[idx] subsets(nums, idx+1, curr, res); curr.pop_back(); // Backtrack (undo include) subsets(nums, idx+1, curr, res); // Exclude nums[idx] } // Generate all permutations void permutations(string s, int start, vector<string>& res) { if(start == s.size()) { res.push_back(s); return; } for(int i=start; i<s.size(); i++) { swap(s[start], s[i]); // Place character i at position start permutations(s, start+1, res); swap(s[start], s[i]); // Backtrack } }
↑ back to top
Chapter 07 · Search & Sort
Searching & Sorting Logic — When to Use What, and Why

7.1   Binary Search — Not Just for Finding Elements Expand Your View

The Real Power of Binary Search

Binary search isn't just "is X in this array?". It's a technique for any monotone decision problem: if answer works for value x, it also works for all values ≤ x (or ≥ x). Whenever you see "find the minimum/maximum value such that some condition holds" — binary search on the answer. This reduces O(n²) or worse to O(n log n).

Find the minimum capacity of a ship that can ship all packages within D days, where packages[i] is the weight of package i and must be shipped in order.

Binary Search on Answer Logic

Key Insight: If capacity C works (all packages shipped in D days), then any capacity > C also works. This monotone property means we can binary search on C.

Search space: Minimum possible C = max(packages) (must fit heaviest package). Maximum possible C = sum(packages) (ship everything in one day).

Check function: Given capacity C, greedily fill each day until adding next package would exceed C. Count days needed. If days ≤ D → C works.

// Minimum ship capacity to deliver in D days bool canShip(vector<int>& p, int D, int cap) { int days=1, load=0; for(int w : p) { if(load + w > cap) { days++; load = 0; } // New day load += w; } return days <= D; } int minCapacity(vector<int>& p, int D) { int lo = *max_element(p.begin(),p.end()); // Min possible capacity int hi = accumulate(p.begin(),p.end(),0); // Max possible capacity while(lo < hi) { int mid = lo + (hi-lo)/2; canShip(p,D,mid) ? hi=mid : lo=mid+1; // mid works → try smaller } return lo; }

Sorting — Choosing the Right Sort for the Problem

SituationBest SortReason
General purposestd::sort (introsort)O(n log n) guaranteed, fastest in practice
Need stable sortstd::stable_sortPreserves relative order of equal elements
Nearly sorted arrayInsertion sortO(n) best case on nearly sorted input
Integer values in range [0,k]Counting sortO(n+k) — linear!
Sort by custom rulesort with comparatorsort(v.begin(),v.end(),[](a,b){ return a.x < b.x; })
Partial sort (need top k)nth_element / partial_sortO(n) average for nth_element
// Custom sort — sort by frequency, then by value sort(v.begin(), v.end(), [&](int a, int b){ if(freq[a] != freq[b]) return freq[a] > freq[b]; // Higher freq first return a < b; // Tie: smaller value first }); // Sort pair of vectors together (sort by first, carry second along) vector<pair<int,int>> combined; for(int i=0;i<n;i++) combined.push_back({a[i], b[i]}); sort(combined.begin(), combined.end()); // Sorts by first element by default // Count inversions using merge sort — O(n log n) long long mergeCount(vector<int>& a, int lo, int hi) { if(lo>=hi) return 0; int mid=(lo+hi)/2; long long cnt = mergeCount(a,lo,mid) + mergeCount(a,mid+1,hi); vector<int> tmp; int i=lo,j=mid+1; while(i<=mid && j<=hi){ if(a[i]<=a[j]) tmp.push_back(a[i++]); else{ cnt += mid-i+1; tmp.push_back(a[j++]); } // All remaining in left half form inversions } while(i<=mid) tmp.push_back(a[i++]); while(j<=hi) tmp.push_back(a[j++]); for(int k=lo;k<=hi;k++) a[k]=tmp[k-lo]; return cnt; }
↑ back to top
Chapter 08 · Math Logic
Mathematical Logic — GCD, Sequences, Combinatorics & the Tricks

8.1   GCD / LCM — Euclid's Algorithm & Extensions Core

Why Euclid's Algorithm Works

GCD(a, b) = GCD(b, a mod b). The key insight: the GCD of a and b is the same as the GCD of b and the remainder when a is divided by b. Why? Because any common divisor of a and b also divides a - k*b (for any integer k), which includes a mod b. Each step shrinks the numbers, converging to GCD in O(log min(a,b)) steps.

Euclidean GCD Trace: GCD(48, 18) GCD(48, 18) = GCD(18, 48 mod 18) = GCD(18, 12) GCD(18, 12) = GCD(12, 18 mod 12) = GCD(12, 6) GCD(12, 6) = GCD( 6, 12 mod 6) = GCD( 6, 0) GCD( 6, 0) = 6 (base case: GCD(x,0) = x)
// GCD — O(log min(a,b)) int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int lcm(int a, int b) { return a / gcd(a,b) * b; } // Divide first: avoid overflow // GCD of entire array int gcdArray(vector<int>& a) { int g = a[0]; for(int x : a) g = gcd(g, x); return g; } // How many integers in [1,n] are coprime to n? — Euler's Totient int eulerTotient(int n) { int result = n; for(int p=2; p*p<=n; p++) { if(n % p == 0) { while(n % p == 0) n /= p; result -= result/p; // φ(n) = n * ∏(1 - 1/p) } } if(n > 1) result -= result/n; return result; }

8.2   Series & Sequence Formulas — The Cheatsheet Reference

SeriesFormulaExample
Sum 1 to nn*(n+1)/21+2+…+100 = 5050
Sum of squaresn*(n+1)*(2n+1)/61²+2²+…+n²
Sum of cubes[n*(n+1)/2]²(1+2+…+n)²
Geometric series (r≠1)a*(rⁿ-1)/(r-1)1+2+4+8+…+2^(n-1) = 2ⁿ-1
Count divisors of nLoop to √n, add pairs12 → {1,2,3,4,6,12} → 6 divisors
Sum of divisorsLoop to √n, sum d and n/d12 → 1+2+3+4+6+12 = 28
Number of digits in nfloor(log10(n))+1digits(1000) = 4
Triangular numbersn*(n+1)/21,3,6,10,15,21...
Catalan numbersC(2n,n)/(n+1)1,1,2,5,14,42... (balanced parens)
// Sum of divisors of n — O(√n) long long sumDivisors(int n) { long long sum = 0; for(int i=1; i*i<=n; i++) { if(n%i==0) { sum += i; if(i != n/i) sum += n/i; // Don't double-count perfect squares } } return sum; } // Count trailing zeros in n! — how many times does 5 divide n! // Each factor of 5 with a factor of 2 makes a trailing zero // There are always more 2s than 5s in n!, so count 5s int trailingZeros(int n) { int count = 0; while(n >= 5) { n /= 5; count += n; } return count; } // 25! → floor(25/5) + floor(25/25) = 5+1 = 6 trailing zeros // Next permutation — find next lexicographically larger arrangement // Algorithm: find rightmost element smaller than its successor, // swap with rightmost element greater than it, then reverse suffix bool nextPermutation(vector<int>& a) { int i = a.size()-2; while(i>=0 && a[i]>=a[i+1]) i--; // Find descent point if(i<0) return false; // Already largest permutation int j = a.size()-1; while(a[j]<=a[i]) j--; // Find smallest element > a[i] swap(a[i],a[j]); reverse(a.begin()+i+1, a.end()); // Reverse suffix return true; }
↑ back to top
Chapter 09 · Pointer Techniques
Two Pointer & Sliding Window — Converting O(n²) to O(n)

9.1   Two Pointer — When and Why It Works Key Pattern

Two Pointer & Sliding Window Signals

Signal 1: Contiguous Segments. "Find the longest/shortest subarray or substring." A sliding window natively represents a contiguous segment.

Signal 2: Monotonicity. Adding an element strictly increases the condition (e.g., sum of positive numbers). Removing an element strictly decreases it. This guarantees we don't have to test every window.

Signal 3: O(n²) is too slow. You see constraints like N = 10⁵ and a brute-force nested loop is timing out.

Given array of positive integers and a target sum S, find if there exists a contiguous subarray with sum equal to S.

Why Two Pointers Work Here

Observation: All elements are positive. This means: expanding the window (moving right) always increases the sum. Shrinking the window (moving left forward) always decreases the sum.

Therefore: If sum < S → expand (move right pointer). If sum > S → shrink (move left pointer). If sum == S → found it. Each pointer moves at most n steps → O(n) total.

Sliding Window: Target=9, Array=[1,2,3,4,5] l=0 r=0: sum=1 < 9 → expand right l=0 r=1: sum=3 < 9 → expand right l=0 r=2: sum=6 < 9 → expand right l=0 r=3: sum=10 > 9 → shrink left l=1 r=3: sum=9 = 9 → FOUND [2,3,4] ✓
// Subarray with given sum — positive integers only, O(n) pair<int,int> subarraySum(vector<int>& a, int S) { int l=0, sum=0; for(int r=0; r<a.size(); r++) { sum += a[r]; while(sum > S && l <= r) sum -= a[l++]; // Shrink from left if(sum == S) return {l, r}; // Found! } return {-1, -1}; } // 3Sum — find triplet summing to 0 — O(n²) vector<vector<int>> threeSum(vector<int>& a) { sort(a.begin(), a.end()); vector<vector<int>> res; for(int i=0; i<int(a.size())-2; i++) { if(i>0 && a[i]==a[i-1]) continue; // Skip duplicates int l=i+1, r=a.size()-1; while(l<r) { int s=a[i]+a[l]+a[r]; if(s==0){ res.push_back({a[i],a[l++],a[r--]}); } else if(s<0) l++; else r--; } } return res; } // Minimum window with at least k distinct characters — sliding window int minWindowDistinct(string s, int k) { unordered_map<char,int> freq; int l=0, distinct=0, minLen=INT_MAX; for(int r=0;r<s.size();r++){ if(freq[s[r]]++==0) distinct++; while(distinct>=k){ minLen = min(minLen, r-l+1); if(--freq[s[l++]]==0) distinct--; } } return minLen==INT_MAX?0:minLen; }
↑ back to top
Chapter 10 · Bit Logic
Bit Manipulation Logic — See Numbers as Binary, Think Differently

10.1   The Bit Mindset — Common Problems & Their Bit Logic Think in Binary

When to Think in Bits

Bit manipulation is your go-to when: the problem involves sets (subsets, membership), powers of 2, toggling states (on/off), the constraints are very tight (O(1) space needed), or you spot that XOR cancels pairs. The key is learning to see problems as operations on binary representations.

Bit OperationWhat it doesWhen to use
n & 1Check if odd (last bit)Parity check, odd/even
n & (n-1)Turn off rightmost set bitCheck power of 2, count bits
n | (1<<i)Set bit iTurn on position i
n & ~(1<<i)Clear bit iTurn off position i
n ^ (1<<i)Toggle bit iFlip position i
a ^ a = 0XOR self = zeroFind unpaired element
a ^ 0 = aXOR with 0 = identityXOR trick for missing numbers
n << kMultiply by 2^kFast multiply/divide by powers of 2
// Power of 2 check — O(1) // If n is power of 2, binary is 1000...0. n-1 is 0111...1. AND = 0. bool isPowerOf2(int n) { return n > 0 && (n & (n-1)) == 0; } // Count set bits (Brian Kernighan's Algorithm) — O(number of set bits) int countBits(int n) { int count = 0; while(n) { n &= (n-1); count++; } // Each step removes one set bit return count; } // Find the one element that appears once (all others appear twice) // XOR trick: a^a=0, 0^b=b → pairs cancel, lone element remains int singleNumber(vector<int>& a) { int xorSum = 0; for(int x : a) xorSum ^= x; return xorSum; } // Find two missing numbers using XOR // XOR all [1..n] with all array elements → get XOR of two missing numbers // Find a set bit in that XOR → use it to split into two groups // Reverse bits of a 32-bit number uint32_t reverseBits(uint32_t n) { uint32_t res = 0; for(int i=0;i<32;i++) { res = (res << 1) | (n & 1); // Shift res left, bring in LSB of n n >>= 1; } return res; } // Swap two numbers without temp variable — XOR swap // a^=b; b^=a; a^=b; // WARNING: fails if a and b are the same variable (a^=a = 0!) // Generate all subsets using bitmask — each bit = include/exclude element void allSubsets(vector<int>& a) { int n = a.size(); for(int mask=0; mask<(1<<n); mask++) { cout << "{ "; for(int i=0;i<n;i++) if(mask & (1<<i)) cout << a[i] << " "; // Bit i set → include a[i] cout << "}\n"; } }
↑ back to top
Chapter 11 · Advanced Patterns
Advanced Logic Patterns — Greedy Thinking, DP Intuition & Graph Logic

11.1   Greedy Logic — When Taking the Best Now Is Always Best Critical

Greedy Works When You Can Prove This

A greedy algorithm is correct when making the locally optimal choice at each step never prevents a globally optimal solution. The classic proof technique: "exchange argument" — if you swap a greedy choice with any other choice, the result doesn't improve. If you can prove this, greedy is correct AND fast.

You have n intervals [start, end]. Find the minimum number of intervals to remove to make all remaining intervals non-overlapping.

Greedy Insight — Sort by End Time

Why end time? At each step, among all intervals that start after our last kept interval ends, we should keep the one that ends earliest. This leaves the maximum room for future intervals. Keeping the one that ends latest would "waste" more future space.

Algorithm: Sort by end time. Keep a "last end" pointer. Greedily include each interval if it starts at or after last end. Count excluded intervals.

Non-Overlapping Intervals: [[1,2],[2,3],[3,4],[1,3]] Sort by end time: [1,2] [2,3] [1,3] [3,4] Greedy pass: [1,2]: include, lastEnd=2 [2,3]: starts at 2 >= lastEnd=2 → include, lastEnd=3 [1,3]: starts at 1 < lastEnd=3 → REMOVE (overlap) [3,4]: starts at 3 >= lastEnd=3 → include, lastEnd=4 Removed: 1 interval. Answer: 1
// Minimum intervals to remove (non-overlapping) int eraseOverlapIntervals(vector<vector<int>>& intervals) { sort(intervals.begin(), intervals.end(), [](auto& a, auto& b){ return a[1] < b[1]; }); // Sort by END time int removed = 0, lastEnd = INT_MIN; for(auto& iv : intervals) { if(iv[0] < lastEnd) removed++; // Overlap → remove this interval else lastEnd = iv[1]; // No overlap → keep, update lastEnd } return removed; } // Meeting rooms — can a person attend all meetings? bool canAttendAll(vector<pair<int,int>>& meetings) { sort(meetings.begin(), meetings.end()); // Sort by start time for(int i=1;i<meetings.size();i++) if(meetings[i].first < meetings[i-1].second) return false; return true; }

11.2   DP Intuition — From Recursion to Bottom-Up Advanced

The DP Thought Process — in Order

Step 1: Solve with recursion first. Don't think about DP yet. Just write the recursive solution that expresses the answer in terms of smaller subproblems.

Step 2: Add memoization. Notice which recursive calls repeat. Cache their results. This is "top-down DP" and it's usually enough.

Step 3: Convert to bottom-up (tabulation). Fill the table from smaller to larger subproblems. Usually gives better constant factors and avoids stack overflow.

Step 4: Optimize space. If you only ever look at the previous row/state, you can compress the 2D table to 1D.

Longest Common Subsequence (LCS) of two strings. This is the classic 2D DP problem.

LCS DP Table: "ABCBDAB" vs "BDCAB" dp[i][j] = LCS of s1[0..i-1] and s2[0..j-1] "" B D C A B "" 0 0 0 0 0 0 A 0 0 0 0 1 1 B 0 1 1 1 1 2 C 0 1 1 2 2 2 B 0 1 1 2 2 3 D 0 1 2 2 2 3 A 0 1 2 2 3 3 B 0 1 2 2 3 4 If s1[i]==s2[j]: dp[i][j] = dp[i-1][j-1] + 1 Else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
// LCS — Classic 2D DP, O(m*n) time and space int lcs(string a, string b) { int m=a.size(), n=b.size(); vector<vector<int>> dp(m+1, vector<int>(n+1, 0)); for(int i=1;i<=m;i++) for(int j=1;j<=n;j++) dp[i][j] = (a[i-1]==b[j-1]) ? dp[i-1][j-1]+1 : max(dp[i-1][j], dp[i][j-1]); return dp[m][n]; } // Space-optimized to O(n): only need previous row int lcsOpt(string a, string b) { int m=a.size(), n=b.size(); vector<int> prev(n+1,0), curr(n+1,0); for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++) curr[j]=(a[i-1]==b[j-1])?prev[j-1]+1:max(prev[j],curr[j-1]); swap(prev,curr); } return prev[n]; } // Longest Palindromic Subsequence = LCS(s, reverse(s)) int longestPalSubseq(string s) { string rev = s; reverse(rev.begin(), rev.end()); return lcs(s, rev); } // Unbounded Knapsack — items can be used multiple times int unboundedKnapsack(vector<int>& wt, vector<int>& val, int W) { vector<int> dp(W+1, 0); for(int w=1;w<=W;w++) for(int i=0;i<wt.size();i++) if(wt[i]<=w) dp[w]=max(dp[w], dp[w-wt[i]]+val[i]); return dp[W]; } // Key difference from 0/1 knapsack: iterate capacity FORWARD (reuse items) // 0/1 knapsack: iterate capacity BACKWARD (each item used at most once)

11.3   Graph Logic — Recognizing Graph Problems in Disguise Pattern Shift

Grid = Graph

Any grid problem where you move between cells (up/down/left/right) is a graph. Cells are nodes, valid moves are edges. BFS finds shortest path. DFS finds connected regions (islands).

Dependencies = DAG

"Task A must complete before B" → directed edge A→B. Finding valid order = topological sort. Detecting impossible (circular dependency) = cycle detection.

States = Nodes

BFS/DFS doesn't need a literal graph. States (board positions, configurations) are nodes, transitions are edges. Shortest path = fewest transitions to reach goal state.

// Number of Islands — count connected regions of '1' in grid // Classic DFS on grid — "flood fill" pattern int numIslands(vector<vector<char>>& grid) { int R=grid.size(), C=grid[0].size(), count=0; int dx[]={0,0,1,-1}, dy[]={1,-1,0,0}; function<void(int,int)> dfs = [&](int r, int c){ if(r<0||r>=R||c<0||c>=C||grid[r][c]!='1') return; grid[r][c] = '0'; // Mark as visited for(int d=0;d<4;d++) dfs(r+dx[d], c+dy[d]); // Visit all 4 neighbors }; for(int i=0;i<R;i++) for(int j=0;j<C;j++) if(grid[i][j]=='1'){ dfs(i,j); count++; } // Each new DFS = new island return count; } // Shortest path in unweighted grid — BFS int shortestPath(vector<vector<int>>& grid, pair<int,int> src, pair<int,int> dst) { int R=grid.size(), C=grid[0].size(); vector<vector<int>> dist(R, vector<int>(C,-1)); queue<pair<int,int>> q; q.push(src); dist[src.first][src.second]=0; int dx[]={0,0,1,-1}, dy[]={1,-1,0,0}; while(!q.empty()){ auto [r,c]=q.front(); q.pop(); if(make_pair(r,c)==dst) return dist[r][c]; for(int d=0;d<4;d++){ int nr=r+dx[d], nc=c+dy[d]; if(nr>=0&&nr<R&&nc>=0&&nc<C&&grid[nr][nc]==0&&dist[nr][nc]==-1){ dist[nr][nc]=dist[r][c]+1; q.push({nr,nc}); } } } return -1; // Unreachable }
↑ back to top
Chapter 12 · The Reference
Logic Master Reference — The Cheatsheet You'll Use Every Session

12.1   Problem Signal → Logic Map Pin This

You Read / NoticeThink AboutStart WithConcrete Example
"Find if exists / is possible"Boolean logic, BFS/DFS, two pointersCan you reduce to yes/no sub-question?"Is there a path from A to B?"
"Minimum / maximum value"Binary search on answer, greedy, DPIs there a monotone property?"Minimum days to ship all cargo."
"Count ways / number of X"DP, combinatorics, inclusion-exclusionCan I define dp[i] = count up to i?"Ways to climb N stairs."
"All / every / enumerate"Backtracking, bitmaskDecision tree: what choices exist at each step?"Generate all valid parentheses."
"Contiguous subarray"Sliding window, prefix sum, Kadane'sTry prefix sum first: sum[r]-sum[l]"Max sum subarray of size K."
"Pair / triplet summing to X"Sort + two pointers, hash mapFix one element, find rest with two pointer"3Sum problem."
"Sorted array / binary search"Binary search variantsLower bound? Upper bound? On answer?"Find first occurrence of X."
"Grid / 2D movement"BFS (shortest), DFS (reachability/area)BFS if shortest path, DFS if just connectivity"Number of Islands."
"Dependencies / ordering"Topological sort, DAG DPBuild adjacency list, run Kahn's algorithm"Course Schedule (prerequisites)."
"Overlapping subproblems"Memoization → DP tableDefine the state first: what uniquely identifies a subproblem?"Longest Common Subsequence."
"Remove/add elements dynamically"Heap, segment tree, BITHeap if only need max/min. SegTree for range ops."Running median of a data stream."
"Digits of a number"Modular arithmetic, digit DPExtract digits: while(n) { d=n%10; n/=10; }"Sum of digits until single digit."
"Strings, character frequency"Frequency array[26], sliding windowBuild freq map in O(n), query O(1)"Longest substring without repeats."
"Intervals / schedules"Sort by start or end, greedy, sweep lineSort by end for max non-overlapping. Sort by start for merge."Merge Overlapping Intervals."
"XOR, bit toggling, subset"Bit manipulationDraw small examples in binary. XOR for pairing/cancellation."Find the single non-repeating number."

12.2   Common Logic Patterns with Complexities Reference

Digit Extraction

Get digits: while(n) { d=n%10; n/=10; }
Reverse digits: while(n) { rev=rev*10+n%10; n/=10; }
Digit count: to_string(n).length()

Array Tricks

Prefix sum for range queries.
Sort + binary search for pair finding.
Two pointers on sorted arrays.
Hash map for O(1) membership check.

String Tricks

Frequency array for anagram/permutation.
Two pointers for palindrome.
Concatenate + Z-function for pattern matching.
Sort chars for canonical anagram key.

DP State Ideas

dp[i] = answer for first i elements.
dp[i][j] = answer for range [i,j].
dp[i][j] = answer using i items with j capacity.
dp[mask] = answer for subset mask.

Recursion Base Cases

n=0 → return 0 or empty.
n=1 → return the element itself.
l > r → return 0 (empty range).
visited → return (avoid infinite loop).

Greedy Signals

Problem has optimal substructure AND greedy choice property.
Often: sort first, then process linearly.
Exchange argument proves correctness.
No "undo" is needed.

12.3   The Logic Builder's Wisdom — 20 Rules for Life Keep These

  • Always solve by hand first. 3 small examples. The pattern appears. Code follows.
  • Read constraints before thinking. They narrow the universe of valid solutions to a handful.
  • Brute force is your friend. It gives you correct answers to test against. It reveals structure.
  • If you need O(n²), ask: is there a sorted structure or hash map that gives O(n log n) or O(n)?
  • Stuck? Simplify. What if n = 2? What if all elements are equal? Edge cases reveal the core logic.
  • Every subarray problem has a prefix sum version. sum(l,r) = prefix[r+1] - prefix[l].
  • Every sorted array problem has a binary search version. Even if the answer is binary searched, not the array.
  • Sliding window when: contiguous elements, fixed or variable window, positive elements (for sum target).
  • Two pointers when: sorted array, pairs/triplets, move each pointer exactly once total.
  • DFS when: you need to explore all paths, connectivity, or enumerate all solutions.
  • BFS when: shortest path, minimum steps, level-order traversal.
  • Greedy when: sorting + one pass solves it, and you can prove no swap improves the answer.
  • DP when: overlapping subproblems, optimal substructure, recursion with memoization feels natural.
  • The state definition is the hardest part of DP. Once you have dp[i] = X correctly, the transition is usually obvious.
  • Don't optimize too early. Correct first. Fast second. O(n²) that's correct beats O(n log n) that's wrong.
  • Integer overflow kills. Use long long whenever multiplication is involved, or values exceed 10⁴.
  • Off-by-one errors live in loops and binary search. Think carefully about loop bounds: < vs <=, 0-indexed vs 1-indexed.
  • XOR cancels pairs. Find the singleton in O(n) time, O(1) space. Remember this forever.
  • Frequency maps are underused. Any character/element counting → build freq in O(n), use O(1).
  • The best programmer isn't the one who codes fastest — it's the one who makes fewest wrong assumptions.
↑ back to top