数据结构和算法十一

剑指 Offer 60. n 个骰子的点数

题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
示例 1:
          输入: 1
          输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:
          输入: 2
          输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
限制:
          1 <= n <= 11

方法一:暴力法

/*方法一:暴力法
此方法超时,但为便于理解「方法二」,建议先理解此方法。
给定n个骰子,可得:
1、每个骰子摇到1至6的概率相等,都为1/6。
2、将每个骰子的点数看作独立情况,共有 6^n种「点数组合」。例如n=2时的点数组合为:
(1,1),(1,2),⋯,(2,1),(2,2),⋯,(6,1),⋯,(6,6)

3、n个骰子「点数和」的范围为[n,6n],数量为6n−n+1=5n+1种。
暴力统计:每个[点数组合]都对应一个[点数和],考虑遍历所有点数组合,统计每个点数和的出现次数,最后除以点数组合的总数(即除以6^n),即可得到每个点数和的出现概率。
如下图所示,为输入n=2时,点数组合、点数和、各点数概率的计算过程。
暴力法需要遍历所有点数组合,因此时间复杂度为O(6^n),观察本题输入取值范围1≤n≤11,可知此复杂度是无法接受的。
*/

在这里插入图片描述

/*方法二:动态规划
设输入n个骰子的解(即概率列表)为f(n),其中[点数和]x的概率为f(n,x) 。

假设已知n−1个骰子的解f(n−1),此时添加一枚骰子,求n个骰子的点数和为x的概率f(n,x) 。

当添加骰子的点数为1时,前n−1个骰子的点数和应为x−1,方可组成点数和x;同理,当此骰子为2时,前n−1个骰子应为 x−2;以此类推,直至此骰子点数为6。将这6种情况的概率相加,即可得到概率f(n,x)。递推公式如下所示:
        	6
	f(n,x)= ∑ f(n−1,x−i)× 1/6
       	   i=1
根据以上分析,得知通过子问题的解f(n−1)可递推计算出f(n),而输入一个骰子的解f(1)已知,因此可通过解f(1)依次递推出任意解f(n) 。
如下图所示,为n=2,x=7的递推计算示例。

在这里插入图片描述

/*观察发现,以上递推公式虽然可行,但f(n−1,x−i)中的x−i会有越界问题。例如,若希望递推计算f(2,2),由于一个骰子的点数和范围为[1,6],因此只应求和f(1,1),即f(1,0), f(1,−1) , ... , f(1,−4)皆无意义。此越界问题导致代码编写的难度提升。

如下图所示,以上递推公式是 “逆向” 的,即为了计算f(n,x) ,将所有与之有关的情况求和;而倘若改换为 “正向” 的递推公式,便可解决越界问题。
*/

在这里插入图片描述

/*具体来看,由于新增骰子的点数只可能为1至6,因此概率f(n−1,x)仅与f(n,x+1),f(n,x+2), ... , f(n,x+6) 相关。因而,遍历f(n−1)中各点数和的概率,并将其相加至f(n) 中所有相关项,即可完成f(n−1)至f(n)的递推。

将f(i)记为动态规划列表形式dp[i],则i=1,2,...,n的状态转移过程如下图所示。
*/

在这里插入图片描述

/*复杂度分析:
	时间复杂度 O(n ^ 2):状态转移循环n−1轮;每轮中,当i=2,3,...,n时,对应循环数量分别为6×6,11×6,...,[5(n−1)+1]×6 ;因此总体复杂度为:O((n−1)×6+[5(n−1)+1]/2×6),即等价于 O(n^2)) 。
	空间复杂度O(n):状态转移过程中,辅助数组tmp最大长度为6(n−1)−[(n−1)−1]=5n−4 ,因此使用O(5n−4)=O(n) 大小的额外空间。
*/

代码实现:

通常做法是声明一个二维数组dp,dp[i][j]代表前i个骰子的点数和j的概率,并执行状态转移。而由于dp[i]仅由 dp[i−1]递推得出,为降低空间复杂度,只建立两个一维数组dp, tmp交替前进即可。

class Method {
    public double[] dicesProbability(int n) {
        double[] dp = new double[6];
        Arrays.fill(dp, 1.0 / 6.0);
        for (int i = 2; i <= n; i++) {
            double[] tmp = new double[5 * i + 1];
            for (int j = 0; j < dp.length; j++) {
                for (int k = 0; k < 6; k++) {
                    tmp[j + k] += dp[j] / 6.0;
                }
            }
            dp = tmp;
        }
        return dp;
    }
}

剑指 Offer 63. 股票的最大利润

题目:假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
示例 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。
限制:
0 <= 数组长度 <= 10^5

解决方案:

我们需要找出给定数组中两个数字之间的最大差值(即,最大利润)。此外,第二个数字(卖出价格)必须大于第一个数字(买入价格)。
形式上,对于每组i和j(其中j>i)我们需要找出max(prices[j]−prices[i])。

方法一:暴力法

/*复杂度分析:
    时间复杂度:O(n^2)。循环运行n(n-1)/2次。
    空间复杂度:O(1)。只使用了常数个变量。
*/
public class Method1{
    public int maxProfit(int prices[]) {
        int maxprofit = 0;
        for (int i = 0; i < prices.length - 1; i++) {
            for (int j = i + 1; j < prices.length; j++) {
                int profit = prices[j] - prices[i];
                if (profit > maxprofit) {
                    maxprofit = profit;
                }
            }
        }
        return maxprofit;
    }
}

方法二:一次遍历

/*
算法:
假设给定的数组为:[7, 1, 5, 3, 6, 4]
如果我们在图表上绘制给定数组中的数字,我们将会得到:
*/

在这里插入图片描述

/*我们来假设自己来购买股票。随着时间的推移,每天我们都可以选择出售股票与否。那么,假设在第 i 天,如果我们要在今天卖股票,那么我们能赚多少钱呢?
显然,如果我们真的在买卖股票,我们肯定会想:如果我是在历史最低点买的股票就好了!太好了,在题目中,我们只要用一个变量记录一个历史最低价格 minprice,我们就可以假设自己的股票是在那天买的。那么我们在第 i 天卖出股票能得到的利润就是 prices[i] - minprice。
因此,我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案。

复杂度分析:
    时间复杂度:O(n),只需要遍历一次。
    空间复杂度:O(1),只使用了常数个变量。
*/
public class Method2{
    public int maxProfit(int prices[]) {
        int minprice = Integer.MAX_VALUE;
        int maxprofit = 0;
        for (int i = 0; i < prices.length; i++) {
            if (prices[i] < minprice) {
                minprice = prices[i];
            } else if (prices[i] - minprice > maxprofit) {
                maxprofit = prices[i] - minprice;
            }
        }
        return maxprofit;
    }
}

总结:

我会继续努力学习更多的知识!哪怕是走到穷途末路都要坚持下去!虽然压力很大,但是我仍然会选择去克服它、改变它;相信在这次的考验后,我会更上一层楼!做一个崭新的自己!
愿我们都能在各行各业中能够取得不同的成就!能够用自身的所学知识为国家贡献出自己的一份力量!加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值