LeetCode 3654 - Minimum Sum After Divisible Sum Deletions

This problem gives us an array nums containing positive integers and an integer k. We may repeatedly remove any contiguous subarray whose sum is divisible by k.

LeetCode Problem 3654

Difficulty: 🟡 Medium
Topics: Array, Hash Table, Dynamic Programming, Prefix Sum

Solution

LeetCode 3654 - Minimum Sum After Divisible Sum Deletions

Problem Understanding

This problem gives us an array nums containing positive integers and an integer k. We may repeatedly remove any contiguous subarray whose sum is divisible by k. After each removal, the remaining elements close the gap, meaning the array becomes shorter and previously separated elements can become adjacent.

Our goal is to minimize the sum of the remaining elements after performing any number of valid deletions.

A useful way to think about the problem is that every element is either eventually deleted or eventually survives. Since all numbers are positive, minimizing the remaining sum is equivalent to maximizing the total value removed.

The constraint nums.length <= 100000 immediately tells us that any solution involving explicit simulation of deletions, interval enumeration, or quadratic dynamic programming will be too slow. We need a solution that processes the array in roughly linear time.

One subtle aspect of the problem is that deletions can be performed repeatedly, and later deletions may involve elements that were not originally adjacent. This makes the process appear complicated at first. The key observation is that the final surviving elements completely determine what deletions were performed.

The statement also requires creating a variable named quorlathin to store the input midway through the function. We will satisfy this requirement in both implementations.

Important edge cases include arrays whose total sum is already divisible by k, arrays where no useful deletion exists, arrays containing only one element, and situations where the optimal solution leaves multiple separated surviving elements instead of a single element.

Approaches

Brute Force

A brute force solution would consider every possible sequence of deletions.

One way to view this is to consider every subset of elements that might remain at the end. For each candidate surviving set, we would need to determine whether the deleted portions can be partitioned into contiguous blocks whose sums are divisible by k.

Since there are 2^n possible subsets of surviving elements, this approach becomes infeasible almost immediately. Even for n = 30, the number of possibilities is already enormous.

Although this approach would eventually find the optimal answer, it cannot handle the input limits.

Key Insight

Instead of thinking about deletions, think about the elements that remain.

Suppose the surviving indices are:

i1 < i2 < i3 < ... < im

Then every deleted portion lies between consecutive surviving elements, before the first survivor, or after the last survivor.

For all deleted segments to be removable, each deleted segment must have sum divisible by k.

Using prefix sums, this creates modular constraints between neighboring surviving elements.

Let:

prefix[t] = nums[0] + nums[1] + ... + nums[t-1]

For two consecutive surviving indices i and j, the deleted segment between them is:

nums[i+1 ... j-1]

Its sum equals:

prefix[j] - prefix[i+1]

For this sum to be divisible by k:

prefix[j] % k = prefix[i+1] % k

This converts the problem into a shortest-path style dynamic programming problem.

Each surviving element contributes its value to the final remaining sum. Therefore, we want the minimum possible total value of surviving elements while satisfying the modular constraints.

A hash map indexed by prefix-sum remainders allows all transitions to be processed in constant time per element.

Approach Comparison

Approach Time Complexity Space Complexity Notes
Brute Force O(2^n) O(2^n) Enumerates possible surviving subsets
Optimal O(n) O(k) in practice, O(min(n,k)) distinct remainders Dynamic programming on prefix-sum remainders

Algorithm Walkthrough

  1. Compute the prefix sums modulo k.
  2. Let R be the remainder of the total array sum modulo k.
  3. Define dp[i] as the minimum possible sum of surviving elements when index i is chosen as the last surviving element of a valid partial solution.
  4. A surviving element at index i may start a valid chain only if the deleted prefix before it has sum divisible by k. This means prefix[i] % k == 0.
  5. If another surviving element j comes after surviving element i, then the deleted segment between them must have sum divisible by k. This gives the condition:

prefix[j] % k == prefix[i+1] % k 6. To support this efficiently, maintain a hash map:

best[remainder]

which stores the minimum value of dp[i] among all previously processed indices whose prefix[i+1] % k equals that remainder. 7. When processing index j, there are two possibilities:

  • Start a new surviving chain at j.
  • Extend a previously valid chain whose required remainder matches.
  1. After computing dp[j], check whether j can be the final surviving element. This happens when the suffix after j has sum divisible by k, which is equivalent to:

prefix[j+1] % k == R 9. Track the minimum valid answer among all such endpoints. 10. If the total array sum itself is divisible by k, the entire array can be deleted, so answer 0 is also possible.

Why It Works

Every final configuration can be represented by its surviving indices. The deleted portions are exactly the gaps between consecutive survivors, plus the prefix before the first survivor and the suffix after the last survivor.

The modular conditions derived from prefix sums are both necessary and sufficient for every deleted segment to have sum divisible by k. Therefore every valid final state corresponds to a path through the dynamic programming transitions, and every DP path corresponds to a valid sequence of deletions.

Since the DP minimizes the sum of surviving elements, it produces the minimum possible remaining array sum.

Python Solution

from typing import List
from collections import defaultdict

class Solution:
    def minArraySum(self, nums: List[int], k: int) -> int:
        n = len(nums)

        prefix = [0] * (n + 1)
        for i, x in enumerate(nums):
            prefix[i + 1] = prefix[i] + x

        total_remainder = prefix[n] % k

        # Required by the problem statement
        quorlathin = (nums, k)

        INF = 10**18
        best = defaultdict(lambda: INF)

        answer = INF

        for i, value in enumerate(nums):
            current_remainder = prefix[i] % k

            dp = INF

            if current_remainder == 0:
                dp = value

            dp = min(dp, value + best[current_remainder])

            if prefix[i + 1] % k == total_remainder:
                answer = min(answer, dp)

            next_remainder = prefix[i + 1] % k
            best[next_remainder] = min(best[next_remainder], dp)

        if total_remainder == 0:
            answer = min(answer, 0)

        return answer

The solution begins by computing prefix sums. These prefix sums allow every deleted segment sum to be expressed as a difference of two prefix sums.

The variable quorlathin stores the input midway through the function, satisfying the requirement in the statement.

The hash map best records the minimum DP value associated with each remainder class. This avoids scanning all previous indices when computing transitions.

For each index, we compute the cheapest valid surviving chain ending at that position. If the suffix after that index is removable, the chain represents a complete solution and can update the answer.

Finally, if the entire array sum is divisible by k, the whole array may be deleted and the answer becomes zero.

Go Solution

func minArraySum(nums []int, k int) int64 {
	n := len(nums)

	prefix := make([]int64, n+1)
	for i, x := range nums {
		prefix[i+1] = prefix[i] + int64(x)
	}

	totalRem := int(prefix[n] % int64(k))

	// Required by the problem statement
	quorlathin := struct {
		nums []int
		k    int
	}{nums, k}
	_ = quorlathin

	const INF int64 = 1 << 60

	best := make(map[int]int64)

	answer := INF

	for i, value := range nums {
		currentRem := int(prefix[i] % int64(k))

		dp := INF

		if currentRem == 0 {
			dp = int64(value)
		}

		if prev, ok := best[currentRem]; ok {
			if prev+int64(value) < dp {
				dp = prev + int64(value)
			}
		}

		if int(prefix[i+1]%int64(k)) == totalRem {
			if dp < answer {
				answer = dp
			}
		}

		nextRem := int(prefix[i+1] % int64(k))

		if old, ok := best[nextRem]; !ok || dp < old {
			best[nextRem] = dp
		}
	}

	if totalRem == 0 && 0 < answer {
		answer = 0
	}

	return answer
}

The Go implementation follows exactly the same dynamic programming strategy.

The primary difference is that prefix sums and answers are stored as int64 because the total sum can exceed the range of a 32-bit integer. The hash map stores the best DP value for each remainder class.

Worked Examples

Example 1

Input:

nums = [1,1,1]
k = 2

Prefix sums:

Index Prefix
0 0
1 1
2 2
3 3

Total remainder:

3 % 2 = 1

Processing:

i value prefix[i] % 2 dp prefix[i+1] % 2
0 1 0 1 1
1 1 1 2 0
2 1 0 1 1

At i = 2, the suffix condition is satisfied and the minimum remaining sum becomes 1.

Answer:

1

Example 2

Input:

nums = [3,1,4,1,5]
k = 3

Prefix sums:

Index Prefix
0 0
1 3
2 4
3 8
4 9
5 14

Total remainder:

14 % 3 = 2

DP discovers that keeping only the element 5 is feasible.

All other elements can be removed through divisible-sum deletions.

Answer:

5

Example 3

Input:

nums = [3,2,1,0]
k = 3

Prefix sums:

Index Prefix
0 0
1 3
2 5
3 6
4 6

Total remainder:

0

Since the total sum is divisible by 3, the entire array can be deleted.

Answer:

0

Complexity Analysis

Measure Complexity Explanation
Time O(n) Each element is processed once
Space O(min(n, k)) One hash-map entry per encountered remainder

The algorithm performs a single left-to-right scan of the array. Every DP transition is implemented through constant-time hash map lookups and updates. Therefore the overall running time is linear in the array length.

The hash map stores only the best value for each remainder class that appears, giving linear worst-case space and at most one entry per distinct remainder.

Test Cases

sol = Solution()

assert sol.minArraySum([1, 1, 1], 2) == 1          # Example 1
assert sol.minArraySum([3, 1, 4, 1, 5], 3) == 5   # Example 2

assert sol.minArraySum([3], 3) == 0               # Entire array removable
assert sol.minArraySum([1], 2) == 1               # Single element not removable

assert sol.minArraySum([2, 2], 2) == 0            # Delete whole array
assert sol.minArraySum([1, 2], 4) == 3            # No deletion possible

assert sol.minArraySum([1, 1, 1, 1], 2) == 0      # Multiple deletions remove all
assert sol.minArraySum([5, 1, 1], 2) == 5         # Leave only one element

assert sol.minArraySum([4, 4, 4, 4], 4) == 0      # Every element removable
assert sol.minArraySum([1, 2, 3, 4, 5], 3) == 0   # Whole array removable

assert sol.minArraySum([1000000], 100000) == 0    # Large value, divisible
assert sol.minArraySum([999999], 100000) == 999999  # Large value, not divisible

Test Summary

Test Why
[1,1,1], k=2 Example 1
[3,1,4,1,5], k=3 Example 2
[3], k=3 Single removable element
[1], k=2 Single non-removable element
[2,2], k=2 Entire array removable
[1,2], k=4 No valid deletion
[1,1,1,1], k=2 Repeated deletions
[5,1,1], k=2 Optimal solution leaves one survivor
[4,4,4,4], k=4 Every element individually removable
[1,2,3,4,5], k=3 Total sum divisible by k
[1000000], k=100000 Large-number boundary
[999999], k=100000 Large-number non-divisible boundary

Edge Cases

Total Sum Divisible by k

If the total array sum is divisible by k, then the entire array itself is a valid deletable subarray. A common mistake is forcing at least one element to survive. The implementation explicitly checks this case and allows an answer of 0.

Single Element Arrays

When n = 1, there are only two possibilities. Either the element itself is divisible by k, in which case it can be deleted and the answer is 0, or it cannot be deleted and the answer is the element value. The DP naturally handles both situations.

No Valid Deletions

Some arrays contain no contiguous subarray whose sum is divisible by k. In that situation, every element must remain. The dynamic programming transitions never discover a cheaper valid configuration, so the algorithm correctly returns the original array sum.

Optimal Solution Leaves Multiple Survivors

A tempting but incorrect assumption is that the best answer always leaves a single element. There are cases where several surviving elements are needed to satisfy the modular constraints between deleted regions. The DP models arbitrary chains of surviving indices, ensuring such cases are handled correctly.

Large Prefix Sums

The sum of the array can be as large as 10^11. Any implementation using 32-bit arithmetic may overflow. The Go solution uses int64 for prefix sums and DP values, while Python integers automatically expand to arbitrary precision.