CodingNeurogrid 2025·

Neurogrid CTF: Human-Only 2025 | Coding

Coding challenges from Neurogrid CTF: Human-Only 2025

hard HackTheBox Neurogrid CTF Coding Algorithms
6 min reading · edit 2025-11-28

Neurogrid CTF: Human-Only 2025 was a 4-day CTF hosted by HackTheBox. I competed solo and finished in the top 4 out of 1,337 players. This post covers the Coding challenges.

Neurogrid CTF: Human-Only 2025

Team Solves Progress


Blade Master [hard]

Description: In the age of still swords, the clans forged their loyalty into iron. Each shrine holds a single blade — humble, ranked, waiting. The disciple’s pilgrimage is simple: begin anywhere, end anywhere, but never tread the same road twice. Collect the blades in a rising harmony of power, or watch the road seal behind you forever.

Points: 975 | Difficulty: Hard

Analysis

This challenge combines graph theory with dynamic programming. We’re given a tree with N nodes (up to 25,000), each assigned a rank value. The goal is to find the maximum LIS along any path in the tree.

The naive approach would enumerate all N(N-1)/2 paths and compute LIS for each - clearly too slow. Instead, use DFS with an incremental LIS that supports backtracking. As we traverse from a starting node, we maintain the standard O(N log N) LIS tails array, updating it as we visit each node and restoring it when we backtrack.

The core algorithm:

 1void dfs(int node, int parent, int lis_tails[], int *lis_size) {
 2    int rank = ranks[node];
 3    int pos = lower_bound(lis_tails, *lis_size, rank);
 4
 5    // Save state for backtracking
 6    int old_size = *lis_size;
 7    int old_value = (pos < *lis_size) ? lis_tails[pos] : -1;
 8
 9    // Update LIS
10    if (pos < *lis_size) {
11        lis_tails[pos] = rank;
12    } else {
13        lis_tails[(*lis_size)++] = rank;
14    }
15
16    if (*lis_size > max_blades) max_blades = *lis_size;
17
18    // Recurse to children
19    for (int i = 0; i < graph_size[node]; i++) {
20        if (graph[node][i] != parent) {
21            dfs(graph[node][i], node, lis_tails, lis_size);
22        }
23    }
24
25    // Backtrack
26    if (pos < old_size) lis_tails[pos] = old_value;
27    else (*lis_size)--;
28}

Exploitation

The real challenge was balancing correctness with performance. Running DFS from every node gives O(N² log N) complexity - too slow for N=25,000. But starting from too few nodes misses optimal paths.

After several iterations (Python TLE’d on tests 8-10, C with all nodes TLE’d on 9-10, C with only leaves gave wrong answers), the solution that passed adapts based on graph size:

 1if (n <= 100) {
 2    // Small: try all nodes
 3    for (int i = 1; i <= n; i++) dfs(i, -1, tails, &size);
 4} else if (n <= 500) {
 5    // Medium: leaves + degree-2 nodes
 6    for (int i = 1; i <= n; i++) {
 7        if (graph_size[i] <= 2) dfs(i, -1, tails, &size);
 8    }
 9} else {
10    // Large: smart selection of ~200 leaves
11    // Prioritize lowest and highest rank leaves
12}

Most optimal paths have at least one leaf endpoint, so focusing on leaves covers most cases. For star-shaped graphs with many leaves, selecting the 100 lowest and 100 highest rank leaves provides good coverage without TLE.

Flag

Flag: HTB{bl4d3_s3qu3nc3_unbr0k3n}


Fivefold Door [medium]

Description: Beneath Ishigaki-tori, the Fivefold Door sleeps—its stone crowded with the beasts of old clans, carved out of order by time and ruin. Only a rising cadence of strength will wake the seal: each sigil stronger than the last. You hold the full sequence. Find the longest ascent the door will still recognize—before the echo fades.

Points: 900 | Difficulty: Medium

Analysis

A direct application of the classic LIS problem. Given N values (up to 200,000), find the length of the longest strictly increasing subsequence.

The O(N²) DP approach won’t work for N=200,000, so we need the O(N log N) solution using binary search. The trick is maintaining a tails array where tails[i] holds the smallest tail element of all increasing subsequences of length i+1.

For each element, binary search for its position in tails:

  • If it’s larger than all elements, append it (found longer subsequence)
  • Otherwise, replace the first element >= it (keep smallest possible tails)

Exploitation

 1def longest_increasing_subsequence(n, arr):
 2    tails = []
 3
 4    for num in arr:
 5        left, right = 0, len(tails)
 6        while left < right:
 7            mid = (left + right) // 2
 8            if tails[mid] < num:
 9                left = mid + 1
10            else:
11                right = mid
12
13        if left == len(tails):
14            tails.append(num)
15        else:
16            tails[left] = num
17
18    return len(tails)
19
20n = int(input())
21arr = list(map(int, input().split()))
22print(longest_increasing_subsequence(n, arr))

The algorithm runs in O(N log N) time with O(N) space. This challenge serves as a foundation for BladeMaster - understanding the tails array and binary search approach is essential for the tree variant.

Flag

Flag: HTB{LIS_0f_th3_f1v3}


Drumming Shrine [easy]

Description: At dusk, Mount Tsukimori breathes. The old shrine’s drums answer with a pulse that never quite fades—steady, familiar, unsettling. Is it the wind finding the same grooves in cracked wood, or a spirit caught in a loop, replaying a perfect rhythm it refuses to forget? Listen closely. If the mountain is repeating itself, you’ll hear the seam.

Points: 875 | Difficulty: Easy

Analysis

Given a sequence of N beats, determine if the entire rhythm can be obtained by repeating a smaller prefix. For example, [2, 1, 2, 1, 2, 1] is [2, 1] repeated 3 times, so the answer is YES.

The approach is straightforward: check each possible prefix length from 1 to N-1. Only lengths that divide N evenly can produce the full sequence through repetition. For each valid length, verify the prefix repeats correctly using modulo indexing.

Exploitation

 1n = int(input())
 2beats = list(map(int, input().split()))
 3
 4found = False
 5for prefix_len in range(1, n):
 6    if n % prefix_len == 0:
 7        prefix = beats[:prefix_len]
 8
 9        matches = True
10        for i in range(n):
11            if beats[i] != prefix[i % prefix_len]:
12                matches = False
13                break
14
15        if matches:
16            found = True
17            break
18
19print("YES" if found else "NO")

The optimization of only checking divisors of N significantly reduces the number of candidates. Worst case is still O(N²) when N has many divisors, but in practice this runs fast enough for N ≤ 200,000.

Flag

Flag: HTB{3t3rn4l_sh1nju_p4tt3rn}


The Paper General’s Army [very easy]

Description: When the moon was high and the world was quiet, the Paper General would whisper a single word: “Fold.” Under that silver glow, each soldier of parchment would split like a reflection in still water, doubling the ranks without a sound. Only a trace of that forgotten ritual remains — the secret mathematics behind a growing legion. Tonight, beneath the same moon, you are given the chance to summon the Folded Army yourself.

Points: 875 | Difficulty: Very Easy

Analysis

The simplest challenge of the set - pure math. Starting with N paper soldiers, each fold doubles the count. After K folds: result = N * 2^K.

Given the constraints could be large (K up to potentially 60+), using pow(2, K) with multiplication works but bit shifting is cleaner and faster.

Exploitation

1T = int(input())
2for _ in range(T):
3    N, K = map(int, input().split())
4    result = N << K  # N * 2^K
5    print(result)

For example:

  • N=3, K=4: 3 « 4 = 3 * 16 = 48
  • N=10, K=2: 10 « 2 = 10 * 4 = 40
  • N=2, K=16: 2 « 16 = 2 * 65536 = 131072

Python handles arbitrary precision integers natively, so no overflow concerns.

Flag

Flag: HTB{th3_f0ld3d_l3g10n_r1s3s_1n_th3_m00nl1ght}