Calculate total number of batteries used - python

Assume you have a phone, and several spare batteries.
arr1 => denotes the minutes for which every battery can last.
arr2 => denotes the minutes that every battery takes to be fully charged.
Once the battery is used up, you can charge it immediately and switch to the next fully charged battery.
You must use the batteries in the order the array denotes.
Suppose you will use the phone for x minute, return the number of batteries you will use.
If it is not possible to use the phone, then return -1
Example:
arr1 = [12, 3, 5, 18]
arr2 = [8, 1, 4, 9]
x = 16
output: 3
My Code:
arr1 = [12, 3, 5, 18]
arr2 = [8, 1, 4, 9]
x = 46 # getting correct result when x=16 but not when x=46
def solution(arr1, arr2, x):
import heapq
ready,charge=list(range(len(arr1))),[]
heapq.heapify(ready)
cur=res=0
while cur<x:
while charge and charge[0][0]<=cur:
heapq.heappush(ready, heapq.heappop(charge)[1])
if not ready:
return -1
i=heapq.heappop(ready)
res += 1
cur+=arr1[i]
heapq.heappush(charge,(cur+arr2[i],i))
return res
solution(arr1, arr2, x)
The code is giving an output 7.
But, the correct output is 5.

Here's an alternate function which doesn't involve iterating to find the solution. It computes the number of batteries required by looking at the total runtimes of the array of batteries, dividing x by the total runtime of all the batteries and then looking up the index of run time which will cover the balance (x % total_runtime). I've given a few ways of doing that lookup, dependent on what libraries (if any) are available.
In terms of whether the call can be completed, it looks at whether there is sufficient charge time (in the run time for the other batteries) for each battery before it has to be used again. If not, and the battery has to be used more than once, the call cannot be completed.
def solution(arr1, arr2, x):
# how many batteries?
numb = len(arr1)
# make cumulative sum of battery runtimes
runtimes = [sum(arr1[:i+1]) for i in range(numb)]
total_runtime = runtimes[numb-1]
# figure out how many batteries we need
batts = x // total_runtime * numb
x = x % total_runtime
if x > 0:
batts += bisect.bisect_left(runtimes, x) + 1
# or
# batts += np.searchsorted(runtimes, x) + 1
# or
# batts += next(idx + 1 for idx, value in enumerate(runtimes) if value >= x)
# check if any battery we use has a charge time greater than the total runtime of the other batteries
excess_charge_times = [total_runtime - runtime - arr2[idx] for idx, runtime in enumerate(arr1)]
# if a battery has to be used more than once and doesn't have enough charge time, fail
if any(batts > idx + numb and excess_charge_times[idx] < 0 for idx in range(numb)):
return -1
return batts

It seems you can implement this by simply looping through the sum of arr1 values until you reach x, maintaining a list of ready times for each battery so you can check whether the battery is currently available to use:
def solution(arr1, arr2, x):
numb = len(arr1)
count = 0
now = 0
ready = [0, 0, 0, 0]
while now < x:
batt = count % numb
if ready[batt] > now:
return -1
now += arr1[batt]
ready[batt] = now + arr2[batt]
count += 1
return count
solution([12, 3, 5, 18], [8, 1, 4, 9], 16)
# 3
solution([12, 3, 5, 18], [8, 1, 4, 9], 46)
# 5
solution([12, 3, 2], [6, 1, 1], 20)
# -1

Related

How do I measure the periodicity (or frequency) of a list of values?

Let's say I have a list of values: [0, 10, 20, 10, 0, 10, 20, 10, 0, ...]
Clearly there's periodicity. We see that there is a cycle every 5 entries. I want to measure the average periodicity, or the average number of entries it takes to complete a cycle, within the list above.
This seems similar to measuring autoocorrelation but I don't know where to begin to get some sort of measure of the "frequency" or "periodicity", aka how fast a cycle is completed.
Minimal version:
a=[0, 10, 20, 10, 0, 10, 20, 10, 0, 10, 20]
n=len(a)
# The idea is to compare the repeated subset of the array with the original array
# while keeping the sizes equal
periods = [i for i in range(2,n//2+1) if a[:i]*(n//i)==a[:n - n % i]]
print('Min period=',periods[0], '\n',a[:periods[0]])
Output:
Min period: 4
[0, 10, 20, 10]
for-loop version:
Here is the same idea with for-loop just to make it more clear:
a=[0, 10, 20, 10, 0, 10, 20, 10, 0, 10, 20]
n = len(a)
periods=[]
for i in range(2, n // 2 + 1): # cycle's max length = 1/2 of sequence
m = n // i
word = a[:i]
repeated_word = [a[:i]*m][0]
same_size_array = a[:len(repeated_word)]
isCycle = repeated_word == same_size_array
if isCycle:
periods.append(i)
print(
'%s-char word\t' % i,word,
'\nRepeated word\t',repeated_word,
'\nSame size array\t',same_size_array,
'\nEqual(a Cycle)?\t',isCycle
,'\n'
)
period = periods[0] # shortest cycle
print('Min period:',period,'\n',a[:period])
Output (long version):
2-char word [0, 10]
Repeated word [0, 10, 0, 10, 0, 10, 0, 10, 0, 10]
Same size array [0, 10, 20, 10, 0, 10, 20, 10, 0, 10]
Equal(a Cycle)? False
3-char word [0, 10, 20]
Repeated word [0, 10, 20, 0, 10, 20, 0, 10, 20]
Same size array [0, 10, 20, 10, 0, 10, 20, 10, 0]
Equal(a Cycle)? False
4-char word [0, 10, 20, 10]
Repeated word [0, 10, 20, 10, 0, 10, 20, 10]
Same size array [0, 10, 20, 10, 0, 10, 20, 10]
Equal(a Cycle)? True
5-char word [0, 10, 20, 10, 0]
Repeated word [0, 10, 20, 10, 0, 0, 10, 20, 10, 0]
Same size array [0, 10, 20, 10, 0, 10, 20, 10, 0, 10]
Equal(a Cycle)? False
Min period: 4
[0, 10, 20, 10]
The "pure" average periodicity will necessarily be equivalent to the length of the list divided by the count of occurrences of an element.
We can also account for first and last appearances, and use that in our calculation, although this may affect you calculation in ways that you might not want:
from collections import Counter
values = [0, 10, 20, 10, 0, 10, 20, 10, 0]
counts = Counter(values)
periodicities = dict()
r_values = values[::-1]
for k, v in counts.items():
print(r_values.index(k), values.index(k))
periodicities[k] = (len(values) - r_values.index(k) - values.index(k) + 1) / v
print(periodicities)
Result:
{
0: 3.3333333333333335,
10: 2.0,
20: 3.0
}
Note: I'm assuming you're referring to exact periodicity rather than some measure of autocorrelation. E.g., [1, 5, 8, 1, 5, 8.0000000001] would have a period of 6 rather than 3.
This is by no means optimal, but in a pinch anyone can brute force a solution that looks something like the following.
def period(L):
n = len(L)
for i in range(1, n):
if n%i:
# save a little work if `i` isn't a factor of `n`
continue
if all(L[k:k+i]==L[:i] for k in range(0, n, i)):
# `L` is just `L[:i]*x` for some `x`, and since `i` is
# increasing this must be the smallest `i` where that
# is true
return i
# if no factor suffices, the smallest generator is the entire list
return n
With a little more effort we can get linear performance rather than quadratic. Optimizing it further is left as an exercise for somebody who isn't me.
def period(L):
if not L:
return 0
guess = 1
for i, x in enumerate(L[1:], 1):
if x != L[i%guess]:
"""
We know for certain the period is not `guess`. Moreover, if we've
gotten this far we've ruled out every option less than `guess`.
Additionally, no multiple of `guess` suffices because the fact
that `L` consists of repetitions of width `guess` up till now means
that `i%(t*guess)!=x` for any `t` so that `t*guess<i`. Interestingly,
that's the precisely the observation required to conclude
`guess >= i+1`; there is some positive integer multiple of `guess`
so that `L[:i+1]` consists of a copy of that width and some number
of elements that are identical to that copy up to and excluding `x`.
Since `L[:i+1]` has that structure, no width between that multiple
of `guess` and `i` can generate `L`. Hence, the answer is at least
`i+1`.
"""
guess = i+1
while len(L)%guess:
"""
Additionally, the answer must be a factor of `len(L)`. This
might superficially look quadratic because of the loop-in-a
-loop aspect to it, but since `1<=guess<=len(L)` and `guess`
is always increasing we can't possibly run this code more
than some linear number of times.
"""
guess += 1
"""
If we've gotten this far, `guess` is a factor of `L`, and it is
exactly as wide as all the elements we've seen so far. If we
continue iterating through `L` and find that it is just a bunch
of copies of this initial segment then we'll be done. Otherwise,
we'll find ourselves in this same if-statement and reset our
`guess` again.
"""
return guess
If you want all periods, then those are simply every multiple of the minimum period which are also factors of the total length. Supposing you have a way to compute the prime factorization or all positive factors (including 1 and the integer itself) of a positive integer, the following routine can get you those. Actually finding the factors of an integer is probably out of scope and is well-answered elsewhere.
def all_periods(minimum_period, collection_size):
p, n = minimum_period, collection_size
if p==0:
yield = 0
return
for f in positive_factors(n / p):
yield f * p
Initiate an empty list
interval = []
and use a recursive function, like so:
def check_for_interval(interval,list):
## step 1: add first list element into your interval
interval.append(list[0])
## step 2: remove that element from your list
list.pop(0)
## step 3: get the current content of your interval, plus the next
## element, and check if the concatenated content appears another time
## in the source list.
## first, make sure you got only strings in your list, for join to work
str_interval = []
for y in interval:
str_interval.append(str(y))
## attach the next element, which now is the first one of the list
## because you popped the "new" first one above
str_interval.append(str(list[0]))
## next, concatenate the list content as string, like so:
current_interval = ",".join(str_interval)
## now, transform the current remaining list (except the "new" first
## element cause added in your test string above) into a string of the
## exact same structure (elements separated by commas)
str_test = []
list_test = list[1:]
for z in list_test:
str_test.append(str(z))
## next,concatenate the list content as string, like so:
remaining_elements = ",".join(str_test)
## finally, check if the current_interval is present inside the
## remaining_elements. If yes
if remaining_elements.find(current_interval) != -1:
## check if the amount of remaining elements is equal to the amount
## of elements constituting the interval -1 at the moment, OR if the
## current_interval is found in the remaining elements, its
## starting index is equal to 0, and the len of str_test is a pair
## entire multiple of str_interval
check_list_test = remaining_elements.split(",")
check_list_interval = current_interval.split(",")
if (len(str_interval) == len(str_test)) or (remaining_elements.find(current_interval) == 0 and len(str_test) % len(str_interval) == 0 and (len(str_test) / len(str_interval)) % 2 == 0 and (len(check_list_test) / len(check_list_interval)) * check_list_interval == check_list_test):
## If yes, attach the "new" first element of the list to the interval
## (that last element was included in str_interval, but is not yet
## present in interval)
interval.append(list[0])
## and print the output
print("your interval is: " + str(interval))
else:
## otherwise, call the function recursively
check_for_interval(interval,list)
else:
## when the current interval is not found in the remaining elements,
## and the source list has been fully iterated (str_test's length
## == 0), this also means that we've found our interval
if len(str_test) == 0:
## add the last list element into the interval
interval.append(list[0])
print("your interval is: " + str(interval))
else:
## In all other cases, again call the function recursively
check_for_interval(interval,list)
OPTIMIZED CODE ONLY, WITHOUT COMMENTS
def int_to_str_list(source):
new_str_list = []
for z in source:
new_str_list.append(str(z))
return new_str_list
def check_for_interval(interval,list):
interval.append(list[0])
list.pop(0)
str_interval = int_to_str_list(interval)
str_interval.append(str(list[0]))
current_interval = ",".join(str_interval)
str_test = int_to_str_list(list[1:])
remaining_elements = ",".join(str_test)
str_exam = remaining_elements.find(current_interval)
if str_exam != -1:
interval_size = len(str_interval)
remaining_size = len(str_test)
rem_div_inter = remaining_size / interval_size
if (interval_size == remaining_size) or (str_exam == 0 and remaining_size % interval_size == 0 and rem_div_inter % 2 == 0 and rem_div_inter * str_interval == str_test):
interval.append(list[0])
print("your interval is: " + str(interval))
else:
check_for_interval(interval,list)
else:
if len(str_test) == 0 :
interval.append(list[0])
print("your interval is: " + str(interval))
else:
check_for_interval(interval,list)
To do what you want, simply run your function after initiating []
interval = []
check_for_interval(interval,list)
should work for pretty much any case, delivering you the interval as output.
Here is one way to approach this problem. Basically, you iterate from 2 to len(lst)//2 + 1 and check if the first n elements matches every next n elements, return n if true. If no match is found, return len(lst)
def get_periodicity(lst):
t = len(lst)
for n in range(2, t//2 + 1):
for p in range(1, t//n):
if lst[:n] != lst[n*p:n*p+n]:
break
else:
rem = t%n
if not rem or lst[-rem:] == lst[:rem]:
return n
else:
return t
Tests
>>> get_periodicity([0, 10, 20, 10, 0, 10, 20, 10, 0, 10, 20])
4
>>> get_periodicity([1,1,2,1,1,2,1,1,2,1,1,2])
3
>>> get_periodicity([1,1,2,1,1,2,1,1,2,1,1,2,3])
13

min sum of consecutive values with Divide and Conquer

Given an array of random integers
N = [1,...,n]
I need to find min sum of two consecutive values using divide and conquer.
What is not working here but my IQ?
def minSum(array):
if len(array) < 2:
return array[0]+array[1]
if (len(a)%2) != 0:
mid = int(len(array)/2)
leftArray = array[:mid]
rightArray = array[mid+1:]
return min(minSum(leftArray),minSum(rightArray),crossSum(array,mid))
else:
mid = int(len(array)/2)
leftArray = array[:mid]
rightArray = array[mid:]
return min(minSum(leftArray), minSum(rightArray), array[mid]+array[mid+1])
def crossSum(array,mid):
return min(array[mid-1]+array[mid],array[mid]+array[mid+1])
The main problem seems to be that the first condition is wrong: If len(array) < 2, then the following line is bound to raise an IndexError. Also, a is not defined. I assume that that's the name of the array in the outer scope, thus this does not raise an exception but just silently uses the wrong array. Apart from that, the function seems to more-or-less work (did not test it thoroughly, though.
However, you do not really need to check whether the array has odd or even length, you can just use the same code for both cases, making the crossSum function unneccesary. Also, it is kind of confusing that the function for returning the min sum is called maxSum. If you really want a divide-and-conquer approach, try this:
def minSum(array):
if len(array) < 2:
return 10**100
elif len(array) == 2:
return array[0]+array[1]
else:
# len >= 3 -> both halves guaranteed non-empty
mid = len(array) // 2
leftArray = array[:mid]
rightArray = array[mid:]
return min(minSum(leftArray),
minSum(rightArray),
leftArray[-1] + rightArray[0])
import random
lst = [random.randint(1, 10) for _ in range(20)]
r = minSum(lst)
print(lst)
print(r)
Random example output:
[1, 5, 6, 4, 1, 2, 2, 10, 7, 10, 8, 4, 9, 5, 7, 6, 5, 1, 4, 9]
3
However, a simple loop would be much better suited for the problem:
def minSum(array):
return min(array[i-1] + array[i] for i in range(1, len(array)))

Tensorflow : How to perform an operation parallel on every n elements?

What should I do if I want to get the sum of every 3 elements?
test_arr = [1,2,3,4,5,6,7,8]
It sounds like a map function
map_fn(arr, parallel_iterations = True, lambda a,b,c : a+b+c)
and the result of map_fn(test_arr) should be
[6,9,12,15,18,21]
which equals to
[(1+2+3),(2+3+4),(3+4+5),(4+5+6),(5+6+7),(6+7+8)]
I have worked out a solution after reviewing the official docs: https://www.tensorflow.org/api_docs/python/tf/map_fn
import tensorflow as tf
def tf_map_elements_every(n, tf_op, input, dtype):
if n >= input.shape[0]:
return tf_op(input)
else:
return tf.map_fn(
lambda params: tf_op(params),
[input[i:i-n+1] if i !=n-1 else input[i:] for i in range(n)],
dtype=dtype
)
Test
t = tf.constant([1, 2, 3, 4, 5, 6, 7, 8])
op = tf_map_elements_every(3, tf.reduce_sum, t, tf.int32)
sess = tf.Session()
sess.run(op)
[Out]: array([ 6, 9, 12, 15, 18, 21])
It's even easier: use a list comprehension.
Slice the list into 3-element segments and take the sum of each.
Wrap those in a list.
[sum(test_arr[i-2:i+1])
for i in range(2, len(test_arr))]
Simply loop through your array until you are 3 from the end.
# Takes a collection as argument
def map_function(array):
# Initialise results and i
results = []
int i = 0
# While i is less than 3 positions away from the end of the array
while(i <= (len(array) - 3)):
# Add the sum of the next 3 elements in results
results.append(array[i] + array[i + 1] + array[i + 2]
# Increment i
i += 1
# Return the array
return results

How can I determine if the numbers in a list initially increase (or stay the same) and then decrease (or stay the same) with Python?

For example, the digits of 123431 and 4577852 increase and then decrease. I wrote a code that breaks the numbers into a list and is able to tell if all of the digits increase or if all of the digits decrease, but I don't know how to check for digits increasing then decreasing. How do I extend this?
x = int(input("Please enter a number: "))
y = [int(d) for d in str(x)]
def isDecreasing(y):
for i in range(len(y) - 1):
if y[i] < y[i + 1]:
return False
return True
if isDecreasing(y) == True or sorted(y) == y:
print("Yes")
Find the maximum element.
Break the list into two pieces at that location.
Check that the first piece is increasing, the second decreasing.
For your second example, 4577852, you find the largest element, 8.
Break the list in two: 4577 and 852 (the 8 can go in either list, both, or neither).
Check that 4577 is increasing (okay) and 852 is decreasing (also okay).
Is that enough to get you to a solution?
Seems like a good opportunity to learn about using itertools and generator pipelines. First we make a few simple, decoupled, and reusable components:
from itertools import tee, groupby
def digits(n):
"""420 -> 4, 2, 0"""
for char in str(n):
yield int(char)
def pairwise(iterable):
"""s -> (s0,s1), (s1,s2), (s2, s3), ..."""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
def deltas(pairs):
"""2 5 3 4 -> 3, -2, 1"""
for left, right in pairs:
yield right - left
def directions(deltas):
"""3 2 2 5 6 -> -1, 0, 1, 1"""
for delta in deltas:
yield -1 if delta < 0 else +1 if delta > 0 else 0
def deduper(directions):
"""3 2 2 5 6 2 2 2 -> 3, 2, 5, 6, 2"""
for key, group in groupby(directions):
yield key
Then we put the pieces together to solve the wider problem of detecting an "increasing then decreasing number":
from itertools import zip_longest
def is_inc_dec(stream, expected=(+1, -1)):
stream = pairwise(stream)
stream = deltas(stream)
stream = directions(stream)
stream = deduper(stream)
for actual, expected in zip_longest(stream, expected):
if actual != expected or actual is None or expected is None:
return False
else:
return True
Usage is like this:
>>> stream = digits(123431)
>>> is_inc_dec(stream)
True
This solution will short-circuit correctly for a number like:
121111111111111111111111111111111111111111111111111...2
I've addressed only the "strictly increasing, and then strictly decreasing" number case. Since this sounds like it might be your homework, I'll leave it as an exercise for you to adapt the code for the "non-decreasing and then non-increasing" case which is mentioned in the question title.
Split the list at the maximum value, then take the min/ max of the diff of each side:
import numpy as np
test1 = [1, 2, 3, 4, 5, 8, 7, 3, 1, 0]
test2 = [1, 2, 3, 4, 5, 8, 7, 3, 1, 0, 2, 5]
test3 = [7, 1, 2, 3, 4, 5, 8, 7, 3, 1, 0]
test4 = [1, 2, 3, 4, 5, 8, 8, 7, 3, 1, 0]
def incdec_test(x):
i = np.array(x).argmax()
return (np.diff(x[0:i]).min() >= 0) and (np.diff(x[i:-1]).max() <= 0)
for test in [test1, test2, test3, test4]:
print 'increase then decrease = {}'.format(incdec_test(test))
Results:
increase then decrease = True
increase then decrease = False
increase then decrease = False
increase then decrease = False

OutputError: Find the longest path in a matrix with given constraints

My matrix is:
4 8 7 3
2 5 9 3
6 3 2 5
4 4 1 6
Problem (Skiing):
Each number represents the elevation of that area of the mountain.
From each area (i.e. box) in the grid, you can go north, south, east, west - but only if the elevation of the area you are going into is less than the one you are in.
I.e. you can only ski downhill.
You can start anywhere on the map and you are looking for a starting point with the longest possible path down as measured by the number of boxes you visit.
And if there are several paths down of the same length, you want to take the one with the steepest vertical drop, i.e. the largest difference between your starting elevation and your ending elevation.
My Solution:
def findSkiPath():
mySolution = [0] * 3
mySolution[0] = 0 # Distance
mySolution[1] = 0 # Drop
cellIndexRow = 0
cellIndexCol = 0
myPath = []
myMatrix = [[4, 5, 8, 7],[1, 1, 5, 9], [0, 7, 5, 5], [7, 4, 2, 9]]
countRows = len(myMatrix)
countCols = len(myMatrix[0])
for i in range(0, countRows - 1):
for j in range(0, countCols - 1):
myValue = myMatrix[i][j]
myPath.append(myValue)
#check east
cellIndexRow = i
cellIndexCol = j + 1
checkAdjacentCells(cellIndexRow, cellIndexCol, myValue, myMatrix , mySolution , myPath )
#check west
cellIndexRow = i
cellIndexCol = j - 1
checkAdjacentCells(cellIndexRow, cellIndexCol, myValue, myMatrix , mySolution , myPath )
#check north
cellIndexRow = i - 1
cellIndexCol = j
checkAdjacentCells(cellIndexRow, cellIndexCol, myValue, myMatrix , mySolution , myPath )
#check south
cellIndexRow = i + 1
cellIndexCol = j
checkAdjacentCells(cellIndexRow, cellIndexCol, myValue, myMatrix , mySolution , myPath )
print (mySolution)
def checkAdjacentCells(cellIndexRow, cellIndexCol, myValue, myMatrix , mySolution , myPath ):
#The base case - If we go beyond the limits of the matrix
if (cellIndexRow < 0 or cellIndexRow > (len(myMatrix) - 1) or cellIndexCol < 0 or cellIndexCol > (len(myMatrix[0]) - 1)):
evaluateSolution(mySolution , myPath )
return
#check if the next cell has a lower value than the current cell
tmpValue = myMatrix[cellIndexRow][cellIndexCol]
if tmpValue < myValue:
newPath = myPath
newPath.append(tmpValue)
r = cellIndexRow
c = cellIndexCol
#check east
cellIndexRow = r
cellIndexCol = c + 1
checkAdjacentCells(cellIndexRow, cellIndexCol, tmpValue, myMatrix , mySolution , newPath )
#check west
cellIndexRow = r
cellIndexCol = c - 1
checkAdjacentCells(cellIndexRow, cellIndexCol, tmpValue, myMatrix , mySolution , newPath )
#check north
cellIndexRow = r - 1
cellIndexCol = c
checkAdjacentCells(cellIndexRow, cellIndexCol, tmpValue, myMatrix , mySolution , newPath )
#check south
cellIndexRow = r + 1
cellIndexCol = c
checkAdjacentCells(cellIndexRow, cellIndexCol, tmpValue, myMatrix , mySolution , newPath )
evaluateSolution(mySolution , myPath )
def evaluateSolution(mySolution , myPath ):
myDistance = 1
mySolutionDistance = int(mySolution[0])
mySolutionDrop = int(mySolution[1])
if myDistance < mySolutionDistance:
return
myDrop = myPath[0] - myPath[-1]
if myDistance > mySolutionDistance or myDrop > mySolutionDrop:
mySolution[0] = myDistance
mySolution[1] = mySolutionDrop
mySolution[2] = myPath
if __name__ == "__main__":
findSkiPath()
Issues:
Current output (distance, drop, path):
[1, 0, [4, 2, 8, 7, 3, 4, 2, 5, 2, 3, 2, 1, 7, 3, 2, 5, 2, 3, 2, 1, 9,
3, 5, 2, 3, 2, 1, 7, 3, 2, 1, 6, 3, 2, 1, 2, 4, 3, 2, 1, 2, 1]]
Expected output:
[5,8,[9,5,3,2,1]]
On this particular map, the longest path down is of length=5, drop=8 (9-1=8), and path: 9-5-3-2-1.
One can approach the in the question described challenge in two different ways:
using a recursive algorithm which walks only valid paths checking already while iterating through the matrix elements if the given requirements are fulfilled
doing it in two steps:
2.1. getting an iterator over all possible paths with a simple call to an in the itertools module available function permutations()
2.2. picking from the generated paths these ones which fulfill the requirements
The code for the second approach is easier to write and to understand, but the huge amount of possible paths already for a 4x4 size of a matrix makes it practically impossible to run it for larger matrix sizes.
The code of the first approach can handle larger sizes of matrices, but has the disadvantage of being harder to grasp how it works to adjust it in case of other constraints.
The question asked here is a 100% 1:1 duplicate of a question asked two years ago here on stackoverflow and titled "Maximum number of elements in the path of a matrix". Anyway here the solution given in an answer to that old question again:
theMatrix = [
[ 4, 8, 7, 3],
[ 2, 5, 9, 3],
[ 6, 3, 2, 5],
[ 4, 4, 1, 6]
]
def longest_path(matrix):
def inner_longest_path(x, y):
best, best_path = 0, []
# for all possible neighbor cells...
for dx, dy in ((+1, 0), (-1, 0), (0, +1), (0, -1)):
# if cell is valid and strictly smaller...
if (0 <= x + dx < len(matrix) and 0 <= y + dy < len(matrix[x])
and matrix[x+dx][y+dy] < matrix[x][y]):
n, path = inner_longest_path(x+dx, y+dy) ### RECURSION
# check if the path starting at that cell is better
if n > best:
best, best_path = n, path
return best + 1, [matrix[x][y]] + best_path
return max(inner_longest_path(x, y) for x, row in enumerate(matrix)
for y, _ in enumerate(row))
print( longest_path(theMatrix) )
The code above prints:
(5, [9, 5, 3, 2, 1])
Now let's take a look also at the code of the non-recursive approach provided here be me myself:
# myMatrix = [[4, 5, 8, 7],[1, 1, 5, 9], [0, 7, 5, 5], [7, 4, 2, 9]]
# 4 5 8 7
# 1 1 5 9
# 0 7 5 5
# 7 4 2 9
myMatrix = [[4, 5, 8],[1, 1, 5], [0, 7, 5]]
# 4 5 8
# 1 1 5
# 0 7 5
# myMatrix = [[4, 5],[1, 1]]
# 4 5
# 1 1
def getAllValidSkiingPathsFrom(myMatrix):
# def getDictRepresentationOf(myMatrix):
dctOfMatrix = {}
for row in range(len(myMatrix)):
for column in range(len(myMatrix[0])):
currPoint = (column, row)
dctOfMatrix[currPoint] = myMatrix[row][column]
lstIndicesOfAllMatrixPoints = list(dctOfMatrix.keys())
setAllPossiblePaths = set()
from itertools import permutations
for pathCandidate in permutations(lstIndicesOfAllMatrixPoints):
lstPossiblePath = []
prevIndexTuple = pathCandidate[0]
lstPossiblePath.append(prevIndexTuple)
for currIndexTuple in pathCandidate[1:]:
if abs(currIndexTuple[0]-prevIndexTuple[0]) + abs(currIndexTuple[1]-prevIndexTuple[1]) > 1:
break # current path indices not allowed in path (no diagonals or jumps)
else:
if dctOfMatrix[currIndexTuple] >= dctOfMatrix[prevIndexTuple]:
break # only "down" is allowed for "skiing"
else:
lstPossiblePath.append(currIndexTuple)
prevIndexTuple = currIndexTuple
if len(lstPossiblePath) > 1 and tuple(lstPossiblePath) not in setAllPossiblePaths:
setAllPossiblePaths.add(tuple(lstPossiblePath))
return setAllPossiblePaths, dctOfMatrix
#:def getAllValidSkiingPathsFrom
setAllPossiblePaths, dctOfMatrix = getAllValidSkiingPathsFrom(myMatrix)
for path in setAllPossiblePaths:
for point in path:
print(dctOfMatrix[point], end=',')
Here the results for the 2x2 and 3x3 versions of myMatrix:
# 4 5
# 1 1
4,1,
5,1,
5,4,
5,4,1,
# 4 5 8
# 1 1 5
# 0 7 5
5,1,
8,5,1,
7,1,
4,1,
5,1,
5,4,
8,5,1,
1,0,
5,4,1,0,
8,5,4,1,
8,5,4,1,0,
8,5,4,
8,5,
7,0,
7,5,
8,5,
4,1,0,
5,4,1,
I hope the code is self-explanatory, but if not here the rough idea:
build a dictionary representing the matrix where the keys are tuples of (column,row)"coordinates" and the values the values in the matrix.
build a list of all possible full paths within the matrix (permutations)
filter the list of all possible full paths to extract only the valid ones according to what is required (apply criteria).
I didn't have run the computational very expensive calculation of the result for the 4x4 matrix as it takes for sure more than several minutes on my box.
For the sake of completeness I would like to mention that there is also another question HERE on stackoverflow which is a variation of THIS question (it has a bit other rules for valid paths and requests an algorithm able to work on irregular matrices).
The main problem with this is that you don't do any sort of backtracking. You are traversing the matrix appropriately, but you don't do anything to maintain the concept of a particular path. Instead, for each square you visit, you simply append all legal moves to a single list.
Instead, consider keeping the current path as a local variable. For each legal move from the given square, you make a separate recursive call to find more moves. Each of those will add a legal move to the end of the local path.
As you return from each call, compare the best found path with the one you've been saving (the best so far); keep the better of those two, and go to the next legal move. When you've considered each of the four directions, then return the best-known path to the calling instance.
There are plenty of examples of backtracking on line and on this site; search out some that you find understandable. You might look under coin combination recursion (finding a set of coins that add to a certain amount) -- they're not the same algorithm, but the backtracking ideas above are shown more clearly.
Does that get you moving toward a solution?

Categories

Resources