Максимальная прибыль от одной продажи


123

Предположим, нам дан массив из n целых чисел, представляющих курсы акций за один день. Мы хотим найти пару (buyDay, sellDay) с buyDay ≤ sellDay , чтобы, если бы мы купили акции в buyDay и продали их в sellDay , мы бы максимизировали нашу прибыль.

Очевидно, что существует решение алгоритма за O (n 2 ), если опробовать все возможные пары (buyDay, sellDay) и извлечь из них все самое лучшее. Однако есть ли лучший алгоритм, возможно, тот, который работает за O (n) время?


2
Это задача подпоследовательности максимальной суммы с уровнем косвенности.
MSN

2
@MSN: Как так? Он вообще смотрит не на суммы, а на разницу между элементами.
PengOne

@ PengOne - Верно, но этот вопрос был закрыт. Я изменил формулировку вопроса, чтобы было легче понять, так что мы можем оставить этот вопрос открытым?
templatetypedef

2
@PengOne, как я уже сказал, у него есть один уровень косвенности. В частности, вы хотите максимизировать сумму прибылей / убытков за непрерывный набор дней. Поэтому преобразуйте список в выигрыши / проигрыши и найдите максимальную сумму подпоследовательности.
MSN

1
@PDN: это не сработает, потому что min может быть раньше max. Вы не можете продать акции (в этом случае) и купить их позже.
Ajeet Ganga

Ответы:


287

Я люблю эту проблему. Это классический вопрос на собеседовании, и в зависимости от того, как вы к нему относитесь, в конечном итоге вы найдете все более и более удачные решения. Конечно, можно сделать это быстрее, чем за O (n 2 ), и я перечислил здесь три различных способа, которыми вы можете подумать о проблеме. Надеюсь, это ответит на ваш вопрос!

Во-первых, решение «разделяй и властвуй». Посмотрим, сможем ли мы решить эту проблему, разделив входные данные пополам, решив проблему в каждом подмассиве, а затем объединив их вместе. Оказывается, мы действительно можем это сделать, и можем делать это эффективно! Интуиция такова. Если у нас один день, лучший вариант - купить в этот день, а затем продать его обратно в тот же день без прибыли. В противном случае разделите массив на две половины. Если мы подумаем о том, какой может быть оптимальный ответ, он должен быть в одном из трех мест:

  1. Правильная пара покупка / продажа происходит полностью в первой половине.
  2. Правильная пара покупка / продажа полностью происходит во второй половине.
  3. Правильная пара покупка / продажа встречается на обеих половинах - мы покупаем в первой половине, а продаем во второй половине.

Мы можем получить значения для (1) и (2), рекурсивно вызывая наш алгоритм на первой и второй половинах. Для варианта (3) способом получения максимальной прибыли будет покупка в самой низкой точке в первой половине и продажа в самой высокой точке во второй половине. Мы можем найти минимальное и максимальное значения в двух половинах, просто выполнив простое линейное сканирование по входу и найдя два значения. Это дает нам алгоритм со следующей повторяемостью:

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

Используя основную теорему для решения рекурсии, мы обнаруживаем, что это выполняется за время O (n lg n) и будет использовать пространство O (lg n) для рекурсивных вызовов. Мы только что обыграли наивное решение O (n 2 )!

Но ждать! Мы можем добиться большего, чем это. Обратите внимание, что единственная причина, по которой у нас есть член O (n) в нашем повторении, заключается в том, что нам пришлось сканировать весь ввод, пытаясь найти минимальное и максимальное значения в каждой половине. Поскольку мы уже рекурсивно исследуем каждую половину, возможно, мы сможем добиться большего, если рекурсия также вернет минимальные и максимальные значения, хранящиеся в каждой половине! Другими словами, наша рекурсия возвращает три вещи:

  1. Время покупки и продажи для максимизации прибыли.
  2. Минимальное общее значение в диапазоне.
  3. Максимальное значение в диапазоне.

Эти последние два значения могут быть вычислены рекурсивно, используя простую рекурсию, которую мы можем запустить одновременно с рекурсией для вычисления (1):

  1. Максимальные и минимальные значения одноэлементного диапазона - это именно тот элемент.
  2. Максимальные и минимальные значения диапазона из нескольких элементов можно найти, разделив входные данные пополам, найдя максимальные и минимальные значения каждой половины, а затем взяв их соответствующие максимальные и минимальные значения.

Если мы воспользуемся этим подходом, наше рекуррентное отношение теперь будет

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

Использование основной теоремы дает нам время выполнения O (n) с пространством O (lg n), что даже лучше, чем наше исходное решение!

Но подождите минутку - мы можем сделать даже лучше, чем это! Давайте подумаем о решении этой проблемы с помощью динамического программирования. Идея будет заключаться в следующем. Предположим, что мы знали ответ на проблему, посмотрев на первые k элементов. Можем ли мы использовать наши знания о (k + 1) -м элементе в сочетании с нашим начальным решением, чтобы решить проблему для первых (k + 1) элементов? Если это так, мы могли бы получить отличный алгоритм, решив проблему для первого элемента, затем для первых двух, затем для первых трех и т. Д., Пока мы не вычислим его для первых n элементов.

Давайте подумаем, как это сделать. Если у нас есть только один элемент, мы уже знаем, что это должна быть лучшая пара покупки / продажи. Теперь предположим, что мы знаем лучший ответ для первых k элементов, и посмотрим на (k + 1) -й элемент. Тогда единственный способ, которым это значение может создать решение лучше, чем то, что мы имели для первых k элементов, - это если разница между наименьшим из первых k элементов и этим новым элементом больше, чем самая большая разница, которую мы вычислили до сих пор. Итак, предположим, что, просматривая элементы, мы отслеживаем два значения - минимальное значение, которое мы видели до сих пор, и максимальную прибыль, которую мы могли бы получить только с первыми k элементами. Изначально минимальное значение, которое мы видели до сих пор, является первым элементом, а максимальная прибыль равна нулю. Когда мы видим новый элемент, Сначала мы обновляем нашу оптимальную прибыль, вычисляя, сколько мы могли бы заработать, купив по самой низкой цене, наблюдаемой до сих пор, и продав по текущей цене. Если это лучше, чем оптимальное значение, которое мы вычислили до сих пор, то мы обновляем оптимальное решение, чтобы оно было новой прибылью. Затем мы обновляем минимальный элемент, который мы видели до сих пор, чтобы он был минимальным из текущего наименьшего элемента и нового элемента.

Поскольку на каждом шаге мы выполняем только O (1) работу и посещаем каждый из n элементов ровно один раз, это занимает O (n) времени! Более того, он использует только вспомогательную память O (1). Это так хорошо, как мы уже сделали!

В качестве примера на ваших входных данных вот как может работать этот алгоритм. Числа между каждым из значений массива соответствуют значениям, удерживаемым алгоритмом в этой точке. На самом деле вы бы не стали хранить все это (это заняло бы O (n) памяти!), Но было бы полезно увидеть, как алгоритм развивается:

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

Ответ: (5, 10)

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

Ответ: (4, 12)

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

Ответ: (1, 5)

Можем ли мы сделать лучше сейчас? К сожалению, не в асимптотическом смысле. Если мы используем время меньше O (n), мы не сможем просмотреть все числа на больших входных данных и, следовательно, не сможем гарантировать, что не пропустим оптимальный ответ (мы могли бы просто «спрятать» его в элементах, которые мы не смотрел). Кроме того, мы не можем использовать пространство меньше O (1). Могут быть некоторые оптимизации постоянных факторов, скрытых в нотации большого O, но в противном случае мы не можем ожидать найти какие-либо радикально лучшие варианты.

В целом это означает, что у нас есть следующие алгоритмы:

  • Наивный: O (n 2 ) раз, O (1) пространство.
  • Разделяй и властвуй: O (n lg n) раз, O (lg n) пространство.
  • Оптимизированный принцип «разделяй и властвуй»: O (n) раз, O (lg n) пространство.
  • Динамическое программирование: O (n) раз, O (1) пространство.

Надеюсь это поможет!

РЕДАКТИРОВАТЬ : Если вам интересно, я закодировал версию этих четырех алгоритмов для Python, чтобы вы могли поиграть с ними и оценить их относительную производительность. Вот код:

# 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. - Место необходимо для обоих рекурсивных вызовов, но обычно эти вызовы выполняются один за другим. Это означает, что компилятор может повторно использовать память между вызовами; как только один вызов возвращается, следующий вызов может повторно использовать его пространство. В результате вам нужна память только для одного вызова функции за раз, поэтому использование памяти пропорционально максимальной глубине стека вызовов. Поскольку рекурсия завершается на уровне O (log n), необходимо использовать только O (log n) памяти. Это проясняет ситуацию?
templatetypedef

Может ли кто-нибудь перенести их на Ruby? Некоторые рекурсии работают не так, как в Python. Также эти решения возвращают только максимальную прибыль; они не возвращают точки массива, которые принесли прибыль (которые можно было бы использовать для отчета о процентном увеличении прибыли за прошлый период)
rcd

Концепция динамического программирования на самом деле не нужна для объяснения решения времени O (n), но здорово, что вы связали все эти типы алгоритмов.
Rn222

Как вы можете использовать любой алгоритм sub O (n ^ 2), чтобы найти все пары, отсортированные по прибыли?
ferk86

@templatetypedef, как бы мы изменили подход к динамическому программированию, если бы мы начали с бюджета в M $ и вместо одной акции у нас было m акций с заданными ценами более n дней? т.е. мы варьируем количество купленных единиц акций и доступные данные о запасах от 1 акции до n акций (как раньше у нас было только для Google, теперь у нас есть и для 5 других компаний),
Ронак Агравал,

32

Это задача подпоследовательности максимальной суммы с небольшим косвенным обращением. Задача о максимальной сумме подпоследовательности задается списком целых чисел, которые могут быть положительными или отрицательными, найти наибольшую сумму непрерывного подмножества этого списка.

Вы можете тривиально преобразовать эту проблему в проблему, зафиксировав прибыль или убыток между днями подряд. Таким образом, вы можете преобразовать список цен на акции, например, [5, 6, 7, 4, 2]в список прибылей / убытков, например [1, 1, -3, -2]. Тогда довольно просто решить проблему суммы подпоследовательностей: найдите подпоследовательность с наибольшей суммой элементов в массиве


1
Я не думаю, что это вполне срабатывает, поскольку, если вы покупаете акцию в какой-то начальный день, вы не получаете выгоды от дельты предыдущего дня. Или это не проблема при таком подходе?
templatetypedef

1
@templatetypedef, поэтому вы отслеживаете наибольшую сумму и текущую сумму последовательности. Когда текущая сумма последовательности опускается ниже нуля, вы знаете, что не заработаете на этой последовательности денег, и можете начать заново. Отслеживая самую большую сумму, вы автоматически найдете лучшие даты покупки / продажи.
MSN

6
@templatetypedef, кстати, вы делаете то же самое в своем ответе.
MSN

16

Я не совсем уверен, почему это считается вопросом динамического программирования. Я видел этот вопрос в учебниках и руководствах по алгоритмам, использующим время выполнения O (n log n) и O (log n) для пространства (например, Elements of Programming Interviews). Это кажется гораздо более простой проблемой, чем люди думают.

Это работает путем отслеживания максимальной прибыли, минимальной цены покупки и, следовательно, оптимальной цены покупки / продажи. Проходя через каждый элемент в массиве, он проверяет, меньше ли данный элемент минимальной цены покупки. Если это так, индекс минимальной закупочной цены ( min) обновляется до индекса этого элемента. Кроме того, для каждого элемента becomeABillionaireалгоритм проверяет, arr[i] - arr[min]превышает ли (разница между текущим элементом и минимальной ценой покупки) текущую прибыль. Если это так, прибыль обновляется до этой разницы, и устанавливается значение покупки, arr[min]а значение продажи - arr[i].

Работает за один проход.

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

}

Соавтор: https://stackoverflow.com/users/599402/ephraim


2

Задача идентична максимальной подпоследовательности.
Я решил ее с помощью динамического программирования. Следите за текущим и предыдущим (прибыль, дата покупки и дата продажи). Если текущее значение выше предыдущего, замените предыдущее на текущее.

    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

вот мое решение 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, да, это решение правильное, но я прошу вас прочитать ответ, предоставленный templatetypedef, поскольку в ответе, предоставленном templatetypedef, упоминаются все возможные решения, включая то, которое опубликовано Rohit. Решение Rohit на самом деле является реализацией последнего решения с O (n) с использованием динамического программирования, упомянутого в ответе, предоставленном templatetypedef.
nits.kk

1
Предположим, ваш массив - int A [] = {5, 4, 6, 7, 6, 3, 2, 5}; Затем, согласно вашей логике, вы будете покупать по индексу 6, а затем продавать по индексу 3. Что неверно. Вы не можете продавать в прошлом. Индекс продажи должен быть больше индекса покупки.
developer747

1
Приведенное выше решение "почти" правильное. но он печатает абсолютный минимальный индекс вместо индекса цены покупки. Для исправления вам потребуется другая переменная, например minBuyIndex, которую вы обновляете только внутри блока «if (profit> maxProfit)», и распечатайте ее.
javabrew

1

Я придумал простое решение - код более понятен. Это один из тех вопросов динамического программирования.

Код не заботится о проверке ошибок и крайних случаях. Это просто образец, чтобы дать представление об основной логике решения проблемы.

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;

    }

Вот мое решение. изменяет алгоритм максимальной подпоследовательности. Решает проблему за O (n). Я думаю, что это невозможно сделать быстрее.


1

Это интересная проблема, потому что она кажется сложной, но тщательное обдумывание дает элегантное и урезанное решение.

Как уже отмечалось, это может быть решено перебором за время O (N ^ 2). Для каждой записи в массиве (или списке) перебирайте все предыдущие записи, чтобы получить минимальное или максимальное значение в зависимости от того, заключается ли проблема в поиске наибольшего прироста или убытка.

Вот как думать о решении в O (N): каждая запись представляет новый возможный максимум (или минимум). Затем все, что нам нужно сделать, это сохранить предыдущий минимум (или максимум) и сравнить разницу с текущим и предыдущим минимумом (или максимумом). Очень просто.

Вот код на Java как тест 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;
    }

}

В случае расчета наибольшего убытка мы отслеживаем максимум в списке (цену покупки) до текущей записи. Затем мы вычисляем разницу между максимальной и текущей записью. Если max - current> maxLoss, то мы сохраняем эту разницу в качестве нового maxLoss. Поскольку индекс max гарантированно будет меньше, чем индекс текущего, мы гарантируем, что дата «покупки» меньше даты «продажи».

В случае расчета наибольшего усиления все происходит наоборот. Мы отслеживаем мин в списке до текущей записи. Мы вычисляем разницу между min и текущей записью (меняя порядок вычитания). Если current - min> maxGain, то мы сохраняем эту разницу в качестве нового maxGain. Опять же, индекс «покупка» (мин.) Стоит перед индексом «текущей» («продажа»).

Нам нужно только отслеживать maxGain (или maxLoss) и индекс min или max, но не оба сразу, и нам не нужно сравнивать индексы, чтобы убедиться, что «покупка» меньше, чем «продажа», поскольку мы получить это естественно.


1

Максимальная прибыль от одной продажи, решение 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
}

Вот проект, который выполняет тестирование временной сложности на подходах o (N) vs o (n ^ 2) на случайном наборе данных на 100k int. O (n ^ 2) занимает 2 секунды, а O (n) - 0,01 секунды

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
}

Это более медленный, o (n ^ 2) подход, который проходит через оставшиеся дни для каждого дня, двойной цикл.


1

Ответ, получивший наибольшее количество голосов, не допускает случаев, когда максимальная прибыль отрицательна, и его следует изменить, чтобы учесть такие случаи. Это можно сделать, ограничив диапазон цикла до (len (a) - 1) и изменив способ определения прибыли, сдвинув индекс на единицу.

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

Сравните эту версию функции с предыдущей для массива:

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

Возможность определить максимальную прибыль может заключаться в отслеживании левого минимума и правого максимума элементов в массиве по каждому индексу в массиве. Когда вы затем перебираете цены на акции, для каждого дня вы будете знать самую низкую цену до этого дня, а также максимальную цену после (включительно) этого дня.

Например, давайте определим min_arrи max_arrс заданным массивом arr. Индекс iin min_arrбудет минимальным элементом arrдля всех индексов <= i(слева и включая i). Индекс iin max_arrбудет максимальным элементом arrдля всех индексов >= i(справа и включая i). Затем вы можете найти максимальную разницу между соответствующими элементами в max_arrи `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

Это должно выполняться за O (n) время, но я считаю, что он занимает много места.


0

Это максимальная разница между двумя элементами в массиве, и это мое решение:

O (N) временная сложность O (1) пространственная сложность

    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

После того, как я провалил этот экзамен по программированию на должность инженера по решениям FB, мне пришлось решать его в спокойной прохладной атмосфере, так что вот мои 2 цента:

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

Ответы только на код не приветствуются.
Притам Банерджи

0

Единственный ответ, действительно отвечающий на вопрос, - это ответ @akash_magoon (и такой простой способ!), Но он не возвращает точный объект, указанный в вопросе. Я немного реорганизовал, и мой ответ в PHP возвращает именно то, что спрашивают:

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

Аккуратное решение:

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

Эта программа на python3 может возвращать цену покупки и цену продажи, которые максимизируют прибыль, вычисленную с временной сложностью O (n) и пространственной сложностью O (1) .


0

Вот мое решение

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

Для всех ответов, отслеживающих минимальные и максимальные элементы, это решение фактически является решением O (n ^ 2). Это потому, что в конце необходимо проверить, произошел ли максимум после минимума или нет. Если это не так, требуются дальнейшие итерации, пока это условие не будет выполнено, и это оставляет наихудший случай O (n ^ 2). А если вы хотите пропустить лишние итерации, потребуется гораздо больше места. В любом случае, нет-нет по сравнению с решением динамического программирования

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.