小白学DP(动态规划)

动态规划

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。

动态规划算法分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次

如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。

概念

动态规划(英语: Dynamic programming,简称 DP) 是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划算法的核心就是记住已经解决过的子问题的解;而记住求解的方式有两种:

自顶向下的备忘录法

比如:斐波拉契数列 Fibonacci。

public static int fibonacci(int n) {
	if (n <= 1)
		return 1;
	if (n == 2)
		return 2;
	return fibonacci(n-1) + fibonacci(n-2);
}

我们分析以前写过的递归就会发现有很多节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。

public class Fibonacci {
	public static void main(String[] args) {
		//创建备忘录
		int[] memo = new int[n+1];
		System.out.println(fibonacci(7));
	}
	/**
	 * 自顶向下备忘录法
	 * @param n
	 * @param memo	备忘录
	 * @return
	 */
	public static int fibonacci(int n, int[] memo) {
		// 如果已经求出了fibonacci(n)的值直接返回
		if(memo[n] != 0) return memo[n];
		// 否则将求出的值保存在 memo 备忘录中。
		if(n<=2)
			memo[n]=1;
		else {
			memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo);
		}
		return memo[n];
	}
}

这个方法是由上至下,比如求f(5),我们要求f(4)和f(3),求出来后放入备忘录,当求f(4)时需要f(3)和f(2),我们可以直接从备忘录取f(3)而不是再去求一遍。

自底向上的动态规划

备忘录法是利用了递归,上面算法不管怎样,计算 fib(6)的时候最后还是要计算出 fib(1), fib(2), fib(3) ……,那么何不先计算出 fib(1), fib(2), fib(3) ……,呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。

public class FibonacciPlus {
	/**
	 * 自底向上的动态规划
	 * @param n
	 * @return
	 */
	public static int fib(int n) {
		if(n<=0)return -1;
		//创建备忘录
		int[] memo = new int[n+1];
		memo[0]=0;
		memo[1]=1;
		for(int i=2;i<=n;i++) {
			memo[i]=memo[i-1]+memo[i-2];
		}
		return memo[n];
	}
	/**
	 * 参与循环的只有 i, i-1 , i-2 三项,可以优化空间
	 * @param n
	 * @return
	 */
	public static int fibPlus(int n) {
		if(n<=0)return -1;
		int memo_i_2=0;
		int memo_i_1=1;
		int memo_i=1;
		for(int i=2;i<=n;i++) {
			memo_i = memo_i_1+memo_i_2;
			memo_i_2 = memo_i_1;
			memo_i_1 = memo_i;
		}
		return memo_i;
	}
}

例题

区域和检索-数组不可变

问题描述

给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。

解法:
暴力循环从下标i到j求和,如果检索次数较多,则会超出时间限制。
- 降低时间复杂度,最理想情况O(1),求前缀和,sumRange(i, j) = (0-j+1)-(0-i-1的和)

代码
Java版

class NumArray {
    int[] sums;

    public NumArray(int[] nums) {
        int n = nums.length;
        sums = new int[n + 1];
        for (int i = 0; i < n; i++) {
            sums[i + 1] = sums[i] + nums[i];
        }
    }
    
    public int sumRange(int i, int j) {
        return sums[j + 1] - sums[i];
    }
}

Python版

class NumArray:

    def __init__(self, nums: List[int]):
        self.sums = [0]
        _sums = self.sums

        for num in nums:
            _sums.append(_sums[-1] + num)

    def sumRange(self, i: int, j: int) -> int:
        _sums = self.sums
        return _sums[j+1] - _sums[i]

复杂度分析

时间复杂度
初始化需要O(n),每次检索、O(1),其中n是nums的长度。
初始化需要检索遍历数组nums的前缀和,时间复杂度O(n)。

空间复杂度:
空间复杂度:O(n)O(n),其中 nn 是数组 \textit{nums}nums 的长度。需要创建一个长度为 n+1n+1 的前缀和数组

使用最小花费爬楼梯

问题描述

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

解法

假设数组 cost 的长度为 n,则 n 个阶梯分别对应下标 0 到 n-1,楼层顶部对应下标 n,问题等价于计算达到下标 nn 的最小花费。可以通过动态规划求解。
创建长度为n+1的数组dp,dp[i]代表达到下标i的最小花费。
由于可以选择下标 00 或 11 作为初始阶梯,因此dp[0]=dp[1]=0。
当 2≤i≤n 时,可以从下标 i-1 使用 cost[i-1]的花费达到下标 i,或者从下标 i−2 使用cost[i-2] 的花费达到下标 i。为了使总花费最小,dp[i] 应取上述两项的最小值,因此状态转移方程如下:dp[i]=min(dp[i−1]+cost[i−1],dp[i−2]+cost[i−2])

Java版

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int[] dp = new int[n+1];
        dp[0] = dp[1] = 0;
        for (int i = 2; i <= n; i++) {
            dp[i] = Math.min(dp[i-1]+cost[i-1], dp[i-2] + cost[i-2]);
        }
        return dp[n];
    }
}

Python版

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        dp = [0, 0]
        n = len(cost)
        for i in range(2, n+1):
            dp.append(min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]))
        return dp[n]

优化版
上述代码的时间复杂度和空间复杂度都是 O(n)。注意到当 i≥2 时,dp[i] 只和 dp[i−1] 与 dp[i−2] 有关,因此可以使用滚动数组的思想,将空间复杂度优化到 O(1)。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int pre = 0, cur = 0, next = 0;
        for (int i = 2; i <= n; i++) {
            next = Math.min(cur+cost[i-1], pre + cost[i-2]);
            pre = cur;
            cur = next;
        }
        return cur;
    }
}

复杂度分析

时间复杂度:O(n),其中 n 是数组 cost 的长度。需要依次计算每个 dp 值,每个值的计算需要常数时间,因此总时间复杂度是 O(n)。

空间复杂度:O(1)。使用滚动数组的思想,只需要使用有限的额外空间。

小白上楼梯(三步问题)

爬楼梯一次可以爬一步,两步,三步,问爬到n阶台阶的方法

Java版 递归

static int f(int n) {
	if (n == 0) return 1;
	if (n == 1) return 1;
	if (n == 2) return 2;
	return f(n-1)+f(n-2)+f(n-3);
}

Python版 递推

class Solution:
    def waysToStep(self, n: int) -> int:
        if n == 1:
            return 1
        if n == 2:
            return 2
        if n == 3:
            return 4
        a = 1; b = 2; c = 4; d = 0
        for i in range(4, n+1):
            d = ((a+b) % 1000000007 + c) % 1000000007
            a = b
            b = c
            c = d
        return d

小白上楼梯2

爬楼梯每次至少爬d阶,爬到n阶台阶的方案数
动态规划,每次向前推d,第i-d的阶数方案会对下一次造成影响。

int sum = 1;
int mod = 1000000007;
System.out.println(mod);
for (int i = d; i <= n; i++) {
    sum += x[i-d];
    x[i] += sum;
    x[i] %= mod;
    sum %= mod;
}
System.out.println(x[n]);

最大字段和

给定序列,找出最大的子段和
累加每个元素,当sum<0,将其初始化为0(前缀和的思想)

每次都有前面最长连续和+上当前与不加当前值的两种选择

long sum = 0, max = 0;
for (int i = 0; i < n; i++) {
     sum += x[i];
     max = Math.max(max, sum);
     if (sum < 0)
         sum = 0;
 }
 System.out.println(max);
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        for i in range(1, len(nums)):
           nums[i] += max(nums[i-1], 0)
        return max(nums)

最长公共子串

在字符串中任意个连续的字符组成的子序列成为该串的子串,给定两个字符串,求出最长的公共子串的长度
思想:从头遍历一个字符串,当字符串不包含则将指针++

String s = sc.nextLine();
String x = sc.nextLine();
 int max = 0, i = 0;
 StringBuilder sb = new StringBuilder();
 for (char c : s.toCharArray()) {
     sb.append(c);
     String ss = sb.substring(i);
     if (x.contains(ss)) {
         max = Math.max(max, ss.length());
     } else {
         i++;
     }
 }
 System.out.println(max);

最长公共子序列

  • 最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。什么是子序列呢? 即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。什么是子串呢 给定串中任意个连续的字符组成的子序列称为该串的子串。

  • 举个例子(S1={1,3,4,5,6,7,7,8}和S2={3,5,7,4,8,6,7,8,2}),

    • 假如S1的最后一个元素与S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS 再加上 S1和S2相等的最后一个元素。
    • 假如S1的最后一个元素与S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于: {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列。
int[] x = {0, 2, 5, 7, 3, 6, 8, 4};
int[] y = {0, 3, 4, 7, 3, 6, 4};
int[][] xx = new int[x.length+1][y.length+1];
for (int i = 1; i < x.length; i++) {
    for (int j = 1; j < y.length; j++) {
        if (x[i] == y[j])
            xx[i][j] = xx[i-1][j-1]+1;
        else
            xx[i][j] = Math.max(xx[i-1][j], xx[i][j-1]);
    }
}
System.out.println(xx[x.length-1][y.length-1]);
  • 输出最长公共子序列
    类似的倒推回去:当x[n] i= y[n]时,比较xx[i-1][j]与x[i][j-1]如果若大于就选i-1与j反之选择i与j-1,当等于时选择不同的方向有不同的结果。
int i = x.length-1, j = y.length-1;
StringBuilder sb = new StringBuilder();
while (xx[i][j] > 0) {
    if (x[i] == y[j]) {
        sb.append(x[i]);
        i--;
        j--;
    } else if (x[i] != y[j]) {
        if (xx[i - 1][j] > xx[i][j - 1]) {
            i--;
        } else {
            j--;
        }
    }
}
System.out.println(sb.reverse().toString());

最长递增子序列

  • DP-动态规划
    状态设计:用一个维护数组dp[i]表示以a[i]结尾的最长递增子序列的长度
    状态转移:之后向前找到一个小于a[i]的进行状态转移dp[i] = Math.max(dp[i], dp[j]+1);
    边界处理:dp[i]=1(0<=j<n)
    时间复杂度:O(n2)
int[] x = {3, 1, 2, 1, 8, 5};
// dp[i]表示以a[i]结尾的最长递增子序列的长度
dp = new int[x.length];
int ans = 0;
for (int i = 0; i < x.length; i++) {
    // 初始化每一个dp[i]=1
    dp[i] = 1;
    for (int j = 0; j < i; j++) {
        if (x[i] > x[j])
            dp[i] = Math.max(dp[i], dp[j]+1);  // 状态转移
    }
    ans = Math.max(ans, dp[i]);  // 比较每一个dp,取最大值
}
System.out.println(ans);
  • 贪心+二分查找
    利用贪心的思想,对于一个上升的子序列,显然当前最后一个元素越小,越有利于添加新的元素,这样LIS长度自然更长。
    因此,我们只需要维护dp数组,其表示就是长度为i+1的LIS结尾元素的最小值,保证每一位都是最小值
    dp数组单调递增,因此对于每一个a[i],先判断是否可以直接插入到dp数组尾部,即比较其与dp数组的最大值,即最后一位;如果不可以则找出dp数组中第一个大于等于a[i]的位置,用a[i]替换之。
int[] dp2 = new int[x.length];
int index = 0;
dp2[0] = x[0];
for (int i = 1; i < x.length; i++) {
    if (x[i] > dp2[index])
        dp2[++index] = x[i];
    else
        dp2[bin(index, x[i])] = x[i];
}
System.out.println(index+1);

// 二分查找
public static int bin(int e, int x) {
int s = 0;
while (s < e) {
    int mid = (s + e) >>> 1;
    if (dp[mid] < x)
        s = mid+1;
    else
        e = mid-1;
}
return s;

按摩师

  • 问题描述
    一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。

  • 解析
    每一次都与前面的选择有关,递推试为:当前不选择,则前面的就是当前的最大值或者选择就是前一个的前一个

  • 代码

class Solution {
    public int massage(int[] nums) {
        if (nums.length == 0)
            return 0;
            
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (i > 1)
                dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i]);
            else
                dp[i] = Math.max(dp[i-1], nums[i]);
        }
        return dp[nums.length-1];
    }
}

只有两个键的键盘

最初在一个记事本上只有一个字符 ‘A’。你每次可以对这个记事本进行两种操作:

Copy All (复制全部) : 你可以复制这个记事本中的所有字符(部分的复制是不允许的)。
Paste (粘贴) :你可以粘贴你上一次复制的字符。 给定一个数字 n 。
你需要使用最少的操作次数,在记事本中打印出恰好 n 个 ‘A’。输出能够打印出 n 个 ‘A’ 的最少操作次数。

算法
假设 g_1, g_2, … 就是 N 的素数分解,则需要的最少操作等于这些素数之和。

复杂度分析

时间复杂度:O( sqrtN ),当 N 是素数的平方时,需要循环 O(sqrtN) 步。

空间复杂度:O(1),ans 和 d 的存储空间。

代码:

class Solution(object):
    def minSteps(self, n):
        ans = 0
        d = 2  # 2 是最小的素数因子,所以从 2 开始
        while n > 1:
            while n % d == 0:  # 如果 d 仍然是当前 n 的最小素数因子,继续遍历
                ans += d
                n /= d
         	# 如果此时 d 不是当前 n 的最小素数因子了,那么 d++ 继续试探
    		# 其实此处应该是把 d 变为比 d 大的下一个素数,但是我们没有必要在构建出一个素数因子的数组
    		# 因为得不偿失(还需要创建一个判断是否为素数的方法),那会花费更多的时间和空间,不如让计算机一个个去试就好了
            d += 1
        return ans

等差数列

函数要返回数组 A 中所有为等差数组的子数组个数。

解法:

  • 暴力
    对于每一个差值都向后遍历,若相等,则++,否则break;
class Solution:
    def numberOfArithmeticSlices(self, nums: List[int]) -> int:
        cnt = 0
        for i in range(len(nums)-2):
            d = nums[i+1]-nums[i]
            for j in range(i+2, len(nums)):
                if (nums[j] - nums[j-1] == d):
                    cnt += 1
                else:
                    break
        return cnt
  • 递归
    自顶向下:从后向前,如果如果第 i 个元素与前一个元素的差值正好等于之前的差值,我们直接就可以算出新增的等差数组的个数 ap,同时可以更新 sum。但是,如果新元素跟前一个元素的差值不等于之前的差值,也就不会增加等差数列的个数。
    代码:
class Solution {
    int sum = 0;
    public int numberOfArithmeticSlices(int[] A) {
        slices(A, A.length-1);
        return sum;
    }
    public int slices(int[] A, int i) {
        if (i < 2)
            return 0;
        int ap = 0;
        if (A[i] - A[i-1] == A[i-1] - A[i-2]) {
            ap = 1 + slices(A, i-1);
            sum += ap;
        } else {
            slices(A, i-1);
        }
        return ap;
    }
}
  • DP
    与递归方法相反,我们从前向后推导,其余思路类似,对于第i个数,判断该元素跟前一个元素的差值是否相同。如果相等则当前新区间中等差数列的个数即为1+dp[i-1]。sum+上这个值更新
    dp[i] 以A[i]结尾的等差数列个数
class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        int[] dp = new int[nums.length];
         int sum = 0;
         for (int i = 2; i < dp.length; i++) {
             if (nums[i]-nums[i-1] == nums[i-1]-nums[i-2]) {
                 dp[i] = dp[i-1] + 1;
                 sum += dp[i];
             }
         }
         return sum;
    }
}
  • 常数空间的动态规划
class Solution:
    def numberOfArithmeticSlices(self, nums: List[int]) -> int:
        cnt = 0
        dp = 0
        for i in range(2, len(nums)):
            if (nums[i] - nums[i-1] == nums[i-1] - nums[i-2]):
                dp = dp+1;
                cnt += dp;
            else:
                dp = 0
        return cnt

复杂度分析

时间复杂度O(n)
只需遍历数组A一次,其大小为n

空间复杂度O(1)
只需常数个额外空间

无矛盾的最佳球队

题目描述

假设你是球队的经理。对于即将到来的锦标赛,你想组合一支总体得分最高的球队。球队的得分是球队中所有球员的分数总和 。

然而,球队中的矛盾会限制球员的发挥,所以必须选出一支 没有矛盾 的球队。如果一名年龄较小球员的分数 严格大于
一名年龄较大的球员,则存在矛盾。同龄球员之间不会发生矛盾。

给你两个列表 scores 和 ages,其中每组 scores[i] 和 ages[i] 表示第 i 名球员的分数和年龄。请你返回 所有可能的无矛盾球队中得分最高那支的分数 。

分析:
根据题目描述,组建球队首先队员之间不能有矛盾,即:年龄小的得分不能大于年龄高的得分

因此需要找得分最高的组合,即寻找最大上升子序列。

我们可以按照年龄升序,年龄相同则分数升序,即可 转化为最大上升子序列

即当分数大于/等于当前队伍最后一个队员的分数时就加入队伍,并更新max,不需考虑年龄。

解法

在Python中

  • 内置函数zip很方便将两个数组对应位置元素压缩为一个元组,这些元组组成列表
  • 内置函数sort默认升序排序,我们可以自定义排序关键字key使用匿名表达式lambda
    arr.sort(key = lambda x: (x[0], x[1]))
    如该例子就是以x[0]排序,备用是x[1]排序
  • 最后返回dp中最大值,max(dp)
class Solution:
    def bestTeamScore(self, scores: List[int], ages: List[int]) -> int:
        n = len(scores)
        arr = list(zip(ages, scores))
        arr.sort(key = lambda x: (x[0], x[1]))

        dp = [arr[i][1] for i in range(n)]
        for i in range(n):
            for j in range(i):
                if (arr[i][1] >= arr[j][1]):
                    dp[i] = max(dp[i], dp[j]+arr[i][1])
        return max(dp)

Java版
对于如何将两个数组合为一起,我起初想创建一个class,发现麻烦了,大多时候创建二维数组还是挺多的,一行对应一个对象。两列分别代表属性,比如:年龄,分数

class Solution {
    public int bestTeamScore(int[] scores, int[] ages) {
        if (scores.length == 1)
            return scores[0];
        int[][] arr = new int[scores.length][2];
        // 将两个内容合并为一行,一行代表一个队员
        for (int i = 0; i < scores.length; i++) {
            arr[i][0] = ages[i];
            arr[i][1] = scores[i];
        }
        Arrays.sort(arr, new Comparator<int[]>() {
            public int compare(int[] o1, int[] o2) {
                if (o1[0] == o2[0])
                    return o1[1] - o2[1];  // 分数升序
                return o1[0] - o2[0];
            }
        });
        int[] dp = new int[scores.length];
        // 初始化dp[i]代表第i个球员为队伍末尾时,队伍最大分数
        for (int i = 0; i < scores.length; i++) {
            dp[i] = arr[i][1];
        }
        int max = 0;
        for (int i = 0; i < scores.length; i++) {
            for (int j = 0; j < i; j++) {
                if (arr[i][1] >= arr[j][1]) {
                    dp[i] = Math.max(dp[i], dp[j] + arr[i][1]);
                }
            }
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

最佳买卖股票时机(冷冻期)

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​ 设计一个算法计算出最大利润。在满足以下约束条件下你可以尽可能地完成更多的交易(多次买卖一支股票):

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

方法一:动态规划

思路与算法

我们用 f[i] 表示第 i 天结束之后的「累计最大收益」。根据题目描述,由于我们最多只能同时买入(持有)一支股票,并且卖出股票后有冷冻期的限制,因此我们会有三种不同的状态:

  • 我们目前持有一支股票,对应的「累计最大收益」记为 f[i][0];

  • 我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 f[i][1];

  • 我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 f[i][2]。

这里的「处于冷冻期」指的是在第 ii 天结束之后的状态。也就是说:如果第 ii 天结束之后处于冷冻期,那么第 i+1i+1 天无法买入股票。

推导式:

  • f[i][0]:代表第i天持有股票的最大收益,那么该股票可能来自第i-1天的股票未卖,或者是第i天买的,那么第i-1天必须是f[i-1][2],未持有股票且不在冷冻期+上当天买入的股票负收益。
  • f[i][1]:代表第i天处于冷冻期,则一定是第i-1天卖了股票
  • f[i][2]:代表不持股票,不出与冷冻期,则可能是i-1天处于冷冻期,或者是i-1天不处于冷冻期。

结果是: Math.max(f[n-1][1], f[n-1][2]);
f[n-1][0]:持有股票是无意义的。

代码

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length == 0)
            return 0;
        int n = prices.length;
        // f[i][0]:手上持有股票的最大收益
        // f[i][1]:手上不持有股票,且处于冷冻期的累计最大收益
        // f[i][2]:手上不持有股票,且不在冷冻期的累计最大收益
        int[][] f = new int[n][3];
        f[0][0] = -prices[0];  // 即第一天买入
        for (int i = 1; i < n; i++) {
            f[i][0] = Math.max(f[i-1][0], f[i-1][2]-prices[i]);
            f[i][1] = f[i-1][0] + prices[i];
            f[i][2] = Math.max(f[i-1][1], f[i-1][2]);
        }
        return Math.max(f[n-1][1], f[n-1][2]);
    }
}

空间优化:
我们注意到当天至于前一天有关,因此使用三个变量将前天数据存储就行,通过他们计算当天的。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        
        n = len(prices)
        f0, f1, f2 = -prices[0], 0 ,0

        for i in range(1, n):
            new0 = max(f0, f2-prices[i])
            new1 = f0 + prices[i]
            new2 = max(f1, f2)
            f0, f1, f2 = new0, new1, new2

        return max(f1, f2)

参考Leetcode题解
感谢!

最佳买卖股票时机(手续费)

跟前面的类似,当天有股票与没有股票,只用加上手续费即可

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

        for i in range(1, n):
            new0 = max(f0, f1 + prices[i] - fee)  # 没有股票
            new1 = max(f1, f0 - prices[i])  # 有股票
            
            f0, f1 = new0, new1

        return 

香槟塔

我们把玻璃杯摆成金字塔的形状,其中第一层有1个玻璃杯,第二层有2个,依次类推到第100层,每个玻璃杯(250ml)将盛有香槟。
从顶层的第一个玻璃杯开始倾倒一些香槟,当顶层的杯子满了,任何溢出的香槟都会立刻等流量的流向左右两侧的玻璃杯。当左右两边的杯子也满了,就会等流量的流向它们左右两边的杯子,依次类推。(当最底层的玻璃杯满了,香槟会流到地板上)

思路
我们可以直接模拟整个过程。我们记录流入每个杯子的香槟的数量之和,例如对于一个杯子,流入的香槟数量为 10。由于这个数值大于 1,因此会有 9 数量的香槟流出这个杯子,并且会有 4.5 数量的香槟分别流入这个杯子下面的两个杯子中。以此类推。

总的来说,如果流入一个杯子的香槟数量为 X,且 X 大于 1,那么就会有 Q = (X - 1.0) / 2 数量的香槟流入下面的两个杯子中。我们可以从第一层的杯子开始,计算出第二层每个杯子中流入的香槟数量,再计算出第三层的数量,直到计算到第 query_row 层。

代码

class Solution {
    public double champagneTower(int poured, int query_row, int query_glass) {
        double[][] A = new double[102][102];
        A[0][0] = (double)poured;
        for (int r = 0; r <= query_row; r++) {
            for (int c = 0; c <= r; c++) {
                double q = (A[r][c] - 1.0) / 2.0;
                if (q > 0) {
                    A[r+1][c] += q;
                    A[r+1][c+1] += q;
                }
            }
        }
        return Math.min(1, A[query_row][query_glass]);
    }
}
  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chaser&upper

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值