Every programming course seems to have the same unit: print a triangle of stars, then a pyramid, and, if the syllabus is feeling ambitious, a diamond. The triangles are usually straightforward. You count. The diamond is where things often get messy: four sloping edges, a pile of possible <= and >= combinations, and a nested if that may work for one input and fail for another.
There is a more systematic way to do it. Instead of guessing the conditions, treat the loop indices as coordinates, treat the edges of the pattern as lines, and derive the boundary the same way you would in coordinate geometry.
The Grid as a Coordinate System
A nested loop like
for i in range(rows):
for j in range(cols):
visits every cell in a grid. Let i be the row and j the column. That gives us a coordinate system, with one small difference from the usual Cartesian plane: row 0 is at the top, and i increases as you move downward.
Once you look at it that way, the pattern is just a region of the grid bounded by straight lines. Give me two points on one of those edges and I can write its equation.
The tool for that is the two-point form of a line:
(j − j₁) / (i − i₁) = (j₂ − j₁) / (i₂ − i₁)
Substitute the two known grid points, simplify the equation, and you end up with a relationship between i and j. That is exactly what a nested-loop if condition needs.
The simplest case: a diagonal
Take the classic lower-triangle pattern, with one star at the top and a full row at the bottom:
*
* *
* * *
* * * *
Its slanted boundary runs from (i, j) = (0, 0) to (n, n), where n is the last row index. Using the two-point formula gives:
(j − 0)/(i − 0) = (n − 0)/(n − 0) → j = i → i = j
That line divides the grid into two regions. To decide which side belongs to the pattern, pick a point that you already know is inside it. The bottom-left cell (n, 0) works. Since n > 0, it satisfies i > j, so the inclusive condition is i >= j.
That is the whole derivation behind the usual lower-triangle condition.
Two points, the other way
Now flip the triangle so that it gets narrower as you move downward. The boundary runs from (0, n) to (n, 0), which is the anti-diagonal:
(j − n)/(i − 0) = (0 − n)/(n − 0) → j − n = −i → i + j = n
Test the top-left corner (0,0). There, i + j = 0, which is less than n. So the triangle containing that corner is described by i + j <= n.
Choose the opposite side of the line instead and you get the complementary triangle, i + j >= n.
So with just two line equations, we already have two basic pattern families.
The real target: a diamond
A diamond is just four of those sloping boundaries combined. Put it inside a (2n + 1) × (2n + 1) grid by using the midpoint of each side of the bounding square as a vertex: (0, n), (n, 0), (n, 2n), (2n, n).
Now apply the two-point formula to each edge:
| Edge | Through | Equation |
|---|---|---|
| top-left | (0, n) → (n, 0) | i + j = n |
| top-right | (0, n) → (n, 2n) | j − i = n |
| bottom-left | (n, 0) → (2n, n) | i − j = n |
| bottom-right | (n, 2n) → (2n, n) | i + j = 3n |
The equations give us the boundaries. We still need to decide which side of each line belongs to the diamond.
There is a simple way to do that. Pick a point that is definitely inside the shape. The centre (n, n) is convenient.
At that point, i + j = 2n, which is greater than n and less than 3n. Also, j − i = 0 and i − j = 0, both less than n. Those comparisons tell us which direction to use for each inequality:
if (
i + j >= n
and j - i <= n
and i - j <= n
and i + j <= 3 * n
):
print("*", end=" ")
else:
print(" ", end=" ")
Four lines, four comparisons, one diamond. There is no need to guess at the conditions.
Getting the complement for free
Now suppose we want the opposite region: the diamond-shaped gap, with the corners of the grid filled instead.
It is tempting to flip every comparator and keep the and:
if (
i + j < n
and j - i > n
and i - j > n
and i + j > 3 * n
): # prints nothing
That produces an empty grid. Not a nearly empty one. No cell satisfies the condition for any positive n.
The arithmetic is fine. The logic is the problem.
The filled diamond is an and of four conditions, so its exact complement follows from De Morgan's law: negate the whole expression, which means flipping each condition and replacing and with or.
if i + j < n or j - i > n or i - j > n or i + j > 3 * n:
print("*", end=" ")
That is the actual complement. Every cell outside the diamond matches it, and no cell inside does.
Run the two patterns together for n=3 and they partition the 49-cell grid into 25 cells and 24 cells. They do not overlap, and no cell is left unclassified. That is simply P and not-P.
A common alternative is to relax the flipped comparators back to non-strict comparisons:
if i + j <= n or j - i >= n or i - j >= n or i + j >= 3 * n:
print("*", end=" ")
That is a different shape from the strict complement above. It is not just another way of writing the same logic.
The two versions share the boundary lines with the diamond itself. For n=3, the two printed patterns overlap on 12 cells. When printed separately, though, the non-strict version often looks like the intended pattern because its own boundary stays closed instead of leaving a one-cell gap.
The useful part here is bigger than this particular diamond. When a shape is formed by intersecting several half-planes, its complement is formed by taking the union of the flipped conditions. Strict versus non-strict boundaries is a separate decision about which pattern gets the edge.
Beyond four lines
None of this depends on having exactly four edges. The same procedure works for any shape made from straight boundaries:
Mark the shape's vertices as grid coordinates.
Connect consecutive vertices and get one line equation for each edge using the two-point formula.
Decide the direction of each inequality by testing a known interior point. Then choose
>=versus>or<=versus<depending on whether the boundary itself belongs to the shape.andtogether all the conditions for one convex piece.If the target shape is not a single convex piece, split it into several pieces and combine those pieces with
or.For the complement version, apply De Morgan's law to the complete Boolean expression rather than rebuilding the logic from scratch: flip every comparator and replace
andwithorat the corresponding level.
That fifth step is also useful for more complicated patterns, where the target shape cannot be represented as a single convex region and must instead be built from multiple pieces.
As a quick check, take the diamond and add two horizontal boundaries: i = a and i = 2n − a, where 0 < a < n. These cut off the top and bottom points. Keep the four diagonal conditions and add the two new ones:
if (
i + j >= n
and j - i <= n
and i - j <= n
and i + j <= 3 * n
and i >= a
and i <= 2 * n - a
):
print("*", end=" ")
Now there are six conditions, but the method has not changed. Two more lines simply mean two more inequalities. The diamond has become a hexagon.
Relation to Existing Approaches
The basic idea of treating a grid position as a coordinate and using line equations to derive pattern conditions is not new. Dilip Kumar's "Pattern printing coding technique" explicitly uses the (i, j) grid representation and derives conditions such as i >= j for simple shapes.
A lot of other pattern-printing tutorials take a different route and build each shape directly from loops. GeeksforGeeks' diamond-pattern reference is one example. It constructs the diamond by handling the upper and lower parts separately, then controlling the spaces and stars with nested loops.
The difference here is mainly in how the condition is derived. Instead of trying combinations until the pattern looks right, you start with the boundary points, derive the line equations, determine the correct half-planes, and combine the resulting inequalities with Boolean logic.



Top comments (0)