携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情
题目链接:2327. 知道秘密的人数
题目描述
在第 1 天,有一个人发现了一个秘密。
给你一个整数 delay ,表示每个人会在发现秘密后的 delay 天之后,每天 给一个新的人 分享 秘密。同时给你一个整数 forget ,表示每个人在发现秘密 forget 天之后会 忘记 这个秘密。一个人 不能 在忘记秘密那一天及之后的日子里分享秘密。
给你一个整数 n ,请你返回在第 n 天结束时,知道秘密的人数。由于答案可能会很大,请你将结果对 取余 后返回。
提示:
示例 1:
输入:n = 6, delay = 2, forget = 4
输出:5
解释:
第 1 天:假设第一个人叫 A 。(一个人知道秘密)
第 2 天:A 是唯一一个知道秘密的人。(一个人知道秘密)
第 3 天:A 把秘密分享给 B 。(两个人知道秘密)
第 4 天:A 把秘密分享给一个新的人 C 。(三个人知道秘密)
第 5 天:A 忘记了秘密,B 把秘密分享给一个新的人 D 。(三个人知道秘密)
第 6 天:B 把秘密分享给 E,C 把秘密分享给 F 。(五个人知道秘密)
示例 2:
输入:n = 4, delay = 1, forget = 3
输出:6
解释:
第 1 天:第一个知道秘密的人为 A 。(一个人知道秘密)
第 2 天:A 把秘密分享给 B 。(两个人知道秘密)
第 3 天:A 和 B 把秘密分享给 2 个新的人 C 和 D 。(四个人知道秘密)
第 4 天:A 忘记了秘密,B、C、D 分别分享给 3 个新的人。(六个人知道秘密)
整理题意
题目规定第一天有一个人新知道了一个秘密,然后在 delay 天后每天会将秘密分享给新的一个人知道,新知道秘密的人同样也会在 delay 天后进行分享秘密,同样也是每天分享给一个人。然后每个人会在知道秘密后的 forget 天之后会停止分享秘密。题目问在第 n 天的时候有多少人知道秘密。
题目规定在忘记的当天无法分享秘密,由于答案可能会很大,需要将结果对 取余 后返回。
解题思路分析
观察题目数据范围为 1000 以内,数据范围较小;
- 由于需要求第
n天知道秘密的人数,我们只能从第一天开始递推到第n天,所以该题采用动态规划递推进行解题。 - 需要注意该题在定义
dp[i]的时候有坑点,如果按照以前方法,题目求啥我们就定义dp[i]为啥的话,假如我们令dp[i]表示第i天知道秘密的人数,会发现在写转移方程的时候很难区分能够分享秘密和不能分享秘密的人,从而无法进行转移。 - 这里我们定义
dp[i]为第i天新知道秘密的人数,这里又有两种考虑方式:- 考虑第
i天知道秘密的人在什么时间范围内做出贡献,也就是进行分享秘密,我们可以推出在第i天知道秘密的人会在[i + delay, i + forget - 1]内每天分享秘密给新的一个人知道。那么就可以从前往后递推,对于dp[i]每次更新[i + delay, i + forget - 1]区间内的人数增加dp[i]。 - 考虑第
i天知道秘密的人数是由什么时间内知道秘密的人分享增加的,我们可以推出是由[i - forget + 1, i - delay]分享增加的。那么对于dp[i]的值我们可以遍历求和[i - forget + 1, i - delay]区间内的人数。
由于在忘记的当天无法分享秘密,需要注意区间边界问题,同时注意数组越界问题,保证遍历区间在
[1, n]内 - 考虑第
优化
对于第 2 种考虑方式涉及求区间和问题,我们可以采用前缀和来进行优化时间复杂度(空间换时间),每次遍历求和 [i - forget + 1, i - delay] 区间内的人数时我们就可以直接利用前缀和的思想进行直接计算。从而将复杂度降低。
具体实现
- 初始化边界,令第一天新增的人数为
1; - 从第二天开始递推,由
[i - forget + 1, i - delay]推dp[i](因为这个可以前缀和优化时间复杂度为 ); - 不断递推
dp[i]的同时记录前缀和pre[i]; - 最后返回第
n天知道秘密的人数即为pre[n] - pre[n - forget]。
需要注意边界问题,以及每次更新
dp[i]时的[i - forget + 1, i - delay]区间边界,取模运算时需要注意减法导致负数的情况。
复杂度分析
- 时间复杂度:,
n为天数,利用前缀和思想优化后时间复杂度为 。优化前为 。 - 空间复杂度:,
n为天数,需要记录和存储n天的新增人数和前缀和的空间。
代码实现
class Solution {
public:
int peopleAwareOfSecret(int n, int delay, int forget) {
int mod = 1e9 + 7;
//定义dp[i]为第i天新直到秘密的人
//两种方法,一种是由 dp[i] 推->[i + delay, i + forget - 1]
// 二是由 [i - forget + 1, i - delay] 推-> dp[i](这个可以前缀和优化)
int dp[n + 1], pre[n + 1];
memset(dp, 0, sizeof(dp));
memset(pre, 0, sizeof(pre));
//第一天新增一个人
dp[1] = pre[1] = 1;
for(int i = 2; i <= n; i++){
//注意边界,dp[0] 为 0 表示没有新增直到秘密的人
//int l = max(1, i - forget + 1);优化
int l = max(0, i - forget);
int r = max(0, i - delay);
dp[i] = (pre[r] - pre[l] + mod) % mod;
pre[i] = (pre[i - 1] + dp[i]) % mod;
}
int l = max(0, n - forget);
return (pre[n] - pre[l] + mod) % mod;
//前缀和的时候使用减法又要取模的时候需要注意负数的情况,需要(ans % mod + mod) % mod 处理
}
};
总结
- 该题不能按照通常情况定义
dp[i],核心在于 定义dp[i]为第i天新知道秘密的人数。 - 不同的递推方式也决定了是否可以优化,如果由
dp[i]推[i + delay, i + forget - 1]是无法利用前缀和进行优化的,但是我们转换一下思想,由[i - forget + 1, i - delay]推dp[i]时可以发现每次更新dp[i]时需要求的是[i - forget + 1, i - delay]区间和,而区间和问题通常都能使用前缀和思想进行优化。 - 在取模运算时需要特别注意减法导致的负数情况,需要
(ans % mod + mod) % mod处理。 - 该题需要特别明确每次求和的区间,以及边界问题的处理(容易出错)。
- 测试结果:
利用前缀和进行优化的效果还是非常明显的。
结束语
愿我们既有敢于亮剑的勇气,勇敢闯过每一道难关,又能用一颗平常心,从容面对每一个困难。在人生的道路上胸有成竹、笑容满面。新的一天,加油!