
How to Play Zip¶
Figure 1:Example of a Zip game board (Source: LinkedIn Zip)
The board of Zip is made up with some numbered squares and walls (walls are optional).
- Objective
- To trace a single path that runs through all the squares on the board.
- Rule
- The path must move through numbered squares in ascending order, starting from square number 1 to the one with the highest number.
Problem Modeling¶
The best way to solve the game is to consider the squares as nodes in a directed graph (digraph), where each node has a set of pairs of edges that arrive at and depart from the nodes, as shown in the following figure.

Figure 2:Zip game modeled on a directed graph. Source: created by the author
Considering this graph model, it was enacted the Abstract LO Model by defining its following components:
Ranges
Sets
Decision variables
Parameter
Objective function
Constraints
Ranges¶
To properly formulate the LO model, we will start from the premise that the squares on the board will be the Cartesian product of the ranges of rows and of columns present on the board. In addition, it is necessary to have defined an interval for the set of numbered squares.
- The row range, where is the total number of rows (in this case, )
- The column range, where is the total number of columns (in this case, as well).
Sets¶
The definition of the sets will be more than fundamental to reducing the complexity of the model, as we will avoid having to deal with numerous exceptions. From the ranges determined previously, we can define the set of vertices () and edges () existing in the network logic. In addition to these, it is also necessary to consider the subsets of numbered vertices () and blocked edges (), since, in some instances of Zip game, there may be walls between two squares that prevent the path from being traced between them.
- Set of all squares (vertices). There must be at least 2 vertices in the grid, as it is only possible to trace a path with an initial and a final square. Therefore,
- Set of all edges present in the graph. Its definition is a bit more complex as it is necessary to clarify that, for each vertex, there will only be edges between neighbors immediately to the left, immediately to the right, immediately above, and immediately below it, if they exist.
- Set of numbered squares, which will consist of a set of all vertices indexed by a number on board game.
- Set of blocked edges (walls) that may appear on the grid.
Decision Variables¶
By using the network logic already presented, it is possible to define the edges as binary decision variables , which again makes our LOP a BLOP. In addition to , variables will also be considered, representing the absolute ordinal position of the square within the path (these variables will be better explained later).
- , if the edge that starts from square and arrives at square is part of the path
- , otherwise
- Absolute ordinal positions of the squares within the path.
Parameters¶
- For each numbered board square in set, it’ll be defined a integer that matches its number displayed on game board.
Objective Function¶
Once again, our problem does not have a function to be optimized, since we only want to find a solution that fulfills all the rules of the game. Therefore, Zip’s BLOP is a feasibility problem, and the objective function of the model consists of maximizing (or minimizing) an arbitrary constant.
Constraints¶
- Decision variables domain constraints
First, let’s not forget to define our variables as binary and the variables as non-negative real numbers. To simplify, whenever possible, we can replace the notation with and with just , since the sequence always refers to some vertex belonging to set.
- Blocked edges constraints
If there is a wall between a pair of squares, then the sum of the edges connecting those ones must be equal to 0.
- Continuity constraints
Since it is only possible to enter and exit a node once (except for the initial and destination nodes), then we will have the following:
- Outgoing-Edges Constraints
- The sum of the edges emanating from a node must equal 1 (except for the destination square).
- Incoming-Edges Constraints
- The sum of the edges arriving at a node must equal 1 (except for the initial square).
- Arrival Square Constraint
- The sum of the edges emanating from the destination node must be 0.
- Starting Square Constraint
- The sum of the edges arriving at the initial node must be 0.
- Subroute Elimination Constraints
The advantage of the MTZ formulation is that the number of constraints to be included to guarantee subroute elimination is equal to , that is, the number of edges in the graph. Thus, the number of constraints grows linearly as a function of the number of edges, instead of exponentially as a function of the number of vertices. For it to work, it requires the addition of more decision variables (the previously mentioned ), which represent the absolute ordinal position of square within the path.
- Position of Starting Square Constraint
- First, the position of the starting square must be equal to 1, since it will always be the first position on the path.
- MTZ Constraints
- Then, the positions of the remaining squares on the board should be given as follows.
- Positon of Arrival Square Constraint
- Although not necessary, one can restrict the position of the arrival square to be equal to the number of squares on the board, just to make the range from 1 to 36.
- Order of Precedence Constraints
- In order for the path to traverse the numbered squares in ascending numerical order, it is necessary to impose the constraint that the position of a numbered square must be at least equal to the position subsequent to the last numbered square.
Abstract Model¶
With all the formulas elaborated, we have the complete abstract model for the Zip minigame.
Concrete Model¶
The game selected to be solved is the Zip No. 166, published on LinkedIn on August 29th, 2025

Figure 3:Zip No. 166, August 29th, 2025 (Source: LinkedIn Zip)
Based on this specific case, we have the following concrete model:
S.t.:
- Outgoing edges constraints
- (Square (1,1))
- (Square (1,2))
- (Square (1,3))
- (Square (1,4))
- (Square (1,5))
- (Square (1,6))
- (Square (2,1))
- (Square (2,2))
- (Square (2,3))
- (Square (2,4))
- (Square (2,5))
- (Square (2,6))
- (Square (3,1))
- (Square (3,2))
- (Square (3,3))
- (Square (3,4))
- (Square (3,5))
- (Square (3,6))
- (Square (4,1))
- (Square (4,2))
- (Square (4,3))
- (Square (4,4))
- (Square (4,5))
- (Square (4,6))
- (Square (5,1))
- (Square (5,2))
- (Square (5,3))
- (Square (5,4))
- (Square (5,5))
- (Square (5,6))
- (Square (6,1))
- (Square (6,2))
- (Square (6,3))
- (Square (6,4))
- (Square (6,5))
- (Square (6,6))
- Incoming edges cosntraints
- (Square (1,1))
- (Square (1,2))
- (Square (1,3))
- (Square (1,4))
- (Square (1,5))
- (Square (1,6))
- (Square (2,1))
- (Square (2,2))
- (Square (2,3))
- (Square (2,4))
- (Square (2,5))
- (Square (2,6))
- (Square (3,1))
- (Square (3,2))
- (Square (3,3))
- (Square (3,4))
- (Square (3,5))
- (Square (3,6))
- (Square (4,1))
- (Square (4,2))
- (Square (4,3))
- (Square (4,4))
- (Square (4,5))
- (Square (4,6))
- (Square (5,1))
- (Square (5,2))
- (Square (5,3))
- (Square (5,4))
- (Square (5,5))
- (Square (5,6))
- (Square (6,1))
- (Square (6,2))
- (Square (6,3))
- (Square (6,4))
- (Square (6,5))
- (Square (6,6))
- Ordinal position constraints
- (Square 1)
- (Square 2)
- (Square 3)
- (Square 4)
- (Square 5)
- (Square 6)
- (Square 7)
- (Square 8)
- (Square 9)
- (Square 10)
- (Square 11)
- (Square 12)
- (Square 13)
- (Square 14)
- (Square 15)
- (Square 16)
- (Square 16)
- Subroute elimination constraints
- Binarity constraints for edge variables
- Non-negativity constraints for positional variables
- (Square (1,1))
- (Square (1,2))
- (Square (1,3))
- (Square (1,4))
- (Square (1,5))
- (Square (1,6))
- (Square (2,1))
- (Square (2,2))
- (Square (2,3))
- (Square (2,4))
- (Square (2,5))
- (Square (2,6))
- (Square (3,1))
- (Square (3,2))
- (Square (3,3))
- (Square (3,4))
- (Square (3,5))
- (Square (3,6))
- (Square (4,1))
- (Square (4,2))
- (Square (4,3))
- (Square (4,4))
- (Square (4,5))
- (Square (4,6))
- (Square (5,1))
- (Square (5,2))
- (Square (5,3))
- (Square (5,4))
- (Square (5,5))
- (Square (5,6))
- (Square (6,1))
- (Square (6,2))
- (Square (6,3))
- (Square (6,4))
- (Square (6,5))
- (Square (6,6))
Solving Zip¶
The linkedin-games library counts on Zip class, that implements the Zip game and its constraints, as well as methods to solve it and visualize the solution.
To instantiate the game, it is necessary to provide two sets of inputs regarding the game in question:
numbered_squares- A dictionary of
(row, column): numberitems representing the starting filled squares, wherenumberis the required relative position of the square on the path. walls- A tuple of pairs of squares (edges) representing walls on game board.
from linkedin_games import Zip
# Solving Zip No. 166
numbered_squares = [
(6,5), (6,6), (5,6), (6,4), (5,3), (4,2), (3,1), (2,1),
(1,1), (1,2), (1,3), (3,5), (2,4), (3,4), (4,3), (4,6)
]
zip_game = Zip(6, numbered_squares)The Zip class features the model attribute, which implements the ZipModel object to structure the Linear Optimization logic behind the Zip’s game.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92from .._mixin._taxicab_distance_mixin import TaxicabDistanceMixin import pyomo.environ as pyo class ZipModel(TaxicabDistanceMixin, pyo.ConcreteModel): """The Linear Optimization Model for LinkedIn Zip game.""" def __init__(self, grid_dims: tuple[int, int], numbered_squares: list[tuple[int, int]], walls: list[tuple[tuple[int, int], tuple[int, int]]] | None ) -> None: """ Args: grid_dims: Grid dimensions as a `(row, column)` tuple. numbered_squares: Squares with a assigned number as a dictionary of `(row, column): number` items. walls: Pairs of squares separated by a walls as a tuple of `((row1, column1), (row2, column2))`. """ super().__init__() # PARAMETERS m, n = grid_dims M = m * n # Big M self.m = pyo.Param(initialize=m, domain=pyo.PositiveIntegers) self.n = pyo.Param(initialize=n, domain=pyo.PositiveIntegers) # RANGE SETS I = self.I = pyo.RangeSet(n) # Rows J = self.J = pyo.RangeSet(m) # Columns K = self.K = pyo.RangeSet(len(numbered_squares)) # COMPOSITE SETS S = self.S = pyo.Set(initialize=lambda model: [(i, j) for i in I for j in J]) # Grid Squares E = self.E = pyo.Set(initialize=lambda model: # Edges [((i,j), (i+1, j)) for i in I for j in J if i+1 in I] + [((i,j), (i-1, j)) for i in I for j in J if i-1 in I] + [((i,j), (i, j+1)) for i in I for j in J if j+1 in J] + [((i,j), (i, j-1)) for i in I for j in J if j-1 in J] ) W = self.W = pyo.Set(initialize=walls, domain=E) # Walls N = self.N = pyo.Set(initialize=numbered_squares, domain=S) # DECISION VARIABLES x = self.x = pyo.Var(E, domain=pyo.Binary, initialize=0) # Decision to go from square (i,j) to (r,s) u = self.u = pyo.Var( # Visitiation order of a square (i, j) S, domain=pyo.PositiveIntegers, initialize=1, bounds=(1, M) ) # OBJECTIVE FUNCTION self.obj = pyo.Objective(expr=0) # feasibility problem # CONSTRAINTS ## Edge constraints neighbors = { # This dictionary is important to access all neighbors of a square quickly. (i, j): [ (r, c) for r, c in [ (i-1, j), (i+1, j), (i, j-1), (i, j+1) ] if (r, c) in S ] for (i, j) in S } self.outgoing_edges_constraints = pyo.Constraint( S, rule=lambda model, i, j: pyo.quicksum(x[(i,j), w] for w in neighbors[(i,j)]) == 0 if N.at(len(K)) == (i,j) else pyo.quicksum(x[(i,j), w] for w in neighbors[(i,j)]) == 1 ) self.incoming_edges_constraints = pyo.Constraint( S, rule=lambda model, i, j: pyo.quicksum(x[s, (i,j)] for s in neighbors[(i,j)]) == 0 if N.at(1) == (i,j) else pyo.quicksum(x[s, (i,j)] for s in neighbors[(i,j)]) == 1 ) ## Blocked paths constraints self.wall_constraints = pyo.Constraint( W, rule=lambda model, i, j, r, s: x[i,j,r,s] + x[r,s,i,j] == 0 ) # Miller-Tucker-Zemlin constraints with lifting self.subroute_elimination_constraints = pyo.Constraint( E, rule=lambda model, i, j, r, s: u[r,s] >= u[i,j] + 1 - M * (1 - x[i,j,r,s]) + (M - 2) * x[r,s,i,j] ) ## Visitation order constraints self.visitation_order_constraints = pyo.Constraint( K, rule= lambda model, k: u[N.at(k)] == 1 if k == 1 else u[N.at(k)] == M if k == len(N) else u[N.at(k)] >= u[N.at(k-1)] + self._taxicab_distance(N.at(k), N.at(k-1)) )
Program 1:Creating the model with Pyomo components.
After the model is built, the public method solve() calls the restricted method _set_solution() to save the game’s solution to _board property, which is a NetworkX Graph representing the game’s board with the positions on the path, and to private attribute __path, which can be accessed by public path attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23@property def path(self) -> list[tuple[int, int]] | None: """ The solving path of Zip game. The path that visits all the grid squares, starting from 1-numbered squared to the highest-numbered square. Returns: The solving path as a list of squares as `(row, column)`. """ if not self.is_solved: return None return self.__path def _set_solution(self, verbose:bool=False) -> None: S = self.model.S E = self.model.E u = self.model.u
Program 2:Saving the game’s solution to path attribute.
With the solution obtained, the method show() can be called to plot the Zip’s board.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81x = self.model.x nx.set_node_attributes( self.grid, name="value", values={(i-1, j-1): round(pyo.value(u[i,j])) for i, j in S} ) nx.set_edge_attributes( self.grid, name="value", values={((i-1, j-1), (r-1, s-1)): round(pyo.value(x[i,j,r,s])) for i, j, r, s in E} ) path = nx.get_node_attributes(self.grid, "value") path = sorted(path.keys(), key=path.get) self.__path = [(i+1, j+1) for (i, j) in path] if verbose: print("This is the path that solves the games:") pprint(self.path) def show(self) -> None: """Show Zip's grid.""" E = self.model.E N = self.model.N K = self.model.K x = self.model.x width = height = self.size * 0.7 plt.figure(figsize=(width, height)) path_color = super()._generate_hex_code() labels = {N.at(k): k for k in K} labels = {(i-1, j-1): k for (i,j), k in labels.items()} pos={(i,j): (j,-i) for i, j in self.grid.nodes()} if self.walls is not None: walls = nx.draw_networkx_edges( self.grid, pos=pos, edgelist=[((i-1, j-1), (r-1, s-1)) for (i,j),(r,s) in self.walls], edge_color="#000000", hide_ticks=True, arrows=False, width=30 ) walls.set_zorder(0) grid_squares = nx.draw_networkx_nodes( self.grid, pos= pos, node_shape="s", node_size= 1100, node_color= "#FFFFFF", linewidths= 2, ) grid_squares.set_zorder(1) nx.draw( # Drawing the path self.grid, pos= pos, with_labels= True, labels=labels, arrows=False, node_shape="o" if self.is_solved else "s", node_size= 800, node_color= [ "white" if (i+1,j+1) in self.numbered_squares else path_color for (i,j) in self.grid.nodes() ], edge_color= path_color, edgecolors= path_color, linewidths= 1, width= 30, edgelist= [ ((i-1, j-1), (r-1, s-1)) for i,j,r,s in E if round(pyo.value(x[i,j,r,s])) == 1 ] ) plt.show()
Program 3:Implementation of show() function.
So to solve the game and display its results, just call the public methods solve() and show(), at this order.
zip_game.solve()
zip_game.show()
As expected, the displayed path satisfies all the rules of Zip No. 166.

Figure 4:Zip game solution. Source: LinkedIn Zip