#          SLIDING WINDOW METHOD
#     illustrated on a simple sum problem
#   =======================================
#
#
# In the given array of positive values
# find the longest contiguous subarray
# which sum is less than 20 (or less than given maxSum).
# For example
# array:            [ 5 2 10 10 10 5 6 5 1 10 10]
# longest subarray:               [5 6 5 1]
#
#
# The solution method is the sliding window method.
# The window is just a contiguous subarray.
# At the beginning it contains only the first element.
#
# The idea:
# When the sum of items in the window is too small
#      expand the right end of the window one position to the right
#      so that the sum if items in the window is increased.
# When the sum of items in the window is too big
#      shrink the window by moving its left end (=begin)
#      one position to the right
#      so that the sum of items inside the window is decreased.
# By repeated application of these two conditions
# the window moves through the array until it finally arrives to the
# end of the array. In the process, it examines all feasible subarrays
# and it skips most subarrays which sums are too big or too small.
#
# The advantage of the method is its speed. In each step, either
# the begin or the end index of the subarray is moved to the right,
# so the total running time of the method is proportional
# to twice the length of the array.
# The running time of a brute force approach which would examine
# each possible subarray is proportional to the square or even
# to the cube of the length of the array.

import display

def longestSubArr( arr, maxSum ):

    # the window starts as very small, it contains only
    # the first element of the array
    begin, end = 0, 0, # begin and end of the sliding window
                       # end refers to the last item in the window
                       # not behind it
    currSum = arr[0]   # initial value

    # best values were not found yet:
    bestBegin, bestEnd, bestSum = -1, -1, -1

    # run sliding window
    while end < len(arr):

        # register best result when it is encountered
        if currSum < maxSum and end-begin > bestEnd - bestBegin:
            bestBegin, bestEnd, bestSum = begin, end, currSum

        # try to expand window to the right
        if currSum < maxSum:
            end += 1
            if end < len(arr): currSum += arr[end]
        # else shrink the window from the left
        else:
            currSum -= arr[begin]
            begin += 1

        # presentation display
        if len(arr) < 30: 
            display.display1( arr, begin, end )

    return bestBegin, bestEnd+1, bestSum

# plain brute force
# Never underestimate the force of brute force!
def longestSubArrBruteForce( arr, maxSum ):
    bestBegin, bestEnd, bestSum = -1, -1, -1
    bestLen = 0
    for iStart in range( 0, len(arr) ):
        for iEnd in range( iStart+1, len(arr)+1 ):
            currSum = sum( arr[iStart:iEnd] )
            if  currSum < maxSum:
               if bestLen < iEnd-iStart:
                  bestLen = iEnd-iStart
                  bestBegin, bestEnd, bestSum = iStart, iEnd, currSum

    return bestBegin, bestEnd, bestSum



# OPTIMIZATION?
# A. Improve brute force by considering only segments which are
# longer than the length of the best segment found so far.
# B. Also, when computing new sum of the segment which is only by one
# item longer than the previous segment, just increase the sum,
# do not calculate it anew for the whole segment.
# Note the refined +/- 1 index calculations, making the code prone to errors.

def longestSubArrBruteForceUpd( arr, maxSum ):
    bestBegin, bestEnd, bestSum = -1, -1, -1
    bestLen = 0
    for iStart in range( 0, len(arr) ):
        currSum = sum( arr[iStart:  iStart+1 + bestLen -1 ]  )
        for iEnd in range( iStart+1 + bestLen, len(arr)+1 ):
            currSum += arr[iEnd-1]
            if currSum < maxSum:
                bestLen = iEnd-iStart
                bestBegin, bestEnd, bestSum = iStart, iEnd, currSum
    return bestBegin, bestEnd, bestSum

# // what about pairs by some numpy combinatori etc?

# ------------ MAIN PROGRAM ------------------------------
#
# do experiment with the data and run the program repeatedly

arr = [2, 3, 7, 2, 6, 3, 4, 24, 1, 2, 6, 1, 4, 2, 7, 6]

bestBegin, bestEnd, bestSum = longestSubArr( arr, 20 )
print("Longest subarray: [" + str(bestBegin)+'..'+ str(bestEnd) + "], with sum", bestSum )

# exit()

# Now compare the speed of the methods using bigger data

import time
import random

arrLen = 10000
subArrLen = 40
print(' Array length: ', arrLen )
random.seed( 12312312 )
# random.choices( range_of_possible_values, k = len_of_returned_random_list )
a = random.choices( range(1, 20), k = arrLen )
print()

t1 = time.time()
bestBegin, bestEnd, bestSum = longestSubArr( a, subArrLen )
print("Longest subarray: [" + str(bestBegin)+'..'+ str(bestEnd) + "], with sum", bestSum )
print( a[bestBegin:bestEnd+1] )
t2 = time.time()
print('time: ', t2-t1 )
print()

'''
t1 = time.time()
bestBegin, bestEnd, bestSum = longestSubArrBruteForce( a, 40 )
print("Longest subarray: [" + str(bestBegin)+'..'+ str(bestEnd) + "], with sum", bestSum )
print( a[bestBegin:bestEnd+1] )
t2 = time.time()
print('time: ', t2-t1 )
'''

t1 = time.time()
print()
bestBegin, bestEnd, bestSum = longestSubArrBruteForceUpd( a, 40 )
print("Longest subarray: [" + str(bestBegin)+'..'+ str(bestEnd) + "], with sum", bestSum )
print( a[bestBegin:bestEnd+1] )
t2 = time.time()
print('time: ', t2-t1 )

# print(a)











