Debugging Under Time Pressure

beginner20 min

The situation

The judge tells you almost nothing. You get a verdict — wrong answer, time limit exceeded, runtime error — and usually not the input that caused it. You cannot print debugging output to the judge and read it back. So contest debugging is mostly about narrowing the possibilities from your own machine.

Working through a checklist beats staring at the code. Most contest bugs are a handful of recurring mistakes rather than deep logic errors.

Read the verdict first

Each verdict points at a different class of problem.

What each verdict usually means

VerdictMost likely causes
Wrong answer on all casesMisread the problem; wrong output format; off-by-one
Wrong answer on large cases onlyInteger overflow; an assumption that holds for small inputs
Wrong answer on a few casesUnhandled edge case — empty input, N = 1, ties, duplicates
Time limit exceededComplexity too high; Scanner; missing flush causing a hang
Runtime errorArray index out of bounds; stack overflow from deep recursion; divide by zero
Memory limit exceededArray too large; boxed collections; a leak in a recursive structure

“Wrong answer on large cases only” is worth calling out. It almost always means overflow. Check every multiplication and every accumulator before looking anywhere else.

The checklist

Work through these in order

  1. Re-read the problem statement. Specifically the output format. Does it want one number per line, or all on one line? Does it want the count or the list? Reading the statement again finds more bugs than reading the code again.
  2. Check the sample. Does your program produce the sample output exactly, including whitespace and line breaks?
  3. Check for overflow. Every int multiplication, every sum. Apply the constraint arithmetic from Data Types and Overflow.
  4. Test N = 1 and the smallest legal input. A surprising number of solutions break here.
  5. Test the extremes. All values equal, all values distinct, already sorted, reverse sorted, all zeros, all negatives.
  6. Check loop bounds. < versus <=, and whether indices are 0-based or 1-based. Problems often state input as 1-based.
  7. Confirm the complexity against the constraint table in Time Complexity. If it does not fit, no amount of debugging will help.

Printing to standard error

You can print diagnostics without polluting your answer. System.err goes to a different stream, so it does not affect the output the judge compares.

System.err.println("after sort: " + Arrays.toString(a));

When you run locally with output redirected to a file, the diagnostics still appear in your terminal while the answer goes to the file. Remove or leave them — either is safe, since the judge ignores standard error. Leaving them in costs a little time on huge outputs, so remove them if you are near the limit.

Stress testing

This is the technique for a wrong answer you cannot find by inspection. You write a slow-but-obviously-correct solution, generate small random inputs, and compare.

How to set it up

  1. Write Brute.java — the simplest correct solution you can, ignoring efficiency entirely.
  2. Write a generator that produces a small random valid input, for example N between 1 and 6 with values between 1 and 5.
  3. Run both programs on the same input and compare outputs.
  4. Repeat until they disagree. The first disagreement is a small failing case you can trace by hand.

Small is the important part. A failing case with N = 4 can be traced on paper; one with N = 100,000 cannot.

// Generator sketch — writes one random case to stdout
import java.util.*;

public class Gen {
    public static void main(String[] args) {
        Random rnd = new Random(Long.parseLong(args[0]));
        int n = 1 + rnd.nextInt(6);
        StringBuilder sb = new StringBuilder();
        sb.append(n).append('\n');
        for (int i = 0; i < n; i++) {
            sb.append(1 + rnd.nextInt(5)).append(i + 1 < n ? ' ' : '\n');
        }
        System.out.print(sb);
    }
}

Passing the seed as an argument means a failing case is reproducible — you can rerun exactly the input that broke it.

Recursion depth

Java’s default thread stack is small enough that recursion depth around 10,000 to 20,000 can throw StackOverflowError. Graph problems on a path-shaped tree with 200,000 nodes will hit this with a recursive DFS.

Two fixes. Rewrite the traversal iteratively with an explicit stack, or run your solution on a thread with a larger stack:

public class Main {
    public static void main(String[] args) {
        new Thread(null, Main::solve, "main", 1 << 26).start();
    }

    static void solve() {
        // recursive code here — now has a 64 MB stack
    }
}

The iterative rewrite is more robust and is what to reach for if you have time. The thread trick is faster to apply mid-contest.

Habits that prevent bugs rather than find them

  • Write the input reading, then immediately print what you read and check it against the sample. Confirm the parse before writing the algorithm.
  • Use long for any accumulator by default. The cost is nothing; the risk of int is a silent wrong answer.
  • Name variables after what the problem calls them. If the statement says N cows and K barns, do not rename them to a and b.
  • Solve the sample by hand before coding. If you cannot, you have misunderstood the problem, and no code will fix that.

Build your stress tester

Do this now, while nothing is at stake:

  1. Take any problem you have already solved.
  2. Write a brute-force version that is obviously correct for tiny inputs.
  3. Write a generator that produces small random cases with a seed argument.
  4. Write a short script that loops over seeds, runs both, and stops on the first mismatch.
  5. Deliberately introduce an off-by-one bug into your fast solution and confirm the tester catches it.

Step 5 is the one that proves the setup works.

Next