Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 1

Defining Problem Contracts, Invariants, and Edge Cases

Recommended resources

Welcome to your first lesson. With your online assessments starting in just a few days, our immediate focus is on maximizing your performance under pressure. This lesson tackles the most critical and often-overlooked step in solving any coding problem: meticulously understanding and defining it before you start writing code. Mastering this will make you faster and more reliable, not by coding quicker, but by thinking more clearly from the start.

Today's learning outcome is to convert a problem statement into a precise input-output contract, invariants, and edge cases before coding. We will build a systematic process for deconstructing any problem, ensuring you solve the right problem correctly on your first attempt, which is crucial in a timed Online Assessment (OA).

The High Cost of Misunderstanding

In a timed OA, the costliest mistake is not a syntax error, but a logical one born from a flawed understanding of the problem. Wasting 15 minutes debugging a wrong approach can be the difference between a pass and a fail. The goal is to trade a few minutes of structured thinking upfront for a smoother, faster, and more correct implementation.

Let's begin by watching a few clips that frame this pre-coding phase from a practical, interview-oriented perspective.

How to Solve ANY Coding Interview Question in 6 Steps

First, watch this video from Anthony D. Mays. It outlines the initial steps to take when you first encounter a problem.

Pay close attention to his advice in the first three steps: repeating the question, asking clarifying questions, and working through examples. These actions are your first line of defense against misunderstanding.

Problem Solving Techniques - For Programming Problems & Interviews

Next, watch this segment from Tech With Tim, where he demonstrates this process on a specific problem.

Focus on how he methodically ensures he understands the problem statement. Watch from understanding the problem and then see how he uses examples and definitions to solidify his grasp of the core requirements and potential edge cases. This is exactly the kind of active, critical reading you should perform.


The Specification as a Contract

The process you just saw—asking questions and creating examples—is how you establish a specification. Think of a specification as a formal contract between you (the implementer) and the online assessment platform (the client). This contract removes ambiguity and defines exactly what is expected.

A good specification has three main parts:

  1. Preconditions: What can you assume about the input? (The requires clause)
  2. Postconditions: What must your code produce as output? (The returns clause)
  3. Edge Cases: How does the contract handle boundary, empty, or unusual inputs?

Abstraction and Specification

This article from Cornell University provides a formal structure for this "contract." It will help you organize the questions you ask and the examples you build into a coherent specification.

Please read the following parts: The introduction, which defines a specification as a contract. The section on "Specifications for functions". Focus on the descriptions of the Returns clause, the Requires clause, and the value of an Examples clause.

Let's apply this to a simple, familiar problem: "Find the first occurrence of a number in a sorted array."

A weak understanding might be just that. A strong contract would be:

  • Function Signature: int find_first(const vector<int>& nums, int target)
  • Preconditions (Requires):
    • nums is a vector of integers.
    • nums is sorted in non-decreasing order.
    • Values in nums and target are within the standard integer range.
  • Postconditions (Returns):
    • Returns the smallest index i such that nums[i] == target.
    • If no such index exists, returns -1.
  • Edge Cases (Examples):
    • nums = [], target = 5 -> returns -1
    • nums = [1, 2, 3], target = 4 -> returns -1
    • nums = [5, 5, 5], target = 5 -> returns 0
    • nums = [1, 2, 4, 4, 8], target = 4 -> returns 2

This level of precision is your primary goal before writing any code. It transforms a vague prompt into a set of verifiable test cases.


Invariants: The Blueprint for Your Algorithm

Once you have the contract (the what), you need a plan for the implementation (the how). This is where invariants come in. An invariant is a property or condition that remains true at a specific point in your code, typically at the beginning of each loop iteration.

Thinking in terms of invariants forces you to build your algorithm's logic step-by-step, ensuring correctness along the way. It is the most powerful technique for avoiding off-by-one errors and flawed pointer logic, especially in array-manipulation problems.

4. Loop Invariants | CS 2110

This next resource, also from Cornell, provides a deep dive into loop invariants. It's dense, but the methodology is a game-changer for writing correct loops on the first try.

First, read the introduction to understand the core idea. Focus on the definition of an invariant and the definition of a loop invariant. Next, review the section "Visualizing Loop Behavior". The key idea is to visualize the state of your data at three points: before the loop (Pre), after the loop (Post), and during an arbitrary step (Inv). The "Inv" diagram is your invariant. Read from the start of the section up to the code block showing the while loop stub: Visualizing Loop Behavior. Finally, read the summary of the strategy for developing a loop, which synthesizes these ideas into a clear, repeatable process: Recapping This Example.


Applying Invariants: The Partition Problem

Let's use this invariant-driven method to solve a classic problem: partitioning an array. This is the core logic behind algorithms like Quicksort and many two-pointer problems.

Problem: Given an array of integers, rearrange the elements such that all even numbers appear before all odd numbers. Return the index of the first odd element.

Here's how we apply the 7-step process:

  1. Identify Variables: We need to scan the array and swap elements. A common approach is to use two pointers, i and j, that define boundaries for different regions of the array. Let i be the boundary of the "even" section and j be the boundary of the "odd" section.

  2. Draw "Pre" and "Post" Diagrams:

    • Pre (Before Loop): The array is in an unknown state. The "even" and "odd" partitions are empty.
    • Post (After Loop): The array is partitioned. All elements from index 0 up to some index are even, and the rest are odd.
  3. Draw "Inv" Diagram (The Invariant): This is the crucial step. We define the meaning of our pointers during the loop. Let's establish an invariant that holds at the start of every loop iteration:

    • nums[0...i-1] contains only even numbers.
    • nums[j...n-1] contains only odd numbers.
    • nums[i...j-1] contains elements we have not yet processed.

    Visually:

    [   evens   |   unknown   |   odds   ]
    ^           ^             ^          ^
    0           i             j          n
    
  4. Write the Invariant: Based on the diagram, our invariant is: "At the start of each iteration, nums[..i) is even and nums[j..) is odd."

  5. Derive Initialization: To make the invariant true before the first iteration, the even and odd partitions must be empty. This happens if we initialize i = 0 (the range nums[..0) is empty) and j = n (the range nums[n..) is empty).

  6. Derive Loop Guard: The loop should continue as long as there are unknown elements to process. The "unknown" region is nums[i...j-1]. This region exists as long as i < j. When i and j meet, the unknown section vanishes, the loop terminates, and our Post-condition is met. The return value will be i (or j), which is the index of the first odd element. So, the guard is while (i < j).

  7. Develop Loop Body: Inside the loop, we consider the element at the boundary of the unknown region. Let's inspect nums[i].

    • If nums[i] is even: It's already in the correct partition. We can expand the even partition by one. We do this by incrementing i. The invariant is maintained.
    • If nums[i] is odd: It belongs in the odd partition. To maintain the invariant, we must move it there. We can swap nums[i] with the element at the edge of the unknown region, nums[j-1]. After the swap, we know the element now at j-1 is odd, so we can shrink the unknown region by decrementing j. We do not increment i, because the new element at nums[i] (from the swap) is still unknown and needs to be processed in the next iteration.

This systematic process, rooted in a visual invariant, builds a correct, bug-free algorithm for a non-trivial problem. This same pattern applies to hundreds of array, string, and list problems.


Conclusion

In this lesson, you've learned a structured, methodical approach to the crucial pre-coding phase of problem-solving. This isn't about adding bureaucracy; it's a "measure twice, cut once" philosophy that builds speed by eliminating wasted time on debugging flawed assumptions.

Key Takeaways:

  • The Contract: Before coding, always establish a precise input-output contract. Define your preconditions (what you can assume), postconditions (what you must deliver), and critical edge cases.
  • The Blueprint: Use invariants, often visualized with array diagrams, to design the internal logic of your loops. An invariant acts as a verifiable blueprint that guides you to a correct implementation.
  • The Process:
    1. Restate the problem and create examples to form a contract.
    2. Identify variables and draw Pre/Post/Inv diagrams to define an invariant.
    3. Derive the initialization, loop guard, and loop body from the invariant to build a correct algorithm.

In our next lesson, we will address another crucial pre-design question: How do you infer the required asymptotic time and space complexity from a problem's input constraints? Knowing whether you're aiming for an , , or solution is the very next step after understanding what the problem is asking.