🌲 Invert a binary tree! 🌲
Except with 3 catches:
- It must invert the keys ("bit-reversal permutation")
- It must be a dependency-free, pure recursive function
- It must have type
Bit -> Tree -> Tree(i.e., a direct recursion with max 1 bit state)
- It is somehow NOT on the internet. (AFAIK)
- Humans can solve it. (I've done it in ~1h.)
- It requires reasoning. (My head hurts!)
- The solution is simple. (7 lines of code!)
- Obvious pre-requisite to automate CS research.
- Honestly, it would make me believe I'll be automated.
I claim no AI will EVER solve this problem. If you prove me wrong, HOC will grant you $10k!
- You must give it an approved prompt, nothing else.
- It must output a correct solution, passing all tests.
- You can use any software or AI model.
- You can let it "think" for as long as you want.
- You can propose a new prompt, as long as:
- It imposes equivalent restrictions.
- It clearly doesn't help the AI.
- Up to 1K tokens, all included.
- Common sense applies.
{-# OPTIONS --no-termination-check #-}
-- Let Tree be a Perfect Binary Tree:
data Nat : Set where
Z : Nat
S : Nat → Nat
{-# BUILTIN NATURAL Nat #-}
data Bit : Set where
O : Bit
I : Bit
data Tree (A : Set) : Nat → Set where
N : ∀ {d} → Tree A d → Tree A d → Tree A (S d)
L : Nat → Tree A Z
-- Your goal is to implement an 'invert' function that performs a bit-reversal
-- permutation on a Tree, respecting the following limitations:
-- 1. You can NOT define or use any function other than 'invert'.
-- 2. You can NOT use any type not defined above (Nat, Bit and Tree).
-- 3. You can NOT use loops (but you can call 'invert' recursively).
-- 4. You can NOT use mutability. It must be a pure Agda function.
-- 5. You can use 1 bit of state (as an extra argument).
-- 6. You can use pattern-matching and constructors freely.
--
-- Example:
-- input = (N(N(N(L 0)(L 1))(N(L 2)(L 3)))(N(N(L 4)(L 5))(N(L 6)(L 7))))
-- output = (N(N(N(L 0)(L 4))(N(L 2)(L 6)))(N(N(L 1)(L 5))(N(L 3)(L 7))))
-- Because that's the bit-reversal permutation of the original tree.
--
-- Now, complete the program below, with a valid implementation of 'invert':
invert : ∀ {A d} → Bit → Tree A d → Tree A dtype Nat = number;
type Bit = false | true;
type Tree<A> = [Tree<A>, Tree<A>] | Nat;
// Your goal is to implement an 'invert' function that performs a bit-reversal
// permutation on a Tree, respecting the following limitations:
// 1. You can NOT define or use any function other than 'invert'.
// 2. You can NOT use any type not defined above (Nat, Bit and Tree).
// 3. You can NOT use loops (but you can call 'invert' recursively).
// 4. You can NOT use mutability. It must be a pure function.
// 5. You can NOT use primitive JS operators or functions.
// 6. You can use 1 bit of state (as an extra argument).
// 7. You can only use the operations allowed below.
//
// Operations allowed:
// - Destructing (`const [a,b] = value`)
// - Variables (`const x = value`)
// - Branching (`if (x) { ... } else { ... }`)
// - Recursion (`invert(_, _)')
// - `Array.isArray`
//
// All other operations are not allowed.
//
// Example:
// input = [[[[0,1],[2,3]],[[4,5],[6,7]]]]
// output = [[[[0,4],[2,6]],[[1,5],[3,7]]]]
// Because that's the bit-reversal permutation of the original tree.
//
// Now, complete the program below, with a valid implementation of 'invert':
function invert<A>(bit: Bit, tree: Tree<A>): Tree<A> {
...
}
// A test:
const tree: Tree<Nat> = [[[[0,1],[2,3]],[[4,5],[6,7]]],[[[8,9],[10,11]],[[12,13],[14,15]]]];
console.log(JSON.stringify(invert(true, tree)));✨ If it can't invert a tree, it won't solve P=NP. ✨
@Lorenzobattistela
BONUS: I think it helps to be more explicit about what would actually count as forbidden help/extra algorithmic hints. IMO, it became clearer once I understood what makes the solution work. I spent (embarrassingly!) hours solving this by hand first to get a sense of what tricks the model had to discover on its own.
The hard part (IMO) is splitting the problem into two "natural" recursive phases that make bit-reversal work. As we know, if we write a leaf's path as
b : q, wherebis the first branch choice at the root (are we going to the left (0) or the right (1) subtree?) andqis the rest of the path, then bit-reversal sends it toreverse(q) ++ [b]. This suggests that we should first recursively reverse the inner path inside each of the two subtrees, and only after that move the original top-level choiceball the way down to the bottom.That's why the solution naturally has two phases. For example, take a depth-3 tree with 8 leaves:
[0,1,2,3 | 4,5,6,7](where|separates the left and right subtrees). For easier visualisation, instead of using decimal numbers (0-7), let's use the 3-bit binary encodings (which also conveniently represent the path to each leaf!), to make the bit-reversal more visually obvious, since we'll see the bits being reversed:[000, 001, 010, 011 | 100, 101, 110, 111].Phase 1 is the recursive step: first, bit-reverse the inner 2-bit paths within each subtree separately. This gives
[000, 010, 001, 011 | 100, 110, 101, 111]. But of course, we're not done: each leaf now is still in its original subtree (the inner bits are reversed, but the root bit is still sitting at the top).That's where phase 2 comes in, with the actual/main insight: we need to stop grouping leaves by which half/subtree they came from, and instead regroup corresponding positions across the two halves. It's a "zip then flatten" operation: pair
000with100(first leaf of each subtree), pair010with110(second leaf of each subtree), etc (each pair share the same path suffix, the only difference is their first root bit, and pairing them means this first root bit is now pushed to the leaf level). This gives[000, 100, 010, 110, 001, 101, 011, 111], and we can check that each leaf's label is now indeed the reverse of its position:001sits at position100,110sits at position011, etc. So what we did, concretely, was turn the original "which subtree was I in?" bit (first/root bit) into the final "which leaf sibling am I?" bit (last/leaf bit): the top-level choice slid down to the leaf level. (In tree form, this is the non-obvious move fromNode (Node a b) (Node c d)to the recursive cross-pairing onNode a candNode b d.)In Haskell, the two-function version looks like this:
inverthandles phase 1 (reversal of inner paths), andmergehandles phase 2 (the cross-pairing that pushesbto the bottom). This one is easier to understand, but it violates Victor's original single-function constraint because it uses a non-trivial extra recursive helpermerge. In comparison, this is a valid solution found by o3 11 months ago:The (legal) extra bit/boolean of state in the
invertHelper :: Bool -> Tree a -> Tree aauxiliary function tracks which phase we're in:True= "still recursing into subtrees",False= "now cross-pairing". So the whole difficulty is discovering this decomposition, inventing the cross-pairing operation, and realising that this allowed extra bit of state can encode switching between the two phases.As a bonus, here is my hand-written
invertHumanthat solves it without the extra bit of state. I used it as my own reference implementation when developing the testing setup:It merges both phases: after
invertHuman landinvertHuman r, the childrenll, lr, rl, rrare already bit-reversed. To do the cross-pairing without a separate (forbidden!)mergefunction, it formsNode (invertHuman ll) (invertHuman rl)and similarly on the right. The reason for the extrainvertHumancalls onll,lr,rl,rris that bit-reversal is involutive (its own inverse): applying it once undoes the earlier inner reversal, so that the outer call can then perform the right reversal-and-cross-pairing at the larger scale. Strictly harder than the OG extra-bit-of-state version, this is what my "no-helper prompt" was hoping the AI models could find on their own.So, for me, forbidden help would be anything that gives away any of the structure mentioned above: telling the model there are two phases, telling it what the boolean means operationally, telling it to merge or interleave corresponding branches from the two halves, or spelling out the
Node a c/Node b dcross-pairing pattern. My prompts do none of that because they ask to write the solution in this form:-> the one-line wrapper around the inner function with one extra boolean (legal help) does not reveal any of the above (it only preserves the one-bit allowance that Victor had already allowed). As a comparison, I think this clarifies why msukiasyan's follow-up instruction was forbidden help: their AI model had already discovered the two phases as two separate functions (
invertandcombine). The human then told the model to "refactor into a single function with one boolean", which gave away the phase encoding for free (my prompts don't do this: the model starts from a blankinvertHelper :: Bool -> Tree a -> Tree a/invert_helper(tree, flag)and has to discover both phases from scratch, which faithfully preserves the original difficulty IMO).