2698. Find the Punishment Number of an Integer
Problem Description
You need to find the "punishment number" of a given positive integer n
.
The punishment number is calculated by finding all integers i
from 1 to n
that satisfy a special property, squaring those integers, and summing up their squares.
The special property is: when you square the integer i
to get i * i
, you should be able to split the decimal digits of i * i
into one or more contiguous groups such that when you add up the numeric values of these groups, the sum equals i
.
For example, if i = 9
:
i * i = 81
- You can split "81" as "8" and "1"
8 + 1 = 9
, which equalsi
- So 9 satisfies the property and
81
would be included in the punishment number
Another example, if i = 10
:
i * i = 100
- You can split "100" as "10" and "0"
10 + 0 = 10
, which equalsi
- So 10 satisfies the property and
100
would be included in the punishment number
The solution uses a recursive helper function check
to try all possible ways of partitioning the string representation of i * i
. For each valid partition that sums to i
, it adds i * i
to the final answer.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves checking integers and their squares, not nodes and edges as in graph problems.
Need to solve for kth smallest/largest?
- No: We're not finding the kth smallest or largest element, but rather summing squares that meet a specific condition.
Involves Linked Lists?
- No: The problem works with integers and their string representations, not linked lists.
Does the problem have small constraints?
- Yes: For each integer
i
, we need to check all possible ways to partition the string representation ofi * i
. The number of possible partitions grows exponentially with the number of digits, but since we're dealing with squares of integers up ton
(typically n ≤ 1000), the string lengths are manageable (at most 7 digits for 1000² = 1,000,000).
Brute force / Backtracking?
- Yes: We need to explore all possible ways to partition each square's string representation and check if any partition sums to the original number. This requires backtracking to try different partition points and backtrack when a partition doesn't lead to the desired sum.
Conclusion: The flowchart correctly leads us to use a Backtracking approach. For each integer from 1 to n, we use backtracking to recursively try all possible ways to split the string representation of its square, checking if any valid partition sums to the original integer.
Intuition
The key insight is recognizing that we need to explore all possible ways to split a number's digits into groups. When we see "all possible ways," this immediately suggests a backtracking approach.
Think about what we're actually doing: for a number like 81
(which is 9²
), we need to check if we can split "81" into contiguous parts that sum to 9
. We could split it as:
- "81" (one group: 81)
- "8" + "1" (two groups: 8 + 1 = 9) ✓
For a longer number like 1296
(which is 36²
), we have many more possibilities:
- "1296" (one group)
- "1" + "296" (two groups)
- "12" + "96" (two groups)
- "129" + "6" (two groups)
- "1" + "2" + "96" (three groups)
- ... and so on
This is a classic decision tree problem. At each position in the string, we face a decision: should we continue extending the current number, or should we "cut" here and start a new number? This branching decision structure naturally leads to a recursive backtracking solution.
The backtracking works by:
- Starting from the beginning of the string
- Trying to form a number by taking 1 digit, 2 digits, 3 digits, etc.
- For each formed number, subtract it from our target sum and recursively check if we can partition the remaining string to get the remaining sum
- If we reach the end of the string and our remaining sum is exactly 0, we found a valid partition
- If at any point our current number exceeds the remaining sum, we can prune that branch (optimization)
This approach ensures we explore all possible partitions systematically without missing any valid combinations or checking the same partition twice.
Learn more about Math and Backtracking patterns.
Solution Approach
The solution implements the backtracking strategy through enumeration and DFS (Depth-First Search).
Main Loop - Enumeration:
The outer structure iterates through all integers from 1
to n
. For each integer i
:
- Calculate its square:
x = i * i
- Convert
x
to a string representation - Use the
check
function to determine if this square can be partitioned to sum toi
- If valid, add
x
to the running answer
DFS Helper Function - check(s, i, x)
:
This recursive function explores all possible partitions of the string s
starting from index i
, trying to reach the target sum x
.
Parameters:
s
: The string representation of the squared numberi
: Current starting index in the stringx
: The remaining target sum we need to achieve
The DFS works as follows:
-
Base Case: If we've processed the entire string (
i >= m
), we check if the remaining targetx
equals 0. If yes, we found a valid partition. -
Recursive Case: Starting from index
i
, try forming numbers of different lengths:- Initialize
y = 0
to build the current number - For each position
j
fromi
to the end of string:- Extend the current number:
y = y * 10 + int(s[j])
- Pruning: If
y > x
, we can stop early since adding more digits will only make it larger - Recursively check if we can partition the rest:
check(s, j + 1, x - y)
- If the recursive call returns
True
, we found a valid partition
- Extend the current number:
- Initialize
Example Walkthrough:
For i = 9
, x = 81
, string s = "81"
:
- Start at index 0, target sum = 9
- Try "8" (y = 8): Recursively check remaining "1" with target 9 - 8 = 1
- At index 1, try "1" (y = 1): Target is 1 - 1 = 0
- Reached end with target = 0, return
True
- Since we found a valid partition, add 81 to the answer
The time complexity is O(n × 2^d) where d is the maximum number of digits in any square, as we potentially explore all partitions for each number.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding the punishment number for n = 12
.
We need to check each integer from 1 to 12 to see if it satisfies the special property.
Checking i = 1:
- Square: 1² = 1, string = "1"
- Can we partition "1" to sum to 1?
- Only partition: "1" = 1 ✓
- Add 1 to answer
Checking i = 9:
- Square: 9² = 81, string = "81"
- Can we partition "81" to sum to 9?
- Call
check("81", 0, 9)
:- Try taking "8": y = 8, remaining target = 9 - 8 = 1
- Call
check("81", 1, 1)
:- Try taking "1": y = 1, remaining target = 1 - 1 = 0
- Reached end with target = 0 → return True
- Call
- Found valid partition "8" + "1" = 9 ✓
- Try taking "8": y = 8, remaining target = 9 - 8 = 1
- Add 81 to answer
Checking i = 10:
- Square: 10² = 100, string = "100"
- Can we partition "100" to sum to 10?
- Call
check("100", 0, 10)
:- Try taking "1": y = 1, remaining target = 10 - 1 = 9
- Call
check("100", 1, 9)
:- Try taking "0": y = 0, remaining = 9
- Call
check("100", 2, 9)
:- Try "0": y = 0, remaining = 9
- Reached end with target = 9 ≠ 0 → return False
- Call
- Try taking "00": y = 0, remaining = 9
- Reached end with target = 9 ≠ 0 → return False
- Try taking "0": y = 0, remaining = 9
- Call
- Try taking "10": y = 10, remaining target = 10 - 10 = 0
- Call
check("100", 2, 0)
:- Try taking "0": y = 0, remaining = 0 - 0 = 0
- Reached end with target = 0 → return True
- Call
- Found valid partition "10" + "0" = 10 ✓
- Try taking "1": y = 1, remaining target = 10 - 1 = 9
- Add 100 to answer
Checking i = 11:
- Square: 11² = 121, string = "121"
- Can we partition "121" to sum to 11?
- Call
check("121", 0, 11)
:- Try "1": y = 1, remaining = 10
- Recursively check "21" for sum 10 → returns False
- Try "12": y = 12, but 12 > 11 (pruned)
- No valid partition found ✗
- Try "1": y = 1, remaining = 10
- Don't add to answer
Checking i = 12:
- Square: 12² = 144, string = "144"
- Can we partition "144" to sum to 12?
- Try all partitions:
- "1" + "44" = 45 ✗
- "1" + "4" + "4" = 9 ✗
- "14" + "4" = 18 ✗
- "144" = 144 ✗
- No valid partition found ✗
Final Answer: 1 + 81 + 100 = 182
The DFS efficiently explores the partition tree, pruning branches where the current number exceeds the remaining target, ensuring we find all valid partitions without redundant computation.
Solution Implementation
1class Solution:
2 def punishmentNumber(self, n: int) -> int:
3 """
4 Calculate the punishment number up to n.
5 A number i contributes to the punishment number if i^2 can be partitioned
6 into contiguous substrings that sum to i.
7
8 Args:
9 n: The upper limit to check for punishment numbers
10
11 Returns:
12 The sum of squares of all punishment numbers from 1 to n
13 """
14
15 def can_partition_to_target(num_str: str, start_idx: int, target_sum: int) -> bool:
16 """
17 Check if we can partition the string from start_idx to form target_sum.
18
19 Args:
20 num_str: The string representation of the square number
21 start_idx: Current starting index in the string
22 target_sum: The remaining sum we need to achieve
23
24 Returns:
25 True if valid partition exists, False otherwise
26 """
27 string_length = len(num_str)
28
29 # Base case: if we've processed the entire string
30 if start_idx >= string_length:
31 # Check if we've achieved exactly the target sum
32 return target_sum == 0
33
34 current_number = 0
35
36 # Try all possible partitions starting from start_idx
37 for end_idx in range(start_idx, string_length):
38 # Build the current number by adding digits
39 current_number = current_number * 10 + int(num_str[end_idx])
40
41 # Optimization: stop if current number exceeds remaining target
42 if current_number > target_sum:
43 break
44
45 # Recursively check if remaining string can form (target_sum - current_number)
46 if can_partition_to_target(num_str, end_idx + 1, target_sum - current_number):
47 return True
48
49 return False
50
51 punishment_sum = 0
52
53 # Check each number from 1 to n
54 for num in range(1, n + 1):
55 square = num * num
56
57 # Check if square can be partitioned to sum to num
58 if can_partition_to_target(str(square), 0, num):
59 punishment_sum += square
60
61 return punishment_sum
62
1class Solution {
2 /**
3 * Calculates the sum of squares of all integers from 1 to n whose squares
4 * can be partitioned into contiguous substrings that sum to the original number.
5 *
6 * @param n The upper limit to check (inclusive)
7 * @return The sum of all punishment numbers up to n
8 */
9 public int punishmentNumber(int n) {
10 int totalSum = 0;
11
12 // Check each number from 1 to n
13 for (int currentNumber = 1; currentNumber <= n; ++currentNumber) {
14 int square = currentNumber * currentNumber;
15 String squareString = String.valueOf(square);
16
17 // Check if the square can be partitioned to sum to the original number
18 if (check(squareString, 0, currentNumber)) {
19 totalSum += square;
20 }
21 }
22
23 return totalSum;
24 }
25
26 /**
27 * Recursively checks if a string representing a number can be partitioned
28 * into contiguous substrings that sum to a target value.
29 *
30 * @param numberString The string representation of the number to partition
31 * @param startIndex The current starting index for partitioning
32 * @param targetSum The remaining sum we need to achieve
33 * @return true if valid partition exists, false otherwise
34 */
35 private boolean check(String numberString, int startIndex, int targetSum) {
36 int stringLength = numberString.length();
37
38 // Base case: if we've processed the entire string
39 if (startIndex >= stringLength) {
40 // Check if we've reached exactly the target sum
41 return targetSum == 0;
42 }
43
44 int currentPartition = 0;
45
46 // Try all possible partitions starting from startIndex
47 for (int endIndex = startIndex; endIndex < stringLength; ++endIndex) {
48 // Build the current partition value digit by digit
49 currentPartition = currentPartition * 10 + (numberString.charAt(endIndex) - '0');
50
51 // Optimization: stop if current partition exceeds remaining target
52 if (currentPartition > targetSum) {
53 break;
54 }
55
56 // Recursively check if the remaining string can sum to (targetSum - currentPartition)
57 if (check(numberString, endIndex + 1, targetSum - currentPartition)) {
58 return true;
59 }
60 }
61
62 return false;
63 }
64}
65
1class Solution {
2public:
3 int punishmentNumber(int n) {
4 int result = 0;
5
6 // Iterate through all numbers from 1 to n
7 for (int num = 1; num <= n; ++num) {
8 int square = num * num;
9 string squareStr = to_string(square);
10
11 // Check if the square can be partitioned to sum up to the original number
12 if (canPartitionToSum(squareStr, 0, num)) {
13 result += square;
14 }
15 }
16
17 return result;
18 }
19
20private:
21 // Recursively check if string can be partitioned into substrings whose sum equals target
22 bool canPartitionToSum(const string& str, int startIndex, int targetSum) {
23 int strLength = str.size();
24
25 // Base case: if we've processed the entire string, check if target sum is reached
26 if (startIndex >= strLength) {
27 return targetSum == 0;
28 }
29
30 int currentNum = 0;
31
32 // Try all possible partitions starting from current index
33 for (int endIndex = startIndex; endIndex < strLength; ++endIndex) {
34 // Build the current number from substring
35 currentNum = currentNum * 10 + (str[endIndex] - '0');
36
37 // Optimization: stop if current number exceeds remaining target
38 if (currentNum > targetSum) {
39 break;
40 }
41
42 // Recursively check if remaining string can sum to (targetSum - currentNum)
43 if (canPartitionToSum(str, endIndex + 1, targetSum - currentNum)) {
44 return true;
45 }
46 }
47
48 return false;
49 }
50};
51
1/**
2 * Calculates the sum of squares of all integers from 1 to n that satisfy the punishment property.
3 * A number i has the punishment property if its square can be partitioned into contiguous substrings
4 * that sum to i itself.
5 * @param n - The upper limit to check for punishment numbers
6 * @returns The sum of squares of all punishment numbers from 1 to n
7 */
8function punishmentNumber(n: number): number {
9 /**
10 * Recursively checks if a string representation of a number can be partitioned
11 * into substrings that sum to a target value.
12 * @param numberString - The string representation of the number to partition
13 * @param startIndex - The current starting position in the string
14 * @param targetSum - The remaining sum we need to achieve
15 * @returns True if a valid partition exists, false otherwise
16 */
17 const checkPartition = (numberString: string, startIndex: number, targetSum: number): boolean => {
18 const stringLength = numberString.length;
19
20 // Base case: if we've processed the entire string, check if we've reached exactly 0
21 if (startIndex >= stringLength) {
22 return targetSum === 0;
23 }
24
25 let currentNumber = 0;
26
27 // Try all possible substrings starting from startIndex
28 for (let endIndex = startIndex; endIndex < stringLength; endIndex++) {
29 // Build the current number by adding digits
30 currentNumber = currentNumber * 10 + Number(numberString[endIndex]);
31
32 // Pruning: if current number exceeds target, no point continuing
33 if (currentNumber > targetSum) {
34 break;
35 }
36
37 // Recursively check if the remaining string can sum to (targetSum - currentNumber)
38 if (checkPartition(numberString, endIndex + 1, targetSum - currentNumber)) {
39 return true;
40 }
41 }
42
43 return false;
44 };
45
46 let totalSum = 0;
47
48 // Check each number from 1 to n
49 for (let currentNumber = 1; currentNumber <= n; currentNumber++) {
50 const square = currentNumber * currentNumber;
51 const squareString = square.toString();
52
53 // Check if the square can be partitioned to sum to the original number
54 if (checkPartition(squareString, 0, currentNumber)) {
55 totalSum += square;
56 }
57 }
58
59 return totalSum;
60}
61
Time and Space Complexity
The time complexity is O(n^(1 + 2*log₁₀(2)))
or approximately O(n^1.602)
.
Let me break down the analysis:
-
The outer loop runs from 1 to n, contributing
O(n)
iterations. -
For each number
i
, we computei²
and convert it to a string. The squarei²
can have up to2*log₁₀(i)
digits, which is at most2*log₁₀(n)
digits. -
The
check
function explores all possible ways to partition the string representation ofi²
. In the worst case, for a string of lengthm
, the number of recursive calls can be exponential inm
. Specifically:- At each position, we can choose to split at any of the remaining positions
- This creates a branching factor that leads to
O(2^m)
possible partitions in the worst case - Since
m = O(log(i²)) = O(log(n²)) = O(2*log₁₀(n))
, the check function has complexityO(2^(2*log₁₀(n)))
-
Using the property that
2^(log₁₀(n)) = n^(log₁₀(2))
, we get:O(2^(2*log₁₀(n))) = O((2^(log₁₀(n)))²) = O((n^(log₁₀(2)))²) = O(n^(2*log₁₀(2)))
-
The total time complexity is:
O(n) * O(n^(2*log₁₀(2))) = O(n^(1 + 2*log₁₀(2)))
Since log₁₀(2) ≈ 0.301
, this gives us O(n^1.602)
.
The space complexity is O(log n)
due to:
- The recursion depth of the
check
function, which is bounded by the length of the string (at mostO(log n)
digits) - The string storage for
str(x)
which takesO(log n)
space
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow in Number Construction
When building current_number
by repeatedly multiplying by 10 and adding digits, large sequences of digits can cause integer overflow in languages with fixed integer sizes. While Python handles arbitrary precision integers automatically, this could be problematic in other languages.
Solution: Add a check to ensure current_number
doesn't exceed reasonable bounds, or use appropriate data types for your language.
2. Missing Early Termination Optimization
A critical optimization that's often overlooked is stopping the partition attempt when current_number > target_sum
. Without this pruning, the algorithm explores unnecessary branches where the current number alone already exceeds what we need.
Example: For string "1000" with target 10, without pruning we'd try "100", "1000", etc., even though "100" already exceeds 10.
Solution: Always include the pruning condition:
if current_number > target_sum: break
3. Incorrect Base Case Handling
A common mistake is checking if start_idx == string_length
instead of if start_idx >= string_length
. This can cause issues if the recursion somehow skips indices or if the logic is modified later.
Solution: Use >=
for defensive programming:
if start_idx >= string_length: return target_sum == 0
4. Leading Zeros in Partitions
The current solution treats "01" as 1, which might not be the intended behavior. For example, if we have "100" and partition it as "1", "00", the "00" becomes 0, which works but might violate the problem's intent if leading zeros aren't allowed in multi-digit numbers.
Solution: If leading zeros should invalidate a partition, add a check:
# Skip partitions with leading zeros (except single zero) if num_str[start_idx] == '0' and end_idx > start_idx: continue
5. Forgetting to Convert Square to String
Attempting to partition the integer directly instead of its string representation will cause errors. The algorithm relies on string indexing and character-by-character processing.
Solution: Always convert to string before calling the partition function:
if can_partition_to_target(str(square), 0, num): # Don't forget str()
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!