Writing a problem
Authoring a coding problem end to end - functions and design classes, test cases including generated ones, how answers are compared, time limits, the checks that must pass before it publishes, importing many problems, and what review looks at.
Problems on Revquix are not all written by us. Anyone can ask for the ability to write one, and the process is deliberately more demanding than a text box - a broken problem costs every person who attempts it far more than it cost to write.
Getting access#
Professional mentors have authoring already. If you conduct paid mock interviews on Revquix, your authoring page is open to you now - nothing to request.
For everyone else authoring is off by default and is requested in one click from the same page. Once granted you can create problems, edit your own drafts, and send them for review.
Publishing directly - and reviewing or editing other people's - is a separate, higher permission held by the Revquix team. Your own problems are yours: no other author can open or change them.
Exam problems#
A problem can be written for your own assessments instead of for Practice. An assessment author starts one from a coding question's Write a coding problem, or by choosing My assessments when asked what a new problem is for.
It is built exactly like a practice problem, and it has to pass the same checks. What differs is where it goes:
- It is never sent for review and never published. Its one step after the checks pass is Make exam-only, which cannot be undone.
- Once exam-only it is asked only inside your assessments - never listed, searched or solvable in Practice, and no other author can put it in their paper.
- It freezes once an assessment that asks it is in review, published or has been sat. Candidates are marked against it, so a change would move their marks under them. To change one, write a new problem and swap it in through a revision of the assessment.
What you build#
A problem is not just a statement. The editor walks you through it:
-
The details - title, difficulty, one to five topics, and the constraints. The first topic is the primary one and is what shows on the problem list.
-
What the solver writes - either a function (a method name, its parameters and a return type) or a class for a design problem (a class name, its constructor and its methods). You write this once, language-neutrally, from a fixed list of types.
From it the platform generates both the stub each solver sees and the code that calls it. You never write either, in any language, which is why adding a language to your problem is a checkbox rather than a rewrite.
-
The statement - the prose, in a rich editor.
-
The test cases - a handful of samples, at least eight hidden ones, and optionally a few generated ones at full size. You supply the inputs only. A sample can also carry a sentence of explanation, shown under that example on the problem page.
-
The reference solution - a correct solution, in every language you offer. Optionally a slow solution too (see below), and a checker if answers are compared by one.
-
Hints and the Approach, optionally - up to five hints a stuck solver opens one at a time, and the walkthrough they read on the Approach tab. The Approach has its own Show to solvers switch, and can be written or changed after the problem is live.
-
An input generator for Analyse my solution, optionally - a small program that builds one input of size
n, which enables Analyse my solution for people solving your problem. It can be added later, to a problem that is already published.
Types#
Parameters and return values come from a fixed list, the same in every language:
| Group | Types |
|---|---|
| Scalars | int, long, double, boolean, char, string |
| Arrays | int[], long[], double[], boolean[], char[], string[] |
| Matrices | int[][], long[][], double[][], boolean[][], char[][], string[][], int[][][] |
| Nodes | ListNode, TreeNode, ListNode[], TreeNode[], NaryTreeNode, GraphNode, RandomListNode, DoublyListNode |
| Collections | Map<string,int>, Map<int,int>, NestedInteger, NestedInteger[] |
| Return only | int?, long?, double?, boolean?, char?, string? (the value or null), ListNodeRef, TreeNodeRef, void |
| Interactive | VersionControl, GuessGame, MountainArray, ArrayReader, BinaryMatrix |
A few of these need a word:
- Lists and trees use the encodings solvers already know:
[1,2,3]for a list, level order withnullfor a tree ([3,9,20,null,null,15,7]). An N-ary tree is level order withnullclosing each node's children ([1,null,3,2,4,null,5,6]); a graph is an adjacency list over the values 1 to n ([[2,4],[1,3],[2,4],[1,3]]); a random-pointer list is[[7,null],[13,0],[11,4]]; a multilevel doubly linked list writes a node with a child list as[value, childList], so[1,2,[3,[7,8]],4]hangs 7 and 8 under 3. - The four shapes
NaryTreeNode,GraphNode,RandomListNodeandDoublyListNodeare all calledNodein code, as solvers expect, so one problem can use only one of them. A returned graph or random-pointer list must be a copy: returning nodes of the input is a wrong answer. - Cycles and shared nodes. A
ListNodeinput can also be written as an object:{"values":[3,2,0,-4],"pos":1}makes the tail link back to node 1 (a cycle);{"values":[5,6,1],"joinParam":0,"joinIndex":2}continues into node 2 of parameter 0, so the two lists share a tail;{"nodeOf":0,"index":3}is node 3 of parameter 0. ATreeNodeinput can be{"nodeOf":0,"val":5}: the node with value 5 in parameter 0's tree, which is what lowest common ancestor needs. ListNodeRefandTreeNodeRefreturn a node of the input - "return the node where the cycle begins". The solver's method returns aListNodeorTreeNodeas usual; the answer is graded by identity, as the node's position in the input (-1for a node that is not from the input,nullfor none).- In place. When the method returns
void, choose which parameter carries the answer. Arrays, matrices, maps, lists and trees can all be modified in place; the judge reads that parameter back after the call. - Interactive problems hide their data behind an API the solver calls.
VersionControlandGuessGameare hidden parameters: they never appear in the solver's method, they installisBadVersion(version)orguess(num).MountainArray,ArrayReaderandBinaryMatrixare passed in as objects. Set a call limit, the most calls one case may make: a solver over it gets Call limit exceeded, which is what stops a linear scan passing a binary-search problem.
Design problems#
Choose A class to write an LRU cache, a min stack, a Trie or an iterator. You declare the class
name, the constructor's parameters and each method with its own parameters and return type (void
included). Every language's stub is that class.
A test case is the operation log solvers know from interviews: the operations and their arguments, in two parallel lists, with the class name first.
[["LRUCache","put","put","get","put","get"], [[2],[1,1],[2,2],[1],[3,3],[2]]]
The judge constructs the class, calls each method in order and records what it returned, with
null for the constructor and for void methods, so this case's answer is
[null,null,null,1,null,-1].
Test cases#
Generated cases#
A typed case is limited by size: one run carries at most 96 KB of test data, so an input of 10^5
numbers cannot be typed or stored. A generated case is built inside the sandbox instead, from a
seed and a size, and is identical in every language. You add one with a seed and an n, for example
{"seed": 7, "n": 100000}, up to six per problem.
You describe once how to build each parameter, and bounds may use n:
{"params": {
"nums": {"kind": "array", "length": "n", "min": -1000000000, "max": 1000000000},
"target": {"kind": "int", "min": "-2*n", "max": "2*n"}
}}
| Parameter type | Kinds |
|---|---|
int, long | int (min, max) |
double | double (min, max, decimals) |
boolean, char, string, char[] | boolean, char (alphabet), string (length, alphabet) |
int[], long[], ListNode | array (length, min, max, order: random, ascending or descending, distinct) or permutation (length, start) |
string[] | strings (count, minLength, maxLength, alphabet) |
int[][], long[][] | grid (rows, cols, min, max), edges (nodes, edges, shape: tree, graph, dag or connected, directed, weights, base), pairs (count, min, max) |
char[][] | grid (rows, cols, alphabet) |
TreeNode | tree (nodes, min, max, shape: random, bst, left, right or complete) |
A generated case is graded like any other when its answer is small. A large answer is compared by a
fingerprint of the exact value, which works for an exact comparison of anything without decimals;
otherwise it is graded on time alone. Mark a case timing only yourself ("graded": false) when
random inputs might break a promise your statement makes, such as "exactly one answer exists".
Comparing answers#
How a returned value is compared with the expected one is part of the problem. Solvers see the rule on the problem page.
| Mode | The rule |
|---|---|
| Exactly | The answer must match exactly. Numbers compare by value, so 2 and 2.0 are equal. |
| Any order (outer only) | The outer list may be in any order. Everything inside each item must match exactly. For permutations, where each inner order is the answer. |
| Any order (inner only) | Each inner list may be in any order. The outer list must keep the expected order. |
| Any order (outer and inner) | Lists at every level may be in any order. For subsets, combination sums and grouped anagrams. |
| Within a tolerance | Numbers match within a tolerance: abs(answer - expected) <= tolerance * max(1, abs(expected)). The default is 1e-6; set your own on the problem. |
| Checker | Your own Python decides. For problems with many correct answers. |
Every order-free mode compares multisets: duplicates count, so [1,1,2] never equals
[1,2,2].
Checkers#
When a problem has many valid answers - any topological order, any peak index, any valid placement -
choose Checker and write check(input, expected, output) in Python:
def check(input, expected, output):
n, edges = input
if sorted(output) != list(range(n)):
return (False, "the answer must list every node exactly once")
position = {v: i for i, v in enumerate(output)}
for u, v in edges:
if position[u] > position[v]:
return (False, "edge %d -> %d is violated" % (u, v))
return True
input is the case's argument list, expected is your reference solution's answer and output is
the solver's. Return True, False, or (False, "message"); the message is shown with the wrong
answer. The checker runs in its own sandbox, separately from the solver's code, and the checks run it
over your reference solution's answers too, so a checker that rejects a correct answer is caught
before anyone else sees it.
Time limits#
Every case of a run shares one five-second budget. On top of that you can set a per-case time limit, which is what makes the stated complexity decide pass or fail. It is stated for C++; other languages get a multiple (Java, JavaScript and TypeScript twice it, Python four times), and the editor shows each language's number. A case that answers correctly but takes longer than its language's limit is Too slow.
To prove the limit actually separates the intended solution from the naive one, add a slow solution: a correct but deliberately slow version, in any language. The checks run it and expect it to time out on at least one case. If it passes everything in time, your constraints do not tell the two apart, and the check says what to change: a larger generated case or a lower limit.
The checks#
Before a problem reaches a reviewer - and before a direct publish - the platform verifies it. The panel runs them on demand and this is your main loop: draft, run the checks, fix what they find, repeat.
| Check | What it needs |
|---|---|
| Title, difficulty and constraints | All present. |
| The problem statement | Not empty after sanitising (embeds and inline styles are removed). |
| Topics | One to five. |
| Test cases | At least two samples and at least eight hidden or generated cases, fifty at most. |
| Reference solutions | One for every language you offer. |
| How answers are judged | A checker when the mode is Checker; a warning when an interactive problem has no call limit. |
| Generate the expected outputs | Your first language's reference solution returns a value for every case. |
| Solve it in each language | Your reference solution is accepted on every case, in every language you offer, uses at most 60% of the run budget, and stays under 60% of each case's limit. |
| The slow solution is too slow | Only when you add one: it must time out on at least one case. |
| Originality | Compared with every Revquix problem, published or in review, by title, examples and behaviour. A warning, never a block. |
| Originality against well-known public problems | Compared with the worked examples and titles of well-known public problems. A warning, never a block. |
Solving it in every language is the one that matters. It means publishing is not a claim that the problem works - the platform has literally run your solution against your tests and watched it pass.
The 60% headroom is deliberate: your reference solution is the optimal one, and a student's correct-but-unoptimised solution is legitimately slower. A budget with no slack would reject correct work.
The checks cost several sandbox runs, so they are run when you ask rather than on every keystroke, and the panel shows progress while they go.
Writing many problems#
The authoring page can import problems from files and export any problem you can edit. A package is one JSON document holding everything the editor collects: details, topics, the signature, the comparison, limits, statement, cases, reference solutions, hints, the Approach, a checker, a slow solution and generators. It never carries expected outputs.
Import validates the whole file first and names every field that is wrong (cases[3].input, say),
and creates nothing until it is right. A good file becomes a draft with its checks queued; it is
never sent for review on its own. Exporting a problem and importing it again gives you the same
draft.
If you curate tracks, a problem's editor also shows which tracks it is in and lets you place it - "Phase 3, position 12" - and the tracks view lists each track's problems in order with their status.
Review#
Sending a problem for review locks it - editing it sends it back to draft, so a reviewer is never reviewing a moving target.
A human reviewer looks at the things a machine cannot:
- Is it original? A pasted statement from elsewhere is the thing this step exists to catch.
- Is the prose comprehensible to the person it is aimed at?
- Is the difficulty honest?
- Are the topics right? Wrong tags poison the filters and the tracks.
- Are the hidden cases adequate? The empty input, the single element, the maximum constraint, the duplicates. This is the check that separates a real problem from a toy.
Submitting sends you a confirmation email, so you have a record of what went in and when.
After that you are notified when it is approved, when changes are requested (with the note explaining what), and when it goes live. You are also notified the first time somebody solves it.
After it is published#
Your problem gets its number at publish, not at creation - so abandoned drafts do not burn numbers and leave the published set reading 1, 2, 5, 9.
Its URL is fixed from that point. A problem can later be unlisted - the link still works, it just leaves the lists - or retired, which keeps it readable for anyone who already solved it.
If either happens, you are told, and you are told why: the reason reaches you in your notifications and by email. A draft that never published can also be deleted outright, and that carries a reason too. Nothing you wrote is ever removed silently, and if you think a decision was wrong, reply to the email and say so.
Related#
Was this page helpful?