Maximaler Einzelverkaufsgewinn


123

Angenommen, wir erhalten ein Array von n ganzen Zahlen, die die Aktienkurse an einem einzelnen Tag darstellen. Wir wollen ein Paar finden (buyDay, sellDay) , mit buyDay ≤ sellDay , so dass , wenn wir den Bestand an gekauft buyDay und verkaufen es an sellDay , würden wir unseren Gewinn maximieren.

Es ist klar, dass es eine O (n 2 ) -Lösung für den Algorithmus gibt, indem alle möglichen Paare (buyDay, sellDay) ausprobiert und das Beste aus allen herausgeholt werden. Gibt es jedoch einen besseren Algorithmus, der möglicherweise in O (n) -Zeit ausgeführt wird?


2
Dies ist das maximale Summen-Subsequenzproblem mit einer Indirektionsebene.
MSN

2
@MSN: Wie so? Er betrachtet überhaupt keine Summen, sondern Unterschiede zwischen Elementen.
PengOne

@ PengOne- Stimmt, aber diese Frage wurde geschlossen. Ich habe die Frage umformuliert, um sie leichter zu verstehen. Könnten wir also versuchen, diese offen zu halten?
Templatetypedef

2
@PengOne, wie gesagt, es hat eine Indirektionsebene. Insbesondere möchten Sie die Summe der Gewinne / Verluste über einen zusammenhängenden Satz von Tagen maximieren. Konvertieren Sie daher die Liste in Gewinne / Verluste und ermitteln Sie die maximale Teilsequenzsumme.
MSN

1
@PDN: Das funktioniert nicht, da min vor max auftreten kann. Sie können (in diesem Fall) keine Aktien verkaufen und später kaufen.
Ajeet Ganga

Antworten:


287

Ich liebe dieses Problem. Es ist eine klassische Interviewfrage und je nachdem, wie Sie darüber denken, erhalten Sie immer bessere Lösungen. Es ist sicherlich möglich, dies in einer besseren Zeit als O (n 2 ) zu tun , und ich habe drei verschiedene Möglichkeiten aufgelistet, wie Sie hier über das Problem nachdenken können. Hoffentlich beantwortet dies Ihre Frage!

Erstens die Divide-and-Conquer-Lösung. Mal sehen, ob wir dies lösen können, indem wir die Eingabe in zwei Hälften teilen, das Problem in jedem Subarray lösen und dann beide miteinander kombinieren. Es stellt sich heraus, dass wir dies tatsächlich und effizient tun können! Die Intuition ist wie folgt. Wenn wir einen einzigen Tag haben, ist es die beste Option, an diesem Tag zu kaufen und ihn dann am selben Tag ohne Gewinn zurück zu verkaufen. Andernfalls teilen Sie das Array in zwei Hälften. Wenn wir darüber nachdenken, wie die optimale Antwort aussehen könnte, muss sie an einer von drei Stellen erfolgen:

  1. Das richtige Kauf / Verkauf-Paar tritt vollständig innerhalb des ersten Halbjahres auf.
  2. Das richtige Kauf / Verkauf-Paar tritt vollständig in der zweiten Hälfte auf.
  3. Das richtige Kauf / Verkauf-Paar tritt in beiden Hälften auf - wir kaufen in der ersten Hälfte und verkaufen dann in der zweiten Hälfte.

Wir können die Werte für (1) und (2) erhalten, indem wir unseren Algorithmus in der ersten und zweiten Hälfte rekursiv aufrufen. Für Option (3) besteht der Weg, den höchsten Gewinn zu erzielen, darin, am niedrigsten Punkt in der ersten Hälfte zu kaufen und am größten Punkt in der zweiten Hälfte zu verkaufen. Wir können die Minimal- und Maximalwerte in den beiden Hälften finden, indem wir einfach einen einfachen linearen Scan über die Eingabe durchführen und die beiden Werte finden. Dies gibt uns dann einen Algorithmus mit der folgenden Wiederholung:

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

Wenn wir den Master-Satz verwenden , um die Wiederholung zu lösen, stellen wir fest, dass dies in O (n lg n) -Zeit ausgeführt wird und O (lg n) -Raum für die rekursiven Aufrufe verwendet. Wir haben gerade die naive O (n 2 ) -Lösung geschlagen!

Aber warte! Wir können es viel besser machen. Beachten Sie, dass der einzige Grund, warum wir in unserer Wiederholung einen O (n) -Term haben, darin besteht, dass wir die gesamte Eingabe scannen mussten, um die minimalen und maximalen Werte in jeder Hälfte zu finden. Da wir bereits jede Hälfte rekursiv untersuchen, können wir es vielleicht besser machen, wenn die Rekursion auch die in jeder Hälfte gespeicherten Minimal- und Maximalwerte zurückgibt! Mit anderen Worten, unsere Rekursion gibt drei Dinge zurück:

  1. Die Kauf- und Verkaufszeiten, um den Gewinn zu maximieren.
  2. Der Mindestwert insgesamt im Bereich.
  3. Der Maximalwert insgesamt im Bereich.

Diese beiden letzten Werte können rekursiv mit einer einfachen Rekursion berechnet werden, die wir gleichzeitig mit der zu berechnenden Rekursion ausführen können (1):

  1. Die Max- und Min-Werte eines Einzelelementbereichs sind genau dieses Element.
  2. Die Max- und Min-Werte eines Bereichs mit mehreren Elementen können ermittelt werden, indem die Eingabe in zwei Hälften geteilt wird, die Max- und Min-Werte jeder Hälfte ermittelt werden und dann die jeweiligen Max- und Min-Werte ermittelt werden.

Wenn wir diesen Ansatz verwenden, ist unsere Wiederholungsbeziehung jetzt

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

Die Verwendung des Master-Theorems hier gibt uns eine Laufzeit von O (n) mit O (lg n) Raum, was sogar besser ist als unsere ursprüngliche Lösung!

Aber Moment mal - wir können es noch besser machen! Lassen Sie uns darüber nachdenken, dieses Problem mithilfe der dynamischen Programmierung zu lösen. Die Idee wird sein, über das Problem wie folgt nachzudenken. Angenommen, wir kennen die Antwort auf das Problem, nachdem wir uns die ersten k Elemente angesehen haben. Könnten wir unser Wissen über das (k + 1) st-Element in Kombination mit unserer ursprünglichen Lösung nutzen, um das Problem für die ersten (k + 1) Elemente zu lösen? Wenn ja, könnten wir einen großartigen Algorithmus in Gang bringen, indem wir das Problem für das erste Element, dann die ersten zwei, dann die ersten drei usw. lösen, bis wir es für die ersten n Elemente berechnet haben.

Lassen Sie uns darüber nachdenken, wie das geht. Wenn wir nur ein Element haben, wissen wir bereits, dass es das beste Kauf / Verkauf-Paar sein muss. Nehmen wir nun an, wir kennen die beste Antwort für die ersten k Elemente und betrachten das (k + 1) st-Element. Der einzige Weg, wie dieser Wert eine bessere Lösung als die für die ersten k Elemente hatte, besteht darin, dass der Unterschied zwischen dem kleinsten der ersten k Elemente und diesem neuen Element größer ist als der größte Unterschied, den wir bisher berechnet haben. Nehmen wir also an, wir verfolgen beim Durchlaufen der Elemente zwei Werte - den minimalen Wert, den wir bisher gesehen haben, und den maximalen Gewinn, den wir mit nur den ersten k Elementen erzielen können. Anfangs ist der minimale Wert, den wir bisher gesehen haben, das erste Element, und der maximale Gewinn ist Null. Wenn wir ein neues Element sehen, Wir aktualisieren zunächst unseren optimalen Gewinn, indem wir berechnen, wie viel wir verdienen würden, indem wir zum niedrigsten bisher gesehenen Preis kaufen und zum aktuellen Preis verkaufen. Wenn dies besser ist als der optimale Wert, den wir bisher berechnet haben, aktualisieren wir die optimale Lösung, um diesen neuen Gewinn zu erzielen. Als nächstes aktualisieren wir das bisher gesehene Minimum-Element so, dass es das Minimum des aktuell kleinsten Elements und des neuen Elements ist.

Da wir bei jedem Schritt nur O (1) arbeiten und jedes der n Elemente genau einmal besuchen, dauert es O (n), bis der Vorgang abgeschlossen ist! Darüber hinaus wird nur O (1) -Hilfsspeicher verwendet. Das ist so gut wie wir es bisher bekommen haben!

Als Beispiel für Ihre Eingaben sehen Sie hier, wie dieser Algorithmus ausgeführt werden kann. Die Zahlen zwischen den einzelnen Werten des Arrays entsprechen den Werten, die der Algorithmus an diesem Punkt hält. Sie würden nicht alle diese speichern (es würde O (n) Speicher benötigen!), Aber es ist hilfreich zu sehen, wie sich der Algorithmus weiterentwickelt:

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

Antwort: (5, 10)

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

Antwort: (4, 12)

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

Antwort: (1, 5)

Können wir es jetzt besser machen? Leider nicht im asymptotischen Sinne. Wenn wir weniger als O (n) Zeit verwenden, können wir nicht alle Zahlen auf großen Eingaben betrachten und können daher nicht garantieren, dass wir die optimale Antwort nicht verpassen (wir könnten sie einfach in den Elementen "verstecken", die wir haben habe nicht angeschaut). Außerdem können wir nicht weniger als O (1) Speicherplatz verwenden. Möglicherweise gibt es einige Optimierungen für die in der Big-O-Notation verborgenen konstanten Faktoren, aber ansonsten können wir keine radikal besseren Optionen erwarten.

Insgesamt bedeutet dies, dass wir die folgenden Algorithmen haben:

  • Naiv: O (n 2 ) Zeit, O (1) Raum.
  • Teilen und Erobern: O (n lg n) Zeit, O (lg n) Raum.
  • Optimiertes Teilen und Erobern: O (n) Zeit, O (lg n) Raum.
  • Dynamische Programmierung: O (n) Zeit, O (1) Raum.

Hoffe das hilft!

BEARBEITEN : Wenn Sie interessiert sind, habe ich eine Python-Version dieser vier Algorithmen codiert, damit Sie mit ihnen herumspielen und ihre relativen Leistungen beurteilen können. Hier ist der Code:

# 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.- Für beide rekursiven Aufrufe wird Speicherplatz benötigt, aber normalerweise werden diese Aufrufe nacheinander ausgeführt. Dies bedeutet, dass der Compiler den Speicher zwischen Aufrufen wiederverwenden kann. Sobald ein Anruf zurückkehrt, kann der nächste Anruf seinen Speicherplatz wiederverwenden. Infolgedessen benötigen Sie jeweils nur Speicher, um einen Funktionsaufruf zu halten, sodass die Speichernutzung proportional zur maximalen Tiefe des Aufrufstapels ist. Da die Rekursion auf O (log n) -Ebenen endet, muss nur O (log n) -Speicher verwendet werden. Klärt das die Dinge?
Templatetypedef

Könnte jemand diese auf Ruby portieren? Einige der Rekursionen funktionieren nicht auf die gleiche Weise wie in Python. Auch diese Lösungen bringen nur den maximalen Gewinn; Sie geben nicht die Array-Punkte zurück, die den Gewinn erbracht haben (die verwendet werden könnten, um den Prozentsatz der Gewinnsteigerung in der Vergangenheit zu melden)
rcd

Das Konzept der dynamischen Programmierung wird nicht wirklich benötigt, um die O (n) -Zeitlösung zu erklären, aber es ist großartig, dass Sie all diese Arten von Algorithmen einbinden.
Rn222

Wie können Sie auf einem der Sub-O (n ^ 2) -Algorithmen aufbauen, um alle nach Gewinn sortierten Paare zu finden?
ferk86

@templatetypedef Wie würden wir den dynamischen Programmieransatz ändern, wenn wir mit einem Budget von M $ beginnen würden und statt einzelner Aktien m Aktien mit Preisen über n Tage wie angegeben hätten? dh wir variieren die Anzahl der gekauften Aktieneinheiten und die verfügbaren Lagerbestandsdaten von 1 Aktie bis n Aktie (wie zuvor hatten wir nur für Google, jetzt haben wir auch für 5 andere Unternehmen)
Ronak Agrawal

32

Dies ist das maximale Summen-Subsequenzproblem mit ein wenig Indirektion. Das maximale Summen-Teilsequenzproblem erhält eine Liste von ganzen Zahlen, die positiv oder negativ sein können. Finden Sie die größte Summe einer zusammenhängenden Teilmenge dieser Liste.

Sie können dieses Problem trivial in dieses Problem umwandeln, indem Sie den Gewinn oder Verlust zwischen aufeinanderfolgenden Tagen mitnehmen. Sie würden also eine Liste von Aktienkursen, z. B. [5, 6, 7, 4, 2]in eine Liste von Gewinnen / Verlusten, z [1, 1, -3, -2]. Das Subsequenzsummenproblem ist dann ziemlich einfach zu lösen: Finden Sie die Subsequenz mit der größten Summe von Elementen in einem Array


1
Ich glaube nicht , es ganz so klappt, denn wenn Sie die Aktie auf anfänglichen Tag kaufen Sie nicht auflaufen die Vorteile des Deltas vom Vortag. Oder ist das bei diesem Ansatz kein Problem?
Templatetypedef

1
@templatetypedef, deshalb verfolgen Sie die größte Summe und die aktuelle Sequenzsumme. Wenn die aktuelle Sequenzsumme unter Null fällt, wissen Sie, dass Sie mit dieser Sequenz kein Geld verdient haben, und können erneut beginnen. Wenn Sie die größte Summe verfolgen, finden Sie automatisch die besten Kauf- / Verkaufstermine.
MSN

6
@templatetypedef Übrigens machen Sie dasselbe in Ihrer Antwort.
MSN

16

Ich bin mir nicht sicher, warum dies als dynamische Programmierfrage angesehen wird. Ich habe diese Frage in Lehrbüchern und Algorithmushandbüchern gesehen, die O (n log n) Laufzeit und O (log n) für Speicherplatz verwenden (z. B. Elemente von Programmierinterviews). Es scheint ein viel einfacheres Problem zu sein, als die Leute es sich vorstellen.

Dies funktioniert, indem der maximale Gewinn, der minimale Kaufpreis und folglich der optimale Kauf- / Verkaufspreis verfolgt werden. Beim Durchlaufen jedes Elements im Array wird überprüft, ob das angegebene Element kleiner als der Mindestkaufpreis ist. Wenn dies der minFall ist, wird der Mindestkaufpreisindex ( ) aktualisiert, um der Index dieses Elements zu sein. Zusätzlich becomeABillionaireprüft der Algorithmus für jedes Element, ob arr[i] - arr[min](die Differenz zwischen dem aktuellen Element und dem Mindestkaufpreis) größer als der aktuelle Gewinn ist. Wenn dies der Fall ist, wird der Gewinn auf diese Differenz aktualisiert und Kauf arr[min]und Verkauf werden auf gesetzt arr[i].

Läuft in einem einzigen Durchgang.

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 );

}

Co-Autor: https://stackoverflow.com/users/599402/ephraim


2

Das Problem ist identisch mit der maximalen Teilsequenz, die
ich mithilfe der dynamischen Programmierung gelöst habe. Verfolgen Sie den aktuellen und den vorherigen Wert (Gewinn, Kauf- und Verkaufsdatum). Wenn der aktuelle Wert höher als der vorherige ist, ersetzen Sie den vorherigen durch den aktuellen.

    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

Hier ist meine Java-Lösung:

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, ja, diese Lösung ist korrekt, aber ich möchte Sie bitten, die Antwort von templatetypedef zu lesen, da in der Antwort von templatetypedef alle möglichen Lösungen aufgeführt sind, einschließlich der von Rohit veröffentlichten. Rohits Lösung ist eigentlich eine Implementierung der letzten Lösung mit O (n) unter Verwendung der dynamischen Programmierung, die in der Antwort von templatetypedef erwähnt wird.
Nits.kk

1
Angenommen, Ihr Array ist int A [] = {5, 4, 6, 7, 6, 3, 2, 5}; Dann kaufen Sie gemäß Ihrer Logik bei Index 6 und verkaufen es dann bei Index 3. Was falsch ist. Sie können in der Vergangenheit nicht verkaufen. Der Verkaufsindex muss größer sein als der Kaufindex.
Entwickler747

1
Die obige Lösung ist "fast" richtig. Es wird jedoch der absolute Min-Index anstelle des Index des "Kauf" -Preises gedruckt. Zur Korrektur benötigen Sie eine andere Variable, z. B. minBuyIndex, die Sie nur innerhalb des Blocks "if (profit> maxProfit)" aktualisieren und ausdrucken.
Javabrew

1

Ich habe eine einfache Lösung gefunden - Code ist eher selbsterklärend. Es ist eine dieser dynamischen Programmierfragen.

Der Code kümmert sich nicht um Fehlerprüfung und Randfälle. Es ist nur ein Beispiel, um die Idee einer grundlegenden Logik zur Lösung des Problems zu vermitteln.

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;

    }

Hier ist meine Lösung. Ändert den maximalen Subsequenzalgorithmus. Löst das Problem in O (n). Ich denke, es geht nicht schneller.


1

Dies ist ein interessantes Problem, denn es scheint hart, aber sorgfältige Überlegung ergibt eine elegante, abgespeckte Lösung.

Wie bereits erwähnt, kann Brute-Force in O (N ^ 2) -Zeit gelöst werden. Durchlaufen Sie für jeden Eintrag im Array (oder in der Liste) alle vorherigen Einträge, um das Minimum oder Maximum zu erhalten, je nachdem, ob das Problem darin besteht, den größten Gewinn oder Verlust zu finden.

So denken Sie über eine Lösung in O (N) nach: Jeder Eintrag repräsentiert ein neues mögliches Maximum (oder Minimum). Dann müssen wir nur noch das vorherige min (oder max) speichern und das Diff mit dem aktuellen und dem vorherigen min (oder max) vergleichen. Kinderleicht.

Hier ist der Code in Java als JUnit-Test:

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;
    }

}

Bei der Berechnung des größten Verlusts behalten wir das Maximum in der Liste (Kaufpreis) bis zum aktuellen Eintrag im Auge. Wir berechnen dann den Unterschied zwischen dem Maximum und dem aktuellen Eintrag. Wenn max - current> maxLoss, behalten wir diesen Unterschied als neuen maxLoss bei. Da der Index von max garantiert unter dem aktuellen Index liegt, garantieren wir, dass das Kaufdatum unter dem Verkaufsdatum liegt.

Bei der Berechnung des größten Gewinns wird alles umgekehrt. Wir verfolgen die min in der Liste bis zum aktuellen Eintrag. Wir berechnen den Unterschied zwischen dem min und dem aktuellen Eintrag (Umkehrung der Reihenfolge in der Subtraktion). Wenn current - min> maxGain, behalten wir diesen Unterschied als neuen maxGain bei. Auch hier steht der Index des "Kaufs" (min) vor dem Index des aktuellen ("Verkaufen").

Wir müssen nur den maxGain (oder maxLoss) und den Index von min oder max verfolgen, aber nicht beide, und wir müssen keine Indizes vergleichen, um zu bestätigen, dass "Kaufen" weniger ist als "Verkaufen", da wir bekomme das natürlich.


1

Maximaler Einzelverkaufsgewinn, O (n) -Lösung

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
}

Hier ist ein Projekt, das Zeitkomplexitätstests für o (N) vs o (n ^ 2) -Ansätze für einen zufälligen Datensatz mit 100.000 Ints durchführt. O (n ^ 2) dauert 2 Sekunden, während O (n) 0,01 Sekunden dauert

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
}

Dies ist der langsamere o (n ^ 2) -Ansatz, der den Rest der Tage für jeden Tag durchläuft, eine doppelte Schleife.


1

Die Antwort mit der höchsten Bewertung berücksichtigt keine Fälle, in denen der maximale Gewinn negativ ist, und sollte geändert werden, um solche Fälle zu berücksichtigen. Man kann dies tun, indem man den Bereich der Schleife auf (len (a) - 1) begrenzt und die Art und Weise ändert, wie der Gewinn bestimmt wird, indem der Index um eins verschoben wird.

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

Vergleichen Sie diese Version der Funktion mit der vorherigen für das Array:

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

Eine Möglichkeit, den maximalen Gewinn zu bestimmen, könnte darin bestehen, die linken minimalen und rechten maximalen Elemente im Array an jedem Index im Array zu verfolgen. Wenn Sie dann die Aktienkurse durchlaufen, kennen Sie für jeden Tag den niedrigsten Preis bis zu diesem Tag und den Höchstpreis nach (und einschließlich) diesem Tag.

Definieren wir zum Beispiel a min_arr und max_arrmit dem angegebenen Array arr. Index iin min_arrwäre das Mindestelement in arrfür alle Indizes <= i(links von und einschließlich i). Index iin max_arrwäre das maximale Element in arrfür alle Indizes >= i(Recht von und einschließlich i). Dann könnten Sie die maximale Differenz zwischen den entsprechenden Elementen in max_arrund `min_arr 'finden:

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

Dies sollte in O (n) Zeit laufen, aber ich glaube, es verbraucht viel Platz.


0

Dies ist der maximale Unterschied zwischen zwei Elementen im Array und dies ist meine Lösung:

O (N) Zeitkomplexität O (1) Raumkomplexität

    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

Nachdem ich dies in einer Live-Codierungsprüfung für eine Position als FB-Lösungsingenieur nicht bestanden hatte, musste ich es in einer ruhigen, kühlen Atmosphäre lösen. Hier sind meine 2 Cent:

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);

Nur-Code-Antworten werden nicht empfohlen.
Pritam Banerjee

0

Die einzige Antwort, die die Frage wirklich beantwortet, ist die von @akash_magoon (und das auf so einfache Weise!), Gibt jedoch nicht das genaue Objekt zurück, das in der Frage angegeben ist. Ich habe ein bisschen umgestaltet und meine Antwort in PHP gibt genau das zurück, was gefragt wird:

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

Eine saubere Lösung:

+ (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))

Dieses Programm in Python3 kann den Kauf- und Verkaufspreis zurückgeben, der den Gewinn maximiert, berechnet mit der Zeitkomplexität von O (n) und der Raumkomplexität von O (1) .


0

Hier ist meine Lösung

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

Für alle Antworten, die die minimalen und maximalen Elemente verfolgen, ist diese Lösung tatsächlich eine O (n ^ 2) -Lösung. Dies liegt daran, dass am Ende überprüft werden muss, ob das Maximum nach dem Minimum aufgetreten ist oder nicht. Falls dies nicht der Fall ist, sind weitere Iterationen erforderlich, bis diese Bedingung erfüllt ist, und dies führt zu einem Worst-Case von O (n ^ 2). Und wenn Sie die zusätzlichen Iterationen überspringen möchten, ist viel mehr Speicherplatz erforderlich. In jedem Fall ein Nein-Nein im Vergleich zur dynamischen Programmierlösung

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.