# https://stackoverflow.com/questions/78359610/split-array-of-integers-into-subarrays-with-the-biggest-sum-of-difference-betwee
# Code by https://stackoverflow.com/users/585411/btilly
from collections import deque
def window_mins_maxes (size, array):
min_values = deque()
min_positions = deque()
max_values = deque()
max_positions = deque()
for i, value in enumerate(array):
if size <= i:
yield (i, min_values[0], max_values[0])
if min_positions[0] <= i - size:
min_values.popleft()
min_positions.popleft()
if max_positions[0] <= i - size:
max_values.popleft()
max_positions.popleft()
while 0 < len(min_values) and value <= min_values[-1]:
min_values.pop()
min_positions.pop()
min_values.append(value)
min_positions.append(i)
while 0 < len(max_values) and max_values[-1] <= value:
max_values.pop()
max_positions.pop()
max_values.append(value)
max_positions.append(i)
yield (len(array), min_values[0], max_values[0])
def partition_array(numbers, min_len, max_len):
if max_len < min_len or len(numbers) < min_len:
return (None, None)
best_weight = [None for _ in numbers]
prev_index = [None for _ in numbers]
# Need an extra entry for off of the end of the array.
best_weight.append(None)
prev_index.append(None)
best_weight[0] = 0
for i, min_value, max_value in window_mins_maxes(min_len, numbers):
window_start_weight = best_weight[i - min_len]
if window_start_weight is not None:
j = i
while j - i < max_len - min_len and j < len(numbers):
new_weight = window_start_weight + max_value - min_value
if best_weight[j] is None or best_weight[j] < new_weight:
best_weight[j] = new_weight
prev_index[j] = i - min_len
if numbers[j] < min_value:
min_value = numbers[j]
if max_value < numbers[j]:
max_value = numbers[j]
j += 1
# And fill in the longest value.
new_weight = window_start_weight + max_value - min_value
if best_weight[j] is None or best_weight[j] < new_weight:
best_weight[j] = new_weight
prev_index[j] = i - min_len
if best_weight[-1] is None:
return (None, None)
else:
path = [len(numbers)]
while prev_index[path[-1]] is not None:
path.append(prev_index[path[-1]])
path = list(reversed(path))
partitioned = [numbers[path[i]:path[i+1]] for i in range(len(path)-1)]
return (best_weight[-1], partitioned)
# End code by https://stackoverflow.com/users/585411/btilly
# Code by https://stackoverflow.com/users/2034787/%d7%92%d7%9c%d7%a2%d7%93-%d7%91%d7%a8%d7%a7%d7%9f
import collections
debug = False
# O(n) method to get maximum value
# of the dp table within an index range.
# Could be done in O(log n) with a tree.
def get_max_dp(dp, l, r):
if r == -1:
return (0, -1)
if r < 0:
return (-float("inf"), -1)
max_val = 0 if l == -1 else dp[l]
index = l
for i in range(l + 1, r + 1):
if dp[i] > max_val:
max_val, index = dp[i], i
return (max_val, index)
# Updates the queue of max values and
# returns the index in the list for the current
# max in the range [i - a + 1, i]
def update_and_get_maxs(maxs, A, i, a, b):
while maxs and maxs[0] <= i - b:
maxs.popleft()
while maxs and A[i] >= A[maxs[-1]]:
maxs.pop()
maxs.append(i)
for j in range(len(maxs)):
if maxs[j] > i - a:
return j
# Updates the queue of min values and
# returns the index in the list for the current
# min in the range [i - a + 1, i]
def update_and_get_mins(mins, A, i, a, b):
while mins and mins[0] <= i - b:
mins.popleft()
while mins and A[i] <= A[mins[-1]]:
mins.pop()
mins.append(i)
for j in range(len(mins)):
if mins[j] > i - a:
return j
# O(n) method to get the best of all windows
# tracked. Could be maintained in O(log n) and
# queried in O(1).
def get_max_from_leftmosts(A, leftmosts):
best = -float("inf")
for (t, max_idx, min_idx, dp_idx) in leftmosts:
best = max(best, t)
return best
# Method to get the left (lower) bound index
# for the choice of dp value for a window.
def get_left(A, leftmosts, j, i, a, b):
if A[leftmosts[j+1][1]] == A[leftmosts[j][1]]:
return leftmosts[j][2]
elif A[leftmosts[j+1][2]] != A[leftmosts[j][2]]:
return max(leftmosts[j][1], leftmosts[j][2])
else:
return leftmosts[j][1]
# Main method
def f(A, a, b):
dp = [-float("inf")] * len(A)
maxs = collections.deque()
mins = collections.deque()
# A list of relevant windows stored as tuples:
# [value, max index, min index, dp choice for window]
leftmosts = collections.deque()
# Previous (max, min) for the [i - a, i] window
prev = (None, None)
def print_state():
print(f"dp: {dp}")
print(f"maxs: {maxs}")
print(f"mins: {mins}")
print(f"leftmosts: {leftmosts}")
print(f"prev: {prev}")
iterations = 0
for i in range(len(A)):
if debug:
print(f"\ni: {i}")
mx_a_idx = update_and_get_maxs(maxs, A, i, a, b)
mn_a_idx = update_and_get_mins(mins, A, i, a, b)
if debug:
print(f"mx_a_idx: {mx_a_idx}")
print(f"mn_a_idx: {mn_a_idx}")
curr_max_idx = maxs[mx_a_idx]
curr_min_idx = mins[mn_a_idx]
if debug:
print(f"curr_max_idx: {curr_max_idx}")
print(f"curr_min_idx: {curr_min_idx}")
curr = (curr_max_idx, curr_min_idx)
# New max or min
if i + 1 == a or (i + 1 > a and prev != curr):
leftmosts.append([
None,
curr_max_idx,
curr_min_idx,
None])
for j in range(len(leftmosts) - 2, - 1, -1):
[t, max_idx, min_idx, dp_idx] = leftmosts[j]
# Similar to the queues for maxes and mins --
# the new max or min should carry through to all
# preceding window choices while it's dominant.
should_update = (A[curr_max_idx] >= A[max_idx]
or A[curr_min_idx] <= A[min_idx])
if should_update:
# Crude count of iterations to try to evaluate
# time complexity of the overall method.
iterations += 1
if A[curr_max_idx] >= A[max_idx]:
leftmosts[j][1] = curr_max_idx
if A[curr_min_idx] <= A[min_idx]:
leftmosts[j][2] = curr_min_idx
# If two windows now have the same max and min,
# remove the window since the dp choice can simply
# extend to the rightmost window.
if (A[leftmosts[j][1]] == A[leftmosts[j+1][1]]
and A[leftmosts[j][2]] == A[leftmosts[j+1][2]]):
del leftmosts[j]
# Otherwise, we can use this updated window to
# update the dp choice for the window to its right.
else:
r = min(i - a, leftmosts[j+1][1] - 1,
leftmosts[j+1][2] - 1)
l = get_left(A, leftmosts, j, i, a, b)
if debug:
print(f"(j, l, r): {(j, l, r)}")
max_dp, max_dp_idx = get_max_dp(dp, l, r)
leftmosts[j+1][0] = (A[leftmosts[j+1][1]]
- A[leftmosts[j+1][2]] + max_dp)
leftmosts[j+1][3] = max_dp_idx
if not should_update:
break
# Remove out-of-bounds windows
if leftmosts and (leftmosts[0][1] <= i - b
or leftmosts[0][2] <= i - b):
leftmosts.popleft()
# Regardless of if the first window, [i - a + 1, i], has
# a new max or min, we need to update its choice of
# dp since we are shifting the window bounds to the
# right.
if len(leftmosts) > 1:
r = min(i - a, leftmosts[-1][1] - 1,
leftmosts[-1][2] - 1)
l = get_left(A, leftmosts, -2, i, a, b)
if debug:
print(f"(j, l, r): {(-2, l, r)}")
max_dp, max_dp_idx = get_max_dp(dp, l, r)
leftmosts[-1][0] = (A[leftmosts[-1][1]]
- A[leftmosts[-1][2]] + max_dp)
leftmosts[-1][3] = max_dp_idx
# Update the dp choice for the leftmost window since
# the previous choice could now be out of bounds.
if leftmosts:
r = min(i - a, leftmosts[0][1] - 1, leftmosts[0][2] - 1)
l = max(i - b, -1)
if debug:
print(f"(j, l, r): {(-1, l, r)}")
max_dp, max_dp_idx = get_max_dp(dp, l, r)
leftmosts[0][0] = (A[leftmosts[0][1]]
- A[leftmosts[0][2]] + max_dp)
leftmosts[0][3] = max_dp_idx
# Save the result for the current dp index
if i + 1 >= a:
dp[i] = get_max_from_leftmosts(A, leftmosts)
if debug:
print_state()
prev = curr
# Crude method to detect if complexity could be
# reaching O(n^2).
if iterations > 3 * len(A):
print((iterations, len(A), a, b, A))
return dp[-1]
import random
num_tests = 500
max_n = 500
max_val = 10000
for _ in range(num_tests):
n = random.randint(2, max_n)
A = [random.randint(1, max_val) for _ in range(n)]
a = random.randint(2, n)
b = random.randint(a, n)
left = partition_array(A, a, b)
right = f(A, a, b)
if not (left[0] is None and right == -float("inf")) and left[0] != right:
print((A, a, b, left, right))
break