Advanced Data Structures: Answer Sheet & Solutions
This is the answer sheet for the advanced exercises. Each solution gives the key idea, the optimal complexity, the code in all four languages (use the switcher), and a common pitfall. Try the problems first — then check your work here.
Q1. Number of Provinces
Idea: Each city starts in its own set. Union every directly connected pair; the number of provinces is the number of distinct roots left. Start a counter at n and decrement on every successful union (one that actually merged two different sets).
Complexity: O(n²·α(n)) time, O(n) space.
int findCircleNum(std::vector<std::vector<int>>& isConnected) {
int n = isConnected.size();
std::vector<int> parent(n);
for (int i = 0; i < n; i++) parent[i] = i;
std::function<int(int)> find = [&](int x) {
while (parent[x] != x) x = parent[x] = parent[parent[x]];
return x;
};
int provinces = n;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (isConnected[i][j]) {
int ra = find(i), rb = find(j);
if (ra != rb) { parent[ra] = rb; provinces--; }
}
return provinces;
}
int[] parent;
int find(int x) {
while (parent[x] != x) x = parent[x] = parent[parent[x]];
return x;
}
int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
int provinces = n;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (isConnected[i][j] == 1) {
int ra = find(i), rb = find(j);
if (ra != rb) { parent[ra] = rb; provinces--; }
}
return provinces;
}
function findCircleNum(isConnected) {
const n = isConnected.length;
const parent = Array.from({ length: n }, (_, i) => i);
const find = (x) => {
while (parent[x] !== x) x = parent[x] = parent[parent[x]];
return x;
};
let provinces = n;
for (let i = 0; i < n; i++)
for (let j = i + 1; j < n; j++)
if (isConnected[i][j] === 1) {
const ra = find(i), rb = find(j);
if (ra !== rb) { parent[ra] = rb; provinces--; }
}
return provinces;
}
def find_circle_num(is_connected):
n = len(is_connected)
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
provinces = n
for i in range(n):
for j in range(i + 1, n):
if is_connected[i][j] == 1:
ra, rb = find(i), find(j)
if ra != rb:
parent[ra] = rb
provinces -= 1
return provinces
Pitfall: Counting edges instead of merges over-counts. Only decrement when the two endpoints had different roots, otherwise a redundant edge wrongly lowers the count.
Q2. Redundant Connection
Idea: Add edges one by one with union-find. An edge whose two endpoints are already in the same set would create a cycle — that’s the redundant one. Because we process edges in input order and return at the first such edge, we naturally get the last edge that completes a cycle for a single-extra-edge graph.
Complexity: O(n·α(n)) time, O(n) space.
std::vector<int> findRedundantConnection(std::vector<std::vector<int>>& edges) {
int n = edges.size();
std::vector<int> parent(n + 1);
for (int i = 1; i <= n; i++) parent[i] = i;
std::function<int(int)> find = [&](int x) {
while (parent[x] != x) x = parent[x] = parent[parent[x]];
return x;
};
for (auto& e : edges) {
int ra = find(e[0]), rb = find(e[1]);
if (ra == rb) return e; // already connected -> cycle
parent[ra] = rb;
}
return {};
}
int[] parent;
int find(int x) {
while (parent[x] != x) x = parent[x] = parent[parent[x]];
return x;
}
int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
parent = new int[n + 1];
for (int i = 1; i <= n; i++) parent[i] = i;
for (int[] e : edges) {
int ra = find(e[0]), rb = find(e[1]);
if (ra == rb) return e; // already connected -> cycle
parent[ra] = rb;
}
return new int[]{};
}
function findRedundantConnection(edges) {
const n = edges.length;
const parent = Array.from({ length: n + 1 }, (_, i) => i);
const find = (x) => {
while (parent[x] !== x) x = parent[x] = parent[parent[x]];
return x;
};
for (const e of edges) {
const ra = find(e[0]), rb = find(e[1]);
if (ra === rb) return e; // already connected -> cycle
parent[ra] = rb;
}
return [];
}
def find_redundant_connection(edges):
n = len(edges)
parent = list(range(n + 1))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for a, b in edges:
ra, rb = find(a), find(b)
if ra == rb: # already connected -> cycle
return [a, b]
parent[ra] = rb
return []
Pitfall: Nodes are labeled
1..n, so size the parent arrayn + 1and skip index 0, or you’ll read out of bounds.
Q3. Range Sum Query — Mutable
Idea: A Fenwick tree gives O(log n) prefix sums with point updates. Keep a copy of the current values so an update can apply the delta value - old. sumRange(l, r) = prefix(r+1) - prefix(l) (the tree is 1-indexed).
Complexity: O(log n) per update/sumRange, O(n log n) to build, O(n) space.
class NumArray {
std::vector<int> nums;
std::vector<long long> tree;
int n;
void add(int i, long long delta) { // 1-indexed
for (; i <= n; i += i & (-i)) tree[i] += delta;
}
long long prefix(int i) {
long long s = 0;
for (; i > 0; i -= i & (-i)) s += tree[i];
return s;
}
public:
NumArray(std::vector<int>& a) : nums(a), tree(a.size() + 1, 0), n(a.size()) {
for (int i = 0; i < n; i++) add(i + 1, a[i]);
}
void update(int index, int value) {
add(index + 1, (long long)value - nums[index]);
nums[index] = value;
}
int sumRange(int left, int right) {
return (int)(prefix(right + 1) - prefix(left));
}
};
class NumArray {
int[] nums;
long[] tree;
int n;
private void add(int i, long delta) { // 1-indexed
for (; i <= n; i += i & (-i)) tree[i] += delta;
}
private long prefix(int i) {
long s = 0;
for (; i > 0; i -= i & (-i)) s += tree[i];
return s;
}
NumArray(int[] a) {
nums = a.clone();
n = a.length;
tree = new long[n + 1];
for (int i = 0; i < n; i++) add(i + 1, a[i]);
}
void update(int index, int value) {
add(index + 1, (long) value - nums[index]);
nums[index] = value;
}
int sumRange(int left, int right) {
return (int) (prefix(right + 1) - prefix(left));
}
}
class NumArray {
constructor(a) {
this.nums = a.slice();
this.n = a.length;
this.tree = new Array(this.n + 1).fill(0);
for (let i = 0; i < this.n; i++) this.add(i + 1, a[i]);
}
add(i, delta) { // 1-indexed
for (; i <= this.n; i += i & (-i)) this.tree[i] += delta;
}
prefix(i) {
let s = 0;
for (; i > 0; i -= i & (-i)) s += this.tree[i];
return s;
}
update(index, value) {
this.add(index + 1, value - this.nums[index]);
this.nums[index] = value;
}
sumRange(left, right) {
return this.prefix(right + 1) - this.prefix(left);
}
}
class NumArray:
def __init__(self, a):
self.nums = a[:]
self.n = len(a)
self.tree = [0] * (self.n + 1)
for i in range(self.n):
self._add(i + 1, a[i])
def _add(self, i, delta): # 1-indexed
while i <= self.n:
self.tree[i] += delta
i += i & (-i)
def _prefix(self, i):
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i)
return s
def update(self, index, value):
self._add(index + 1, value - self.nums[index])
self.nums[index] = value
def sumRange(self, left, right):
return self._prefix(right + 1) - self._prefix(left)
Pitfall:
updatesets a value; the Fenwick tree only knows how to add. Apply the deltavalue - oldand remember to store the new value, or every later update drifts.
Q4. Design an LRU Cache
Idea: Pair a hash map (O(1) lookup) with an ordering that supports O(1) move-to-front and drop-from-back. C++/Java use an explicit doubly linked list; JS Map and Python OrderedDict give the ordering for free.
Complexity: O(1) per get/put, O(capacity) space.
#include <list>
#include <unordered_map>
class LRUCache {
int cap;
std::list<std::pair<int,int>> items; // front = most recent
std::unordered_map<int, std::list<std::pair<int,int>>::iterator> pos;
public:
LRUCache(int capacity) : cap(capacity) {}
int get(int key) {
auto it = pos.find(key);
if (it == pos.end()) return -1;
items.splice(items.begin(), items, it->second);
return it->second->second;
}
void put(int key, int value) {
auto it = pos.find(key);
if (it != pos.end()) {
it->second->second = value;
items.splice(items.begin(), items, it->second);
return;
}
if ((int)items.size() == cap) {
pos.erase(items.back().first);
items.pop_back();
}
items.emplace_front(key, value);
pos[key] = items.begin();
}
};
import java.util.HashMap;
class LRUCache {
static class Node { int key, val; Node prev, next; Node(int k, int v){key=k;val=v;} }
private final int cap;
private final HashMap<Integer, Node> map = new HashMap<>();
private final Node head = new Node(0,0), tail = new Node(0,0);
LRUCache(int capacity) { cap = capacity; head.next = tail; tail.prev = head; }
private void remove(Node n) { n.prev.next = n.next; n.next.prev = n.prev; }
private void addFront(Node n) {
n.next = head.next; n.prev = head; head.next.prev = n; head.next = n;
}
int get(int key) {
Node n = map.get(key);
if (n == null) return -1;
remove(n); addFront(n);
return n.val;
}
void put(int key, int value) {
Node n = map.get(key);
if (n != null) { n.val = value; remove(n); addFront(n); return; }
if (map.size() == cap) { Node lru = tail.prev; remove(lru); map.remove(lru.key); }
Node fresh = new Node(key, value);
addFront(fresh); map.put(key, fresh);
}
}
class LRUCache {
constructor(capacity) { this.cap = capacity; this.map = new Map(); }
get(key) {
if (!this.map.has(key)) return -1;
const val = this.map.get(key);
this.map.delete(key);
this.map.set(key, val); // move to most recent
return val;
}
put(key, value) {
if (this.map.has(key)) this.map.delete(key);
else if (this.map.size === this.cap) this.map.delete(this.map.keys().next().value);
this.map.set(key, value);
}
}
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cap = capacity
self.map = OrderedDict()
def get(self, key):
if key not in self.map:
return -1
self.map.move_to_end(key)
return self.map[key]
def put(self, key, value):
if key in self.map:
self.map.move_to_end(key)
self.map[key] = value
if len(self.map) > self.cap:
self.map.popitem(last=False) # evict least recent
Pitfall: A
getis a use — forgetting to move the entry to the front corrupts the eviction order. Also remember to delete the evicted key from the map, not just the list.
Q5. Implement strStr() (KMP)
Idea: Build the KMP prefix table for needle, then scan haystack once. On a mismatch, fall back via the table instead of restarting, so the haystack pointer never moves backward — O(n + m).
Complexity: O(n + m) time, O(m) space.
int strStr(const std::string& haystack, const std::string& needle) {
int m = needle.size();
if (m == 0) return 0;
std::vector<int> lps(m, 0);
for (int i = 1, len = 0; i < m; ) {
if (needle[i] == needle[len]) lps[i++] = ++len;
else if (len > 0) len = lps[len - 1];
else lps[i++] = 0;
}
for (int i = 0, j = 0; i < (int)haystack.size(); i++) {
while (j > 0 && haystack[i] != needle[j]) j = lps[j - 1];
if (haystack[i] == needle[j]) j++;
if (j == m) return i - m + 1;
}
return -1;
}
int strStr(String haystack, String needle) {
int m = needle.length();
if (m == 0) return 0;
int[] lps = new int[m];
for (int i = 1, len = 0; i < m; ) {
if (needle.charAt(i) == needle.charAt(len)) lps[i++] = ++len;
else if (len > 0) len = lps[len - 1];
else lps[i++] = 0;
}
for (int i = 0, j = 0; i < haystack.length(); i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) j = lps[j - 1];
if (haystack.charAt(i) == needle.charAt(j)) j++;
if (j == m) return i - m + 1;
}
return -1;
}
function strStr(haystack, needle) {
const m = needle.length;
if (m === 0) return 0;
const lps = new Array(m).fill(0);
for (let i = 1, len = 0; i < m; ) {
if (needle[i] === needle[len]) lps[i++] = ++len;
else if (len > 0) len = lps[len - 1];
else lps[i++] = 0;
}
for (let i = 0, j = 0; i < haystack.length; i++) {
while (j > 0 && haystack[i] !== needle[j]) j = lps[j - 1];
if (haystack[i] === needle[j]) j++;
if (j === m) return i - m + 1;
}
return -1;
}
def str_str(haystack, needle):
m = len(needle)
if m == 0:
return 0
lps = [0] * m
length = 0
i = 1
while i < m:
if needle[i] == needle[length]:
length += 1
lps[i] = length
i += 1
elif length > 0:
length = lps[length - 1]
else:
lps[i] = 0
i += 1
j = 0
for i in range(len(haystack)):
while j > 0 and haystack[i] != needle[j]:
j = lps[j - 1]
if haystack[i] == needle[j]:
j += 1
if j == m:
return i - m + 1
return -1
Pitfall: Decide the empty-needle policy explicitly (here it returns 0). Forgetting it can cause an out-of-bounds read on
needle[0].
Q6. Repeated Substring Pattern
Idea: Build the prefix function and look at its last value k = lps[n-1] — the longest proper prefix that is also a suffix. The shortest repeating unit has length n - k. The string is a repetition iff k > 0 and n is divisible by n - k.
Complexity: O(n) time, O(n) space.
bool repeatedSubstringPattern(const std::string& s) {
int n = s.size();
std::vector<int> lps(n, 0);
for (int i = 1, len = 0; i < n; ) {
if (s[i] == s[len]) lps[i++] = ++len;
else if (len > 0) len = lps[len - 1];
else lps[i++] = 0;
}
int k = lps[n - 1];
return k > 0 && n % (n - k) == 0;
}
boolean repeatedSubstringPattern(String s) {
int n = s.length();
int[] lps = new int[n];
for (int i = 1, len = 0; i < n; ) {
if (s.charAt(i) == s.charAt(len)) lps[i++] = ++len;
else if (len > 0) len = lps[len - 1];
else lps[i++] = 0;
}
int k = lps[n - 1];
return k > 0 && n % (n - k) == 0;
}
function repeatedSubstringPattern(s) {
const n = s.length;
const lps = new Array(n).fill(0);
for (let i = 1, len = 0; i < n; ) {
if (s[i] === s[len]) lps[i++] = ++len;
else if (len > 0) len = lps[len - 1];
else lps[i++] = 0;
}
const k = lps[n - 1];
return k > 0 && n % (n - k) === 0;
}
def repeated_substring_pattern(s):
n = len(s)
lps = [0] * n
length = 0
i = 1
while i < n:
if s[i] == s[length]:
length += 1
lps[i] = length
i += 1
elif length > 0:
length = lps[length - 1]
else:
lps[i] = 0
i += 1
k = lps[n - 1]
return k > 0 and n % (n - k) == 0
Pitfall: The
k > 0guard is essential — without it a string of all-distinct characters (wherek = 0, son - k = n) would wrongly reporttruesincen % n == 0.
Q7. Count of Smaller Numbers After Self
Idea: Process the array right to left, inserting each value into a Fenwick tree keyed by rank (coordinate-compressed value). Before inserting nums[i], query the count of already-seen values with a strictly smaller rank — those are the smaller elements to its right.
Complexity: O(n log n) time, O(n) space.
std::vector<int> countSmaller(std::vector<int>& nums) {
std::vector<int> sorted = nums;
std::sort(sorted.begin(), sorted.end());
sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end());
int m = sorted.size();
std::vector<int> tree(m + 1, 0);
auto add = [&](int i) { for (; i <= m; i += i & (-i)) tree[i]++; };
auto query = [&](int i) { int s = 0; for (; i > 0; i -= i & (-i)) s += tree[i]; return s; };
std::vector<int> res(nums.size());
for (int i = (int)nums.size() - 1; i >= 0; i--) {
int rank = std::lower_bound(sorted.begin(), sorted.end(), nums[i]) - sorted.begin() + 1;
res[i] = query(rank - 1); // strictly smaller
add(rank);
}
return res;
}
int[] countSmaller(int[] nums) {
int[] sorted = nums.clone();
Arrays.sort(sorted);
// compress to ranks via a map of value -> 1-based rank
TreeMap<Integer, Integer> rankOf = new TreeMap<>();
int r = 0;
for (int v : sorted) if (!rankOf.containsKey(v)) rankOf.put(v, ++r);
int m = r;
int[] tree = new int[m + 1];
int[] res = new int[nums.length];
for (int i = nums.length - 1; i >= 0; i--) {
int rank = rankOf.get(nums[i]);
int s = 0;
for (int j = rank - 1; j > 0; j -= j & (-j)) s += tree[j]; // strictly smaller
res[i] = s;
for (int j = rank; j <= m; j += j & (-j)) tree[j]++;
}
return res;
}
function countSmaller(nums) {
const sorted = [...new Set(nums)].sort((a, b) => a - b);
const rankOf = new Map();
sorted.forEach((v, i) => rankOf.set(v, i + 1)); // 1-based rank
const m = sorted.length;
const tree = new Array(m + 1).fill(0);
const res = new Array(nums.length);
for (let i = nums.length - 1; i >= 0; i--) {
const rank = rankOf.get(nums[i]);
let s = 0;
for (let j = rank - 1; j > 0; j -= j & (-j)) s += tree[j]; // strictly smaller
res[i] = s;
for (let j = rank; j <= m; j += j & (-j)) tree[j]++;
}
return res;
}
def count_smaller(nums):
sorted_vals = sorted(set(nums))
rank_of = {v: i + 1 for i, v in enumerate(sorted_vals)} # 1-based rank
m = len(sorted_vals)
tree = [0] * (m + 1)
res = [0] * len(nums)
for i in range(len(nums) - 1, -1, -1):
rank = rank_of[nums[i]]
s, j = 0, rank - 1
while j > 0: # strictly smaller
s += tree[j]
j -= j & (-j)
res[i] = s
j = rank
while j <= m:
tree[j] += 1
j += j & (-j)
return res
Pitfall: Query
rank - 1, notrank— including the current value’s own rank would count equal elements as smaller. Coordinate-compress first so the Fenwick tree size depends on the number of distinct values, not the value range.
That’s the full answer sheet. Back to the advanced exercises or explore the problem-solving patterns.