Maksymalny zysk ze sprzedaży jednostkowej


123

Załóżmy, że mamy tablicę n liczb całkowitych reprezentujących ceny akcji w jednym dniu. Chcemy znaleźć parę (buyDay, sellDay) , gdzie buyDay ≤ sellDay , taką, że gdybyśmy kupili akcje w buyDay i sprzedali w sellDay , zmaksymalizowalibyśmy nasz zysk.

Oczywiście istnieje rozwiązanie algorytmu O (n 2 ) polegające na wypróbowaniu wszystkich możliwych par (buyDay, sellDay) i wyciągnięciu z nich wszystkiego, co najlepsze. Czy istnieje jednak lepszy algorytm, może taki, który działa w czasie O (n) ?


2
To jest problem z maksymalną sumą podciągów z poziomem niekierunkowania.
MSN

2
@MSN: Jak to? W ogóle nie patrzy na sumy, ale raczej na różnice między elementami.
PengOne

@ PengOne - prawda, ale to pytanie zostało zamknięte. Przeformułowałem pytanie, aby było łatwiejsze do zrozumienia, więc czy możemy spróbować pozostawić to otwarte?
templatetypedef

2
@PengOne, jak powiedziałem, ma jeden poziom pośredni. W szczególności chcesz zmaksymalizować sumę zysków / strat w ciągu kilku kolejnych dni. Dlatego zamień listę na zyski / straty i znajdź maksymalną sumę podciągów.
MSN

1
@PDN: To nie zadziała, ponieważ min może wystąpić przed maks. Nie możesz sprzedawać zapasów (w tym przypadku) i kupować je później.
Ajeet Ganga

Odpowiedzi:


287

Uwielbiam ten problem. To klasyczne pytanie do rozmowy kwalifikacyjnej iw zależności od tego, jak o tym myślisz, będziesz otrzymywać coraz lepsze rozwiązania. Z pewnością można to zrobić lepiej niż O (n 2 ), a tutaj wymieniłem trzy różne sposoby, w jakie możesz pomyśleć o problemie. Mam nadzieję, że to odpowiada na Twoje pytanie!

Po pierwsze, rozwiązanie typu „podziel i rządź”. Zobaczmy, czy możemy rozwiązać ten problem, dzieląc dane wejściowe na pół, rozwiązując problem w każdej podtablicy, a następnie łącząc oba razem. Okazuje się, że faktycznie możemy to zrobić i możemy to zrobić skutecznie! Intuicja jest następująca. Jeśli mamy tylko jeden dzień, najlepszą opcją jest zakup tego dnia, a następnie odsprzedanie go tego samego dnia bez zysku. W przeciwnym razie podziel tablicę na dwie połowy. Jeśli zastanawiamy się, jaka może być optymalna odpowiedź, to musi ona znajdować się w jednym z trzech miejsc:

  1. Prawidłowa para kupna / sprzedaży występuje w całości w pierwszej połowie.
  2. Prawidłowa para kupna / sprzedaży występuje w całości w drugiej połowie.
  3. Prawidłowa para kupna / sprzedaży występuje w obu połowach - kupujemy w pierwszej połowie, a następnie sprzedajemy w drugiej połowie.

Możemy uzyskać wartości (1) i (2), rekurencyjnie wywołując nasz algorytm w pierwszej i drugiej połowie. W przypadku opcji (3) sposobem na osiągnięcie największego zysku byłoby kupowanie w najniższym punkcie pierwszej połowy i sprzedaż w najlepszym momencie drugiej połowy. Możemy znaleźć wartości minimalne i maksymalne w dwóch połówkach, wykonując proste skanowanie liniowe na wejściu i znajdując dwie wartości. To daje nam algorytm z następującą powtarzalnością:

T(1) <= O(1)
T(n) <= 2T(n / 2) + O(n)

Używając Twierdzenia głównego do rozwiązania rekurencji, stwierdzamy, że działa to w czasie O (n lg n) i będzie używać przestrzeni O (lg n) dla wywołań rekurencyjnych. Właśnie pokonaliśmy naiwne rozwiązanie O (n 2 )!

Ale poczekaj! Możemy zrobić znacznie lepiej niż to. Zwróć uwagę, że jedynym powodem, dla którego mamy składnik O (n) w naszym powtórzeniu, jest to, że musieliśmy przeskanować całe dane wejściowe, próbując znaleźć minimalne i maksymalne wartości w każdej połowie. Ponieważ już rekurencyjnie badamy każdą połowę, być może możemy zrobić to lepiej, jeśli rekurencja również zwróci minimalne i maksymalne wartości przechowywane w każdej połowie! Innymi słowy, nasza rekurencja zwraca trzy rzeczy:

  1. Czas kupna i sprzedaży, aby zmaksymalizować zysk.
  2. Minimalna ogólna wartość w zakresie.
  3. Maksymalna ogólna wartość w zakresie.

Te dwie ostatnie wartości można obliczyć rekurencyjnie przy użyciu prostej rekurencji, którą możemy uruchomić w tym samym czasie co rekurencję do obliczenia (1):

  1. Maksymalne i minimalne wartości zakresu jednoelementowego są właśnie tym elementem.
  2. Maksymalne i minimalne wartości zakresu wielu elementów można znaleźć, dzieląc dane wejściowe na pół, znajdując wartości maksymalne i minimalne każdej połowy, a następnie biorąc ich odpowiednie wartości maksymalne i minimalne.

Jeśli użyjemy tego podejścia, nasza relacja powtarzania jest teraz

T(1) <= O(1)
T(n) <= 2T(n / 2) + O(1)

Użycie tutaj twierdzenia głównego daje nam czas wykonania O (n) z przestrzenią O (lg n), co jest nawet lepsze niż nasze oryginalne rozwiązanie!

Ale chwileczkę - możemy zrobić jeszcze lepiej! Pomyślmy o rozwiązaniu tego problemu za pomocą programowania dynamicznego. Chodzi o to, aby przemyśleć problem w następujący sposób. Załóżmy, że znaliśmy odpowiedź na problem po przyjrzeniu się pierwszym k elementom. Czy moglibyśmy wykorzystać naszą wiedzę o (k + 1) elemencie, w połączeniu z naszym początkowym rozwiązaniem, do rozwiązania problemu dla pierwszych (k + 1) elementów? Jeśli tak, możemy uzyskać świetny algorytm, rozwiązując problem dla pierwszego elementu, potem dla pierwszych dwóch, potem pierwszych trzech itd., Aż obliczymy go dla pierwszych n elementów.

Zastanówmy się, jak to zrobić. Jeśli mamy tylko jeden element, już wiemy, że musi to być najlepsza para kupna / sprzedaży. Teraz załóżmy, że znamy najlepszą odpowiedź dla pierwszych k elementów i spójrzmy na (k + 1) element. Zatem jedynym sposobem, w jaki ta wartość może stworzyć rozwiązanie lepsze niż to, które mieliśmy dla pierwszych k elementów, jest to, że różnica między najmniejszym z pierwszych k elementów a tym nowym elementem jest większa niż największa różnica, jaką do tej pory obliczyliśmy. Załóżmy więc, że przechodząc przez elementy, śledzimy dwie wartości - minimalną wartość, jaką widzieliśmy do tej pory, i maksymalny zysk, jaki moglibyśmy osiągnąć, mając tylko pierwsze k elementów. Początkowo minimalna wartość, jaką widzieliśmy do tej pory, to pierwszy element, a maksymalny zysk wynosi zero. Kiedy widzimy nowy element, najpierw aktualizujemy nasz optymalny zysk, obliczając, ile byśmy zarobili, kupując po najniższej dotychczasowej cenie i sprzedając po aktualnej cenie. Jeśli jest to lepsze niż optymalna wartość, którą do tej pory obliczyliśmy, aktualizujemy optymalne rozwiązanie, aby było to nowe zyski. Następnie aktualizujemy minimalny dotychczas widziany element, tak aby był minimalnym obecnym najmniejszym elementem i nowym elementem.

Ponieważ na każdym kroku wykonujemy tylko O ​​(1) pracę i odwiedzamy każdy z n elementów dokładnie raz, wykonanie tego zajmuje O (n) czasu! Ponadto wykorzystuje tylko pamięć dyskową O (1). To jest tak dobre, jak dotychczas!

Jako przykład, na twoich danych wejściowych, oto jak ten algorytm może działać. Liczby pomiędzy każdą z wartości tablicy odpowiadają wartościom przechowywanym przez algorytm w tym momencie. W rzeczywistości nie przechowywałbyś ich wszystkich (zajęłoby to O (n) pamięci!), Ale pomocne jest obserwowanie ewolucji algorytmu:

            5        10        4          6         7
min         5         5        4          4         4    
best      (5,5)     (5,10)   (5,10)     (5,10)    (5,10)

Odpowiedź: (5, 10)

            5        10        4          6        12
min         5         5        4          4         4    
best      (5,5)     (5,10)   (5,10)     (5,10)    (4,12)

Odpowiedź: (4, 12)

            1       2       3      4      5
min         1       1       1      1      1
best      (1,1)   (1,2)   (1,3)  (1,4)  (1,5)

Odpowiedź: (1, 5)

Czy możemy teraz zrobić lepiej? Niestety nie w asymptotycznym sensie. Jeśli użyjemy mniej niż O (n) czasu, nie możemy spojrzeć na wszystkie liczby na dużych wejściach, a tym samym nie możemy zagwarantować, że nie przegapimy optymalnej odpowiedzi (moglibyśmy po prostu „ukryć” ją w elementach, które nie patrzył). Ponadto nie możemy użyć mniej niż O (1) przestrzeni. Mogą istnieć pewne optymalizacje stałych czynników ukrytych w notacji duże-O, ale w przeciwnym razie nie możemy spodziewać się znalezienia radykalnie lepszych opcji.

Ogólnie oznacza to, że mamy następujące algorytmy:

  • Naiwny: O (n 2 ) czas, O (1) przestrzeń.
  • Dziel i rządź: O (n lg n) czas, O (lg n) przestrzeń.
  • Zoptymalizowany dziel i rządź: O (n) czas, O (lg n) przestrzeń.
  • Programowanie dynamiczne: O (n) czas, O (1) przestrzeń.

Mam nadzieję że to pomoże!

EDYCJA : Jeśli jesteś zainteresowany, napisałem wersję Pythona tych czterech algorytmów , abyś mógł się nimi bawić i oceniać ich względne działanie. Oto kod:

# Four different algorithms for solving the maximum single-sell profit problem,
# each of which have different time and space complexity.  This is one of my
# all-time favorite algorithms questions, since there are so many different
# answers that you can arrive at by thinking about the problem in slightly
# different ways.
#
# The maximum single-sell profit problem is defined as follows.  You are given
# an array of stock prices representing the value of some stock over time.
# Assuming that you are allowed to buy the stock exactly once and sell the
# stock exactly once, what is the maximum profit you can make?  For example,
# given the prices
#
#                        2, 7, 1, 8, 2, 8, 4, 5, 9, 0, 4, 5
#
# The maximum profit you can make is 8, by buying when the stock price is 1 and
# selling when the stock price is 9.  Note that while the greatest difference
# in the array is 9 (by subtracting 9 - 0), we cannot actually make a profit of
# 9 here because the stock price of 0 comes after the stock price of 9 (though
# if we wanted to lose a lot of money, buying high and selling low would be a
# great idea!)
#
# In the event that there's no profit to be made at all, we can always buy and
# sell on the same date.  For example, given these prices (which might
# represent a buggy-whip manufacturer:)
#
#                            9, 8, 7, 6, 5, 4, 3, 2, 1, 0
#
# The best profit we can make is 0 by buying and selling on the same day.
#
# Let's begin by writing the simplest and easiest algorithm we know of that
# can solve this problem - brute force.  We will just consider all O(n^2) pairs
# of values, and then pick the one with the highest net profit.  There are
# exactly n + (n - 1) + (n - 2) + ... + 1 = n(n + 1)/2 different pairs to pick
# from, so this algorithm will grow quadratically in the worst-case.  However,
# it uses only O(1) memory, which is a somewhat attractive feature.  Plus, if
# our first intuition for the problem gives a quadratic solution, we can be
# satisfied that if we don't come up with anything else, we can always have a
# polynomial-time solution.

def BruteForceSingleSellProfit(arr):
    # Store the best possible profit we can make; initially this is 0.
    bestProfit = 0;

    # Iterate across all pairs and find the best out of all of them.  As a
    # minor optimization, we don't consider any pair consisting of a single
    # element twice, since we already know that we get profit 0 from this.
    for i in range(0, len(arr)):
        for j in range (i + 1, len(arr)):
            bestProfit = max(bestProfit, arr[j] - arr[i])

    return bestProfit

# This solution is extremely inelegant, and it seems like there just *has* to
# be a better solution.  In fact, there are many better solutions, and we'll
# see three of them.
#
# The first insight comes if we try to solve this problem by using a divide-
# and-conquer strategy.  Let's consider what happens if we split the array into
# two (roughly equal) halves.  If we do so, then there are three possible
# options about where the best buy and sell times are:
#
# 1. We should buy and sell purely in the left half of the array.
# 2. We should buy and sell purely in the right half of the array.
# 3. We should buy in the left half of the array and sell in the right half of
#    the array.
#
# (Note that we don't need to consider selling in the left half of the array
# and buying in the right half of the array, since the buy time must always
# come before the sell time)
#
# If we want to solve this problem recursively, then we can get values for (1)
# and (2) by recursively invoking the algorithm on the left and right
# subarrays.  But what about (3)?  Well, if we want to maximize our profit, we
# should be buying at the lowest possible cost in the left half of the array
# and selling at the highest possible cost in the right half of the array.
# This gives a very elegant algorithm for solving this problem:
#
#    If the array has size 0 or size 1, the maximum profit is 0.
#    Otherwise:
#       Split the array in half.
#       Compute the maximum single-sell profit in the left array, call it L.
#       Compute the maximum single-sell profit in the right array, call it R.
#       Find the minimum of the first half of the array, call it Min
#       Find the maximum of the second half of the array, call it Max
#       Return the maximum of L, R, and Max - Min.
#
# Let's consider the time and space complexity of this algorithm.  Our base
# case takes O(1) time, and in our recursive step we make two recursive calls,
# one on each half of the array, and then does O(n) work to scan the array
# elements to find the minimum and maximum values.  This gives the recurrence
#
#    T(1)     = O(1)
#    T(n / 2) = 2T(n / 2) + O(n)
#
# Using the Master Theorem, this recurrence solves to O(n log n), which is
# asymptotically faster than our original approach!  However, we do pay a
# (slight) cost in memory usage.  Because we need to maintain space for all of
# the stack frames we use.  Since on each recursive call we cut the array size
# in half, the maximum number of recursive calls we can make is O(log n), so
# this algorithm uses O(n log n) time and O(log n) memory.

def DivideAndConquerSingleSellProfit(arr):
    # Base case: If the array has zero or one elements in it, the maximum
    # profit is 0.
    if len(arr) <= 1:
        return 0;

    # Cut the array into two roughly equal pieces.
    left  = arr[ : len(arr) / 2]
    right = arr[len(arr) / 2 : ]

    # Find the values for buying and selling purely in the left or purely in
    # the right.
    leftBest  = DivideAndConquerSingleSellProfit(left)
    rightBest = DivideAndConquerSingleSellProfit(right)

    # Compute the best profit for buying in the left and selling in the right.
    crossBest = max(right) - min(left)

    # Return the best of the three
    return max(leftBest, rightBest, crossBest)

# While the above algorithm for computing the maximum single-sell profit is
# better timewise than what we started with (O(n log n) versus O(n^2)), we can
# still improve the time performance.  In particular, recall our recurrence
# relation:
#
#    T(1) = O(1)
#    T(n) = 2T(n / 2) + O(n)
#
# Here, the O(n) term in the T(n) case comes from the work being done to find
# the maximum and minimum values in the right and left halves of the array,
# respectively.  If we could find these values faster than what we're doing
# right now, we could potentially decrease the function's runtime.
#
# The key observation here is that we can compute the minimum and maximum
# values of an array using a divide-and-conquer approach.  Specifically:
#
#    If the array has just one element, it is the minimum and maximum value.
#    Otherwise:
#       Split the array in half.
#       Find the minimum and maximum values from the left and right halves.
#       Return the minimum and maximum of these two values.
#
# Notice that our base case does only O(1) work, and our recursive case manages
# to do only O(1) work in addition to the recursive calls.  This gives us the
# recurrence relation
#
#    T(1) = O(1)
#    T(n) = 2T(n / 2) + O(1)
#
# Using the Master Theorem, this solves to O(n).
#
# How can we make use of this result?  Well, in our current divide-and-conquer
# solution, we split the array in half anyway to find the maximum profit we
# could make in the left and right subarrays.  Could we have those recursive
# calls also hand back the maximum and minimum values of the respective arrays?
# If so, we could rewrite our solution as follows:
#
#    If the array has size 1, the maximum profit is zero and the maximum and
#       minimum values are the single array element.
#    Otherwise:
#       Split the array in half.
#       Compute the maximum single-sell profit in the left array, call it L.
#       Compute the maximum single-sell profit in the right array, call it R.
#       Let Min be the minimum value in the left array, which we got from our
#           first recursive call.
#       Let Max be the maximum value in the right array, which we got from our
#           second recursive call.
#       Return the maximum of L, R, and Max - Min for the maximum single-sell
#           profit, and the appropriate maximum and minimum values found from
#           the recursive calls.
#
# The correctness proof for this algorithm works just as it did before, but now
# we never actually do a scan of the array at each step.  In fact, we do only
# O(1) work at each level.  This gives a new recurrence
#
#     T(1) = O(1)
#     T(n) = 2T(n / 2) + O(1)
#
# Which solves to O(n).  We're now using O(n) time and O(log n) memory, which
# is asymptotically faster than before!
#
# The code for this is given below:

def OptimizedDivideAndConquerSingleSellProfit(arr):
    # If the array is empty, the maximum profit is zero.
    if len(arr) == 0:
        return 0

    # This recursive helper function implements the above recurrence.  It
    # returns a triple of (max profit, min array value, max array value).  For
    # efficiency reasons, we always reuse the array and specify the bounds as
    # [lhs, rhs]
    def Recursion(arr, lhs, rhs):
        # If the array has just one element, we return that the profit is zero
        # but the minimum and maximum values are just that array value.
        if lhs == rhs:
            return (0, arr[lhs], arr[rhs])

        # Recursively compute the values for the first and latter half of the
        # array.  To do this, we need to split the array in half.  The line
        # below accomplishes this in a way that, if ported to other languages,
        # cannot result in an integer overflow.
        mid = lhs + (rhs - lhs) / 2

        # Perform the recursion.
        ( leftProfit,  leftMin,  leftMax) = Recursion(arr, lhs, mid)
        (rightProfit, rightMin, rightMax) = Recursion(arr, mid + 1, rhs)

        # Our result is the maximum possible profit, the minimum of the two
        # minima we've found (since the minimum of these two values gives the
        # minimum of the overall array), and the maximum of the two maxima.
        maxProfit = max(leftProfit, rightProfit, rightMax - leftMin)
        return (maxProfit, min(leftMin, rightMin), max(leftMax, rightMax))

    # Using our recursive helper function, compute the resulting value.
    profit, _, _ = Recursion(arr, 0, len(arr) - 1)
    return profit

# At this point we've traded our O(n^2)-time, O(1)-space solution for an O(n)-
# time, O(log n) space solution.  But can we do better than this?
#
# To find a better algorithm, we'll need to switch our line of reasoning.
# Rather than using divide-and-conquer, let's see what happens if we use
# dynamic programming.  In particular, let's think about the following problem.
# If we knew the maximum single-sell profit that we could get in just the first
# k array elements, could we use this information to determine what the
# maximum single-sell profit would be in the first k + 1 array elements?  If we
# could do this, we could use the following algorithm:
#
#   Find the maximum single-sell profit to be made in the first 1 elements.
#   For i = 2 to n:
#      Compute the maximum single-sell profit using the first i elements.
#
# How might we do this?  One intuition is as follows.  Suppose that we know the
# maximum single-sell profit of the first k elements.  If we look at k + 1
# elements, then either the maximum profit we could make by buying and selling
# within the first k elements (in which case nothing changes), or we're
# supposed to sell at the (k + 1)st price.  If we wanted to sell at this price
# for a maximum profit, then we would want to do so by buying at the lowest of
# the first k + 1 prices, then selling at the (k + 1)st price.
#
# To accomplish this, suppose that we keep track of the minimum value in the
# first k elements, along with the maximum profit we could make in the first
# k elements.  Upon seeing the (k + 1)st element, we update what the current
# minimum value is, then update what the maximum profit we can make is by
# seeing whether the difference between the (k + 1)st element and the new
# minimum value is.  Note that it doesn't matter what order we do this in; if
# the (k + 1)st element is the smallest element so far, there's no possible way
# that we could increase our profit by selling at that point.
#
# To finish up this algorithm, we should note that given just the first price,
# the maximum possible profit is 0.
#
# This gives the following simple and elegant algorithm for the maximum single-
# sell profit problem:
#
#   Let profit = 0.
#   Let min = arr[0]
#   For k = 1 to length(arr):
#       If arr[k] < min, set min = arr[k]
#       If profit < arr[k] - min, set profit = arr[k] - min
#
# This is short, sweet, and uses only O(n) time and O(1) memory.  The beauty of
# this solution is that we are quite naturally led there by thinking about how
# to update our answer to the problem in response to seeing some new element.
# In fact, we could consider implementing this algorithm as a streaming
# algorithm, where at each point in time we maintain the maximum possible
# profit and then update our answer every time new data becomes available.
#
# The final version of this algorithm is shown here:

def DynamicProgrammingSingleSellProfit(arr):
    # If the array is empty, we cannot make a profit.
    if len(arr) == 0:
        return 0

    # Otherwise, keep track of the best possible profit and the lowest value
    # seen so far.
    profit = 0
    cheapest = arr[0]

    # Iterate across the array, updating our answer as we go according to the
    # above pseudocode.
    for i in range(1, len(arr)):
        # Update the minimum value to be the lower of the existing minimum and
        # the new minimum.
        cheapest = min(cheapest, arr[i])

        # Update the maximum profit to be the larger of the old profit and the
        # profit made by buying at the lowest value and selling at the current
        # price.
        profit = max(profit, arr[i] - cheapest)

    return profit

# To summarize our algorithms, we have seen
#
# Naive:                        O(n ^ 2)   time, O(1)     space
# Divide-and-conquer:           O(n log n) time, O(log n) space
# Optimized divide-and-conquer: O(n)       time, O(log n) space
# Dynamic programming:          O(n)       time, O(1)     space

1
@ FrankQ. - Spacja jest potrzebna dla obu wywołań rekurencyjnych, ale zwykle te wywołania są wykonywane jeden po drugim. Oznacza to, że kompilator może ponownie używać pamięci między wywołaniami; po powrocie jednego połączenia następne połączenie może ponownie wykorzystać swoją przestrzeń. W rezultacie potrzebujesz tylko pamięci do przechowywania jednego wywołania funkcji naraz, więc użycie pamięci jest proporcjonalne do maksymalnej głębokości stosu wywołań. Ponieważ rekursja kończy się na poziomie O (log n), należy użyć tylko pamięci O (log n). Czy to wyjaśnia sprawy?
templatetypedef

Czy ktoś mógłby przenieść to do Rubiego? Część rekursji nie działa tak samo, jak w Pythonie. Również te rozwiązania zwracają tylko maksymalny zysk; nie zwracają punktów tablicowych, które przyniosły zysk (które można by wykorzystać do
podania

Koncepcja programowania dynamicznego nie jest naprawdę potrzebna do wyjaśnienia rozwiązania opartego na czasie O (n), ale wspaniale jest, że łączysz wszystkie te typy algorytmów.
Rn222,

Jak możesz skorzystać z dowolnego algorytmu podrzędnego O (n ^ 2), aby znaleźć wszystkie pary posortowane według zysku?
ferk86

@templatetypedef jak zmienilibyśmy podejście do programowania dynamicznego, gdybyśmy zaczęli z budżetem w wysokości M $ i zamiast pojedynczych akcji mielibyśmy m akcji z podanymi cenami przez n dni? tzn. zmieniamy liczbę zakupionych jednostek magazynowych i dostępne dane giełdowe od 1 do n (tak jak poprzednio mieliśmy tylko dla Google, teraz mamy też 5 innych firm)
Ronak Agrawal

32

To jest problem z maksymalną sumą podciągów z odrobiną niekierowania. Problem z maksymalną sumą podciągów ma listę liczb całkowitych, które mogą być dodatnie lub ujemne, znajdź największą sumę ciągłego podzbioru tej listy.

Możesz w prosty sposób przekształcić ten problem w ten problem, biorąc zysk lub stratę między kolejnymi dniami. Więc przekształciłbyś listę cen akcji, np. W [5, 6, 7, 4, 2]listę zysków / strat, np [1, 1, -3, -2]. Problem z sumą podciągów jest wtedy całkiem łatwy do rozwiązania: znajdź podciąg o największej sumie elementów w tablicy


1
Wydaje mi się, że nie działa to całkiem w ten sposób, ponieważ jeśli kupisz akcje w którymś początkowym dniu, nie uzyskasz korzyści z delty z poprzedniego dnia. A może nie stanowi to problemu w tym podejściu?
templatetypedef

1
@templatetypedef, dlatego śledzisz największą sumę i bieżącą sumę sekwencji. Kiedy bieżąca suma sekwencji spadnie poniżej zera, wiesz, że nie zarobiłeś żadnych pieniędzy na tej sekwencji i możesz zacząć od nowa. Śledząc największą sumę, automatycznie znajdziesz najlepsze daty kupna / sprzedaży.
MSN

6
@templatetypedef, nawiasem mówiąc, robisz to samo w swojej odpowiedzi.
MSN

16

Nie jestem pewien, dlaczego jest to uważane za pytanie dotyczące programowania dynamicznego. Widziałem to pytanie w podręcznikach i przewodnikach po algorytmach używających środowiska uruchomieniowego O (n log n) i O (log n) dla przestrzeni (np. Elementy wywiadów programistycznych). Wydaje się, że jest to znacznie prostszy problem, niż się ludziom wydaje.

Działa to poprzez śledzenie maksymalnego zysku, minimalnej ceny zakupu, a tym samym optymalnej ceny kupna / sprzedaży. Podczas przeglądania każdego elementu tablicy sprawdza, czy dany element jest mniejszy niż minimalna cena zakupu. Jeśli tak, minimalny indeks ceny zakupu ( min) jest aktualizowany tak, aby był indeksem tego elementu. Dodatkowo dla każdego elementu becomeABillionairealgorytm sprawdza, czy arr[i] - arr[min](różnica między obecnym elementem a minimalną ceną zakupu) jest większa od aktualnego zysku. Jeśli tak jest, zysk jest aktualizowany zgodnie z tą różnicą i kupuj, arr[min]a sprzedawaj arr[i].

Działa w jednym przejeździe.

static void becomeABillionaire(int arr[]) {
    int i = 0, buy = 0, sell = 0, min = 0, profit = 0;

    for (i = 0; i < arr.length; i++) {
        if (arr[i] < arr[min])
            min = i;
        else if (arr[i] - arr[min] > profit) {
            buy = min; 
            sell = i;
            profit = arr[i] - arr[min];
        }

    }

    System.out.println("We will buy at : " + arr[buy] + " sell at " + arr[sell] + 
            " and become billionaires worth " + profit );

}

Współautor: https://stackoverflow.com/users/599402/ephraim


2

Problem jest identyczny z maksymalną sekwencją cząstkową,
którą rozwiązałem za pomocą programowania dynamicznego. Śledź bieżące i poprzednie (zysk, data zakupu i sprzedaży) Jeśli prąd jest wyższy niż poprzedni, zastąp poprzedni bieżącym.

    int prices[] = { 38, 37, 35, 31, 20, 24, 35, 21, 24, 21, 23, 20, 23, 25, 27 };

    int buyDate = 0, tempbuyDate = 0;
    int sellDate = 0, tempsellDate = 0; 

    int profit = 0, tempProfit =0;
    int i ,x = prices.length;
    int previousDayPrice = prices[0], currentDayprice=0;

    for(i=1 ; i<x; i++ ) {

        currentDayprice = prices[i];

        if(currentDayprice > previousDayPrice ) {  // price went up

            tempProfit = tempProfit + currentDayprice - previousDayPrice;
            tempsellDate = i;
        }
        else { // price went down 

            if(tempProfit>profit) { // check if the current Profit is higher than previous profit....

                profit = tempProfit;
                sellDate = tempsellDate;
                buyDate = tempbuyDate;
            } 
                                     // re-intialized buy&sell date, profit....
                tempsellDate = i;
                tempbuyDate = i;
                tempProfit =0;
        }
        previousDayPrice = currentDayprice;
    }

    // if the profit is highest till the last date....
    if(tempProfit>profit) {
        System.out.println("buydate " + tempbuyDate + " selldate " + tempsellDate + " profit " + tempProfit );
    }
    else {
        System.out.println("buydate " + buyDate + " selldate " + sellDate + " profit " + profit );
    }   

2

oto rozwiązanie My Java:

public static void main(String[] args) {
    int A[] = {5,10,4,6,12};

    int min = A[0]; // Lets assume first element is minimum
    int maxProfit = 0; // 0 profit, if we buy & sell on same day.
    int profit = 0;
    int minIndex = 0; // Index of buy date
    int maxIndex = 0; // Index of sell date

    //Run the loop from next element
    for (int i = 1; i < A.length; i++) {
        //Keep track of minimum buy price & index
        if (A[i] < min) {
            min = A[i];
            minIndex = i;
        }
        profit = A[i] - min;
        //If new profit is more than previous profit, keep it and update the max index
        if (profit > maxProfit) {
            maxProfit = profit;
            maxIndex = i;
        }
    }
    System.out.println("maxProfit is "+maxProfit);
    System.out.println("minIndex is "+minIndex);
    System.out.println("maxIndex is "+maxIndex);     
}

@Nitiraj, tak, to rozwiązanie jest poprawne, ale proszę o przeczytanie odpowiedzi udzielonej przez templatetypedef, ponieważ w odpowiedzi udzielonej przez templatetypedef wymieniono wszystkie możliwe rozwiązania, w tym to, które opublikował Rohit. Rozwiązanie Rohita jest w rzeczywistości implementacją ostatniego rozwiązania z O (n) przy użyciu dynamicznego programowania, o którym mowa w odpowiedzi udzielonej przez templatetypedef.
nits.kk

1
Załóżmy, że twoja tablica to int A [] = {5, 4, 6, 7, 6, 3, 2, 5}; Następnie, zgodnie z Twoją logiką, będziesz kupować po indeksie 6, a następnie sprzedawać go po indeksie 3. Co jest złe. Nie możesz sprzedawać w przeszłości. Indeks sprzedaży musi być większy niż indeks kupna.
programista747

1
Powyższe rozwiązanie jest „prawie” poprawne. ale wypisuje bezwzględny indeks minimalny zamiast indeksu ceny „kupna”. Aby poprawić, potrzebujesz innej zmiennej, powiedzmy minBuyIndex, którą aktualizujesz tylko w bloku "if (profit> maxProfit)" i wydrukuj ją.
javabrew

1

Wymyśliłem proste rozwiązanie - kod jest bardziej zrozumiały. To jedno z pytań dotyczących programowania dynamicznego.

Kod nie zajmuje się sprawdzaniem błędów i skrajnymi przypadkami. To tylko próbka, która daje wyobrażenie o podstawowej logice rozwiązania problemu.

namespace MaxProfitForSharePrice
{
    class MaxProfitForSharePrice
    {
        private static int findMax(int a, int b)
        {
            return a > b ? a : b;
        }

        private static void GetMaxProfit(int[] sharePrices)
        {
            int minSharePrice = sharePrices[0], maxSharePrice = 0, MaxProft = 0;
            int shareBuyValue = sharePrices[0], shareSellValue = sharePrices[0];

            for (int i = 0; i < sharePrices.Length; i++)
            {
                if (sharePrices[i] < minSharePrice )
                {
                    minSharePrice = sharePrices[i];
                    // if we update the min value of share, we need to reset the Max value as 
                    // we can only do this transaction in-sequence. We need to buy first and then only we can sell.
                    maxSharePrice = 0; 
                }
                else 
                {
                    maxSharePrice = sharePrices[i];
                }

                // We are checking if max and min share value of stock are going to
                // give us better profit compare to the previously stored one, then store those share values.
                if (MaxProft < (maxSharePrice - minSharePrice))
                {
                    shareBuyValue = minSharePrice;
                    shareSellValue = maxSharePrice;
                }

                MaxProft = findMax(MaxProft, maxSharePrice - minSharePrice);
            }

            Console.WriteLine("Buy stock at ${0} and sell at ${1}, maximum profit can be earned ${2}.", shareBuyValue, shareSellValue, MaxProft);
        }

        static void Main(string[] args)
        {
           int[] sampleArray = new int[] { 1, 3, 4, 1, 1, 2, 11 };
           GetMaxProfit(sampleArray);
            Console.ReadLine();
        }
    }
}

1
public static double maxProfit(double [] stockPrices)
    {
        double initIndex = 0, finalIndex = 0;

        double tempProfit = list[1] - list[0];
        double maxSum = tempProfit;
        double maxEndPoint = tempProfit;


        for(int i = 1 ;i<list.length;i++)
        {
            tempProfit = list[ i ] - list[i - 1];;

            if(maxEndPoint < 0)
            {
                maxEndPoint = tempProfit;
                initIndex = i;
            }
            else
            {
                maxEndPoint += tempProfit;
            }

            if(maxSum <= maxEndPoint)
            {
                maxSum = maxEndPoint ;
                finalIndex = i;
            }
        }
        System.out.println(initIndex + " " + finalIndex);
        return maxSum;

    }

Oto moje rozwiązanie. modyfikuje algorytm maksymalnej sekwencji podrzędnej. Rozwiązuje problem w O (n). Myślę, że nie można tego zrobić szybciej.


1

Jest to interesujący problem, ponieważ wydaje się trudny, ale uważne przemyślenie daje eleganckie, oszczędne rozwiązanie.

Jak już wspomniano, można go rozwiązać brutalną siłą w czasie O (N ^ 2). Dla każdego wpisu w tablicy (lub liście) wykonaj iterację po wszystkich poprzednich wpisach, aby uzyskać wartość minimalną lub maksymalną, w zależności od tego, czy problemem jest znalezienie największego zysku czy straty.

Oto jak myśleć o rozwiązaniu w O (N): każdy wpis reprezentuje nowe możliwe maksimum (lub min). Następnie wszystko, co musimy zrobić, to zapisać poprzednie min (lub max) i porównać różnicę z bieżącym i poprzednim min (lub max). Bułka z masłem.

Oto kod w Javie jako test JUnit:

import org.junit.Test;

public class MaxDiffOverSeriesProblem {

    @Test
    public void test1() {
        int[] testArr = new int[]{100, 80, 70, 65, 95, 120, 150, 75, 95, 100, 110, 120, 90, 80, 85, 90};

        System.out.println("maxLoss: " + calculateMaxLossOverSeries(testArr) + ", maxGain: " + calculateMaxGainOverSeries(testArr));
    }

    private int calculateMaxLossOverSeries(int[] arr) {
        int maxLoss = 0;

        int idxMax = 0;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] > arr[idxMax]) {
                idxMax = i;
            }

            if (arr[idxMax] - arr[i] > maxLoss) {
                maxLoss = arr[idxMax] - arr[i];
            }           
        }

        return maxLoss;
    }

    private int calculateMaxGainOverSeries(int[] arr) {
        int maxGain = 0;

        int idxMin = 0;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] < arr[idxMin]) {
                idxMin = i;
            }

            if (arr[i] - arr[idxMin] > maxGain) {
                maxGain = arr[i] - arr[idxMin];
            }           
        }

        return maxGain;
    }

}

W przypadku wyliczenia największej straty śledzimy maksimum na liście (cenę kupna) aż do aktualnego wpisu. Następnie obliczamy różnicę między wartością maksymalną a bieżącą pozycją. Jeśli max - current> maxLoss, to zachowujemy tę różnicę jako nową maxLoss. Ponieważ indeks max jest na pewno mniejszy niż indeks bieżącego, gwarantujemy, że data „kupna” jest krótsza niż data „sprzedaży”.

W przypadku obliczenia największego zysku wszystko jest odwrotne. Śledzimy min na liście aż do aktualnego wpisu. Obliczamy różnicę między min a bieżącą pozycją (odwracając kolejność odejmowania). Jeśli current - min> maxGain, to zachowujemy tę różnicę jako nową wartość maxGain. Ponownie, indeks „kup” (min) znajduje się przed indeksem prądu („sprzedaj”).

Musimy tylko śledzić maxGain (lub maxLoss) i indeks min lub max, ale nie obu, i nie musimy porównywać indeksów, aby potwierdzić, że „kup” jest mniejsze niż „sprzedaj”, ponieważ uzyskać to naturalnie.


1

Maksymalny zysk ze sprzedaży jednostkowej, rozwiązanie O (n)

function stocks_n(price_list){
    var maxDif=0, min=price_list[0]

    for (var i in price_list){
        p = price_list[i];
        if (p<min)
            min=p
        else if (p-min>maxDif)
                maxDif=p-min;
   }

    return maxDif
}

Oto projekt, który przeprowadza testy złożoności czasowej na podejściach o (N) vs o (n ^ 2) na losowym zestawie danych na 100 tys. Liczb całkowitych. O (n ^ 2) zajmuje 2 sekundy, a O (n) 0,01 s

https://github.com/gulakov/complexity.js

function stocks_n2(ps){
    for (maxDif=0,i=_i=0;p=ps[i++];i=_i++)
        for (;p2=ps[i++];)
            if (p2-p>maxDif)
                maxDif=p2-p
    return maxDif
}

Jest to wolniejsze podejście, o (n ^ 2), które trwa przez resztę dni każdego dnia, podwójna pętla.


1

Najwyżej głosowana odpowiedź nie dopuszcza przypadków, w których maksymalny zysk jest ujemny i należy go zmodyfikować, aby uwzględnić takie przypadki. Można to zrobić, ograniczając zakres pętli do (len (a) - 1) i zmieniając sposób określania zysku poprzez przesunięcie indeksu o jeden.

def singSellProfit(a):
profit = -max(a)
low = a[0]

for i in range(len(a) - 1):
    low = min(low, a[i])
    profit = max(profit, a[i + 1] - low)
return profit

Porównaj tę wersję funkcji z poprzednią dla tablicy:

s = [19,11,10,8,5,2]

singSellProfit(s)
-1

DynamicProgrammingSingleSellProfit(s)
0

0
static void findmaxprofit(int[] stockvalues){
    int buy=0,sell=0,buyingpoint=0,sellingpoint=0,profit=0,currentprofit=0;
    int finalbuy=0,finalsell=0;
    if(stockvalues.length!=0){
        buy=stockvalues[0];
    }           
    for(int i=1;i<stockvalues.length;i++){  
        if(stockvalues[i]<buy&&i!=stockvalues.length-1){                
            buy=stockvalues[i];
            buyingpoint=i;
        }               
        else if(stockvalues[i]>buy){                
            sell=stockvalues[i];
            sellingpoint=i;
        }
        currentprofit=sell-buy;         
        if(profit<currentprofit&&sellingpoint>buyingpoint){             
            finalbuy=buy;
            finalsell=sell;
            profit=currentprofit;
        }

    }
    if(profit>0)
    System.out.println("Buy shares at "+finalbuy+" INR and Sell Shares "+finalsell+" INR and Profit of "+profit+" INR");
    else
        System.out.println("Don't do Share transacations today");
}

0

Możliwością określenia maksymalnego zysku może być śledzenie elementów minimum po lewej stronie i maksimum po prawej stronie w tablicy przy każdym indeksie tablicy. Podczas iteracji kursów akcji w danym dniu poznasz najniższą cenę do tego dnia, a także maksymalną cenę po tym dniu (włącznie).

Na przykład zdefiniujmy a min_arri max_arr, przy czym podana tablica jest arr. Indeks iw min_arrbyłby minimalnym elementem we arrwszystkich indeksach <= i(po lewej stronie i włącznie). Indeks iw max_arrbyłby maksymalnym elementem we arrdla wszystkich indeksów >= i(prawo do i włącznie). Wtedy możesz znaleźć maksymalną różnicę między odpowiednimi elementami w max_arri `min_arr ':

def max_profit(arr)
   min_arr = []
   min_el = arr.first
   arr.each do |el|
       if el < min_el
           min_el = el
           min_arr << min_el
       else
           min_arr << min_el
       end
   end

   max_arr = []
   max_el = arr.last
   arr.reverse.each do |el|
       if el > max_el
           max_el = el
           max_arr.unshift(max_el)
       else
           max_arr.unshift(max_el)
       end

   end

   max_difference = max_arr.first - min_arr.first
   1.upto(arr.length-1) do |i|
        max_difference = max_arr[i] - min_arr[i] if max_difference < max_arr[i] - min_arr[i]  
   end

   return max_difference 
end

Powinno to działać w czasie O (n), ale uważam, że zajmuje dużo miejsca.


0

To maksymalna różnica między dwoma elementami w tablicy i to jest moje rozwiązanie:

O (N) złożoność czasowa O (1) złożoność przestrzenna

    int[] arr   =   {5, 4, 6 ,7 ,6 ,3 ,2, 5};

    int start   =   0;
    int end     =   0;
    int max     =   0;
    for(int i=1; i<arr.length; i++){
        int currMax =   arr[i] - arr[i-1];
        if(currMax>0){
            if((arr[i] -arr[start])>=currMax && ((arr[i] -arr[start])>=(arr[end] -arr[start]))){

                 end    =   i;
            }
            else if(currMax>(arr[i] -arr[start]) && currMax >(arr[end] - arr[start])){
                start   =   i-1;
                end =   i;
            }
        }
    }
    max =   arr[end] - arr[start];
    System.out.println("max: "+max+" start: "+start+" end: "+end);

0

Po tym, jak nie zdałem egzaminu z kodowania na żywo na stanowisko inżyniera rozwiązań FB, musiałem rozwiązać go w spokojnej, chłodnej atmosferze, więc oto moje 2 centy:

var max_profit = 0;
var stockPrices = [23,40,21,67,1,50,22,38,2,62];

var currentBestBuy = 0; 
var currentBestSell = 0;
var min = 0;

for(var i = 0;i < (stockPrices.length - 1) ; i++){
    if(( stockPrices[i + 1] - stockPrices[currentBestBuy] > max_profit) ){
        max_profit = stockPrices[i + 1] - stockPrices[currentBestBuy];
        currentBestSell = i + 1;  
    }
    if(stockPrices[i] < stockPrices[currentBestBuy]){
            min = i;
        }
    if( max_profit < stockPrices[i + 1] - stockPrices[min] ){
        max_profit = stockPrices[i + 1] - stockPrices[min];
        currentBestSell = i + 1;
        currentBestBuy = min;
    }
}

console.log(currentBestBuy);
console.log(currentBestSell);
console.log(max_profit);

Tylko kod odpowiedzi są odradzane.
Pritam Banerjee

0

Jedyną odpowiedzią naprawdę odpowiadającą na pytanie jest @akash_magoon (i to w tak prosty sposób!), Ale nie zwraca ona dokładnego obiektu określonego w pytaniu. Trochę refaktoryzowałem i moja odpowiedź w PHP zwracała tylko to, o co pytano:

function maximizeProfit(array $dailyPrices)
{
    $buyDay = $sellDay = $cheaperDay = $profit = 0;

    for ($today = 0; $today < count($dailyPrices); $today++) {
        if ($dailyPrices[$today] < $dailyPrices[$cheaperDay]) {
            $cheaperDay = $today;
        } elseif ($dailyPrices[$today] - $dailyPrices[$cheaperDay] > $profit) {
            $buyDay  = $cheaperDay;
            $sellDay = $today;
            $profit   = $dailyPrices[$today] - $dailyPrices[$cheaperDay];
        }
    }
    return [$buyDay, $sellDay];
}

0

Zgrabne rozwiązanie:

+ (int)maxProfit:(NSArray *)prices {
    int maxProfit = 0;

    int bestBuy = 0;
    int bestSell = 0;
    int currentBestBuy = 0;

    for (int i= 1; i < prices.count; i++) {
        int todayPrice = [prices[i] intValue];
        int bestBuyPrice = [prices[currentBestBuy] intValue];
        if (todayPrice < bestBuyPrice) {
            currentBestBuy = i;
            bestBuyPrice = todayPrice;
        }

        if (maxProfit < (todayPrice - bestBuyPrice)) {
            bestSell = i;
            bestBuy = currentBestBuy;
            maxProfit = (todayPrice - bestBuyPrice);
        }
    }

    NSLog(@"Buy Day : %d", bestBuy);
    NSLog(@"Sell Day : %d", bestSell);

    return maxProfit;
}

0
def get_max_profit(stock):
    p=stock[0]
    max_profit=0
    maxp=p
    minp=p
    for i in range(1,len(stock)):
        p=min(p,stock[i])
        profit=stock[i]-p
        if profit>max_profit:
            maxp=stock[i]
            minp=p
            max_profit=profit
    return minp,maxp,max_profit



stock_prices = [310,315,275,295,260,270,290,230,255,250]
print(get_max_profit(stock_prices))

Ten program w pythonie3 może zwrócić cenę zakupu i cenę sprzedaży, która zmaksymalizuje zysk, obliczoną ze złożonością czasową O (n) i złożonością przestrzeni O (1) .


0

Oto moje rozwiązanie

public static int maxProfit(List<Integer> in) {
    int min = in.get(0), max = 0;
    for(int i=0; i<in.size()-1;i++){

        min=Math.min(min, in.get(i));

        max = Math.max(in.get(i) - min, max);
     }

     return max;
 }
}

-1

Dla wszystkich odpowiedzi śledzących minimum i maksimum elementów, to rozwiązanie jest w rzeczywistości rozwiązaniem O (n ^ 2). Dzieje się tak, ponieważ na koniec należy sprawdzić, czy maksimum wystąpiło po minimum, czy nie. Jeśli tak się nie stało, wymagane są dalsze iteracje, aż warunek zostanie spełniony, a to pozostawia najgorszy przypadek O (n ^ 2). A jeśli chcesz pominąć dodatkowe iteracje, potrzeba dużo więcej miejsca. Tak czy inaczej, nie-nie w porównaniu z rozwiązaniem do programowania dynamicznego

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.