Compare commits
7 Commits
6b9a0d3161
...
f603236a48
| Author | SHA1 | Date | |
|---|---|---|---|
| f603236a48 | |||
| fa64e776ca | |||
| 127aad7003 | |||
| defa2145e8 | |||
| 978ab00faa | |||
| 3ab8ba001d | |||
| 69676a84be |
@@ -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
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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~.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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**
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user