Thinking analysis of dynamic programming problem in algorithmic interview

Posted by bluns on Wed, 02 Feb 2022 20:09:27 +0100

I wish you all good health and happiness in the new year. According to the observation and experience in the past decades, congratulations alone can't make people rich. Only when we successfully find a good job and win a satisfactory career can we have the opportunity to make money. Being able to enter a good enterprise is naturally a reasonable way to improve our wealth. I heard recently that Tencent has at least three months at the end of the year, ”The "sunshine" award can be 100 shares, equivalent to more than 30000 yuan, so solving the interview questions of a large factory and getting a formal position in a large factory should be a good way for ordinary people to realize rapid accumulation of wealth.

For large factory technical posts, the interview algorithm is often unavoidable, and the dynamic programming problem is a difficult threshold to cross. Therefore, in order to make a better money process, we need to work hard to solve it. In the last section, we talked about a dynamic programming algorithm problem I encountered. I thought it was an example. Later, after investigation, it was found that the corresponding problems often appear. At the same time, the description was rough last time, and there were problems in the solution. This time, I intend to further elaborate the treatment method of dynamic programming problem through slow disassembly.

When the algorithm problem requires you to give the words "best", "best", "most", "least", nine times out of ten is dynamic programming, and its processing usually has fixed steps as follows:
1. If the scale of the problem is n, disassemble the problem into n-1 and the last element, analyze the corresponding optimal solution of the first n-1 element in different states, and then combine the last element to find the overall optimal solution.
2. To solve the problem of dynamic programming, we must consider the list to record information, otherwise the time complexity will become exponential.
3. After the first part of the problem is disassembled into n-1, it is often necessary to recursively disassemble the problem into n-2. When solving recursively, you need to look up the table to see if there is an answer to the problem.
4. Pay attention to dealing with boundary problems in the recursive process.

Let's analyze the stock trading problem last time. Given the price change of a stock in the next period of time, if there is no limit on the investor's budget, that is, no matter how high the stock price is, he can afford it, but it is required that he can only hold one share at most at a time, and he can only buy without holding shares, please design the best investment strategy to maximize the profit of stock trading. For example, [2,5,1] is the change in three days, Then the best strategy is to buy it for 2 yuan on the first day and sell it for 5 yuan on the second day. For example, the maximum is 3 yuan. If the stock price changes to [2,5,1,3], it is to buy on the first day, sell on the second day, then buy on the third day and sell on the fourth day, so the profit is 5 = 3 + 2.

Let's take a look at the application of the conclusion steps mentioned above. Let's take [2,5,1,3] as an example. First, we disassemble it into n-1 and the last element, that is [2,5,1] and 3. So let's first look at the optimal solution under different states when the stock changes to [2,5,1]. The so-called "different state" is whether to hold stocks or not on the last day, so we first find the best profit when holding stocks on the last day when the stock price is [2,5,1] and the best profit when not holding stocks.

If the stock is held on the last day of [2,51], the optimal income corresponding to [2,5,1,3] is to sell the stock for 3 yuan on the last day. If there is no stock on the last day of [2,5,1], then don't do anything on the last day of [2,5,1,3], and then we compare the two cases, which has the greatest return.

Therefore, when calculating [2,5,1], we can decompose it into [2,5] and 1, and still deal with it with the same logic. Next, we will look at the second step, that is, to record the information with a table. Here, we want to record the best return of holding stocks and no stocks on the last day after the end of n days. For example, when we need to solve [2,5,1], the maximum return of holding stocks and not having stocks on the last day. If this information is recorded in the table at this time, we can obtain it directly by looking up the table, so as to save the time for subsequent solution. Similarly, when the scale of the problem is [2,5,1], if we can obtain the maximum return of holding stocks and no stocks in two days by looking up the table, we can quickly solve the problem. Therefore, the corresponding step here is step 3.

Since the problem recurses constantly, we must make the recursion stop. When the scale of the problem is small enough to give the answer directly, it is where the problem should stop. In this example, when there is only one day, we can give the answer directly when the scale of the problem is [2]. Therefore, we need to stop recursion here. Next, let's look at the implementation code:

profit_table = {}
HAVE_STOCK = 1  # Holding shares
CLEAR_STOCK = 2 # No stock

#Calculate the best return in two states after a given number of days
def max_profit_by_status(day, status, price_list):
    if day <= 0:  #First, confirm the condition of recursive stop
        #When there is only one day, the income from not holding stocks is 0. If you hold stocks, the income is negative, that is, you have to spend money to buy stocks
        if status is CLEAR_STOCK:
            return 0
        else:
            return -price_list[0]

    if (day, status) in profit_table:
        #Before solving the problem, check the table to see if there is an answer to the problem
        return profit_table[(day, status)]


    if status == HAVE_STOCK:
        '''
        In the first n The maximum return on holding shares depends on the day n-1 Do not hold shares for the first day, and then n Buy stocks on the second day, or n-1 Days holding shares,The first n Days continue to hold,
        Then judge which of the two situations is more beneficial
        '''
        max_profit_have_stock = max(max_profit_by_status(day - 1, CLEAR_STOCK, price_list) - price_list[day],
                                    max_profit_by_status(day - 1, HAVE_STOCK, price_list))
        #Record the best case record
        profit_table[(day, HAVE_STOCK)] = max_profit_have_stock
    else:
        '''
        The first n The maximum return without stocks depends on two situations, which are the second n-1 Hold shares on the first day, and then n Days to sell. Or the third n-1 Days without stocks, then
        The first n Don't buy one day. Look at two situations. Which one is more profitable
        '''
        max_profit_by_clear_stock = max(max_profit_by_status(day - 1, HAVE_STOCK, price_list) + price_list[day],
                                        max_profit_by_status(day-1, CLEAR_STOCK, price_list))
        # Record the best case record
        profit_table[(day, CLEAR_STOCK)] = max_profit_by_clear_stock
    # Returns the optimal return in a given state after a given number of days
    return profit_table[(day, status)]


price_list = [199, 193, 201, 172, 159, 106, 42, 70, 118, 209, 202, 108, 189,
              162, 283, 5, 123, 43, 127, 128, 105, 90, 91, 225, 192, 37, 251, 77, 195, 64, 7, 289,
              24, 59, 84, 110, 48, 88, 248, 174, 131, 258, 244, 58, 50, 169,
              217, 160, 41, 95, 283, 200, 149, 249, 106, 116, 174, 47, 159, 21, 119, 105, 42, 56]

#The last day to get the maximum return must be the case of not holding stocks
max_profit = max_profit_by_status(len(price_list) - 1, CLEAR_STOCK, price_list)
print(f"max profit is:{max_profit}")

Students can experience the processing steps mentioned above in combination with the code. Usually, during the interview, the interviewer will prepare multiple backhands, that is, after you solve the first problem, he is likely to change the conditions and let you continue to solve after increasing the difficulty. Suppose we add a constraint to the original problem, that is, the investor has a budget, and he can't buy when the stock price is higher than the funds in his hand. After this condition is added, the above steps of purchasing stocks must be judged accordingly. If the conditions are not met, it cannot be executed.

At the same time, when we decompose the problem in step 1, some situations may not be achieved. For example, considering the situation of holding stocks in n-1 days, investors must buy stocks in n-1 days. If the funds are insufficient, the return after buying stocks is negative. We should eliminate this situation. Therefore, in a comprehensive consideration, the code is implemented as follows:

profit_table = {}
HAVE_STOCK = 1
CLEAR_STOCK = 2

SAVING = 100

def max_profit_by_status(day, status, price_list):
    if day <= 0:
        if status is CLEAR_STOCK: #If you don't buy stocks on the first day, the investor's capital limit will remain unchanged
            return SAVING
        else:
            return SAVING - price_list[0]  #If you buy a stock, you need to subtract the price of the stock. If there are not enough bytes, it will become a negative value

    if (day, status) in profit_table:  #Check the table to see if you have the answer
        return profit_table[(day, status)]

    '''
    Calculate the maximum return under the state of holding stocks, which is divided into two cases: the second case n-1 There are no stocks on the first day, and then on the second day n Day purchase, note that the purchased shares may form a negative value,
    The second is in the second case n-1 Day is to hold stocks. Note that the return may still be negative at this time
    '''
    max_profit_have_stock = 0
    max_profit_have_stock = max(max_profit_by_status(day - 1, CLEAR_STOCK, price_list) - price_list[day],
                                max_profit_by_status(day - 1, HAVE_STOCK, price_list))

    '''
    Calculation section n The maximum return of days without holding stocks can be divided into two cases, in which n-1 Shares held on day n Days to sell, note that we need to make sure n-1 When holding stocks for days, the return cannot be negative because
    Because the budget does not allow investors to buy stocks whose price exceeds their capital, when n-1 When the stock held on the day is negative, we can't sell it because it's not a legal operation.
    The second case is n-1 There are no stocks on the first day, and then the second day n God, do nothing
    '''
    max_profit_clear_stock = 0
    profit1 = 0
    if max_profit_by_status(day - 1, HAVE_STOCK, price_list) >= 0:
        profit1 = max_profit_by_status(day - 1, HAVE_STOCK, price_list) + price_list[day]
    profit2 = max_profit_by_status(day-1, CLEAR_STOCK, price_list)
    max_profit_clear_stock = max(profit1, profit2)

    if status is HAVE_STOCK:
        profit_table[(day, HAVE_STOCK)] = max_profit_have_stock
    else:
        profit_table[(day, CLEAR_STOCK)] = max_profit_clear_stock

    return profit_table[(day, status)]

The last variant is also the most complex case, which is to limit the number of sales. For example, investors are limited to no more than 10 times. In this case, there is one more variable in the state of the problem. The original state of the problem is whether to hold the stock or not on the last day. Now, one more variable is the number of sales. Therefore, we should consider holding the stock on the last day, and then sell the pool once or twice... 10 times. At the same time, consider that there are no stocks on the last day, and then sell them once and twice respectively... After 10 times of these situations, the state to be considered has changed from 2 to 20, and the corresponding code is implemented as follows:

profit_table = {}
HAVE_STOCK = 1
CLEAR_STOCK = 2

TX_LIMIT = 20 #You can't sell more than 20 times at most

def  max_profit_by_status(day, status, count ,price_list): #One more variable we need to consider is the number of sales, corresponding to count
    if count < 0:
        return -float('inf')

    if day == 0:
        if status is CLEAR_STOCK:
            profit_table[(0, CLEAR_STOCK, 0)] = 0
        else:
            profit_table[(0, HAVE_STOCK, 0)] = -price_list[0]
        for i in range(1, count+1):  #When there is only one day, you can only buy but not sell, so the income corresponding to the number of sales is infinitesimal
            profit_table[(0, HAVE_STOCK, i)] = -float('inf')
            profit_table[(0, CLEAR_STOCK, i)] = -float('inf')
        return profit_table[(0, status, count)]

    if (day, status, count) in profit_table: #Find the maximum return under a given number of days, a given state and a given number of sales
        return profit_table[(day, status, count)]

    '''
    The first n Corresponding income of buying stocks in one day
    '''
    profit1 = max_profit_by_status(day - 1, CLEAR_STOCK, count, price_list)
    profit1 -= price_list[day]
    profit2 = max_profit_by_status(day - 1, HAVE_STOCK, count, price_list)
    max_profit_buying = max(profit2, profit1)
    profit_table[(day, status, count)] = max_profit_buying #Record the maximum benefit for a given situation

    '''
    The first n Note that if we don't hold the corresponding return of stocks on the first day n Days ago n-1 You can only sell once a day
    '''
    profit1 = max_profit_by_status(day - 1, HAVE_STOCK, count - 1, price_list)
    profit1 += price_list[day]
    profit2 = max_profit_by_status(day - 1, CLEAR_STOCK, count, price_list)
    max_profit_clear = max(profit1, profit2)
    profit_table[(day, CLEAR_STOCK, count)] = max_profit_clear

    return profit_table[(day, status, count)] #Record the maximum benefit for a given situation

price_list = [199, 193, 201, 172, 159, 106, 42, 70, 118, 209, 202, 108, 189,
              162, 283, 5, 123, 43, 127, 128, 105, 90, 91, 225, 192, 37, 251, 77, 195, 64, 7, 289,
              24, 59, 84, 110, 48, 88, 248, 174, 131, 258, 244, 58, 50, 169,
              217, 160, 41, 95, 283, 200, 149, 249, 106, 116, 174, 47, 159, 21, 119, 105, 42, 56]


profits = []
for i in range(TX_LIMIT + 1):  #Calculate the best return under a given number of sales
    profit = max_profit_by_status(len(price_list) - 1, CLEAR_STOCK, i, price_list)
    profits.append(profit)


print(profits)

The running results of the code given above are as follows:

[0, 284, 543, 787, 1028, 1245, 1433, 1600, 1718, 1835, 1947, 2048, 2148, 2246, 2326, 2394, 2456, 2494, 2521, 2535, 2543]

The result of the code operation is worth analyzing. When the number of times allowed to sell is 0, the best profit is of course 0, because if you only buy or not, you will inevitably lose money. When you only run the sell once, the algorithm buys at the lowest point of the stock price, that is, 5 yuan, and sells at the highest point, that is, 289, so the income is 284, which is easier to test. According to these two results, we can confidently believe that the algorithm logic should be correct.

Finally, we analyze the time complexity of the algorithm. In the first two cases, there are only two states every day. Given n days, we need to calculate 2n cases, so the complexity is O(n). In the last case, there are 20 states every day, so we need to calculate 20n cases. Therefore, the algorithm complexity is still O(n). Similarly, the space complexity can also be calculated.

The code can be downloaded here: https://github.com/wycl16514/dynamic_programming_analyze.git

Topics: Algorithm Interview Dynamic Programming