面试题60、61、62、63

面试题60.n个骰子的点数

在这里插入图片描述

动态规划

1、状态定义

分析问题的状态时,不要分析整体,只分析最后一个阶段即可!因为动态规划问题都是划分为多个阶段的,各个阶段的状态表示都是一样,而我们的最终答案在就是在最后一个阶段。

通过题目我们知道一共投掷 n 枚骰子,那最后一个阶段很显然就是:当投掷完 n 枚骰子后,各个点数出现的次数

注意,这里的点数指的是前 n 枚骰子的点数和,而不是第 n 枚骰子的点数,下文同理。

找出了最后一个阶段,那状态表示就简单了。

  • 首先用数组的第一维来表示阶段,也就是投掷完了几枚骰子。

  • 然后用第二维来表示投掷完这些骰子后,可能出现的点数。

  • 数组的值就表示,该阶段各个点数出现的次数。

所以状态表示就是这样的:dp[i][j] ,表示投掷完 i 枚骰子后,点数 j 的出现次数。

2、状态转移方程

找状态转移方程也就是找各个阶段之间的转化关系,同样我们还是只需分析最后一个阶段,分析它的状态是如何得到的。

最后一个阶段也就是投掷完 n 枚骰子后的这个阶段,我们用 dp[n][j] 来表示最后一个阶段点数 j 出现的次数。

单单看第 n 枚骰子,它的点数可能为 1,2,3,…,6,因此投掷完 n 枚骰子后点数 j 出现的次数,可以由投掷完 n−1 枚骰子后,对应点数 j−1,j−2,j−3,…,j−6 出现的次数之和转化过来。

for (第n枚骰子的点数 i = 1; i <= 6; i ++) {
dp[n][j] += dp[n-1][j - i]
}

3、边界处理

这里的边界处理很简单,只要我们把可以直接知道的状态初始化就好了。

我们可以直接知道的状态是啥,就是第一阶段的状态:投掷完 1 枚骰子后,它的可能点数分别为 1,2,3,…,6,并且每个点数出现的次数都是 1 .

class Solution {
    public double[] dicesProbability(int n) {
        if(n < 1) return new double[0];
        //dp[i][j]表示:当有i个骰子时,掷出和为j的几率
        double[][] dp = new double[n + 1][6 * n + 1];
        //初始化一个骰子的情况
        for(int i = 1; i <= 6; i++) dp[1][i] = 1;
        //投掷第i个骰子后的情况
        for(int i = 2; i <= n; i++) {
        	//投掷第i个骰子后,出现的点数
        	for(int j = i; j <= i * 6; j++) {
        		//表示当前要投的骰子的1-6个点数
        		for(int k = 1; k <= 6; k++) {
        			//骰子的点数不可能小于等于0
        			if(j - k <= 0) break;
        			dp[i][j] += dp[i - 1][j - k];
        		}
        	}
        }
        int all = (int) Math.pow(6, n);
        double[] res = new double[6 * n - n + 1];
        for(int i = n; i <= n * 6; i++) {
        	res[i - n] = dp[n][i] / all;
        }
        return res;
    }
}

———————————————————————————————————————

面试题61.扑克牌中的顺子

在这里插入图片描述
根据题意,此 5 张牌是顺子的充分条件如下:

  • 除大小王外,所有牌无重复

  • 设此 5 张牌中最大的牌为 max,最小的牌为 min(大小王除外),则需满足:max−min < 5

在这里插入图片描述

方法一:集合 Set + 遍历

  • 遍历五张牌,遇到大小王(即 0 )直接跳过。

  • 判别重复: 利用 Set 实现遍历判重, Set 的查找方法的时间复杂度为 O(1);

  • 获取最大 / 最小的牌: 借助辅助变量 max 和 min ,遍历统计即可。

class Solution {
    public boolean isStraight(int[] nums) {
		Set<Integer> set = new HashSet<>();
		int max = 0, min = 14;
		for(int num : nums) {
			if(num == 0) continue; //跳过大小王
			if(set.contains(num)) return false; //如果有重复牌,直接返回false
			set.add(num); //将牌加入判重集合中
			max = Math.max(max, num); //最大牌
			min = Math.min(min, num); //最小牌
		}
		return max - min < 5; //最大牌 - 最小牌 < 5 即可构成顺子
    }
}
  • 时间复杂度 O(N)=O(5)=O(1) : 其中 N 为 nums 长度,本题中 N ≡ 5 ;遍历数组使用 O(N) 时间。
  • 空间复杂度 O(N)=O(5)=O(1) : 用于判重的辅助 Set 使用 O(N) 额外空间。

方法二:排序 + 遍历

  • 先对数组执行排序。

  • 判别重复: 排序数组中的相同元素位置相邻,因此可通过遍历数组,判断 nums[i]=nums[i+1] 是否成立来判重。

  • 获取最大 / 最小的牌: 排序后,数组末位元素 nums[4] 为最大牌;元素 nums[joker] 为最小牌,其中 joker 为大小王的数量。

class Solution {
    public boolean isStraight(int[] nums) {
        int joker = 0;
        Arrays.sort(nums); // 数组排序
        for(int i = 0; i < 4; i++) {
            if(nums[i] == 0) joker++; // 统计大小王数量
            else if(nums[i] == nums[i + 1]) return false; // 若有重复,提前返回 false
        }
        return nums[4] - nums[joker] < 5; // 最大牌 - 最小牌 < 5 则可构成顺子
    }
}
  • 时间复杂度 O(Nlog⁡N)=O(5log⁡5)=O(1): 其中 N 为 nums 长度,本题中 N≡5;数组排序使用 O(Nlog⁡N) 时间。
  • 空间复杂度 O(1): 变量 joker 使用 O(1) 大小的额外空间。

———————————————————————————————————————

面试题62.圆圈中最后剩下的数字

在这里插入图片描述
我们定义F(n,m)表示最后剩下那个人的索引号,因此我们只关系最后剩下来这个人的索引号的变化情况即可

在这里插入图片描述

从8个人开始,每次杀掉一个人,去掉被杀的人,然后把杀掉那个人之后的第一个人作为开头重新编号

  • 第一次C被杀掉,人数变成7,D作为开头,(最终活下来的G的编号从6变成3)
  • 第二次F被杀掉,人数变成6,G作为开头,(最终活下来的G的编号从3变成0)
  • 第三次A被杀掉,人数变成5,B作为开头,(最终活下来的G的编号从0变成3)
  • 以此类推,当只剩一个人时,他的编号必定为0!(重点!)

现在我们知道了G的索引号的变化过程,那么我们反推一下从N = 7 到N = 8 的过程,如何才能将N = 7 的排列变回到N = 8 呢?我们先把被杀掉的C补充回来,然后右移m个人,发现溢出了,再把溢出的补充在最前面,经过这个操作就恢复了N = 8 的排列了!
在这里插入图片描述
因此我们可以推出递推公式 f(8,3) = [f(7,3) + 3] % 8
进行推广泛化,即 f(n,m) = [f(n−1,m) + m] % n

class Solution {
    public int lastRemaining(int n, int m) {
		int res = 0;
		for(int i = 2; i <= n; i++) {
			res = (res + m) % i;
		}
		return res;
    }
}
  • 时间复杂度 O(n): 状态转移循环 n−1 次使用 O(n) 时间,状态转移方程计算使用 O(1) 时间;
  • 空间复杂度 O(1): 使用常数大小的额外空间;

——————————————————————————————————————

面试题63.股票的最大利润

在这里插入图片描述

普通法

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length < 2) return 0;
        int minDay = 0; //表示股票价格最低的那一天,从0开始,表示第一天
        int maxMoney = 0; //表示买卖股票的最大差值
        for(int i = 1; i < prices.length; i++) {
        	//如果这一天的股票价格比之前的最低价格还低,就更新最低价格
            if(prices[i] <= prices[minDay]) {
                minDay = i;
            //如果当前股票价格比最低价格高
            } else {
            	//如果今天的价格减去最低价格,比之前保存的最大差值还大,就说明今天卖的话能
            	//赚更多的钱,就更新买卖股票的最大差值
                if(prices[i] - prices[minDay] > maxMoney) {
                    maxMoney = prices[i] - prices[minDay];
                } 
            }
        }
        return maxMoney;
    }
}
  • 时间复杂度 O(N) :N为所有的股票价格,遍历一遍即可
  • 空间复杂度 O(1) :仅需两个常量来保存中间值

动态规划

1、状态定义:设动态规划列表 dp,dp[i] 代表以 prices[i] 为结尾的子数组的最大利润(以下简称为 前 i 日的最大利润 )。

2、状态转移方程:由于题目限定 “买卖该股票一次” ,因此前 i 日最大利润 dp[i] 等于前 i−1 日最大利润 dp[i−1] 和第 i 日卖出的最大利润中的最大值。

  • 前i日最大利润 = max(前 (i−1) 日最大利润,第 i 日价格 − 前 i 日最低价格)
  • dp[i] = max⁡(dp[i−1],prices[i] − min⁡(prices[0:i]))

前 iii 日的最低价格 min⁡(prices[0:i]) 时间复杂度为 O(i) 。而在遍历 prices 时,可以借助一个变量(记为成本 cost )每日更新最低价格。
在这里插入图片描述

class Solution {
    public int maxProfit(int[] prices) {
		if(prices.length < 2) return 0;
		int[] dp = new int[prices.length];
		dp[0] = 0;
		int cost = prices[0]; //价格最低时候的价格
		for(int i = 1; i <prices.length; i++) {
			dp[i] = Math.max(dp[i - 1], prices[i] - cost);
			cost = Math.min(cost, prices[i]);
		}
		return dp[prives.length - 1];
    }
}

优化:

由于 dp[i] 只与 dp[i−1] , prices[i] , cost 相关,因此可使用一个变量(记为利润 profit )代替 dp 列表。优化后的转移方程为:

profit = max(profit,prices[i] − min(cost,prices[i])
class Solution {
    public int maxProfit(int[] prices) {
        int cost = Integer.MAX_VALUE, profit = 0;
        for(int price : prices) {
            cost = Math.min(cost, price);
            profit = Math.max(profit, price - cost);
        }
        return profit;
    }
}
  • 时间复杂度 O(N): 其中 N 为 prices 列表长度,动态规划需遍历 prices 。
  • 空间复杂度 O(1): 变量 cost 和 profit 使用常数大小的额外空间。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值