Skip to content

Latest commit

 

History

History
1028 lines (835 loc) · 37.2 KB

动态规划.md

File metadata and controls

1028 lines (835 loc) · 37.2 KB

1. 经典算法题记录(动态规划)

1.1. 矩阵相关

1.1.1. 矩形面积

1.1.1.1. 柱状图中最大矩形(84,困难

题目: 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。

暴力法:


思路1:索引i和j围成的矩形面积为s=(j-i+1)*min(heights[i:j+1])

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        #暴力法
        if not heights:return 0
        res = 0
        for i in range(0,len(heights)):
            Min = heights[i]
            for j in range(i,len(heights)):
                Min = min(Min,heights[j])
                res = max(res,(j-i+1)*Min)
                #print(i,j,Min)
        return res
#改进:第二次循环可以将j移到最后一个大于Min的位置,避免多余计算。

思路2:以索引i为中心向左右扩展,找到左右最后一个大于heights[i]的索引left和right,此时矩形面积为s=heights[i]*(right-left+1).矩形意义是以heights[i]为高能围成的最大矩形。

进阶


单调栈:单调栈是一种特殊的栈,栈内元素保持递增或者递减。 单调栈有两个性质: 1.满足从栈顶到栈底的元素具有严格的单调性 2.满足栈的后进先出特性越靠近栈底的元素越早进栈 利用单调栈,可以找到从左/右遍历第一个比它小/大的元素的位置,在某些问题中可以将时间复杂度从O(N^2)降低为O(N)
维护逻辑:元素大于栈顶进栈,小于则栈顶出栈直到栈顶小于该元素

#stack存元素的索引
class Solution(object):
    def largestRectangleArea(self, heights):
        heights.append(-1)
        stack = []
        maxArea = 0
        for i in range(len(heights)):
            while stack and heights[stack[-1]] > heights[i]:
                height = heights[stack.pop()]
                width = i if not stack else i-stack[-1]-1
                maxArea = max(maxArea,height*width)
            stack.append(i)
            #print(stack)
        return maxArea

1.1.1.2. 矩阵中最大长方形(85,困难

题目:给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

思路:对每一行求出向上的连续为1的柱状图,使用题目84的解法即可。

class Solution:
    
    def largestRectangleArea(self, heights):
        heights.append(-1)
        stack = []
        maxArea = 0
        for i in range(len(heights)):
            while stack and heights[stack[-1]] > heights[i]:
                height = heights[stack.pop()]
                width = i if not stack else i-stack[-1]-1
                maxArea = max(maxArea,height*width)
            stack.append(i)
            #print(stack)
        return maxArea
        
    #柱状图采取递归的方式求
    def maximalRectangle(self, matrix: List[List[str]]) -> int:
        if not matrix:return 0
        last = [int(i) for i in matrix[0]]
        res = self.largestRectangleArea(last)
        for i in range(1,len(matrix)):
            heights = [int(i)+j if int(i) > 0 else 0 for i,j in zip(matrix[i],last)]
            print(heights)
            res = max(res,self.largestRectangleArea(heights))
            last = heights
        return res

1.1.1.3. 矩阵中最大正方形(221,中等

题目:在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。

思路1:沿用最大长方形的思路,面积计算修改。

class Solution:

    def largestRectangleArea(self, heights):
        heights.append(-1)
        stack = []
        maxArea = 0
        for i in range(len(heights)):
            while stack and heights[stack[-1]] > heights[i]:
                height = heights[stack.pop()]
                width = i if not stack else i-stack[-1]-1
                #修改面积计算公式
                maxArea = max(maxArea,min(height,width)**2)
            stack.append(i)
            #print(stack)
        return maxArea
    
    #柱状图采取递归的方式求
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        if not matrix:return 0
        last = [int(i) for i in matrix[0]]
        res = self.largestRectangleArea(last)
        for i in range(1,len(matrix)):
            heights = [int(i)+j if int(i) > 0 else 0 for i,j in zip(matrix[i],last)]
            print(heights)
            res = max(res,self.largestRectangleArea(heights))
            last = heights
        return res

思路2:直接动态规划

定义dp矩阵,dp[i][j]表示以第i行第j列为右下角的正方形的最大边长。则有递推关系如下
if matrix[i][j] == 0:dp[i][j] = 0
if matrix[i][j] != 0:dp[i][j]=1+min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])
注:补充全零的行列,便于计算。

#java version
class Solution {

    public int maximalSquare(char[][] matrix) {
        int m = matrix.length;
        if(m < 1) return 0;
        int n = matrix[0].length;
        int max = 0;
        int[][] dp = new int[m+1][n+1];
        
        for(int i = 1; i <= m; ++i) {
            for(int j = 1; j <= n; ++j) {
                if(matrix[i-1][j-1] == '1') {
                    dp[i][j] = 1 + Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1]));
                    max = Math.max(max, dp[i][j]); 
                }
            }
        }

        return max*max;
    }
}   

1.1.2. 最短路径(带权重)

1.1.2.1. 地下城游戏(174,困难

题目:一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。为了尽快到达公主,骑士决定每次只向右或向下移动一步。

1.1.3. 查找

1.1.3.1. 有序矩阵第k小的元素()

题目:给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k小的元素。请注意,它是排序后的第k小元素,而不是第k个元素。

思路1:建立最小堆,大小为n。依次输出栈顶元素,第k次输出的元素即为所求。时间复杂度为O(n)+k*log(n)

class Solution:
    def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
        
        def heap_sink(heap,index):
            left = index*2 + 1
            right = index*2 + 2
            smallest = index
            if left < len(heap) and matrix[heap[left][0]][heap[left][1]] < matrix[heap[smallest][0]][heap[smallest][1]]:
                smallest = left
            if right < len(heap) and matrix[heap[right][0]][heap[right][1]] < matrix[heap[smallest][0]][heap[smallest][1]]:
                smallest = right
            if smallest == index:
                return
            else:
                heap[smallest],heap[index] = heap[index],heap[smallest]
                heap_sink(heap,smallest)
        
        n = len(matrix)
        indexs = [(i,0) for i in range(n)]
        
        for line in matrix:
            print(line)
        
        count = 0
        heap = sorted(indexs,key=lambda x:matrix[x[0]][x[1]])
        while count < k:
            top = heap[0]
            elem = matrix[top[0]][top[1]]
            if top[0] < n and top[1]+1 < n:
                heap[0] = (top[0],top[1]+1)
            else:
                heap[0] = heap[-1]
                heap.pop()
            heap_sink(heap,0)
            count += 1
            
        return elem

思路2:二分查找

1.1.4. N皇后问题(52,困难

题目:n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击(即两个棋子不能相邻)。

思路:进行递归求解。设当前进行到第i行,前面的棋子记录为record,将i行的棋子每一个与record进行匹配,看是否满足与rocord中的任意棋子不冲突,对满足的棋子继续进行递归。

class Solution:
    def totalNQueens(self, n: int) -> int:
        
        def is_valid(record,x,y):
            for x1,y1 in record:
                if x1 == x or y1 == y or abs(y1-y) == abs(x1-x):
                    return False
            return True
        
        def helper(record,i,n):
            if i == n:
                self.solution += 1
                return
            #print(record)
            for j in range(n):
                if is_valid(record,i,j):
                    helper(record+[(i,j)],i+1,n)
        
        self.solution = 0
        helper([],0,n)
        return self.solution

1.2. 字符串相关

1.2.1. 最长公共子串

1.2.2. 最长公共子序列

1.2.3. 编辑距离(189,困难

题目:给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:插入一个字符/删除一个字符/替换一个字符

思路:动态规划。建立dp矩阵,dp[i][j]表示string1[:i]string2[:j]的编辑距离。则有如下递推关系:
dp[i][j] = min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+(string1[i-1] != string2[j-1]))
矩阵初始化dp[0][j]=j,dp[i][0]=i
解释:在计算string1[:i]string2[:j]的编辑距离时,对其末尾有如下四种处理方式,对应递推式右边的四种情况。

1.删掉string1[i-1]
2.删掉string2[j-1]
3.同时删掉string1[i-1]string2[j-1]
4.同时保留string1[i-1]string2[j-1],此时有string1[i-1] == string2[j-1]

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        
        if not word1:
            return len(word2)
        if not word2:
            return len(word1)
        
        dp = [[0]*(len(word2)+1) for _ in range(len(word1)+1)]
        
        for i in range(len(word1)):
            dp[i+1][0] = i+1
        for j in range(len(word2)):
            dp[0][j+1] = j+1
        #print(dp)
        for i in range(1,len(word1)+1):
            for j in range(1,len(word2)+1):
                #print(i,j)
                dp[i][j] = min(dp[i-1][j-1]+(word1[i-1]!=word2[j-1]),dp[i-1][j]+1,dp[i][j-1]+1)
        return dp[-1][-1]

1.2.4. 两个字符串的删除操作(583,中等

题目:给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

思路:和编辑距离的思路一致。唯一不同的是,在string1[i-1] != string2[j-1]时,删除操作的步数加2。

#略

1.2.5. 回文系列

1.2.5.1. 最长回文子串(5,中等

题目:给定一个字符串 s,找到 s 中最长的回文子串

思路:s前加上标记字符#,建立dp数组,dp[i]指以s[dp[i-1]]为结尾的最长回文子串。则有如下递推关系:
if s[i] == s[i-1-dp[i-1]]:dp[i]=2+dp[i-1]
if s[i] != s[i-1-dp[i-1]]:dp[i]=max(i-j+1,j从i-1-dp[i-1]取到i使得s[j:i+1]是回文串)
注:第二种情况j的取值不能超出以s[i-1]为结尾的最长回文子串的范围,否则最长的定义相悖。

class Solution:
    def longestPalindrome(self, s: str) -> str:
    
        #判断回文串
        def is_palindromic(string):
            start = 0
            end = len(string)-1
            while start < end:
                if string[start] != string[end]:
                    return False
                start += 1
                end -= 1
            return True
        
        s = '#' + s
        dp = [0]*(len(s))
        
        for i in range(1,len(s)):
            if s[i] == s[i-1-dp[i-1]]:
                dp[i] = 2 + dp[i-1]
            else:
                for j in range(i-1-dp[i-1],i+1):
                    if is_palindromic(s[j:i+1]):
                        dp[i] = i-j+1
                        break
        Max = max(dp)
        for i in range(len(dp)):
            if dp[i] == Max:
                return s[i-Max+1:i+1]

1.2.5.2. 最长回文子序列(516,中等

题目:给定一个字符串s,找到其中最长的回文子序列。

思路:建立dp矩阵,dp[i][j]表示s[i:j+1]的最长回文序列长度,则递推关系如下:
dp[i][j] = max(dp[i+1][j],dp[i][j-1],dp[i+1][j-1]+2*(s[i] == s[j]))
注:dp应该依对角线更新。

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        
        if not s:return 0
        if s == s[::-1]:return len(s)
        
        df = [[0]*len(s) for _ in range(len(s))]
        
        for i in range(len(s)):
            df[i][i] = 1
    
        for margin in range(1,len(s)):
            for i in range(len(s)-margin):
                j = i + margin
                #print(i,j)
                df[i][j] = max(df[i+1][j],df[i][j-1],df[i+1][j-1]+2*(s[i] == s[j]))
        
        return df[0][len(s)-1]

1.2.5.3. 回文子串数目(647,中等

题目:给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。

思路:建立dp数组,dp[i]表示s[:i+1]的回文子串数目。则有:
dp[i] = dp[i-1] + #(以s[i]结尾的回文串数目)

class Solution:
    def countSubstrings(self, s: str) -> int:
        
        def is_palindrome(s):
            start = 0
            end = len(s) - 1
            while start < end:
                if s[start] != s[end]:
                    return False
                start += 1
                end -= 1
            return True
        
        df = [0]*len(s)
        df[0] = 1
        #记录从头开始一直相同的序列长度
        Len = 0
        for i in range(len(s)):
            if s[i] != s[0]:
                break
            Len += 1
        #print(Len)
        
        for i in range(1,len(s)):
            #特殊情况
            if i <= Len-1:
                df[i] = (i+1)*(i+2)//2
                continue
            df[i] = df[i-1]
            for j in range(i+1):
                if is_palindrome(s[j:i+1]):
                    df[i] += 1
        return dp[-1]

1.2.6. 最长上升子序列(300,中等

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

思路1:直接dp。复杂度较高O(n^2)

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        
        if not nums:return 0
        
        #以nums[i]结尾的上升子序列的长度
        dp = [0]*len(nums)
        dp[0] = 1
        
        for i in range(1,len(nums)):
            res = 1
            for j in range(i):
                if nums[i] > nums[j]:
                    res = max(res,1+dp[j])
            dp[i] = res
        #print(dp)
        return max(dp)

思路2:O(n*logn)。todo

#java版本
class Solution {
    public int lengthOfLIS(int[] nums) {
        /**
        dp[i]: 所有长度为i+1的递增子序列中, 最小的那个序列尾数.
        由定义知dp数组必然是一个递增数组, 可以用 maxL 来表示最长递增子序列的长度. 
        对数组进行迭代, 依次判断每个数num将其插入dp数组相应的位置:
        1. num > dp[maxL], 表示num比所有已知递增序列的尾数都大, 将num添加入dp
           数组尾部, 并将最长递增序列长度maxL加1
        2. dp[i-1] < num <= dp[i], 只更新相应的dp[i]
        **/
        int maxL = 0;
        int[] dp = new int[nums.length];
        for(int num : nums) {
            // 二分法查找, 也可以调用库函数如binary_search
            int lo = 0, hi = maxL;
            while(lo < hi) {
                int mid = lo+(hi-lo)/2;
                if(dp[mid] < num)
                    lo = mid+1;
                else
                    hi = mid;
            }
            dp[lo] = num;
            if(lo == maxL)
                maxL++;
        }
        return maxL;
    }
}

1.2.7. 判断子序列(392,中等

题目:给定字符串 s 和 t ,判断 s 是否为 t 的子序列。你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度<=100)。字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

思路:双指针+贪心即可

class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        
        if not s:
            return True
        
        index_s = 0
        index_t = 0
        while index_s < len(s) and index_t < len(t):
            if s[index_s] == t[index_t]:
                index_s += 1
                index_t += 1
            else:
                index_t += 1
                
        return index_s == len(s)

扩展:如果有大量输入的 S,称作S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

思路:用一个二维的数组存每个字母在t中出现的位置。第一次遍历t字符串后数组被保存下来,后续判断s是否是子串只需要直接找二维数组中对于s的所有字母是否有一个递增的位置序列即可。 代码如下:

#java version
class Solution {
    public boolean isSubsequence(String s, String t) {
        if(t == null){
            return false;
        }
        if(s == null || s.length() == 0){
            return true;
        }
        //记录26个字母的出现位置
        ArrayList<ArrayList<Integer>> list = new ArrayList<>(26);
        for(int i = 0; i < 26; i++){
            list.add(new ArrayList<Integer>());
        }
        for(int i = 0; i < t.length(); i++){
            //添加位置信息
            list.get(t.charAt(i) - 'a').add(i);
        }
        int prePos = -1;
        ArrayList<Integer> tempList = null;
        for(int i = 0; i < s.length(); i++){
            tempList = list.get(s.charAt(i) - 'a');
            //找到比prePos大的位置的值
            boolean flag = false;
            for(int j = 0; j < tempList.size(); j++){
                //这个地方其实可以用二分法优化因为一定是有序的
                if(tempList.get(j) > prePos){
                    prePos = tempList.get(j);
                    flag = true;
                    break;
                }
            }
            //没找到
            if(flag == false){
                return false;
            }
        }
        return true;   
    } 
}

1.2.8. 最长连续序列(128,困难

题目:给定一个未排序的整数数组,找出最长连续序列的长度。要求算法的时间复杂度为 O(n)。

思路

  • 用哈希表存储每个端点值对应连续区间的长度
  • 若数已在哈希表中:跳过不做处理
  • 若是新数加入:
    • 取出其左右相邻数已有的连续区间长度 left 和 right
    • 计算当前数的区间长度为:cur_length = left + right + 1
    • 根据 cur_length 更新最大长度 max_length 的值
    • 更新区间两端点的长度值
class Solution(object):
    def longestConsecutive(self, nums):
        hash_dict = dict()
        
        max_length = 0
        for num in nums:
            if num not in hash_dict:
                left = hash_dict.get(num - 1, 0)
                right = hash_dict.get(num + 1, 0)
                
                cur_length = 1 + left + right
                if cur_length > max_length:
                    max_length = cur_length
                
                hash_dict[num] = cur_length
                hash_dict[num - left] = cur_length
                hash_dict[num + right] = cur_length
                
        return max_length

1.2.9. 至少有k个重复字符的最小子串

题目:找到给定字符串(由小写字符组成)中的最长子串 T , 要求 T 中的每一字符出现次数都不少于 k 。输出 T 的长度。

思路

1.3. 二叉树相关

1.4. 图相关

1.5. 背包问题

1.6. 斐波拉其数列原型

1.7. 其他动态规划

1.7.1. 买卖股票问题

1.7.1.1. 一次买卖股票最大利润(121,简单

题目:给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。注意你不能在买入股票前卖出股票。

思路:维护历史最小价格,当前价格大于最小价格时,更新结果。

#一次买进卖出的最大利润
def maxProfitByOne(prices):
    profit = 0
    #维护历史最小价格
    min_stack = []
    for i in prices:
        if not min_stack or min_stack[-1] >= i:
            min_stack.append(i)
        else:
            profit = max(profit,i-min_stack[-1])
    return profit

1.7.1.2. 尽可能买卖股票最大利润(122,简单

题目:给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

思路:对所有连续递增子数组首尾进行计算即可。简化计算方式:profit += max(0,prices[j]-prices[j-1])

#略

1.7.1.3. 两次买进卖出的最大利润(123,困难

题目:给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

思路1:借助买卖一次股票的最大利润,暴力将prices数组拆成两部分。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        
        #一次买进卖出的最大利润
        def maxProfitByOne(prices):
            profit = 0
            #维护历史最小价格
            min_stack = []
            for i in prices:
                if not min_stack or min_stack[-1] >= i:
                    min_stack.append(i)
                else:
                    profit = max(profit,i-min_stack[-1])
            return profit
        
        res = 0
        for i in range(len(prices)):
            res = max(res,maxProfitByOne(prices[:i])+maxProfitByOne(prices[i:]))
            
        return res

思路2:维护四个状态min_stack,max_stack,dp1,dp2min_stack[i]表示前i个元素的最小值,max_stack[i]表示后i个元素的最大值,dp1[i]表示对prices[:i]一次买入卖出的最大利润,dp2[i]表示对prices[-i-1:]一次买入卖出的最大利润。更新模式如下:
dp1[i] = max(dp1[i-1],prices[i-1]-min_stack[i-1])
dp2[i] = max(dp2[i-1],max_stack[i-1]-prices[::-1][i-1])

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        
        if len(prices) <= 1:
            return 0
        #可以同时更新
        #min_stack和max_stack计算
        min_stack = []
        max_stack = []
        for i in prices:
            if not min_stack or i < min_stack[-1]:
                min_stack.append(i)
            else:
                min_stack.append(min_stack[-1])
        for i in prices[::-1]:
            if not max_stack or i > max_stack[-1]:
                max_stack.append(i)
            else:
                max_stack.append(max_stack[-1])
        
        #dp1和dp2计算
        dp1 = [0]*(len(prices)+1)
        dp2 = [0]*(len(prices)+1)
        for i in range(2,len(prices)+1):
            dp1[i] = max(dp1[i-1],prices[i-1]-min_stack[i-1])
        
        for j in range(2,len(prices)+1):
            dp2[j] = max(dp2[j-1],max_stack[j-1]-prices[-j])
        
        return max([i+j for i,j in zip(dp1,dp2[::-1])])

思路3:巧妙定义dp变量。对于任意一天考虑四个变量: fstBuy: 在该天第一次买入股票可获得的最大收益 fstSell: 在该天第一次卖出股票可获得的最大收益 secBuy: 在该天第二次买入股票可获得的最大收益 secSell: 在该天第二次卖出股票可获得的最大收益 分别对四个变量进行相应的更新, 最后secSell就是最大 收益值(secSell >= fstSell)

#java version
class Solution {
    public int maxProfit(int[] prices) {
        
        int fstBuy = Integer.MIN_VALUE, fstSell = 0;
        int secBuy = Integer.MIN_VALUE, secSell = 0;
        for(int p : prices) {
            fstBuy = Math.max(fstBuy, -p);
            fstSell = Math.max(fstSell, fstBuy + p);
            secBuy = Math.max(secBuy, fstSell - p);
            secSell = Math.max(secSell, secBuy + p); 
        }
        return secSell;
    }
}

1.7.1.4. 最多k次买入卖出的最大利润(188,困难

题目:给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

思路1:暴力递归

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        
        if len(prices) <= 1:return 0
        
        self.res = 0
        
        def helper(profit,prices,k):
            #print(profit,prices,k)
            #简单情况
            if len(prices) <= 1 or k == 0:
                self.res = max(self.res,profit)
                return
            #找到第一个非递减的
            for j in range(1,len(prices)):
                if prices[j] > prices[j-1]:
                    break
            Min = prices[j-1]
            #可能不取
            helper(profit,prices[j:],k)
            for i in range(j,len(prices)):
                if prices[i] > Min:
                    helper(profit+prices[i]-Min,prices[i+1:],k-1)
                    
        helper(0,prices,k)
        return self.res

思路2:巧妙的dp。dp数组的定义如下。当k大于等于数组长度一半时, 问题退化为贪心问题此时采用尽可能买卖股票最大利润的贪心方法解决可以大幅提升时间性能, 对于其他的k,可以采用两次买进卖出的最大利润的方法来解决, 两次买进卖出的最大利润[思路3]中定义了两次买入和卖出时最大收益的变量, 在这里就是k组这样的变量, t[i][0]t[i][1]分别表示第i笔交易买入和卖出时各自的最大收益。

#java version
class Solution {
    public int maxProfit(int k, int[] prices) {
        
        #简单情况
        if(k < 1) return 0;
        #贪心情况
        if(k >= prices.length/2) return greedy(prices);
        #一般情况
        int[][] t = new int[k][2];
        for(int i = 0; i < k; ++i)
            t[i][0] = Integer.MIN_VALUE;
        for(int p : prices) {
            t[0][0] = Math.max(t[0][0], -p);
            t[0][1] = Math.max(t[0][1], t[0][0] + p);
            for(int i = 1; i < k; ++i) {
                t[i][0] = Math.max(t[i][0], t[i-1][1] - p);
                t[i][1] = Math.max(t[i][1], t[i][0] + p);
            }
        }
        return t[k-1][1];
    }
    #贪心情况
    private int greedy(int[] prices) {
        int max = 0;
        for(int i = 1; i < prices.length; ++i) {
            if(prices[i] > prices[i-1])
                max += prices[i] - prices[i-1];
        }
        return max;
    }
}

1.7.1.5. 最佳买卖股票含冷冻期(309,中等

题目:给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):1.你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。2.卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

思路

1.7.1.6. 买卖股票的最佳时机含手续费(714,中等

题目:给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每次交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

思路:维护两个变量cash和hold,分别表示当前不持股和持股的最大收益。然后dp即可。

class Solution(object):
    def maxProfit(self, prices, fee):
        cash, hold = 0, -prices[0]
        for i in range(1, len(prices)):
            cash = max(cash, hold + prices[i] - fee)
            hold = max(hold, cash - prices[i])
        return cash

1.7.2. 数学

1.7.2.1. 第n个丑数(264,中等

题目:编写一个程序,找出第 n 个丑数。丑数就是只包含质因数 2, 3, 5 的正整数。

思路:维护3个指针id2,id3,id5,每一次向res添加
min(res[id2]*2,res[id3]*3,res[id5]*5)。得出最小数的指针加一。

class Solution(object):
    def nthUglyNumber(self, n):
        """
        :type n: int
        :rtype: int
        """
        res = [1]
        idx2 = 0
        idx3 = 0
        idx5 = 0
        for i in range(n-1):
            res.append(min(res[idx2]*2,res[idx3]*3,res[idx5]*5))
            if res[-1] == res[idx2]*2:
                idx2 += 1
            if res[-1] == res[idx3]*3:
                idx3 += 1
            if res[-1] == res[idx5]*5:
                idx5 += 1
        return res[-1]

1.7.2.2. 完全平方数(279,中等

题目:给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

思路1:建立dp数组。dp[i]表示i的最小平方加和数。则有如下的关系:
dp[i] = min([1+dp[i-j*j] for j in range(1,int(i**0.5)+1)])
但是算法的效率不高,主要在于额外计算了不需要的dp[i]

#花式超时
class Solution(object):       
        
    def numSquares(self, n):
        """
        :type n: int
        :rtype: int
        """        
        dp = {0:0,1:1,2:2,3:3}
        
        for i in range(4,n+1):
            res = i
            sqrti = int(i**0.5)
            #简单情况
            if sqrti*sqrti == i:
                dp[i] = 1
                continue
            for j in range(1,sqrti+1):
                res = min(res,1+dp[i-j*j])
            dp[i] = res
            
        return dp[n]

思路2:todo

#todo

1.7.3. 约瑟夫问题

题目:约瑟夫问题的三种解法:1.数组模拟2.循环单链表模拟3.递归解法。n个人排成环,编号0到n-1,从0开始报数,报到m的人出去,然后下一个人继续报数,求第k个出来的人的编号。

思路1:数组模拟

def josephus_sequence_solution(n,m,k):
    '''
    顺序表模拟
    '''
    index_table = [1]*n

    cur_index = 0 #当前位置
    count1 = 0 #记录当前报数数
    count2 = 0 #记录出队人数
    res = []
    while count2 < k:
        if index_table[cur_index] == 1:
            count1 += 1
            if count1 == m:
                index_table[cur_index] = 0
                res.append(cur_index)
                count1 = 0
                count2 += 1
                if count2 == k:break
        cur_index = (cur_index+1) % n
    #print(res)
    return res

思路2:链表模拟(循环单链表)

def josephus_linkList_solution(n,m,k):
    '''
    循环单链表解法
    '''
    head = CycleSingleLinkList(range(n))
    pre = None
    cur = head.next
    count1 = 0 #记录当前报数数
    count2 = 0 #记录出队人数
    res = []
    while count2 < k:
        if cur:
            count1 += 1
        if count1 == m:
            # 记录
            res.append(cur.val)
            count2 += 1
            count1 = 0
            # 删节点
            if not pre:
                head.next = cur.next # 表长为1怎么办?
            else:
                pre.next = cur.next
            cur = pre.next
        else:
            pre,cur = cur,cur.next
        if count2 == k:
            #print(res)
            return res

递归

def _josephus_recursive_solution(n,m,k):
    '''
    递归解法
    此时的含义是第k次出队的序号
    '''
    if k > n:return
    if k == 1:
        return (n+m-1) % n
    else:
        return (_josephus_recursive_solution(n-1,m,k-1)+m) % n

def josephus_recursive_solution(n,m,k):
    '''
    用矩阵来记录老的计算结果
    '''
    res = []
    for i in range(1,k+1):
        res.append(_josephus_recursive_solution(n,m,i))
    #print(res)
    return res