04 ·automation tool ·2026
LeetCode TrackR
A daily job that logs every solved LeetCode problem into Notion and schedules it for spaced-repetition review.
LeetCode TrackR logs every problem I solve into a Notion tracker and decides, on its own, when to put it back in front of me to review.
Inspiration
As a student, I credit much of my academic success to two habits: spaced repetition and active recall. They were simply the best way for me to revise. When I started on data structures and algorithms, I wanted to study them the same way, but two things got in the way. Solving a question once did not make it stick, so a near-identical question weeks later would send me back to square one. And keeping track of which topic and subsection of DSA to revisit next was a chore in itself.
Spaced repetition fixes the forgetting. I wanted that same discipline for algorithms, without hand-building and scheduling every card myself. LeetCode TrackR grew out of wanting the review to run itself, and to do the headache work for me.
What it does
A GitHub Actions cron runs daily, pulling my latest accepted submissions from LeetCode’s GraphQL API. For each one it records how I approached it: my first intuition, why that intuition was wrong, what actually worked, and what I optimised. Then it assigns a Leitner interval and sets a Next Review date. A Notion view filtered to “due today” becomes my revision queue, so the problems I am about to forget come back first.
Give it an Anthropic API key and it will also have Claude draft a two or three sentence summary of the method and complexity for each solve. Without a key there is no summary; it still logs everything and leaves the notes to me.
Inside the log
Below is a rebuild of my Notion tracker. Click any row to open that solve: its properties, the auto-generated approach, and the submitted code.
LeetCode Log
Auto-populated log of accepted LeetCode submissions. Each row links to one problem; the page body holds the submitted code, an auto-summary of the approach, and a notes section.
Click a row to open the logged entry →
1358. Number of Substrings Containing All Three Characters
- Date Solved
- June 30, 2026
- Difficulty
- Medium
- Language
- python3
- Memory
- 19.4 MB
- Problem #
- 1358
- Runtime
- 342 ms
- Status
- Logged
- Tags
- Hash TableStringSliding Window
Approach
Sliding window over the string keeping running counts of a, b and c. For each right end, advance the left boundary as far as the window still contains all three characters; the number of valid substrings ending at that right end is then the left index, since any earlier start also contains all three. Single pass, O(N) time and O(1) space.
Submitted Code
class Solution:
def numberOfSubstrings(self, s: str) -> int:
count = {"a": 0, "b": 0, "c": 0}
left = 0
ans = 0
for right, ch in enumerate(s):
count[ch] += 1
while count["a"] and count["b"] and count["c"]:
count[s[left]] -= 1
left += 1
ans += left
return ans257. Binary Tree Paths
- Date Solved
- June 16, 2026
- Difficulty
- Easy
- Language
- python3
- Memory
- 19.4 MB
- Problem #
- 257
- Runtime
- 34 ms
- Status
- Logged
- Tags
- TreeDepth-First SearchBinary Tree
Approach
Depth-first traversal that carries the running root-to-node path down the tree. At each leaf the collected values are joined with "->" and pushed to the answer; passing a fresh copy of the path per branch avoids explicit backtracking. O(N) nodes, and O(N x H) for the emitted strings in the worst case.
Submitted Code
class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
ans = []
def dfs(node, path):
if not node:
return
path = path + [str(node.val)]
if not node.left and not node.right:
ans.append("->".join(path))
return
dfs(node.left, path)
dfs(node.right, path)
dfs(root, [])
return ans802. Find Eventual Safe States
- Date Solved
- June 11, 2026
- Difficulty
- Medium
- Language
- python3
- Memory
- 42.5 MB
- Problem #
- 802
- Runtime
- 892 ms
- Status
- Logged
- Tags
- Depth-First SearchGraphTopological Sort
Approach
A node is safe only if every path leaving it ends at a terminal node, so a three-colour DFS marks nodes in progress to catch cycles. Any node that reaches an in-progress node lies on a cycle and is unsafe; a node whose successors are all safe is itself marked safe and memoised. O(V + E).
Submitted Code
class Solution:
def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
n = len(graph)
color = [0] * n # 0 = unvisited, 1 = in progress, 2 = safe
def safe(u):
if color[u] != 0:
return color[u] == 2
color[u] = 1
for v in graph[u]:
if not safe(v):
return False
color[u] = 2
return True
return [u for u in range(n) if safe(u)]1857. Largest Color Value in a Directed Graph
- Date Solved
- June 10, 2026
- Difficulty
- Hard
- Language
- python3
- Memory
- 106.9 MB
- Problem #
- 1857
- Runtime
- 1104 ms
- Status
- Logged
- Tags
- Dynamic ProgrammingGraphTopological SortCounting
Approach
The value of a path is the highest frequency of any single colour along it, so a topological order lets each node carry the best count of all 26 colours over paths reaching it. Kahn's algorithm processes nodes in order, adds each node's own colour, and relaxes its successors. If the sort cannot cover every node the graph has a cycle, so the answer is -1. O((V + E) x 26).
Submitted Code
from collections import deque
class Solution:
def largestPathValue(self, colors: str, edges: List[List[int]]) -> int:
n = len(colors)
adj = [[] for _ in range(n)]
indeg = [0] * n
for u, v in edges:
adj[u].append(v)
indeg[v] += 1
dp = [[0] * 26 for _ in range(n)]
q = deque(u for u in range(n) if indeg[u] == 0)
seen = 0
ans = 0
while q:
u = q.popleft()
seen += 1
c = ord(colors[u]) - 97
dp[u][c] += 1
ans = max(ans, dp[u][c])
for v in adj[u]:
for k in range(26):
if dp[u][k] > dp[v][k]:
dp[v][k] = dp[u][k]
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return ans if seen == n else -12360. Longest Cycle in a Graph
- Date Solved
- June 9, 2026
- Difficulty
- Hard
- Language
- python3
- Memory
- 112.9 MB
- Problem #
- 2360
- Runtime
- 764 ms
- Status
- Logged
- Tags
- GraphDepth-First Search
Approach
Every node has at most one outgoing edge, so following edges from any start eventually meets a previously seen node. Global timestamps record when each node was first visited; if a walk re-enters a node it stamped on this same walk, the difference between the current time and that node's timestamp is the cycle length. Each node is visited once, O(N).
Submitted Code
class Solution:
def longestCycle(self, edges: List[int]) -> int:
n = len(edges)
visit_time = [0] * n
ans = -1
t = 1
for start in range(n):
if visit_time[start]:
continue
path_start = t
u = start
while u != -1 and visit_time[u] == 0:
visit_time[u] = t
t += 1
u = edges[u]
if u != -1 and visit_time[u] >= path_start:
ans = max(ans, t - visit_time[u])
return ans1192. Critical Connections in a Network
- Date Solved
- April 28, 2026
- Difficulty
- Hard
- Language
- python3
- Memory
- 99.1 MB
- Problem #
- 1192
- Runtime
- 187 ms
- Status
- Logged
- Tags
- Depth-First SearchGraph TheoryBiconnected Component
Approach
Classic Tarjan bridge-finding algorithm. A DFS assigns each node a discovery time disc[u] and a low-link value low[u] representing the smallest discovery time reachable from u's subtree (excluding the parent edge). After recursing into a child v, we update low[u] = min(low[u], low[v]) and identify edge (u, v) as a bridge whenever low[v] > disc[u], since that means v cannot reach u or any ancestor of u via a back-edge. Runs in O(V + E) time and O(V + E) space for the adjacency list and metadata arrays.
Submitted Code
class Solution:
def criticalConnections(self, n: int, connections: List[List[int]]) -> List[List[int]]:
visited = set()
adjlist = [[] for _ in range(n)]
for u,v in connections :
adjlist[u].append(v)
adjlist[v].append(u)
disc = [-1] * n #first time we found this node
low = [-1] * n
ans = []
time = 0
def dfs(u,parent) :
nonlocal time
disc[u] = time
low[u] = time
time +=1
for v in adjlist[u] :
if v == parent :
continue
if disc[v] == -1 :
dfs(v,u)
low[u] = min(low[v],low[u])
if low[v] > disc[u] :
ans.append((v,u))
else :
low[u] = min(low[u],disc[v])
dfs(0,-1)
return ansDesign choices
- Runs daily. It runs entirely on GitHub Actions, on a cron fired at 01:00 UTC.
- Zero-config to start. It works off a public LeetCode username alone. Add a session cookie and it also pulls the submitted code, runtime, and memory.
- Prevents duplicates. Dedup is by problem number, so re-solving a problem updates the entry instead of creating a duplicate.