Facebook Pixel

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 equals i
  • 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 equals i
  • 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 of i * i. The number of possible partitions grows exponentially with the number of digits, but since we're dealing with squares of integers up to n (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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 ), 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:

  1. Starting from the beginning of the string
  2. Trying to form a number by taking 1 digit, 2 digits, 3 digits, etc.
  3. 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
  4. If we reach the end of the string and our remaining sum is exactly 0, we found a valid partition
  5. 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:

  1. Calculate its square: x = i * i
  2. Convert x to a string representation
  3. Use the check function to determine if this square can be partitioned to sum to i
  4. 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 number
  • i: Current starting index in the string
  • x: The remaining target sum we need to achieve

The DFS works as follows:

  1. Base Case: If we've processed the entire string (i >= m), we check if the remaining target x equals 0. If yes, we found a valid partition.

  2. Recursive Case: Starting from index i, try forming numbers of different lengths:

    • Initialize y = 0 to build the current number
    • For each position j from i 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

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 Evaluator

Example 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
    • Found valid partition "8" + "1" = 9 ✓
  • 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
        • Try taking "00": y = 0, remaining = 9
          • Reached end with target = 9 ≠ 0 → return False
    • 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
    • Found valid partition "10" + "0" = 10 ✓
  • 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 ✗
  • 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:

  1. The outer loop runs from 1 to n, contributing O(n) iterations.

  2. For each number i, we compute and convert it to a string. The square can have up to 2*log₁₀(i) digits, which is at most 2*log₁₀(n) digits.

  3. The check function explores all possible ways to partition the string representation of . In the worst case, for a string of length m, the number of recursive calls can be exponential in m. 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 complexity O(2^(2*log₁₀(n)))
  4. 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)))
  5. 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 most O(log n) digits)
  • The string storage for str(x) which takes O(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()
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More