Compare commits

...

7 Commits

20 changed files with 2128 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"initialized": true,
"learning_mode": "balanced",
"macos_reminders_enabled": false
}
@@ -0,0 +1,20 @@
# C++ Pointers and References - Mastery Checklist
## Core Concepts
Track your understanding of key concepts:
- [ ] Declaration syntax: & and * placement
- [ ] Pointers vs references: mental model
- [ ] Pointer arithmetic
- [ ] const correctness with pointers and references
- [ ] References as function parameters
- [ ] Pointer-to-pointer
- [ ] Null pointers and nullptr
- [ ] Dangling pointers and references
- [ ] Smart pointers (unique_ptr, shared_ptr, weak_ptr)
- [ ] Lvalue references vs rvalue references
- [ ] Move semantics
- [ ] Perfect forwarding
- [ ] Reference collapsing rules
- [ ] Common interview traps
@@ -0,0 +1,9 @@
{
"topic": "C++ Pointers and References",
"created_at": "2026-05-20T12:00:00",
"status": "in_progress",
"syllabus_generated": true,
"total_sessions": 0,
"last_session": null,
"last_reviewed": null
}
@@ -0,0 +1,5 @@
# C++ Pointers and References - Learning Progress
## Daily Logs
<!-- Daily learning entries will be added here -->
@@ -0,0 +1,3 @@
{
"reviews": []
}
@@ -0,0 +1,86 @@
# C++ Pointers and References — Syllabus
## Overview
Master C++ indirection mechanisms from declaration syntax through move semantics. Emphasis on building correct mental models, real-world usage patterns, and surviving interview questions about pointers and references.
## Prerequisites
- Basic C++ syntax (variables, functions, structs/classes)
- Understanding of stack vs heap (conceptual)
## Learning Objectives
By completion you will be able to:
- Read and write any pointer/reference declaration confidently
- Explain the difference between `&`/`*` in declarations vs expressions
- Use const-correctness with pointers and references correctly
- Understand ownership semantics and choose the right smart pointer
- Handle move semantics and forwarding references
- Answer common interview questions on pointers and references
---
## Phase 1: Foundations — Declaration Syntax & Mental Models
- [ ] `&` and `*` in declarations vs expressions — the two contexts
- [ ] Does `*` / `&` bind to the type or the variable? (C++ parsing rules)
- [ ] Multiple declarations on one line: `int *a, b` — what is b?
- [ ] Pointers: what they are (address), what they hold, how to dereference
- [ ] References: what they are (alias), how they differ from pointers
- [ ] Pointer vs reference: when to use which (guidelines)
- [ ] Null pointers: NULL vs nullptr vs 0
**Teaching Milestone:** Explain the "declaration follows use" principle to a beginner.
## Phase 2: const Correctness & Function Signatures
- [ ] `const int*` vs `int* const` vs `const int* const`
- [ ] `const int&` — why it's the default parameter style
- [ ] Top-level const vs low-level const
- [ ] East const vs West const style
- [ ] Passing by value vs by reference vs by pointer — tradeoffs
- [ ] Return by reference: when safe, when dangerous
- [ ] Pointer arithmetic basics
**Teaching Milestone:** Read a complex function signature and explain what each parameter accepts and whether it can be modified.
## Phase 3: Ownership & Smart Pointers
- [ ] Raw pointer ownership problems (leaks, double free, dangling)
- [ ] RAII principle
- [ ] `std::unique_ptr` — exclusive ownership
- [ ] `std::shared_ptr` — shared ownership and reference counting
- [ ] `std::weak_ptr` — breaking cycles
- [ ] Choosing the right smart pointer (decision tree)
- [ ] `std::make_unique` / `std::make_shared` — why prefer them
**Teaching Milestone:** Refactor a raw-pointer design to use smart pointers and explain your choices.
## Phase 4: Move Semantics & Advanced References
- [ ] Lvalue vs rvalue — what are they really
- [ ] Lvalue references (`T&`) vs rvalue references (`T&&`)
- [ ] `std::move` — what it does and doesn't do
- [ ] Move constructors and move assignment
- [ ] Reference collapsing rules
- [ ] Forwarding references (`T&&` in templates)
- [ ] `std::forward` — perfect forwarding
- [ ] Common interview traps and trick questions
**Teaching Milestone:** Explain why `std::move` doesn't actually move anything.
---
## Resources
- cppreference.com — authoritative reference
- "Effective Modern C++" by Scott Meyers — Items 1-6, 23-26
- Herb Sutter's GotW articles on const correctness
## Success Criteria
- Can read any declaration with `*`, `&`, `const` and explain it
- Can choose between pointer, reference, and smart pointer for any use case
- Can explain move semantics without hand-waving
- Can spot pointer/reference bugs in code review
+9
View File
@@ -31,3 +31,12 @@ code
## Self-Improvement
Periodically review this file and suggest improvements to the user if you notice gaps, inconsistencies, or missing conventions.
## Active Context
<!-- AI assistant maintains this section. Keep under 20 lines. -->
<!-- Updated automatically by /self-improve. Remove stale entries. -->
- Branch: `master`, 1 commit ahead of origin (unpushed)
- Untracked files: `org/cpp/dsa/` and `org/cpp/ufds.org` (not yet committed)
- Current work: UFDS flashcards (402-line proper card set) + DSA subdirectory
- Inbox items: binary search, `using` keyword — need cards created
- Possible cleanup: `org/cpp/dsa/udfs.org` may be a superseded draft of `org/cpp/ufds.org`
+46
View File
@@ -0,0 +1,46 @@
# Project Learnings
> Auto-maintained by the self-improve skill. Read at session start, updated at session end.
## Patterns That Work
<!-- Approaches that produced good results -->
## Mistakes to Avoid
<!-- Failed approaches and why they failed -->
## Codebase Conventions
**[2026-05-04] — File organization**
- Observation: Most flashcard files are flat in `org/cpp/`, but subdirectories exist (`tricks/`, `dsa/`) for topic grouping. AGENTS.md only documents the flat convention.
- Action: When creating new cards, use flat `org/cpp/topic.org` for STL/language features; subdirectories for broader categories (DSA, tricks). Propose updating AGENTS.md if this solidifies.
- Confidence: medium
**[2026-05-04] — Card format variance**
- Observation: `org/cpp/dsa/udfs.org` uses raw `#+title:` + code blocks without ANKI properties. `org/cpp/ufds.org` follows the proper Anki card format. The proper format (with ANKI_NOTE_TYPE, Front/Back sections) is what gets exported.
- Action: Always use the full Anki card format from AGENTS.md when creating flashcards. Raw code files in dsa/ may be scratch/reference, not export-ready cards.
- Confidence: medium
**[2026-05-04] — Naming: UFDS not UDFS**
- Observation: `org/cpp/dsa/udfs.org` is a typo — the data structure is "Union-Find Disjoint Set" = UFDS. The properly-formatted file `org/cpp/ufds.org` uses the correct name.
- Action: Use "ufds" spelling. The dsa/udfs.org appears to be an earlier draft.
- Confidence: high
## Environment & Config
**[2026-05-04] — Git state**
- Observation: Single branch `master` with remote `origin/master`. No branching workflow — commits go directly to master.
- Action: Commit directly to master. Push when work is complete.
- Confidence: high
## Business Context
**[2026-05-04] — Study focus**
- Observation: Recent commits cover STL containers (deque, array, set, map, iterators) and DSA (UFDS). Inbox has LeetCode solutions (two sum, max consecutive ones) with notes to learn binary search and `using` keyword.
- Action: Current study trajectory is STL containers + competitive programming DSA. Prioritize cards for these topics.
- Confidence: high
## Open Questions
**[2026-05-04] — Duplicate UFDS files**
- Question: Are both `org/cpp/ufds.org` (402 lines, proper format) and `org/cpp/dsa/udfs.org` (41 lines, raw code) needed? The former seems to supersede the latter.
- Action: Ask user if `dsa/udfs.org` should be removed or merged.
+16
View File
@@ -81,3 +81,19 @@ public:
}
};
#+end_src
* TODO study: why min doesn't work with Fenwick tree (no inverse, update breaks on increase)
* Binary search
#+begin_src python
def bs(a, x, l, r):
if l >= r:
return l
m = l + (r-l) // 2
if (a[m] < x):
return bs(a, x, m+1, r);
else:
return bs(a, x, l, m)
#+end_src
+109
View File
@@ -0,0 +1,109 @@
#+title: C++ Containers
* Container Concept: Common Interface :containers:concept:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504002
:END:
** Front
What interface is shared by all C++ standard containers?
** Back
- *Element access*: ~at~, ~operator[]~, ~front~, ~back~, ~data~
- *Iterators*: ~begin~, ~end~, ~cbegin~, ~cend~, ~rbegin~, ~rend~
- *Capacity*: ~empty~, ~size~, ~max_size~
- *Modifiers*: ~clear~, ~insert~, ~erase~, ~push_back~, ~pop_back~, ~resize~, ~swap~
* Do C++ containers inherit from a common base? :containers:concept:inheritance:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504003
:END:
** Front
Do C++ containers (vector, list, map, etc.) inherit from a common base class?
** Back
No. Standard containers are standalone classes. Shared interfaces come from conventions and C++20 concepts (~Container~, ~SequenceContainer~), not inheritance.
* Sequence Containers :containers:sequence:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504004
:END:
** Front
What are the C++ sequence containers?
** Back
~vector~, ~deque~, ~list~, ~forward_list~, ~array~
Sequence containers maintain insertion order.
* Associative Containers :containers:associative:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504005
:END:
** Front
What are the C++ associative containers?
** Back
- *Ordered*: ~set~, ~multiset~, ~map~, ~multimap~
- *Unordered*: ~unordered_set~, ~unordered_multiset~, ~unordered_map~, ~unordered_multimap~
* Container Adapters :containers:adapter:
:PROPERTIES:
:ANKI_NOTE_ID: 1777826871054
:END:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504006
** Front
What are the C++ container adapters?
** Back
~stack~, ~queue~, ~priority_queue~
Adapters provide a restricted interface (e.g., LIFO for stack, FIFO for queue).
* What does vector have that other containers don't? :containers:vector:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504007
:END:
** Front
Which methods does ~vector~ have that other containers typically don't?
** Back
~reserve~, ~capacity~, ~shrink_to_fit~
These manage the internal memory buffer.
* Unique methods on sequence containers (list, deque) :containers:sequence:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504008
:END:
** Front
Which sequence containers support ~push_front~ and ~pop_front~?
** Back
~deque~ and ~list~ (not ~vector~)
These containers support insertion/removal at both ends.
* Unique methods on associative containers :containers:associative:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504009
:END:
** Front
What tree-based methods do ordered associative containers have?
** Back
~find~, ~count~, ~lower_bound~, ~upper_bound~, ~equal_range~
These use tree traversal (BST behind the scenes).
* Unique methods on unordered containers :containers:unordered:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504010
:END:
** Front
What hash-specific methods do unordered containers have?
** Back
~bucket_count~, ~load_factor~, ~max_load_factor~, ~rehash~, ~reserve~
These manage hash table internals.
+30 -1
View File
@@ -1,6 +1,7 @@
* Task: construct an empty deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650432
:END:
** Front
Write how to create an empty deque of ints
@@ -12,6 +13,7 @@ std::deque<int> d;
* Task: construct deque with 5 copies of value :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650427
:END:
** Front
Write how to create a deque with 5 elements, all initialized to 42
@@ -23,6 +25,7 @@ std::deque<int> d(5, 42); // {42, 42, 42, 42, 42}
* Task: construct deque from initializer list :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650422
:END:
** Front
Write how to create a deque initialized with {1, 2, 3}
@@ -34,6 +37,7 @@ std::deque<int> d = {1, 2, 3};
* Task: construct deque from a range :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650418
:END:
** Front
Write how to create a deque from a vector's begin/end iterators
@@ -46,6 +50,7 @@ std::deque<int> d(v.begin(), v.end());
* Task: access element at index with bounds checking :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650414
:END:
** Front
Write how to safely access element at index 2 in a deque (throws on invalid index)
@@ -58,6 +63,7 @@ int x = d.at(2); // returns 30, throws std::out_of_range if invalid
* Task: access element at index with operator[] :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650409
:END:
** Front
Write how to access element at index 1 using operator[]
@@ -70,6 +76,7 @@ int x = d[1]; // returns 20, no bounds check
* Task: access first and last elements :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650404
:END:
** Front
Write how to get the first and last element of a deque
@@ -83,6 +90,7 @@ int last = d.back(); // 30
* Task: iterate over deque with begin/end :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650399
:END:
** Front
Write a range-based for loop over a deque using iterators
@@ -99,6 +107,7 @@ for (int x : d) { std::cout << x << " "; }
* Task: reverse iterate over deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650395
:END:
** Front
Write how to iterate backwards through a deque using reverse iterators
@@ -113,6 +122,7 @@ for (auto it = d.rbegin(); it != d.rend(); ++it) {
* Task: check if deque is empty :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650391
:END:
** Front
Write how to check if a deque is empty
@@ -125,6 +135,7 @@ if (d.empty()) { /* ... */ }
* Task: get size of deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650387
:END:
** Front
Write how to get the number of elements in a deque
@@ -137,6 +148,7 @@ std::size_t n = d.size(); // returns 3
* Task: clear all elements from deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650383
:END:
** Front
Write how to remove all elements from a deque
@@ -149,6 +161,7 @@ d.clear(); // size becomes 0
* Task: push elements to back of deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650379
:END:
** Front
Write how to add elements 10, 20, 30 to the back of a deque
@@ -163,6 +176,7 @@ d.push_back(30);
* Task: push elements to front of deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650375
:END:
** Front
Write how to add elements 30, 20, 10 to the front of a deque (pushing in that order)
@@ -177,6 +191,7 @@ d.push_front(10); // {10, 20, 30}
* Task: pop elements from back of deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650371
:END:
** Front
Write how to remove the last element from a deque
@@ -189,6 +204,7 @@ d.pop_back(); // {1, 2}
* Task: pop elements from front of deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650367
:END:
** Front
Write how to remove the first element from a deque
@@ -201,6 +217,7 @@ d.pop_front(); // {2, 3}
* Task: emplace back into deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650363
:END:
** Front
Write how to construct a pair in-place at the back of a deque (without copying)
@@ -213,6 +230,7 @@ d.emplace_back(1, 2); // constructs pair{1,2} directly
* Task: emplace front into deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650359
:END:
** Front
Write how to construct an element in-place at the front of a deque
@@ -225,6 +243,7 @@ d.emplace_front("hello"); // constructs string directly at front
* Task: resize deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650354
:END:
** Front
Write how to resize a deque to 10 elements, filling new spots with 0
@@ -238,6 +257,7 @@ d.resize(5, 42); // resize to 5, filling with 42 if growing
* Task: insert element at position :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650349
:END:
** Front
Write how to insert value 99 at the beginning of a deque
@@ -250,6 +270,7 @@ d.insert(d.begin(), 99); // {99, 1, 2, 3}
* Task: insert multiple copies :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650344
:END:
** Front
Write how to insert 5 copies of value 42 starting at position begin
@@ -262,6 +283,7 @@ d.insert(d.begin(), 5, 42); // {42, 42, 42, 42, 42, 1, 2, 3}
* Task: insert range into deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650339
:END:
** Front
Write how to insert all elements from a vector into a deque at position begin
@@ -275,6 +297,7 @@ d.insert(d.begin(), v.begin(), v.end()); // {10, 20, 1, 2, 3}
* Task: erase single element :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650335
:END:
** Front
Write how to erase the element at position begin + 1 in a deque
@@ -287,6 +310,7 @@ d.erase(d.begin() + 1); // {1, 3}
* Task: erase range of elements :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650331
:END:
** Front
Write how to erase elements from begin to begin+2 (2 elements) in a deque
@@ -299,6 +323,7 @@ d.erase(d.begin(), d.begin() + 2); // {3, 4, 5}
* Task: swap two deques :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650326
:END:
** Front
Write how to swap the contents of two deques
@@ -312,6 +337,7 @@ d1.swap(d2); // d1 = {4,5,6}, d2 = {1,2,3}
* Task: emplace at specific position :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650322
:END:
** Front
Write how to construct a pair in-place at position begin of a deque
@@ -324,6 +350,7 @@ d.emplace(d.begin(), 1, 2); // constructs pair{1,2} at begin
* Task: assign new contents to deque :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650318
:END:
** Front
Write how to replace all contents of a deque with 5 copies of 99
@@ -336,6 +363,7 @@ d.assign(5, 99); // {99, 99, 99, 99, 99}
* Task: assign from range :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650314
:END:
** Front
Write how to replace deque contents with all elements from a vector
@@ -349,6 +377,7 @@ d.assign(v.begin(), v.end()); // {10, 20, 30}
* Task: use deque as FIFO queue :cpp:deque:production:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 1777825650305
:END:
** Front
Write how to use deque as a FIFO queue (enqueue 1, 2, 3 then dequeue all)
@@ -364,4 +393,4 @@ while (!q.empty()) {
q.pop_front(); // dequeue
}
// Output: 1 2 3
#+end_src
#+end_src
+41
View File
@@ -0,0 +1,41 @@
#+title: Udfs
* impl
#+begin_src cpp
#include <vector>
#include <numeric>
#include <algorithm>
class Ufs {
private:
std::vector<int> p;
std::vector<int> s;
std::vector<int> r;
public:
Ufds(int n) {
p.resize(n);
std::iota(p.begin(), p.end(), 0);
s.assign(n, 1); // test on equality with the assign fill and iota explain the difference
std::fill(r.begin(),r.end(),0);
numSets = n;
}
~Ufds() {}
~Ufds() = default;
int find(int x) {
if (p[x] == x) return x;
return p[x] = find(p[x]);
}
int find(int x) {
int px = p[x];
if (px != x) {
px = find(px);
}
p[x] = px;
return px;
}
};
#+end_src
should ask what new returns? delete? what happens? why are we deleting it ? heap allocated?
should show equivalent version of using std::array to vector
should ask what resize does?
what about a true dynammic version where we create it upon calling find()
+3
View File
@@ -44,6 +44,9 @@ What are the four required operations for a C++ iterator?
** Front
List the five iterator categories in C++ (from simplest to most advanced)
** Back
:PROPERTIES:
:ANKI_NOTE_ID: 1777826854918
:END:
1. Input Iterator — read values, advance once
2. Output Iterator — write values, advance once
3. Forward Iterator — read/write, multiple passes
+17
View File
@@ -0,0 +1,17 @@
#+title: C++ Tricks
* Rotate String Check :string:trick:search:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:ANKI_NOTE_ID: 20260504001
:END:
** Front
How to check if string ~s~ can be rotated to match string ~goal~ in O(n)?
** Back
#+begin_src c++
bool rotateString(string s, string goal) {
return s.size() == goal.size() && (s + s).find(goal) != string::npos;
}
#+end_src
If ~goal~ is a substring of ~s + s~, then ~s~ can be rotated to match ~goal~.
+402
View File
@@ -0,0 +1,402 @@
* Ufds: Union-Find Disjoint Set :cpp:datastructure:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Write a minimal Ufds class with vector parent, constructor initializing parents to self
** Back
#+begin_src c++
#include <vector>
#include <numeric>
class Ufds {
private:
std::vector<int> parent;
public:
Ufds(int n) : parent(n) {
std::iota(parent.begin(), parent.end(), 0);
}
};
#+end_src
* Ufds find() with path compression :cpp:datastructure:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Write the ~find()~ method for Ufds with path compression
** Back
#+begin_src c++
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
#+end_src
* Concise Ufds find() :cpp:datastructure:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Is this correct? What does it do?
#+begin_src c++
int find(int x) {
if (parent[x] == x) return x;
return parent[x] = find(parent[x]);
}
#+end_src
** Back
Yes, correct and equivalent to the longer version.
- Base case: if ~x~ is its own parent, return ~x~
- Recursive case: find root of parent, then assign and return
The assignment ~parent[x] = find(parent[x])~ returns the result while compressing the path.
* Ufds constructor: initializer list vs body :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What is the difference between these two Ufds constructors?
#+begin_src c++
// Initializer list
Ufds(int n) : parent(n) {
std::iota(parent.begin(), parent.end(), 0);
}
// Body style
Ufds(int n) {
parent.resize(n);
std::iota(parent.begin(), parent.end(), 0);
}
#+end_src
** Back
Initializer list: direct constructs ~parent~ with size ~n~ (one allocation)
Body style: default constructs ~parent~, then resize (potential double allocation)
Both produce same result, initializer list is more efficient.
* What does std::vector::resize do? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What does ~resize(n)~ do on a ~std::vector<int>~?
** Back
Resizes the vector to have ~n~ elements:
- If ~n~ > current size: adds elements (value-initialized, usually 0)
- If ~n~ < current size: truncates the vector
- Reallocates if capacity < n
#+begin_src c++
std::vector<int> v;
v.resize(5); // v = {0, 0, 0, 0, 0}
#+end_src
* Ufds destructor: when to omit :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Should Ufds have a destructor? ~Ufds() {}~ vs ~Ufds() = default~
** Back
Omit entirely if only using ~std::vector~ (auto-cleanup)
If you must write one, prefer ~= default~ to explicitly show intent
Empty ~{}~ works but ~= default~ is clearer intent for future maintenance
* Correct Ufds constructor: is this correct? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Is this correct?
#+begin_src c++
Ufds(int n) {
p.resize(n);
std::iota(p.begin(), p.end(), 0);
}
#+end_src
** Back
Yes, correct. This is the body-style initialization.
One potential issue: ~p~ is default-constructed first, then ~resize~ may reallocate.
For efficiency, prefer initializer list: ~Ufds(int n) : p(n)~
* Incorrect Ufds constructor: correct or wrong? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Is this correct?
#+begin_src c++
std::vector<int> p;
Ufds(int n) {
p = new std::vector<int>(n);
}
#+end_src
** Back
Wrong.
~new std::vector<int>(n)~ returns a ~vector<int>*~ (pointer), but ~p~ is a ~vector<int>~ (not a pointer).
Can't assign a pointer to a vector.
* What does new return? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What does ~new Type~ return? And ~new Type[n]~?
** Back
~new Type~ returns a pointer to that type: ~Type*~
~new Type[n]~ returns a pointer to an array: ~Type*~
#+begin_src c++
int* p1 = new int; // single int
int* p2 = new int[10]; // array of 10 ints
#+end_src
* What does delete do? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What does ~delete~ do? ~delete[]~?
** Back
~delete~ frees a single object allocated with ~new~
~delete[]~ frees an array allocated with ~new[]~
#+begin_src c++
int* p1 = new int;
int* p2 = new int[10];
delete p1; // free single object
delete[] p2; // free array
#+end_src
Mismatching ~delete~ with ~new[]~ causes undefined behavior.
* Is this correct Ufds with C-style array? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Is this correct?
#+begin_src c++
class Ufds {
private:
int* parent;
public:
Ufds(int n) : parent(new int[n]) {}
~Ufds() { delete[] parent; }
};
#+end_src
** Back
Correct. This manually manages the heap-allocated array.
- ~new int[n]~ allocates array of ~n~ ints on heap
- ~delete[] parent~ in destructor frees it
This works but requires manual cleanup — error prone compared to ~std::vector~.
* Equivalent std::array version: correct or wrong? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Is this correct?
#+begin_src c++
#include <array>
#include <numeric>
class Ufds {
private:
std::array<int, 10> parent;
public:
Ufds() {
std::iota(parent.begin(), parent.end(), 0);
}
};
#+end_src
** Back
Correct for compile-time fixed size.
But size ~10~ is hardcoded — not parameterized.
~std::array<T, N>~ requires ~N~ be a compile-time constant.
For runtime size, must use ~std::vector~.
* std::array vs vector vs C-style array :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Compare ~std::array<T, N>~, ~std::vector<T>~, and ~T*~ for Ufds parent array
** Back
| Type | Size | Cleanup | Use when |
|------------------+--------------+-------------------+----------------------------|
| ~std::array<T, N>~ | compile-time | automatic | size known at compile time |
| ~std::vector<T>~ | runtime | automatic | size known at runtime |
| ~T* p = new T[n]~ | runtime | manual (~delete[]~) | legacy code only |
* Vector vs Array: is vector backed by array? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Is ~std::vector~ backed by an array? How does it support variable length?
** Back
Yes, ~std::vector~ allocates a contiguous memory block (like a heap array).
Typical implementation: three pointers
- ~start~ — pointer to data
- ~finish~ — pointer to last element
- ~end_of_storage~ — pointer to end of allocated capacity
#+begin_src c++
vector<int> v(5);
v.push_back(1); // may trigger reallocation if capacity exceeded
#+end_src
When capacity is exceeded, vector: allocates larger block, copies elements, deallocates old block.
* Vector size vs capacity :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What is the difference between ~size()~ and ~capacity()~ in ~std::vector~?
** Back
- ~size()~ — number of actual elements stored
- ~capacity()~ — allocated storage space
#+begin_src c++
std::vector<int> v(3); // size=3, capacity=3
v.push_back(1); // size=4, capacity may be >=4
v.push_back(1); // size=5, capacity may be >=5
#+end_src
~reserve(n)~ pre-allocates capacity without resizing.
* Comparison: which Ufds storage to use? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
When would you choose ~std::array~, ~std::vector~, or ~T* new~ for Ufds parent array?
** Back
~std::array<T, N>~ — only if N is known at compile time
~std::vector<T>~ — if size is determined at runtime (typical Ufds case)
~T* new T[n]~ — legacy code only; ~vector~ is safer and equivalent performance
For Ufds: ~std::vector~ is the idiomatic choice because size is runtime-determined.
* C-style array key properties :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What are key properties of C-style arrays (raw arrays)?
** Back
#+begin_src c++
int arr[5]; // fixed size, stack-allocated
int* p = new int[n]; // dynamic size, heap-allocated
#+end_src
- Decay to pointer on function call (lose size info)
- No bounds checking
- ~sizeof(arr)~ gives bytes, not element count
- Must manage lifetime manually for heap arrays
C-style arrays in C++ are generally avoided in favor of ~std::array~/~std::vector~.
* std::iota for Ufds initialization :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
How to use ~std::iota~ to initialize Ufds parent array to ~[0, 1, 2, ..., n-1]~
** Back
#+begin_src c++
#include <numeric>
std::vector<int> parent(n);
std::iota(parent.begin(), parent.end(), 0);
#+end_src
~iota~ fills the range with consecutive values starting from given start value
* Path compression in find() :cpp:datastructure:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What does "path compression" mean in ~find()~?
** Back
After finding the root, point each visited node directly to the root:
#+begin_src c++
parent[x] = find(parent[x]); // compresses path
#+end_src
This flattens the tree structure, making future ~find()~ calls O(α(n)) ≈ O(1)
* Original Ufds mistakes: is this correct? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Is this correct?
#+begin_src c++
class Udfs {
int parent[];
vector<int> p;
public:
Ufds(int n) {
p = new vector<int>;
}
~Ufds() {
p ? free p;
}
};
#+end_src
** Back
Wrong. Multiple errors:
1. ~int parent[]~ — illegal incomplete array type
2. ~Ufds(int n)~ constructor but class is ~Udfs~ (typo)
3. ~p = new vector<int>~ — can't assign pointer to vector
4. ~free p~ — can't free a vector, and ~p~ is not a pointer
5. ~Ufds~ destructor but class is ~Udfs~ (typo)
* Heap allocation: what and why? :cpp:cpp:
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What does "heap allocated" mean? Why use it?
** Back
Heap allocation with ~new~ lives until ~delete~ is called or program ends.
Stack allocation (local variables) is自动 freed when out of scope.
#+begin_src c++
int arr[10]; // stack — freed when function returns
int* p = new int[10]; // heap — lives until delete[]
#+end_src
Heap needed when: size unknown at compile time, or object must outlive scope.
+77
View File
@@ -0,0 +1,77 @@
#+title: Bit Tree
* binary indexd tree
#+begin_src python :results output
class BinaryIndexedTree:
def __init__(self, n: int):
self.tree = [0] * (n + 1)
def update(self, i: int, delta: int) -> None:
"""Add delta to element at index i (0-based)."""
idx = i + 1
while idx < len(self.tree):
self.tree[idx] += delta
idx += idx & (-idx)
def query(self, i: int) -> int:
"""Return prefix sum from 0 to i (0-based)."""
idx = i + 1
s = 0
while idx > 0:
s += self.tree[idx]
idx -= idx & (-idx)
return s
def range_query(self, l: int, r: int) -> int:
"""Return sum from l to r (0-based, inclusive)."""
if l == 0:
return self.query(r)
return self.query(r) - self.query(l - 1)
#+end_src
- update(i, delta) — adds delta to index i in O(log n)
- query(i) — prefix sum [0..i] in O(log n)
- range_query(l, r) — sum [l..r] in O(log n)
* min bit tree
That's a min-Fenwick (prefix minimum). Unlike sum-Fenwick, updates are point-set (not add-delta), and increasing a value is expensive — you'd need a segment tree for that.
#+begin_src python
import math
class MinFenwick:
def __init__(self, n: int, inf: int = math.inf):
self.n = n
self.inf = inf
self.tree = [inf] * (n + 1)
self.arr = [inf] * n # track actual values for rebuild
def update(self, i: int, val: int) -> None:
"""Set element at index i to val (0-based)."""
self.arr[i] = val
idx = i + 1
while idx <= self.n:
# rebuild this node from its covered range
lo = idx - (idx & (-idx)) # 0-based: lo
hi = idx - 1 # 0-based: hi
self.tree[idx] = min(self.arr[j] for j in range(lo, hi + 1))
idx += idx & (-idx)
def query(self, i: int) -> int:
"""Return min from 0 to i (0-based, inclusive)."""
idx = i + 1
res = self.inf
while idx > 0:
res = min(res, self.tree[idx])
idx -= idx & (-idx)
return res
#+end_src
- update(i, val) — set index i to val, O(log n · span) where span is the size of the covered range
- query(i) — prefix min [0..i], O(log n)
Caveat: If you need to increase a value (removing a minimum from consideration), this rebuilds the range each time — worst case O(n). For full range-min with arbitrary increases/decreases, use a segment tree instead.
* why doesn't min work with BIT?
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Why doesn't min work well with Fenwick/Bit trees? What property does addition have that min lacks?
** Back
#+begin_quote
Fenwick trees require an =invertible= operation (a group). Addition is invertible via subtraction: range_query(l,r) = prefix(r) - prefix(l-1). Min has no inverse — you can't "subtract out" the minimum of [0..l-1] from prefix(r) to get [l..r].
Additionally, min's =update= breaks on value increases. Decreasing a value works fine (like sum's decrease), but increasing a value means the old minimum might have been removed, and you'd need to scan all elements in the node's covered range to find the new minimum. This makes updates O(span) instead of O(log n).
Good Fenwick operations are associative, have an identity element, and are incrementally updateable: sum, xor, gcd. Min/max work for prefix queries only, not range queries.
#+end_quote
+763
View File
@@ -0,0 +1,763 @@
#+title: Subarrays with Sum Equal to K
* Subarray Sum Equals K - Brute Force [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/subarray-sum-equals-k/description/][LC 560]]. Find the total number of continuous subarrays whose sum equals k. Solve using the brute force approach.
Example: nums = [1,2,3], k = 3 → Output: 2 ([1,2] and [3])
** Back
#+begin_src c++
int subarraySum(vector<int>& nums, int k) {
int count = 0;
for (int i = 0; i < nums.size(); i++) {
int sum = 0;
for (int j = i; j < nums.size(); j++) {
sum += nums[j];
if (sum == k) count++;
}
}
return count;
}
#+end_src
Time: O(n^2), Space: O(1)
* Subarray Sum Equals K - Prefix Sum + Hash Map [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/subarray-sum-equals-k/description/][LC 560]]. Find the total number of continuous subarrays whose sum equals k. Solve optimally using prefix sum with a hash map.
Example: nums = [1,2,3], k = 3 → Output: 2
** Back
#+begin_src c++
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> prefixCount;
prefixCount[0] = 1; // base case: sum of 0 appears once
int sum = 0, count = 0;
for (int num : nums) {
sum += num;
// If (sum - k) exists, those prefixes form valid subarrays
count += prefixCount[sum - k];
prefixCount[sum]++;
}
return count;
}
#+end_src
Time: O(n), Space: O(n)
* Subarray Sum Equals K - Why prefixCount[0] = 1 [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/subarray-sum-equals-k/description/][LC 560]]. In the optimal subarray sum solution (prefix sum + hash map), why is `prefixCount[0] = 1` initialized?
** Back
It handles the case where a subarray starts from index 0 and its sum equals k.
When sum == k at some index, we look up prefixCount[sum - k] = prefixCount[0].
Without the initialization, this lookup would return 0, missing valid subarrays like [3] where k = 3.
The base case means: "a prefix sum of 0 exists once (before any element)" — conceptually the empty prefix.
* Subarray Sum Divisible by K [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/subarray-sums-divisible-by-k/description/][LC 974]]. Find the total number of continuous subarrays whose sum is divisible by k.
Example: nums = [23,2,4,6,7], k = 6 → Output: 2 ([2,4] and [7])
** Back
#+begin_src c++
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int,int> prefixCount;
prefixCount[0] = 1;
int sum = 0, count = 0;
for (int num : nums) {
sum += num;
// Modulo can be negative in C++, fix with ((sum % k) + k) % k
int remainder = ((sum % k) + k) % k;
count += prefixCount[remainder];
prefixCount[remainder]++;
}
return count;
}
#+end_src
Key insight: If two prefix sums have the same remainder mod k, their subarray sum is divisible by k.
Time: O(n), Space: O(min(n, k))
* Longest Subarray Sum Equals K [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/maximum-size-subarray-sum-equals-k/description/][LC 325]]. Find the maximum length of a contiguous subarray that sums to k.
Example: nums = [1,-1,5,-2,3], k = 3 → Output: 4 ([1,-1,5,-2])
** Back
#+begin_src c++
int maxSubArrayLen(vector<int>& nums, int k) {
unordered_map<int,int> firstOccurrence;
firstOccurrence[0] = 0; // sum 0 at index 0 (1-based)
int sum = 0, maxLen = 0;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
// Check if subarray ending here sums to k
if (firstOccurrence.count(sum - k)) {
maxLen = max(maxLen, i + 1 - firstOccurrence[sum - k]);
}
// Store first occurrence only (for longest)
if (!firstOccurrence.count(sum)) {
firstOccurrence[sum] = i + 1;
}
}
return maxLen;
}
#+end_src
Key difference from counting: store only the *first* occurrence of each prefix sum to maximize length.
Time: O(n), Space: O(n)
* Subarray Sum Equals K - Negative Numbers? [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/subarray-sum-equals-k/description/][LC 560]]. Does the prefix sum + hash map approach for "subarray sum equals k" work when the array contains negative numbers? Why?
** Back
Yes, it works with negative numbers.
The prefix sum approach does NOT require positive-only elements. It works for any integer array because:
- prefix[j] - prefix[i] = sum[i+1..j] is always true regardless of sign
- The hash map tracks ALL prefix sums seen, positive or negative
- We only need (sum - k) to exist in the map
This makes it superior to the sliding window approach, which only works for non-negative arrays.
* Subarray Sum Equals K - Sliding Window Limitation [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/subarray-sum-equals-k/description/][LC 560]]. Can you use the sliding window technique to solve "subarray sum equals k"? When does it work?
** Back
Sliding window ONLY works when all elements are non-negative (positive or zero).
When all elements >= 0:
- Expanding the window increases the sum
- Shrinking the window decreases the sum
- This monotonicity lets us adjust the window
Sliding window fails with negative numbers because adding/removing elements doesn't monotonically change the sum.
Prefer prefix sum + hash map — it works for all cases (positive, negative, zero).
* Subarray Sum Variations — Master Table [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What's the master table of subarray sum variations and their approaches?
** Back
| Question | Constraint | Approach | Key Data Structure | Time |
|----------|-----------|----------|-------------------|------|
| Count subarrays sum = K | Any integers | Prefix sum | unordered_map<sum, frequency> | O(n) |
| Longest subarray sum = K | Any integers | Prefix sum | unordered_map<sum, first_index> | O(n) |
| Shortest subarray sum ≥ K | Any integers | Prefix sum + monotonic deque | deque of indices | O(n) |
| Shortest subarray sum = K | Any integers | Prefix sum + ordered map | map<sum, last_index> | O(n log n) |
| Divisible by K | Any integers | Prefix sum + modulo | unordered_map<remainder, freq> | O(n) |
| Sum in range [lower, upper] | Any integers | Prefix sum + BST | multiset / Fenwick / segment tree | O(n log n) |
| Max circular subarray sum | Any integers | Kadane's + total - min_subarray | 2x Kadane | O(n) |
| Subarray sum with only positives | Positives only | Sliding window | Two pointers | O(n) |
| 2D matrix subarray sum = K | 2D grid | Fix rows, compress to 1D | Prefix sum on compressed row | O(n^3) |
| At most K distinct elements | Frequency constraint | Sliding window + freq map | Hash map of counts | O(n) |
| Subarray with exactly K ones | Binary array | Store indices of 1s | Vector of positions | O(n) |
| Two non-overlapping subarrays each = K | Any integers | Prefix sum + track both sides | 2 pass with prefix map | O(n) |
Core pattern: prefix[j] - prefix[i] = subarray(i+1..j). The variation changes what you store and query.
* Subarray Shortest Sum ≥ K — Monotonic Deque [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/description/][LC 862]]. Find the length of the shortest contiguous subarray with sum ≥ K. Works with negative numbers.
Example: nums = [84,-37,32,40,95], K = 167 → Output: 3
** Back
#+begin_src c++
int shortestSubarray(vector<int>& nums, int k) {
int n = nums.size();
vector<long long> prefix(n + 1, 0);
for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
deque<int> dq; // stores indices, prefix[dq[j]] is increasing
int minLen = INT_MAX;
for (int i = 0; i <= n; i++) {
// If prefix[i] - prefix[dq.front()] >= K, pop front and record length
while (!dq.empty() && prefix[i] - prefix[dq.front()] >= k) {
minLen = min(minLen, i - dq.front());
dq.pop_front();
}
// Maintain monotonic increasing order of prefix values in deque
while (!dq.empty() && prefix[i] < prefix[dq.back()]) {
dq.pop_back();
}
dq.push_back(i);
}
return minLen == INT_MAX ? -1 : minLen;
}
#+end_src
Time: O(n), Space: O(n)
Key insight: Maintain a deque of indices where prefix sums are increasing.
If prefix[i] - prefix[dq.front()] ≥ K, then any index after dq.front() gives a shorter valid subarray.
* Subarray Sum in Range [lower, upper] [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/count-of-range-sum/description/][LC 327]]. Count the number of subarrays whose sum lies in range [lower, upper].
Example: nums = [-2,5,-1], lower = -2, upper = 2 → Output: 3 ([-2], [-1], [5,-1])
** Back
#+begin_src c++
int countRangeSum(vector<int>& nums, int lower, int upper) {
int n = nums.size();
vector<long long> prefix(n + 1, 0);
for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
// Use merge sort to count valid pairs
return mergeCount(prefix, 0, n, lower, upper);
}
int mergeCount(vector<long long>& P, int lo, int hi, int lower, int upper) {
if (lo >= hi - 1) return 0;
int mid = lo + (hi - lo) / 2;
int count = mergeCount(P, lo, mid, lower, upper)
+ mergeCount(P, mid, hi, lower, upper);
// Count valid (i, j) pairs where P[j] - P[i] in [lower, upper]
int j = mid, k = mid, t = mid;
for (int i = lo; i < mid; i++) {
while (k <= hi && P[k] - P[i] < lower) k++;
while (t <= hi && P[t] - P[i] <= upper) t++;
count += (t - k);
}
// Merge step
vector<long long> temp;
int p1 = lo, p2 = mid;
while (p1 < mid && p2 < hi) {
if (P[p1] <= P[p2]) temp.push_back(P[p1++]);
else temp.push_back(P[p2++]);
}
while (p1 < mid) temp.push_back(P[p1++]);
while (p2 < hi) temp.push_back(P[p2++]);
for (int i = 0; i < (int)temp.size(); i++) P[lo + i] = temp[i];
return count;
}
#+end_src
Time: O(n log n), Space: O(n)
Why not hash map? Hash map can't count how many prefix sums fall in a range.
Merge sort counts (i, j) pairs where P[j] - P[i] ∈ [lower, upper] during the merge step.
* Max Circular Subarray Sum [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/maximum-sum-circular-subarray/description/][LC 918]]. Find the maximum sum of a non-empty subarray in a circular array.
Example: nums = [1,-2,3,-2] → Output: 3
Example: nums = [5,-3,5] → Output: 10 (wraps around: [5, 5])
** Back
#+begin_src c++
int maxSubarraySumCircular(vector<int>& nums) {
int total = 0, maxSum = nums[0], curMax = 0;
int minSum = nums[0], curMin = 0;
for (int num : nums) {
curMax = max(curMax + num, num);
maxSum = max(maxSum, curMax);
curMin = min(curMin + num, num);
minSum = min(minSum, curMin);
total += num;
}
// If all numbers are negative, maxSum is the answer (total - minSum = 0)
return maxSum < 0 ? maxSum : max(maxSum, total - minSum);
}
#+end_src
Time: O(n), Space: O(1)
Two cases:
1. Maximum subarray does NOT wrap — standard Kadane's (maxSum)
2. Maximum subarray DOES wrap — total - minSubarray (remove the minimum subarray from the middle)
Edge case: all negative → total - minSum = 0, which is wrong. Return maxSum instead.
* 2D Matrix Subarray Sum = K [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/number-of-submatrices-that-sum-to-target/description/][Submatrices Sum to Target]]. Given a 2D matrix, find the number of submatrices whose sum equals K.
Example: matrix = [[0,1,0],[1,1,1],[0,1,0]], K = 0 → Output: 4
** Back
#+begin_src c++
int numSubmatrixSumTarget(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int count = 0;
// Fix top and bottom rows, compress to 1D array
for (int r1 = 0; r1 < m; r1++) {
vector<int> colSum(n, 0);
for (int r2 = r1; r2 < m; r2++) {
// Add row r2 to the compressed column sums
for (int c = 0; c < n; c++) {
colSum[c] += matrix[r2][c];
}
// Now solve 1D subarray sum = target
unordered_map<int,int> prefixCount;
prefixCount[0] = 1;
int sum = 0;
for (int val : colSum) {
sum += val;
count += prefixCount[sum - target];
prefixCount[sum]++;
}
}
}
return count;
}
#+end_src
Time: O(m^2 * n), Space: O(n)
Key idea: Fix two rows (r1, r2), compress columns between them into a 1D array.
Then the problem reduces to 1D subarray sum = K.
* Exactly K Distinct Elements with Sum = K [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the length of the longest subarray with at most K distinct elements and sum = K.
** Back
#+begin_src c++
int longestSubarray(vector<int>& nums, int k) {
unordered_map<int,int> freq;
int sum = 0, distinct = 0, maxLen = 0;
int left = 0;
for (int right = 0; right < nums.size(); right++) {
if (freq[nums[right]] == 0) distinct++;
freq[nums[right]]++;
sum += nums[right];
// Shrink while distinct > K or sum > k
while (distinct > k || sum > k) {
freq[nums[left]]--;
if (freq[nums[left]] == 0) distinct--;
sum -= nums[left];
left++;
}
if (sum == k) maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
#+end_src
Time: O(n), Space: O(min(n, K))
This combines sliding window (distinct elements constraint) with sum check.
The two constraints (distinct count + sum) make it trickier than simple sliding window.
* Binary Array — Exactly K Ones [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
See [[https://leetcode.com/problems/max-consecutive-ones-iii/description/][LC 1004]]. Given a binary array nums and an integer k, return the maximum number of consecutive 1's in the array if you can flip at most k 0's.
Example: nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2 → Output: 6
** Back
#+begin_src c++
int longestOnes(vector<int>& nums, int k) {
int left = 0, maxLen = 0;
for (int right = 0; right < nums.size(); right++) {
if (nums[right] == 0) k--;
while (k < 0) {
if (nums[left] == 0) k++;
left++;
}
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
#+end_src
Time: O(n), Space: O(1)
Sliding window: expand right, count zeros. When zeros exceed k, shrink from left.
* Two Non-Overlapping Subarrays Each Sum = K [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the maximum sum of two non-overlapping subarrays with lengths L and M.
Example: nums = [0,6,5,2,2,5,1,9,4], L = 1, M = 2 → Output: 20 ([9] and [6,5])
** Back
#+begin_src c++
int maxSumTwoNoOverlap(vector<int>& nums, int L, int M) {
int n = nums.size();
// Prefix sums for O(1) subarray sum queries
vector<int> prefix(n + 1, 0);
for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
auto sum = [&](int i, int j) { return prefix[j + 1] - prefix[i]; };
int maxL = 0, maxM = 0, result = 0;
// Case 1: L comes before M
for (int i = L + M; i <= n; i++) {
maxL = max(maxL, sum(i - L - M, i - M - 1));
maxM = max(maxM, sum(i - M, i - 1));
result = max(result, maxL + maxM);
}
// Case 2: M comes before L
maxL = 0; maxM = 0; result = 0;
for (int i = L + M; i <= n; i++) {
maxM = max(maxM, sum(i - L - M, i - L - 1));
maxL = max(maxL, sum(i - L, i - 1));
result = max(result, maxL + maxM);
}
return result;
}
#+end_src
Time: O(n), Space: O(n)
Two cases: L before M, or M before L.
Track the running maximum of the first subarray while computing the second.
* Subarray Sum — All Relationships Diagram [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What's the decision tree for choosing the right subarray sum approach?
** Back
1. **Are there negative numbers?**
→ YES: Prefix sum approach (hash map, BST, or merge sort)
→ NO: Sliding window is possible
2. **What are you counting?**
→ Count all: hash_map<sum, frequency>
→ Longest: hash_map<sum, first_index>
→ Shortest: hash_map<sum, last_index> or monotonic deque
3. **Is the target a range [lower, upper]?**
→ YES: Merge sort (count pairs in range) or Fenwick tree / BST
4. **Is it divisible by K?**
→ YES: hash_map<remainder, frequency> with modulo
5. **Is the array circular?**
→ YES: Kadane's twice — max(max_subarray, total - min_subarray)
6. **Is it 2D?**
→ YES: Fix 2 rows, compress to 1D, then prefix sum
7. **Are elements only 0/1?**
→ YES: Store indices of 1s, or sliding window counting zeros
8. **Are there constraints on distinct elements?**
→ YES: Sliding window + frequency map
9. **Are there multiple non-overlapping subarrays?**
→ YES: 2-pass — track running max of first while computing second
The fundamental identity: subarray(i,j) = prefix[j+1] - prefix[i].
Everything else is about what you do with the prefix array.
* Subarray Sum — Core Trigger Heuristic [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What is the most reliable heuristic for recognizing when to use Prefix Sums?
** Back
When you see **"subarray"** and **"sum"** in the same problem description, **Prefix Sums** should be your immediate first thought.
The core formula:
Sum(i..j) = PrefixSum[j] - PrefixSum[i-1]
This turns a **range query** into a **difference between two points**, eliminating the need to re-scan the array.
The mental trigger is simple: subarray + sum → prefix sums. Always.
* Subarray Sum — When to Use Sliding Window vs Prefix Sum [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
When should you use Sliding Window vs Prefix Sum + Hash Map for subarray sum problems?
** Back
| Condition | Use | Why |
|-----------|-----|-----|
| All numbers ≥ 0, need target sum or max length | **Sliding Window** | Monotonic: expanding always increases sum, shrinking always decreases. O(1) space vs O(n). |
| Numbers can be negative, need exact target sum | **Prefix Sum + Hash Map** | Monotonicity broken. Adding an element could make sum smaller. Must remember past states. |
| Frequent updates between queries | **Fenwick Tree / Segment Tree** | Prefix sum array takes O(n) to update. Trees give O(log n) update + query. |
Rule of thumb: check for negative numbers first. If none exist, sliding window wins.
* Subarray Product — Prefix Product Pattern [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
How does the prefix sum pattern translate to subarray **product** problems?
** Back
The prefix sum pattern translates directly into a **Prefix Product**:
Product(i..j) = PrefixProduct[j] / PrefixProduct[i-1]
Instead of subtracting, you divide.
Edge case: zeros break division. Handle by:
1. Segmentation — split the array at zeros, solve each segment independently
2. Track position of last seen zero to reset boundaries
Example: count subarrays with product < K (all positive):
#+begin_src c++
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if (k == 0) return 0;
int count = 0, product = 1, left = 0;
for (int right = 0; right < nums.size(); right++) {
product *= nums[right];
while (left <= right && product >= k) product /= nums[left++];
count += right - left + 1;
}
return count;
}
#+end_src
* Subarray with Equal 0s and 1s — Value Mapping [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the maximum length of a contiguous subarray with equal number of 0s and 1s.
Example: nums = [1,0,1] → Output: 2 ([1,0] or [0,1])
** Back
Treat 0 as -1 and 1 as +1. The problem becomes: **"Find the longest subarray whose sum equals 0."**
#+begin_src c++
int findMaxLength(vector<int>& nums) {
unordered_map<int,int> firstOccurrence;
firstOccurrence[0] = 0; // sum 0 at index 0 (1-based)
int sum = 0, maxLen = 0;
for (int i = 0; i < nums.size(); i++) {
sum += (nums[i] == 1) ? 1 : -1;
if (firstOccurrence.count(sum)) {
maxLen = max(maxLen, i + 1 - firstOccurrence[sum]);
} else {
firstOccurrence[sum] = i + 1;
}
}
return maxLen;
}
#+end_src
Time: O(n), Space: O(n)
This is the **"prefix state"** generalization: map values to +1/-1, then it's just prefix sum = 0.
* Subarray with Equal Odd and Even Numbers [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the longest subarray with equal number of odd and even numbers.
** Back
Map every even number to +1 and every odd number to -1 (or vice versa). The problem becomes:
**"Find the longest subarray whose sum equals 0."**
Same approach as equal 0s and 1s: prefix sum + hash map storing first occurrence.
This is the same "prefix state" generalization applied to a different domain.
* Multi-Category Balance — (A, B, C) Equal Counts [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the longest subarray with equal number of 'A's, 'B's, and 'C's.
** Back
You can't use a single scalar (+1/-1) for three categories. Instead, track **relative differences** between counts.
Maintain running counts: c_A, c_B, c_C.
At each step, compute the tuple of differences: (c_A - c_B, c_B - c_C).
If this tuple repeats later in the array, the elements between those indices have perfectly balanced A, B, C counts.
Data structure: hash map where the key is the state tuple:
map<pair<int,int>, int> firstOccurrence;
The tuple (diff_AB, diff_BC) captures the full relative state. If two positions share the same tuple, the subarray between them has zero net change in all three relative differences → equal counts.
Time: O(n), Space: O(n)
* Subarray Product Is Positive / Negative [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the maximum length of a subarray with a positive (or negative) product.
** Back
Map: positive → +1, negative → -1, zero → resets the window.
A subarray has positive product if it contains an **even** number of negatives.
A subarray has negative product if it contains an **odd** number of negatives.
Track the parity (odd/even count) of negative numbers as you traverse:
- If parity is even at index i and was even at index j (j < i), the subarray (j+1..i) has positive product.
- If parity is odd at index i and was even at index j, the subarray (j+1..i) has negative product.
Data structure: two hash maps (or arrays) — first occurrence of even-parity index and first occurrence of odd-parity index.
Time: O(n), Space: O(n)
* Bitwise Subarray — OR / AND / XOR [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Can you use prefix sums for subarray problems with Bitwise OR, AND, or XOR?
** Back
**XOR:** YES — XOR is invertible (XOR is its own inverse).
XOR(i..j) = PrefixXOR[j] ^ PrefixXOR[i-1]
Same hash map pattern as prefix sum.
**OR / AND:** NO — not invertible. You cannot "undo" an OR or AND operation.
For OR/AND, exploit the key property: as you expand a subarray, the OR/AND result can only change at most **32 times** (for 32-bit integers) because bits only transition 0→1 (OR) or 1→0 (AND).
Strategy: maintain a **set** of all possible OR results ending at the current index. The set never exceeds size 32.
#+begin_src c++
int subarrayBitwiseORs(vector<int>& arr) {
unordered_set<int> result, current;
for (int x : arr) {
unordered_set<int> next;
next.insert(x);
for (int val : current) {
next.insert(val | x);
}
current = next;
for (int val : current) result.insert(val);
}
return result.size();
}
#+end_src
Time: O(n * 32), Space: O(32) per step
* Subarray Sum — Keyword-to-Algorithm Mapping [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What is the master keyword-to-algorithm mapping for subarray problems?
** Back
| Problem Phrase | Array Property | Algorithm |
|---------------|---------------|-----------|
| "Continuous subarray + Sum = K" | Only positive numbers | **Sliding Window** (O(1) space) |
| "Continuous subarray + Sum = K" | Positive & negative | **Prefix Sum + Hash Map** (O(n) space) |
| "Divisible by K" or "Multiple of X" | Any numbers | **Prefix Remainder + Hash Map** (sum % K) |
| "Equal number of X and Y" | Any numbers | **Value Mapping** (X→1, Y→-1) + Prefix Sum Map |
| "Maximum / Minimum Sum" | Any numbers | **Kadane's Algorithm** (DP) |
| "Subarray Sum + Frequent Updates" | Element mutations | **Fenwick Tree / Segment Tree** |
| "Subarray product = K" | No zeros | **Prefix Product** (division) |
| "Subarray product positive/negative" | Any numbers | **Parity tracking** of negative count |
| "Subarray XOR = K" | Any numbers | **Prefix XOR + Hash Map** |
| "Subarray OR / AND" | Any numbers | **Set of results** (bounded by 32 changes) |
* Subarray Sum — Modular Arithmetic Insight [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Why does "subarray sum divisible by K" work with prefix remainders? Prove it.
** Back
If Sum(i..j) mod K = 0, then:
(Prefix[j] - Prefix[i-1]) mod K = 0
By modular arithmetic:
Prefix[j] mod K = Prefix[i-1] mod K
**Proof:**
Prefix[j] - Prefix[i-1] = m * K (for some integer m)
Prefix[j] = Prefix[i-1] + m * K
Prefix[j] mod K = (Prefix[i-1] + m * K) mod K
Prefix[j] mod K = Prefix[i-1] mod K (since m*K mod K = 0)
So we just need to find pairs of indices with the same prefix remainder.
**Caveat:** In C++/Java, % can return negative values for negative operands. Fix:
remainder = ((prefix_sum % K) + K) % K
In Python, % always returns non-negative, so no fix needed.
* Subarray Sum — Prefix State Generalization [algorithm:concept]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What is the "prefix state" generalization, and how does it extend beyond addition?
** Back
The core philosophy of prefix sums is: **accumulate history as you traverse linearly, and use a hash map to track state.**
This generalizes far beyond addition:
| Problem | Mapping | Reduces To |
|---------|---------|-----------|
| Equal 0s and 1s | 0→-1, 1→+1 | Subarray sum = 0 |
| Equal odd/even | even→+1, odd→-1 | Subarray sum = 0 |
| Equal vowels/consonants | vowel→+1, consonant→-1 | Subarray sum = 0 |
| Equal A/B/C counts | Track (c_A-c_B, c_B-c_C) | Prefix state tuple repeats |
| Subarray product | Prefix product | Division (handle zeros) |
| Subarray XOR | Prefix XOR | XOR is invertible |
| Subarray sum | Prefix sum | Subtraction |
The pattern:
1. Define a state that accumulates as you traverse
2. Find a way to "undo" or compare states (subtract, XOR, divide, compare tuples)
3. Use a hash map to find when the same state (or target difference) appears
+268
View File
@@ -0,0 +1,268 @@
#+title: Master Subarray Pattern Sheet
* Subarray Divisible by K — Remainder Pattern [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Why does "subarray sum divisible by K" work with prefix remainders?
** Back
If Sum(i..j) mod K = 0, then:
(Prefix[j] - Prefix[i-1]) mod K = 0
By modular arithmetic:
Prefix[j] mod K = Prefix[i-1] mod K
So we find pairs of indices with the same prefix remainder.
Transformation: store current_sum % K in hash map.
Data structure: hash map tracking frequencies of remainders.
C++ caveat: % can return negative for negative operands. Fix:
remainder = ((prefix_sum % K) + K) % K
Python: % always non-negative, no fix needed.
* Subarray Equal 0s and 1s — Value Mapping [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the longest subarray with equal number of 0s and 1s.
Example: nums = [0,1] → Output: 2
** Back
Replace all 0s with -1. The problem becomes:
**"Find a subarray whose sum equals 0."**
#+begin_src c++
int findMaxLength(vector<int>& nums) {
unordered_map<int,int> firstOccurrence;
firstOccurrence[0] = 0;
int sum = 0, maxLen = 0;
for (int i = 0; i < nums.size(); i++) {
sum += (nums[i] == 1) ? 1 : -1;
if (firstOccurrence.count(sum)) {
maxLen = max(maxLen, i + 1 - firstOccurrence[sum]);
} else {
firstOccurrence[sum] = i + 1;
}
}
return maxLen;
}
#+end_src
Time: O(n), Space: O(n)
Data structure: hash map tracking raw prefix sums (first occurrence).
* Subarray Equal Odd/Even Numbers — Same Pattern [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the longest subarray with equal number of odd and even numbers.
** Back
Map every even number to +1 and every odd number to -1.
The problem becomes: **"Find a subarray whose sum equals 0."**
Same approach as equal 0s and 1s: prefix sum + hash map storing first occurrence.
This is the same value mapping pattern applied to a different domain.
* Subarray Equal Vowels and Consonants [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the longest subarray with equal number of vowels and consonants.
** Back
Map: vowel → +1, consonant → -1.
The problem becomes: **"Find a subarray whose sum equals 0."**
Same prefix sum + hash map approach.
This demonstrates the general principle: any binary-counting problem can be reduced to "subarray sum = 0" via value mapping.
* Multi-Category Balance — Equal A/B/C Counts [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the longest subarray with equal number of 'A's, 'B's, and 'C's.
** Back
You can't use a single scalar (+1/-1) for three categories. Track **relative differences** between counts.
Maintain running counts: c_A, c_B, c_C.
At each step, compute the tuple of differences: (c_A - c_B, c_B - c_C).
If this tuple repeats later in the array, the elements between those indices have perfectly balanced A, B, C counts.
Data structure: hash map where the key is the state tuple:
map<pair<int,int>, int> firstOccurrence;
The tuple (diff_AB, diff_BC) captures the full relative state. If two positions share the same tuple, the subarray between them has zero net change in all three relative differences → equal counts.
Time: O(n), Space: O(n)
* Subarray Product Equals K — Prefix Product [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the number of subarrays with product equal to K.
** Back
Use a **Prefix Product** array:
Product(i..j) = PrefixProduct[j] / PrefixProduct[i-1]
Instead of subtracting, you divide.
Data structure: hash map searching for current_product / K.
Edge case: zeros reset the product to 0. Handle by:
1. Segmentation — split the array at zeros, solve each segment independently
2. Track position of last seen zero to reset boundaries
Example: subarray product less than K (all positive):
#+begin_src c++
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if (k <= 1) return 0;
int count = 0, product = 1, left = 0;
for (int right = 0; right < nums.size(); right++) {
product *= nums[right];
while (left <= right && product >= k) product /= nums[left++];
count += right - left + 1;
}
return count;
}
#+end_src
* Subarray Product Positive/Negative — Parity Tracking [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the maximum length of a subarray with a positive (or negative) product.
** Back
Map: positive → +1, negative → -1, zero → resets the window.
A subarray has positive product if it contains an **even** number of negatives.
A subarray has negative product if it contains an **odd** number of negatives.
Track the parity (odd/even count) of negative numbers as you traverse:
- If parity is even at index i and was even at index j (j < i), the subarray (j+1..i) has positive product.
- If parity is odd at index i and was even at index j, the subarray (j+1..i) has negative product.
Data structure: two hash maps — first occurrence of even-parity index and first occurrence of odd-parity index.
Time: O(n), Space: O(n)
* Subarray Bitwise XOR — Prefix XOR [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Find the number of subarrays with XOR equal to K.
** Back
XOR is invertible (XOR is its own inverse), so the prefix pattern works:
XOR(i..j) = PrefixXOR[j] ^ PrefixXOR[i-1]
Same hash map pattern as prefix sum:
#+begin_src c++
int subarrayXOR(vector<int>& nums, int k) {
unordered_map<int,int> prefixCount;
prefixCount[0] = 1;
int sum = 0, count = 0;
for (int num : nums) {
sum ^= num;
count += prefixCount[sum ^ k];
prefixCount[sum]++;
}
return count;
}
#+end_src
Time: O(n), Space: O(n)
* Subarray Bitwise OR/AND — Non-Invertible [algorithm:array]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
Can you use prefix sums for subarray problems with Bitwise OR or AND?
** Back
**NO.** Unlike XOR, OR/AND are **not invertible**. You cannot "undo" an OR or AND operation.
Exploit the key property: as you expand a subarray, the OR/AND result can only change at most **32 times** (for 32-bit integers) because bits only transition 0→1 (OR) or 1→0 (AND).
Strategy: maintain a **set** of all possible OR results ending at the current index. The set never exceeds size 32.
#+begin_src c++
int subarrayBitwiseORs(vector<int>& arr) {
unordered_set<int> result, current;
for (int x : arr) {
unordered_set<int> next;
next.insert(x);
for (int val : current) {
next.insert(val | x);
}
current = next;
for (int val : current) result.insert(val);
}
return result.size();
}
#+end_src
Time: O(n * 32), Space: O(32) per step
* Master Keyword-to-Algorithm Mapping [algorithm:interview]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What is the master keyword-to-algorithm mapping for subarray problems?
** Back
| Problem Phrase | Array Property | Algorithm |
|---------------|---------------|-----------|
| "Continuous subarray + Sum = K" | Only positive numbers | **Sliding Window** (O(1) space) |
| "Continuous subarray + Sum = K" | Positive & negative | **Prefix Sum + Hash Map** (O(n) space) |
| "Divisible by K" or "Multiple of X" | Any numbers | **Prefix Remainder + Hash Map** (sum % K) |
| "Equal number of X and Y" | Any numbers | **Value Mapping** (X→1, Y→-1) + Prefix Sum Map |
| "Maximum / Minimum Sum" | Any numbers | **Kadane's Algorithm** (DP) |
| "Subarray Sum + Frequent Updates" | Element mutations | **Fenwick Tree / Segment Tree** |
| "Subarray product = K" | No zeros | **Prefix Product** (division) |
| "Subarray product positive/negative" | Any numbers | **Parity tracking** of negative count |
| "Subarray XOR = K" | Any numbers | **Prefix XOR + Hash Map** |
| "Subarray OR / AND" | Any numbers | **Set of results** (bounded by 32 changes) |
* Prefix State Generalization — The Unifying Concept [algorithm:concept]
:PROPERTIES:
:ANKI_NOTE_TYPE: Basic
:END:
** Front
What is the "prefix state" generalization that unifies all subarray patterns?
** Back
The core philosophy of prefix sums is: **accumulate history as you traverse linearly, and use a hash map to track state.**
| Problem | Mapping | Reduces To |
|---------|---------|-----------|
| Equal 0s and 1s | 0→-1, 1→+1 | Subarray sum = 0 |
| Equal odd/even | even→+1, odd→-1 | Subarray sum = 0 |
| Equal vowels/consonants | vowel→+1, consonant→-1 | Subarray sum = 0 |
| Equal A/B/C counts | Track (c_A-c_B, c_B-c_C) | Prefix state tuple repeats |
| Subarray product | Prefix product | Division (handle zeros) |
| Subarray XOR | Prefix XOR | XOR is invertible |
| Subarray sum | Prefix sum | Subtraction |
The pattern:
1. Define a state that accumulates as you traverse
2. Find a way to "undo" or compare states (subtract, XOR, divide, compare tuples)
3. Use a hash map to find when the same state (or target difference) appears
+67
View File
@@ -0,0 +1,67 @@
#+title: Subarray Sum — Pattern Recognition Guide
#+AUTHOR: Noramyll
* Core Trigger: Subarray + Sum
When you see **"subarray"** and **"sum"** in the same problem description, **Prefix Sums** should almost always be your immediate first thought.
The fundamental formula:
Sum(i..j) = PrefixSum[j] - PrefixSum[i-1]
This algebraic rewrite turns a **range query** into a **difference between two points**. It completely eliminates the need to re-scan the array.
The mental trigger is simple: subarray + sum → prefix sums. Always.
**Variations of this pattern:**
- **"Count of subarrays where sum equals K"**: Use the hash map approach, looking for `current_sum - K`.
- **"Longest/Shortest subarray where sum equals K"**: Instead of storing the *frequency* of the prefix sum in your map, store its **first seen index** (`map[prefix_sum] = index`). This lets you track length via `current_index - map[current_sum - K]`.
- **"Subarray sum divisible by K"**: Store `prefix_sum % K` in your hash map instead of the raw sum.
* The Multiplicative Variant: Subarray + Product
If the problem asks for a **subarray product** (e.g., "number of subarrays where product equals K"), the prefix sum pattern translates directly into a **Prefix Product**:
Product(i..j) = PrefixProduct[j] / PrefixProduct[i-1]
Instead of subtracting, you divide.
**Edge case:** Zeros reset the product to 0. Division by zero breaks the pattern. Handle by:
1. Segmentation — split the array at zeros, solve each segment independently
2. Track position of last seen zero to reset boundaries
* The Counter-Pattern: When Is It *Not* Prefix Sums?
While "subarray + sum" strongly hints at prefix sums, you need to look at the **constraints** and **types of numbers** to choose the absolute best tool.
| If you see "Subarray + Sum" AND... | The Real Pattern Is... | Why? |
|-------------------------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| All numbers are strictly positive (>= 0) and you need to find a target sum or max length. | **Sliding Window (Two Pointers)** | Because the window sum is monotonic (expanding always increases the sum; shrinking always decreases it). Sliding window optimizes space to O(1) compared to O(n) for prefix sums. |
| Numbers can be negative, or you need to find an exact target sum. | **Prefix Sum + Hash Map** | Monotonicity is broken. Adding an element could make the sum smaller, so sliding window fails. You *must* use a hash map to remember past states. |
| The array is **constantly being updated** (dynamic updates) between sum queries. | **Segment Tree or Fenwick Tree (BIT)** | A standard prefix sum array takes O(n) to update if an element changes. Segment/Fenwick trees drop update and query times to O(log n). |
* Generalizing the Concept: "Prefix State"
Don't limit this trick just to addition. The core philosophy of a prefix sum is **accumulating history as you traverse linearly**. You can use a hash map to track the "prefix state" for things that don't look like math at first.
| Problem | Mapping | Reduces To |
|-------------------------+------------------------------+----------------------------|
| Equal 0s and 1s | 0 → -1, 1 → +1 | Subarray sum = 0 |
| Equal odd/even | even → +1, odd → -1 | Subarray sum = 0 |
| Equal vowels/consonants | vowel → +1, consonant → -1 | Subarray sum = 0 |
| Equal A/B/C counts | Track (c_A - c_B, c_B - c_C) | Prefix state tuple repeats |
| Subarray product | Prefix product | Division (handle zeros) |
| Subarray XOR | Prefix XOR | XOR is invertible |
| Subarray sum | Prefix sum | Subtraction |
The pattern:
1. Define a state that accumulates as you traverse
2. Find a way to "undo" or compare states (subtract, XOR, divide, compare tuples)
3. Use a hash map to find when the same state (or target difference) appears
* Summary Checklist
1. Subarray + Sum + Negative Numbers → **Prefix Sum + Hash Map**
2. Subarray + Sum + Only Positive Numbers → **Sliding Window**
3. Subarray + Sum + Frequent Updates → **Segment/Fenwick Tree**
+152
View File
@@ -0,0 +1,152 @@
#+title: Segment Tree
* Segment
#+begin_src python
class seg_tree():
def __init__(self, arr):
self.n = len(arr)
self.t = [0]*self.n + arr
for i in range(self.n-1, -1, -1):
self.t[i] = self.t[i<<1] + self.t[(i<<1)+1]
def q(l, r):
class Fenwick:
def __init__(self, lst: List[int], lamd):
self.n = len(lst)
self.t = [0] * self.n + lst
print(self.t)
for i in range(self.n-1, 1, -1):
self.t[i] = self.t[i<<1] + self.t[(i<<1)+1]
self.f = lamd
def update(self, i, x):
i += self.n
self.t[i] = x
while i > 1:
self.t[i>>1] = self.t[i] + self.t[i^1]
i >>= 1
def query(self, lo, hi):
ans = 0
lo += self.n
hi += self.n
while lo < hi:
if lo & 1:
ans = min(ans, self.t[lo])
lo += 1
if hi & 1:
hi -= 1
ans = min(ans, self.t[hi])
lo >>= 1
hi >>= 1
return ans
#+end_src
#+begin_src python :results output
class Fenwick:
def __init__(self, lst: list[int]):
self.n = len(lst)
self.t = [float('inf')] * self.n + lst
for i in range(self.n-1, 1, -1):
self.t[i] = min(self.t[i<<1], self.t[(i<<1)+1])
print(self.t)
def update(self, i, x):
i += self.n
self.t[i] = x
while i > 1:
self.t[i>>1] = self.t[i] + self.t[i^1]
i >>= 1
def query(self, lo, hi):
ans = 0
lo += self.n
hi += self.n
while lo < hi:
if lo & 1:
ans = min(ans, self.t[lo])
lo += 1
if hi & 1:
hi -= 1
ans = min(ans, self.t[hi])
lo >>= 1
hi >>= 1
return ans
fw = Fenwick([999,2,1,999,999,999,999])
i = fw.query(0, 2)
print(i)
#+end_src
#+RESULTS:
: [inf, inf, 1, 999, 1, 999, 999, 999, 2, 1, 999, 999, 999, 999]
: 0
#+begin_src python :results output
class Fenwick:
def __init__(self, lst):
self.n = len(lst)
self.t = [float('inf')] * self.n + lst
for i in range(self.n - 1, 0, -1): # include root
self.t[i] = min(self.t[i<<1], self.t[(i<<1)+1])
def update(self, i, x):
i += self.n
self.t[i] = x
while i > 1:
self.t[i>>1] = min(self.t[i], self.t[i^1]) # min, not +
i >>= 1
def query(self, lo, hi):
ans = float('inf') # min identity
lo += self.n; hi += self.n
while lo < hi:
if lo & 1: ans = min(ans, self.t[lo]); lo += 1
if hi & 1: hi -= 1; ans = min(ans, self.t[hi])
lo >>= 1; hi >>= 1
return ans
fw = Fenwick([999,2,1,999,999,999,999])
i = fw.query(0, 3)
print(i)
#+end_src
#+RESULTS:
: 1
#+begin_src markdown
The code you provided is actually a *Segment Tree*, not a Fenwick Tree. It works by using a compact array representation of a complete binary tree to perform range minimum queries.
,*** 1. Structure
- *Array Layout:* For an input list of size $n$, it creates an array =t= of size $2n$.
,* Indices =n= to =2n-1= store the original elements (leaves).
,* Indices =1= to =n-1= store the minimum of their children (internal nodes).
- *Parent-Child Logic:* For any node at index =i=:
,* Left child: =i << 1= (or =2i=)
,* Right child: =(i << 1) + 1= (or =2i + 1=)
,* Parent: =i >> 1= (or =i // 2=)
,*** 2. Why it works
,**** Initialization (=__init__=)
It populates the leaves first, then iterates backwards from =n-1= down to =1=. This ensures that when calculating the minimum for node =i=, its children (=2i= and =2i+1=) have already been processed (or are leaves).
,**** Point Update (=update=)
1. Updates the leaf at =i + n=.
2. Moves up the tree (=i >>= 1=), re-calculating the minimum of the current node by comparing the updated node with its sibling (=i ^ 1=). This propagates the change up to the root in $O(\log n)$ time.
,**** Range Query (=query=)
This uses a "bottom-up" approach on the interval =[lo, hi)=:
1. If =lo= is a right child (=lo & 1=), it's the only child of its parent inside the range. We include it in the minimum and move to the next subtree (=lo += 1=).
2. If =hi= is a right child, the element at =hi-1= is inside the range. We include it and move left.
3. We then move both pointers to their parents (=>> 1=).
4. This collects only the "minimal" set of nodes covering the range in $O(\log n)$ time.
,*** Summary
While a *Fenwick Tree* (Binary Indexed Tree) is usually used for prefix sums and handles point updates in $O(\log n)$, it cannot efficiently perform range *minimum* queries for arbitrary ranges. This *Segment Tree* does exactly that by explicitly storing the minimum of power-of-two blocks.
,***
#+end_src