473. Matchsticks to Square
Problem Description
In this problem, you are given a set of matchsticks, each with a different length. The objective is to determine whether these matchsticks can be arranged to form a perfect square without breaking any of them. This means that each matchstick must be used exactly once and all must contribute to the shape of the square.
To imagine this more concretely, think of it as a puzzle where we must create four equal-length sides (as only a square has equal sides) with the given set of matchsticks. If we can successfully do this, the function should return true
, otherwise, it should return false
.
Flowchart Walkthrough
Let's determine the appropriate solving technique for Leetcode 473. Matchsticks to Square using the Flowchart. We'll proceed step-by-step:
Is it a graph?
- No: The problem does not describe relationships or connections typical of graph problems.
Need to solve for kth smallest/largest?
- No: The challenge does not involve finding a specific ordinal element within a set.
Involves Linked Lists?
- No: This problem involves an array of integers (matchsticks), not linked lists.
Does the problem have small constraints?
- Yes: The problem can work within reasonably small constraints since it involves a limited collection of matchsticks, allowing feasible exhaustive searches.
Brute force / Backtracking?
- Yes: Backtracking is suitable for exploring combinations of matchsticks to meet specific conditions (e.g., forming sides of a square).
Conclusion: Following the analysis via the flowchart, we confirm that using a backtracking approach is appropriate for trying different combinations of matchsticks to form a square.
Intuition
The solution to this problem relies on a depth-first search (DFS) algorithm, which attempts to build the square by adding matchsticks one by one to each of the four sides until all matchsticks have been placed. The key insights behind this approach include:
-
The Total Length of Matchsticks: Before attempting to place matchsticks, we calculate the total length of all matchsticks by summing them. For a square to be possible, this total must be divisible by 4 equally (since a square has four equal sides). If it is not divisible, we know right away that we can't make a square, so we return
false
. -
Avoiding Useless Work: Two optimizations help us reduce the computation time. First, we sort the matchsticks in descending order, beginning with the longest. This helps because if the longest matchstick is longer than the side of the square, we know it's impossible to form a square and can return
false
immediately. Second, when trying to place a matchstick on a side that is equal in length to a side we just attempted, we skip that attempt since the order of matchsticks on the sides doesn't matter. -
Recursive DFS: Using a recursive function
dfs(u)
, we try to place theu
th matchstick on each side. We only proceed with the recursion if adding the matchstick to the current side does not exceed the expected length of a side (which is the total length of the matchsticks divided by 4). If, at any point, a side becomes too long, we backtrack by removing the matchstick from that side (edges[i] -= matchsticks[u]
) and trying other options. If we can place the last matchstick without exceeding the side length, we know a square is possible and returntrue
.
By carefully placing each matchstick using DFS and backtracking when necessary, the algorithm systematically explores all possible placements until it either succeeds in forming a square or exhausts all options and returns false
.
Learn more about Dynamic Programming, Backtracking and Bitmask patterns.
Solution Approach
The implementation of this solution is a classic example of utilizing Depth-First Search (DFS) alongside backtracking to explore all possible combinations of placing matchsticks on four sides of a square. Here's a step-by-step explanation of the different parts of the code:
-
Preparation Step: We begin by adding up the total length of all matchsticks and trying to divide it by 4 to get the target length for each side of the square (
x
). If there’s a remainder (mod
), or if the longest matchstick is longer than the target side length, we cannot form a square and returnfalse
. -
Sorting: The matchsticks are sorted in descending order because placing longer matchsticks earlier can help us reach the conclusion faster. This is because if a long matchstick doesn't fit, we don't need to consider combinations of smaller ones that would also not fit.
-
Depth-First Search (DFS): The recursive function
dfs(u)
tries to place theu
th matchstick in turn on each of the four sides. For this, we utilize an arrayedges
of length 4, initialized with zeroes, to keep track of the current length of each side as we add matchsticks. -
Branching and Pruning:
- The DFS explores different branches, each representing a different combination of matchstick placements.
- While placing matchsticks, we avoid redundant work by skipping a side if it is of the same length as the previous one (
i > 0 and edges[i - 1] == edges[i]
), because different permutations of sides of the same current length don't change the overall problem. - We ensure that adding a matchstick does not make any side longer than the target (
edges[i] <= x
).
-
- After trying to add a matchstick to a side, if adding the next matchstick results in exceeding the target length or if all matchsticks up to the last are placed without forming a square, the function backtracks. This means undoing the last matchstick addition (
edges[i] -= matchsticks[u]
) and trying a different placement.
- After trying to add a matchstick to a side, if adding the next matchstick results in exceeding the target length or if all matchsticks up to the last are placed without forming a square, the function backtracks. This means undoing the last matchstick addition (
-
Completion: If all matchsticks are placed,
dfs(u)
will eventually reach past the last matchstick indicating that all matchsticks have been used to form a square, and will returntrue
.
The entry point is calling dfs(0)
, indicating we start with the first matchstick. The process will recurse, branching, and backtracking until a solution is found or all possibilities are exhausted. The final outcome will be the return of either true
or false
from the makesquare
function indicating the possibility of forming a square with the given matchsticks.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
To illustrate the solution approach, let's use a small example. Suppose we have the set of matchsticks with lengths [3, 3, 3, 3, 4]
.
-
Preparation Step: We add up the lengths of all matchsticks. Here, the sum is
3+3+3+3+4=16
. If we divide 16 by 4, the target length for each side of the square (x) is 4, and since there's no remainder, we can proceed. The longest matchstick is exactly 4 units long, which matches our target side length, so it's possible to form a square. -
Sorting: We sort the matchsticks in descending order to get
[4, 3, 3, 3, 3]
. -
Depth-First Search (DFS): The recursive function
dfs(u)
will try to add theu
th matchstick to one of the sides. -
Branching and Pruning: We start with
dfs(0)
and attempt to place the matchstick of length 4 on a side. Since the arrayedges
starts as[0, 0, 0, 0]
, after placing the first matchstick, it becomes[4, 0, 0, 0]
.-
Next, we call
dfs(1)
to place the matchstick of length 3. As per our pruning rule, we start withedges[1]
becauseedges[0]
is already 4, which is the target length for a side. Nowedges
becomes[4, 3, 0, 0]
. -
Then
dfs(2)
is called, and we place another 3 length matchstick onedges[2]
.edges
now is[4, 3, 3, 0]
. -
With
dfs(3)
, we can't place the next 3 onedges[1]
because it would exceed our target. We put it inedges[2]
to get[4, 3, 3, 3]
. However, this is a side with the same length as the previous side (edges[1]
), so we should skip this step and place it inedges[3]
instead, resulting in[4, 3, 3, 3]
. -
Finally, we have the last matchstick with
dfs(4)
. We can't add it toedges[1]
,edges[2]
, oredges[3]
as they're already at 3, but we can add it toedges[1]
to fill the side up to the target of 4, resulting in[4, 4, 3, 3]
.
-
-
Backtracking: If at any point a move had been invalid, we would have removed the last matchstick tried, and attempted another configuration. However, in this example, all moves were valid.
-
Completion: All matchsticks have been placed and the
edges
array reads[4, 4, 3, 3]
. The target length for each side was 4, anddfs(4)
will exit successfully, showing that these matchsticks can indeed form a perfect square, returningtrue
.
As such, we can conclude that [3, 3, 3, 3, 4]
can form a square based on the described DFS and backtracking solution approach.
Solution Implementation
1from typing import List
2
3class Solution:
4 def makesquare(self, matchsticks: List[int]) -> bool:
5 def backtrack(index):
6 # If we have considered all matchsticks, we're done and can potentially form a square.
7 if index == len(matchsticks):
8 return True
9
10 for edge in range(4):
11 # If the current edge and the previous one are the same, skip to avoid redundant work.
12 if edge > 0 and edges[edge - 1] == edges[edge]:
13 continue
14 # Try to add the current matchstick to the current edge.
15 edges[edge] += matchsticks[index]
16 # If adding the current matchstick didn't exceed the target length and the remaining matchsticks can be placed, return True.
17 if edges[edge] <= target_per_edge and backtrack(index + 1):
18 return True
19 # Otherwise, backtrack by removing the matchstick from the current edge.
20 edges[edge] -= matchsticks[index]
21
22 # If no combination worked, return False.
23 return False
24
25 # Calculate the total length of all matchsticks and the target length per edge.
26 total_length, remainder = divmod(sum(matchsticks), 4)
27 # If the total length of matchsticks is not divisible by 4 or the longest matchstick is longer
28 # than the target per edge, we cannot possibly form a square.
29 if remainder or total_length < max(matchsticks):
30 return False
31
32 # Initialize the array representing the four edges of the potential square.
33 edges = [0] * 4
34 # Sort the matchsticks in descending order to improve efficiency.
35 matchsticks.sort(reverse=True)
36 # Start the backtracking process from the first matchstick.
37 return backtrack(0)
38
39# Example usage:
40# sol = Solution()
41# result = sol.makesquare([1,1,2,2,2])
42# print(result) # Output: True, since the matchsticks can form a square
43
1class Solution {
2 public boolean makesquare(int[] matchsticks) {
3 int sum = 0; // This will hold the total length of all matchsticks
4 int maxStickLength = 0; // This will hold the length of the longest matchstick
5
6 // Calculate the total sum of matchsticks' lengths and find the longest matchstick
7 for (int length : matchsticks) {
8 sum += length;
9 maxStickLength = Math.max(maxStickLength, length);
10 }
11
12 // Each side of the square should be sum / 4
13 int sideLength = sum / 4;
14 int remainder = sum % 4; // Remainder should be zero for a perfect square
15
16 // If remainder is not zero or any matchstick is longer than the calculated side length, we can't form a square
17 if (remainder != 0 || sideLength < maxStickLength) {
18 return false;
19 }
20
21 // Sort the array in ascending order to optimize the dfs function later
22 Arrays.sort(matchsticks);
23
24 // This will keep track of the lengths of the four sides of the square
25 int[] sides = new int[4];
26
27 // Start the depth-first search from the last (and largest due to sorting) matchstick
28 return dfs(matchsticks.length - 1, sideLength, matchsticks, sides);
29 }
30
31 private boolean dfs(int index, int targetSideLength, int[] matchsticks, int[] sides) {
32 // If we have considered all matchsticks, check if all sides are equal to target side length
33 if (index < 0) {
34 return true;
35 }
36
37 // Try to place current matchstick on each side and see if it helps in making a square
38 for (int i = 0; i < 4; ++i) {
39 // This check ensures we do not place matchsticks redundantly when previous side is the same as the current one
40 if (i > 0 && sides[i - 1] == sides[i]) {
41 continue;
42 }
43
44 // Add the current matchstick to the i-th side
45 sides[i] += matchsticks[index];
46
47 // If the side does not exceed the target side length, recursively continue to place the next matchstick
48 if (sides[i] <= targetSideLength && dfs(index - 1, targetSideLength, matchsticks, sides)) {
49 // If dfs returns true, this means all matchsticks can be placed to form a square
50 return true;
51 }
52
53 // If adding the matchstick to the i-th side doesn't lead to a solution, remove it
54 sides[i] -= matchsticks[index];
55 }
56
57 // If none of the sides can accommodate the current matchstick, return false
58 return false;
59 }
60}
61
1#include <vector>
2#include <algorithm>
3
4using namespace std;
5
6class Solution {
7public:
8 // Function to check whether it's possible to make a square with the matchsticks.
9 bool makesquare(vector<int>& matchsticks) {
10 int totalLength = 0, maxLength = 0;
11
12 // Calculate the total length of matchsticks and the length of the longest matchstick.
13 for (int stick : matchsticks) {
14 totalLength += stick;
15 maxLength = max(maxLength, stick);
16 }
17
18 // The sum of all matchsticks lengths should be divisible by 4 to make a square,
19 // and no single matchstick should be longer than the side length of the square.
20 int sideLength = totalLength / 4;
21 int remainder = totalLength % 4;
22 if (remainder != 0 || sideLength < maxLength) return false;
23
24 // Sort the matchsticks in decreasing order to optimize the DFS process.
25 sort(matchsticks.begin(), matchsticks.end(), greater<int>());
26
27 // Create an array to store the lengths of the 4 sides of the square.
28 vector<int> sides(4, 0);
29
30 // Start the DFS search to see if a square can be formed.
31 return dfs(0, sideLength, matchsticks, sides);
32 }
33
34private:
35 // Helper function to perform DFS (depth-first search).
36 bool dfs(int index, int sideLength, vector<int>& matchsticks, vector<int>& sides) {
37 // Base case: if all matchsticks are used.
38 if (index == matchsticks.size()) return true;
39
40 // Try to place the current matchstick on each side of the square.
41 for (int i = 0; i < 4; ++i) {
42 // Skip trying the same length as previous side to reduce repetition.
43 if (i > 0 && sides[i - 1] == sides[i]) continue;
44
45 sides[i] += matchsticks[index];
46
47 // Recur only if the current side does not exceed the intended side length
48 // and if the rest of the matchsticks can be placed to make a square.
49 if (sides[i] <= sideLength && dfs(index + 1, sideLength, matchsticks, sides)) return true;
50
51 // This is backtracking - if adding the current matchstick doesn't lead to a solution,
52 // remove it from the current side and try the next side.
53 sides[i] -= matchsticks[index];
54 }
55
56 // Return false if placing the current matchstick on any side
57 // doesn't lead to a solution (doesn't make a square).
58 return false;
59 }
60};
61
1const totalLength = (sticks: number[]): number => sticks.reduce((acc, length) => acc + length, 0);
2
3// Function to perform depth-first search to find if it's possible to construct a square
4const dfs = (index: number, sideLength: number, matchsticks: number[], sides: number[]): boolean => {
5 // Base case: all matchsticks are used, true if all sides are equal to side length
6 if (index === matchsticks.length) {
7 return sides.every(side => side === sideLength);
8 }
9
10 for (let i = 0; i < 4; ++i) {
11 // Skip trying the same length as previous side to reduce repetition
12 if (i > 0 && sides[i - 1] === sides[i]) continue;
13
14 // Place the current matchstick on the i-th side of the square if it doesn't exceed the side length
15 if (sides[i] + matchsticks[index] <= sideLength) {
16 sides[i] += matchsticks[index];
17 // Recursively try to place the rest of the matchsticks
18 if (dfs(index + 1, sideLength, matchsticks, sides)) return true;
19 // Backtrack if unable to form a square with current placement
20 sides[i] -= matchsticks[index];
21 }
22 }
23
24 // Couldn't place the current matchstick in any side to form a square
25 return false;
26}
27
28// Function to check whether it's possible to form a square with the matchsticks
29const makesquare = (matchsticks: number[]): boolean => {
30 const sumOfLength = totalLength(matchsticks);
31 // The sum of all matchsticks lengths should be divisible by 4 to form a square
32 if (sumOfLength % 4 !== 0) return false;
33
34 const sideLength = sumOfLength / 4;
35 const maxLength = Math.max(...matchsticks);
36 // No single matchstick should be longer than the side length of the square
37 if (sideLength < maxLength) return false;
38
39 // Sort matchsticks in decreasing order to help optimize the search
40 matchsticks.sort((a, b) => b - a);
41
42 // Initialize sides of the square with zero length
43 const sides = [0, 0, 0, 0];
44
45 // Perform DFS to determine if a square can be formed
46 return dfs(0, sideLength, matchsticks, sides);
47}
48
49// Example of how to use the global function makesquare
50const matchsticks = [1,1,2,2,2];
51console.log(makesquare(matchsticks)); // Output should be either true or false based on the input array
52
Time and Space Complexity
The provided code aims to determine if an array of integers (matchsticks
) can be used to form a square. The core of this approach is to use Depth-First Search (DFS) to try to place each matchstick on one of the four sides of the square until all matchsticks are placed or no valid solution can be found.
Time Complexity:
The time complexity of the given algorithm is primarily dependent on the DFS search. In the worst-case scenario, DFS would explore all possible placements of matchsticks across the four sides. This exponential behavior essentially looks at 4^n
combinations, where n
is the number of matchsticks. However, some optimizations are present:
- The matchsticks are sorted in descending order, which can lead to earlier termination in certain branches of the search tree when the matchstick is too long to fit on any side.
- The algorithm also skips over sides with the same current length to avoid redundant placements.
Considering these optimizations, the upper bound on the time complexity still remains exponential, close to O(4^n), due to the nature of the problem which is essentially a NP-Complete problem akin to the Subset Sum or Partition problem. However, the actual performance can be somewhat better than O(4^n) on average due to the early pruning of the search space.
Space Complexity:
The space complexity of the algorithm is mainly due to the recursive call stack of the DFS function and the storage of the sides of the square. In the worst-case scenario, DFS recursion could go as deep as n
levels where n
is the number of matchsticks. Each level of recursion uses additional space for the execution context, leading to a space complexity of O(n). The edges
array contains 4 integers, so it uses O(1) space.
Combining both factors, the overall space complexity is O(n), mainly due to the call stack depth in the recursive DFS search.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!