面试算法题&高频题

文章目录

  • 1. 最长上升子序列[面试]
  • 2. vivo智能手机产能[面试]
  • 3.数位之积[面试]
  • 4. 手机屏幕解锁模式[面试]
  • 5.不同字符个数不超过K的最长的连续子串
  • 6. 目标和
  • 7. 股票买卖
  • 8. 股票买卖II
  • 9. 股票买卖III
  • 10. 股票买卖IV
  • 11. 股票买卖V
  • 12. 股票买卖VI
  • 13 股票价格跨度
  • 14. 乘积最大子数组

1. 最长上升子序列[面试]

给定一个无序的整数数组,找到其中最长上升子序列的长度

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

解法一:时间复杂度 O ( n 2 ) O(n^2) O(n2)
统计每个数比在他前面数大的个数

class Solution(object):
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums:
            return 0
        dp = [1]*len(nums)
        for i in range(1, len(nums)):
            for j in range(i):
                if nums[i]>nums[j]:
                    dp[i] = max(dp[i], dp[j]+1)
        return max(dp)

解法2:时间复杂度:O(n^2)
思路:如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

以输入序列 [0, 8, 4, 12, 2]为例:
第一步插入 0,d = [0];
第二步插入 8,d = [0, 8];
第三步插入 4,d=[0,4];
第四步插入 12,d=[0,4,12];
第五步插入 2,d=[0,2,12]。
最终得到最大递增子序列长度为 33。

即每次插入一个数首先如果大于所有数,直接加在最后,否则找到第一个大于其的数,将其替换

class Solution(object):
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums:
            return 0

        dp = []
        dp.append(nums[0])
        for i in range(1, len(nums)):
            if nums[i]>dp[-1]:
                dp.append(nums[i])
            else:
                for j in range(i):
                    if nums[i] <= dp[j]:
                        dp[j] = nums[i]
                        break
        return len(dp)

解法3:对解法二进行改进,在查找第一个大于它的数时使用二分查找,那么时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

class Solution(object):
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums:
            return 0

        dp = []
        dp.append(nums[0])
        for i in range(1, len(nums)):
            if nums[i]>dp[-1]:
                dp.append(nums[i])
            else:
                l, r =0, len(dp)
                while l<=r:
                    mid = (l+r)//2
                    if nums[i]>dp[mid]:
                        l = mid+1
                    else:
                        r = mid-1
                        loc = mid
                dp[loc] = nums[i]
        return len(dp)

2. vivo智能手机产能[面试]

题目描述
在vivo产线上,每位职工随着对手机加工流程认识的熟悉和经验的增加,日产量也会不断攀升。
假设第一天量产1台,接下来2天(即第二、三天)每天量产2件,接下来3天(即第四、五、六天)每天量产3件 … …
以此类推,请编程计算出第n天总共可以量产的手机数量。

程序

def solution(n):
    res = 0
    i = 1
    while n>0:
        res += i*min(i, n)
        n -= i
        i += 1
    return res
print(solution(6))

思路
产能为1的天数是1,产能为2的天数是2天,产能为3的天数是3天,因此我们用循环来遍历产能, i表示产能,下面以n=5为例

i = 1: res=1, n剩余5-1天,即n=4
i = 2:产能为2有两天,且2天<4天够用,res += 2*2 = 5, n =n-2=2,剩余2天
i = 3:产能为3有3天,而我们只剩下2天,因此产能为3的三天不能全用,只能用其中2天,res = res+3*2= 11, n=n-3=-1<0,结束。

3.数位之积[面试]

题目描述
现给定任意正整数 n,请寻找并输出最小的正整数 m(m>9),使得 m 的各位(个位、十位、百位 … …)之乘积等于n,若不存在则输出 -1。

输入例子1:
36
输出例子1:
49
输入例子2:
100
输出例子2:
455

使用递归

#
# 输入一个整形数值,返回一个整形值
# @param n int整型 n>9
# @return int整型
#
class Solution:
    def recursion(self, n):
        if n<10:
            return n
        else:
            for i in range(9, 1, -1):
                if n%i == 0:
                    return self.recursion(n//i)*10+i
            return -1
    def solution(self , n ):
        result = self.recursion(n)
        return result if result>0 else -1

4. 手机屏幕解锁模式[面试]

题目描述
现有一个 3x3 规格的 Android 智能手机锁屏程序和两个正整数 m 和 n ,请计算出使用最少m 个键和最多 n个键可以解锁该屏幕的所有有效模式总数。
其中有效模式是指:
1、每个模式必须连接至少m个键和最多n个键;
2、所有的键都必须是不同的;
3、如果在模式中连接两个连续键的行通过任何其他键,则其他键必须在模式中选择,不允许跳过非选择键(如图);
4、顺序相关,单键有效(这里可能跟部分手机不同)。

输入:m,n
代表允许解锁的最少m个键和最多n个键
输出
满足m和n个键数的所有有效模式的总数
面试算法题&高频题_第1张图片

思路
将锁屏看成1~9的九个点组成,采用深度优先遍历算法,其中data存放的是不可直接相连的两个点,但是可以发现如果遍历过两点平均数,那么这两个点可以直接相连(例如,点1和点3本来是不能直接相连的,但是如果点(1+3)/2=2被遍历过的化,那么点1和点3可以直接相连)。其中canInsert(self, stack, i)判断一个点 i 是否可以添加到之前的连线stack中。根据对称原理,我们只需要求点1 、点2 、点5 即可

程序

class Solution:
    def __init__(self):
        self.data = [[1,3], [1,9], [1,7], [2,8],[3,9],[3,7],[4,6],[7,9]]

    def canInsert(self, stack, i):
        if i in stack:
            return False
        for j in range(len(self.data)):
            if ((stack[-1]==self.data[j][0] and i == self.data[j][1]) or (stack[-1]==self.data[j][1] and i==self.data[j][0])) and (stack[-1]+i)//2 not in stack:
                return False
        return True
    
    def dfs(self, stack, m, n):
        count = 0
        if len(stack)==n:
            return 1
        elif len(stack)>=m:
            count = 1
        for i in range(1, 10):
            if self.canInsert(stack, i):
                stack.append(i)
                count = count + self.dfs(stack, m, n)
                stack.pop()
        return count
    
    def getCount(self, m, n):
        count = 0
        stack = []
        stack.append(1)
        count += 4*self.dfs(stack, m, n)
        stack.pop()
        stack.append(2)
        count += 4*self.dfs(stack, m, n)
        stack.pop()
        stack.append(5)
        count += self.dfs(stack, m, n)
        return count
    
    def solution(self , m , n ):
        # write code here
        if n == 0:
            return 0
        result = 0
        result += self.getCount(m, n)
        return result
# 测试
sol = Solution()
print(sol.solution(1,2))

参考
手机屏幕解锁模式JAVA版

5.不同字符个数不超过K的最长的连续子串

题目描述
给定一个字符串,找到包含的不同字符个数不超过K的最长的连续子串

例子1:
输入: String="araaci", K=2 
输出: 4 
解释: 包含不同字符个数不超过2的最长的连续子串是 "araa"
例子2:
输入: String="araaci", K=1 
输出: 2 
解释: 包含不同字符个数不超过1的最长的连续子串是 "aa"
例子3:
输入: String="cbbebi", K=3 
输出: 5 
解释: 包含不同字符个数不超过3的最长的连续子串是 "cbbeb" 或者 "bbebi"

思路
首先,从数组开头累积出一个最长的符合条件的子字符串,作为我们的窗口。其次,当滑动窗口后,尽可能避免收缩窗口,除非新窗口中包含的字符种类超过了限制。最后,我们需要实时记录窗口中包含的不同字符的个数,以便判断窗口是否符合K个不同字符的要求

程序

def longest_substring_with_k_distinct(str1, k):
	start = 0
	freq_map = {}
	maxlength = 0
	for end in range(len(str1)):
		value = str1[end]
		if value not in freq_map:
			freq_map[value] = 0
		freq_map[value] += 1

		while len(freq_map)>k:
			temp = str1[start]
			freq_map[temp] -= 1
			if freq_map[temp] == 0:
				del freq_map[temp]
			start += 1
		maxlength = max(maxlength, end-start+1)
	return maxlength
# 测试
if __name__ == '__main__':
	string = "cbbebi"
	k = 3
	length = longest_substring_with_k_distinct(string, 3)
	print(length)

输出

5

6. 目标和

给定一个非负整数数组a1, a2, ..., an, 和一个目标数S。现在你有两个符号 +-。对于数组中的任意一个整数,你都可以从+-中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

提示:
数组非空,且长度不会超过 20 。
初始的数组的和不会超过 1000 。
保证返回的最终结果能被 32 位整数存下。

reference
leetcode494
回溯算法和动态规划,到底谁是谁爹?
1. 回溯法

class Solution:
    def findTargetSumWays(self, nums: List[int], S: int) -> int:
        self.result = 0
        def traceback(nums, i, tar):
            if i == len(nums):
                if tar == 0:
                    self.result += 1
                return
            tar -= nums[i]
            traceback(nums, i+1, tar)
            tar += nums[i]
            tar += nums[i]
            traceback(nums, i+1, tar)

        if len(nums)==0:
            return 0
        memory = {}
        traceback(nums, 0, S)
        return self.result

时间复杂度: O ( 2 n ) O(2^n) O(2n)

改进的回溯法
如何发现重叠子问题?看是否可能出现重复的「状态」。对于递归函数来说,函数参数中会变的参数就是「状态」,对于 backtrack 函数来说,会变的参数为 i 和 rest。

void backtrack(int i, int rest) {
    backtrack(i + 1, rest - nums[i]);
    backtrack(i + 1, rest + nums[i]);
}

举个简单的例子,如果 nums[i] = 0,会发生什么?

void backtrack(int i, int rest) {
    backtrack(i + 1, rest);
    backtrack(i + 1, rest);
}

这样就出现了两个「状态」完全相同的递归函数,无疑这样的递归计算就是重复的。这就是重叠子问题,而且只要我们能够找到一个重叠子问题,那一定还存在很多的重叠子问题。
因此,状态 (i, rest) 是可以用备忘录技巧进行优化的
优化后的程序

class Solution:
    def findTargetSumWays(self, nums: List[int], S: int) -> int:
    	# 使用memory来记录已经算过的值:备忘录
        memory = {}
        def traceback(nums, i, target):
            if i == len(nums):
                if target == 0:
                    return 1
                return 0
            key = str(i)+"_"+str(target)
            if key in memory:
                return memory.get(key)
            result = traceback(nums, i+1, target-nums[i])+traceback(nums, i+1, target+nums[i])
            memory[key] = result
            return result
        return traceback(nums, 0, S)

时间复杂度最坏情况下: O ( 2 n ) O(2^n) O(2n)
2. 动态规划
首先,如果我们把 nums 划分成两个子集 A 和 B,分别代表分配 +的数和分配-的数,那么他们和 target 存在如下关系:

sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)

综上,可以推出sum(A) = (target + sum(nums)) / 2,也就是把原问题转化成:nums 中存在几个子集A,使得 A 中元素的和为 (target + sum(nums)) / 2

定义状态
dp[i][j] = x 表示只在 nums 的前 i个元素中选择,若目标和为j,则最多有x种方法划分子集

状态转移
如果不把 nums[i] 算入子集,那么恰好装满背包的方法数就取决于上一个状态 dp[i-1][j],继承之前的结果。

如果把nums[i]算入子集,或者说你把这第i个物品装入了背包,那么只要看前i - 1 个物品有几种方法可以装满j - nums[i-1] 的重量就行了,所以取决于状态dp[i-1][j-nums[i-1]]

由于 dp[i][j] 为装满背包的总方法数,所以应该以上两种选择的结果求和,得到状态转移方程:

dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];

程序

# nums = [1,1,1,1,1]
# s = 3
# nums = [27,22,39,22,40,32,44,45,46,8,8,21,27,8,11,29,16,15,41,0]
# s = 10
nums = [2,20,24,38,44,21,45,48,30,48,14,9,21,10,46,46,12,48,12,38]
s = 48
def sumTarget(nums, s):
	if (sum(nums)-s)%2 != 0:
		return 0
	temp = (sum(nums)-s)//2

	dp = [[0 for i in range(temp+1)] for j in range(len(nums)+1)]
	for i in range(len(nums)+1):
		dp[i][0] = 1

	for i in range(1, len(nums)+1):
		for j in range(temp+1):
			if j < nums[i-1]:
				dp[i][j] = dp[i-1][j]
			else:
				dp[i][j] = dp[i-1][j]+dp[i-1][j-nums[i-1]]
	return (dp[len(nums)][temp])

print(sumTarget(nums, s))

输出

5401

7. 股票买卖

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
     
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

参考
leetcode 121
股票问题的一种通用解法

暴力法

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        maxmoney = 0
        for i in range(len(prices)-1):
            for j in  range(i+1, len(prices)):
                maxmoney = max(maxmoney, prices[j]-prices[i])
        return maxmoney

暴力法改进

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        maxmoney = 0
        for i in range(len(prices)-1):
            maxmoney = max(maxmoney, max(prices[i+1:])-prices[i])
        return maxmoney

以上两种时间复杂度都为: O ( n 2 ) O(n^2) O(n2)

改进2

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        current_min = prices[0]
        result = 0
        for i in range(1, len(prices)):
            current_min = min(current_min, prices[i])
            result = max(result, prices[i]-current_min)
        return result

时间复杂度: O ( n ) O(n) O(n)
上面三种说明:
方法1,2都是固定买入时的价格,来遍历卖出的价格(最大值),当买入价格往后移一个时,卖出价格必须重头遍历,才能获得最大值;而方法3是固定卖出价格,寻找最小的买入价格,当卖出价格往后移时,买入价格最小值的获得只需将上一个最小值和当前值比较即可。

动态规划
具体详细说明:包括状态定义、状态转移等见【股票买卖IV
由股票买卖IV中分析可得状态转移方程为:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])

此处见k设定为1:

dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1]+prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0]-prices[i]) = max(dp[i-1][1][1], -prices[i])
也即
dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        dp = [[0 for i in range(2)]for j in range(n)]
        # 初始状态
        # 第一次不持有股票收益为0
        dp[0][0] = 0
        # 第一次就持有股票,也就是第一次买的股票,因此收益为负,就是花掉了钱
        dp[0][1] = -prices[0]
        for i in range(1, n):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
            dp[i][1] = max(dp[i-1][1], -prices[i])
        return dp[n-1][0]

8. 股票买卖II

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
     
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
     
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

参考
leetcode 122
股票问题的一种通用解法

暴力法
穷举所有可能

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        result = 0
        for i in range(len(prices)-1):
            for j in range(i+1, len(prices)):
                result = max(result, self.maxProfit(prices[j+1:])+(prices[j]-prices[i]))
        return result

暴力法改进

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 使用备忘录
        memory = [-1]*len(prices)    
        def dp(start):
            if start >= len(prices):
                return 0
            res = 0
            curmin = prices[start]
            if memory[start] != -1:
                return memory[start]
            # 固定sell
            for sell in range(start+1, len(prices)):
                curmin = min(curmin, prices[sell])
                res = max(res, dp(sell+1)+prices[sell]-curmin)
            memory[start] = res
            return res
        return dp(0)

贪心算法

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        maxprofit = 0
        for i in range(1, len(prices)):
            if prices[i]>prices[i-1]:
                maxprofit += prices[i]-prices[i-1]
        return maxprofit

动态规划
具体详细说明:包括状态定义、状态转移等见【股票买卖IV
由股票买卖IV中分析可得状态转移方程为:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])

此处见k设定为正无穷, k 与 k-1相等:


dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] -prices[i])
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        dp = [[0 for i in range(2)]for j in range(n)]
        dp[0][0] = 0
        dp[0][1] = -prices[0]
        for i in range(1, n):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
        return dp[n-1][0]

9. 股票买卖III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
     
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
    
示例 3:
输入: [7,6,4,3,1] 
输出: 0 
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。

暴力法
使用了买卖股票1中的方法

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        curmin = prices[0]
        result = 0
        res = 0
        for i in range(1, len(prices)):
            flag = False
            curmin = min(prices[i], curmin)
            res = max(res, prices[i]-curmin)

            if i<=len(prices)-2:
                flag = True
                res2 = 0
                curmin2=prices[i+1]
                for j in range(i+2, len(prices)):
                    curmin2 = min(prices[j],curmin2)
                    res2 = max(res2, prices[j]-curmin2)
            # 使用flag来判断是否进入了第二个循环
            if flag:
                result = max(result, res+res2)
            else:
                result = max(result, res)
        return result

方法2
使用股票买卖IV 的方法,见股票买卖IV
动态规划
具体详细说明:包括状态定义、状态转移等见【股票买卖IV
由股票买卖IV中分析可得状态转移方程为:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])

此处见k设定为2:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0

        dp = [[[0 for i in range(2)] for k in range(3)] for j in range(len(prices))]
        # 初始化状态
        # k=0表示不允许买卖
        for k in range(1, 3):
            dp[0][k][0] = 0
            dp[0][k][1] = -prices[0]
        for i in range(len(prices)):
            dp[i][0][0] = 0
            dp[i][0][1] = float("-inf")
        
        for i in range(1, len(prices)):
            for k in range(1, 3):
                dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])
                dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i])
        return dp[len(prices)-1][2][0]

10. 股票买卖IV

参考
leetcode题解

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: [2,4,1], k = 2
输出: 2
解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:
输入: [3,2,6,5,0,3], k = 2
输出: 7
解释: 在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 

使用之前的套路

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        memory = {}
        def dp(start, k):
            if start >= len(prices):
                return 0
            if k == 0:
                return 0
            if (start, k) in memory:
                return memory[(start, k)]
            res = 0
            curmin = prices[start]
            for i in range(start+1, len(prices)):
                curmin = min(curmin, prices[i])
                res = max(res, prices[i]-curmin+dp(i+1, k-1))
            memory[(start, k)] = res
            return res
        return dp(0, k)

时间复杂度: O ( K n 2 ) O(Kn^2) O(Kn2)

动态规划
(1)定义状态
每天都有三种「选择」:买入、卖出、无操作,我们用 buy, sell, rest 表示这三种选择

但并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后(第一次除外)。那么 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。而且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作

这个问题的「状态」有三个,第一个是天数,第二个是当天允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)

将状态定义如下:

dp[i][k][s]表示第i天最大收益,其中:
i: 表示第几天
k:表示最大交易次数
s:股票持有状态, {0, 1}

比如说 dp[3][2][1] 的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 2 次交易。再比如 dp[2][3][0] 的含义:今天是第二天,我现在手上没有持有股票,至今最多进行 3 次交易的最大收益

我们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多允许 K次交易,所能获取的最大利润。为什么不是 dp[n - 1][K][1]?因为 [1] 代表手上还持有股票,[0] 表示手上的股票已经卖出去了,很显然后者得到的利润一定大于前者

(2) 状态转移

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i])
解释:
今天没有持有股票有两种可能:
(1)昨天也没有持股票,今天也没买入
(2)昨天持有股票,今天卖了

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])
解释:
今天持有股票,有两种情况
(1)昨天持有股票,今天没有买卖
(2)昨天没有持有股票, 今天买入了

如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把最大交易数 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。

(3)状态初始化

dp[-1][k][0]=0
解释:
i 是从 0 开始的,i=-1表示还没开始,这时候利润当然为0

dp[-1][k][1] = -inf
还没开始时是不能持有股票的,负无穷表示不可能

dp[i][0][0] = 0
k从1开始, k=0表示不允许交易,利润为0

dp[i][0][1] = -inf
不允许交易情况下是不能持有股票的,负无穷表示不可能发生

程序

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if not prices:
            return 0
        # 如果k>len(prices)//2说明k就没限制,因为如果对最大交易次数k有限制,k最大为len(prices)//2(最小是两天进行一次交易))
        if k > len(prices)//2:
            dp = [[0 for _ in range(2)] for i in range(len(prices))]
            dp[0][0] = 0
            dp[0][1] = -prices[0]
            for i in range(1, len(prices)):
                dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
                dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
            return dp[len(prices)-1][0]
        
        # 对k进行限制的处理方法
        else:
            dp = [[[0 for i in range(2)]for _ in range(k+1)]for j in range(len(prices))]
            for i in range(1, k+1):
                dp[0][i][0] = 0
                dp[0][i][1] = -prices[0]
            for j in range(len(prices)):
                dp[j][0][0] = 0
                dp[j][0][1] = -float("inf")
            for i in range(1, len(prices)):
                for j in range(1, k+1):
                    dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1]+prices[i])
                    dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i])
            return dp[len(prices)-1][k][0]

这题和 k = 2 没啥区别,可以直接套上一题的第一个解法。但是提交之后会出现一个超内存的错误,原来是传入的 k 值可以任意大,导致 dp 数组太大了。现在想想,交易次数 k 最多能有多大呢?

一次交易由买入和卖出构成,至少需要两天。所以说有效的限制次数 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是之前解决过的。直接把之前的代码重用:

11. 股票买卖V

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:
输入: [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

程序

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        memory = {}
        def dp(start):
            if start >= len(prices):
                return 0

            res = 0
            if start in memory:
                return memory[start]

            curmin = prices[start]
            for i in range(start+1, len(prices)):
                curmin = min(curmin, prices[i])
                res = max(res, prices[i]-curmin+dp(i+2))
            memory[start] = res
            return res
        return dp(0)

时间复杂度: O ( N 2 ) O(N^2) O(N2)

动态规划

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        dp = [[0 for i in range(2)] for _ in range(len(prices))]
        dp[0][0] = 0
        dp[0][1] = -prices[0]
        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
            if i == 1:
                # 注意初始值:第一天的值可能是第0天继承过来的,也有可能是第一天买的
                dp[i][1] = max(dp[i-1][1], -prices[i])
            else:
            # 第i天买的时候要从第i-2时候转移
                dp[i][1] = max(dp[i-1][1], dp[i-2][0]-prices[i])
        return dp[len(prices)-1][0]

12. 股票买卖VI

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

程序

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        memory = {}
        def dp(start):
            if start >= len(prices):
                return 0
            curmin = prices[start]
            res = 0
            if start in memory:
                return memory[start]
            for i in range(start+1, len(prices)):
                curmin = min(curmin, prices[i])
                res = max(res, prices[i]-curmin+dp(i+1)-fee)
            memory[start] = res
            return res
        return dp(0)

动态规划

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        if not prices:
            return 0

        # dp = [[0 for _ in range(2)]for i in range(len(prices))]
        # dp[0][0] = 0
        # dp[0][1] = -prices[0]

        dp_i_0 = 0
        # 初始值,买入要减去fee, 如果设定卖出时减fee,此处就不需要设置
        dp_i_1 = -prices[0]-fee

        for i in range(len(prices)):
            # dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]-fee)
            # dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
            dp_i_0 = max(dp_i_0, dp_i_1+prices[i])
            # 此处减fee也可设置在卖出时减
            dp_i_1 = max(dp_i_1, dp_i_0-prices[i]-fee)
        # return dp[len(prices)-1][0]
        return dp_i_0

13 股票价格跨度

编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。

今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。

例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]。

示例:

输入:["StockSpanner","next","next","next","next","next","next","next"], [[],[100],[80],[60],[70],[60],[75],[85]]
输出:[null,1,1,1,2,1,4,6]
解释:
首先,初始化 S = StockSpanner(),然后:
S.next(100) 被调用并返回 1,
S.next(80) 被调用并返回 1,
S.next(60) 被调用并返回 1,
S.next(70) 被调用并返回 2,
S.next(60) 被调用并返回 1,
S.next(75) 被调用并返回 4,
S.next(85) 被调用并返回 6。

注意 (例如) S.next(75) 返回 4,因为截至今天的最后 4 个价格
(包括今天的价格 75) 小于或等于今天的价格。

思路
用单调栈维护一个单调递减的价格序列,并且对于每个价格,存储一个 weight 表示它离上一个价格之间(即最近的一个大于它的价格之间)的天数。如果是栈底的价格,则存储它本身对应的天数。例如 [11, 3, 9, 5, 6, 4, 7] 对应的单调栈为 (11, weight=1), (9, weight=2), (7, weight=4)。

当我们得到了新的一天的价格,例如 10,我们将所有栈中所有小于等于 10 的元素全部取出,将它们的 weight 进行累加,再加上 1 就得到了答案。在这之后,我们把 10 和它对应的 weight 放入栈中,得到 (11, weight=1), (10, weight=7)。

参考
901. 股票价格跨度

程序

class StockSpanner:
    def __init__(self):
        self.prices = []
        self.weight = []
        

    def next(self, price: int) -> int:
        w = 1
        while len(self.prices) != 0 and price >= self.prices[-1]:
            self.prices.pop()
            w += self.weight.pop()
        self.prices.append(price)
        self.weight.append(w)
        return w

14. 乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6

示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
    # dp[i][0] 以nums[i]结尾的最小乘积
    # dp[i][1] 以nums[i]结尾的最大乘积
        length = len(nums)
        dp  = [[0, 0] for _ in range(length)]
        dp[0][0], dp[0][1] = nums[0], nums[0]
        for i in range(1, length):
            if nums[i] > 0:
                dp[i][0] = min(nums[i], nums[i]*dp[i-1][0])
                dp[i][1] = max(nums[i], nums[i]*dp[i-1][1])
            else:
                dp[i][0] = min(nums[i], nums[i]*dp[i-1][1])
                dp[i][1] = max(nums[i], nums[i]*dp[i-1][0])
        max_result = -float("inf")
        for i in range(length):
            max_result = max(max_result, dp[i][1])
        return max_result

时间复杂度 O ( n 2 ) O(n^2) O(n2)
空间复杂度 n n n

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0
        if len(nums) == 1:
            return nums[0]
        length = len(nums)
        minim, maxim = nums[0], nums[0]
        max_result = maxim
        for i in range(1, length):
            if nums[i] >= 0:
                minim = min(nums[i], nums[i]*minim)
                maxim = max(nums[i], nums[i]*maxim)
            else:
                minim, maxim = min(nums[i], nums[i]*maxim), max(nums[i], nums[i]*minim)
            max_result = max(max_result, maxim)
        return max_result

时间复杂度: o ( n 2 ) o(n^2) o(n2)
空间复杂度: o ( 1 ) o(1) o(1)

参考: 152. 题解

你可能感兴趣的:(算法)