两种 O(n) 做法:差分数组/前缀和(Python/Java/C++/C/Go/JS/Rust)
理解题意
恰好在第 $i$ 天得知秘密的人,会在 $[i+\textit{delay}, i+\textit{forget}-1]$ 中的每一天分享秘密,每天给一个新的人分享秘密。
新的得知秘密的人,会按照同样的规则,继续分享秘密。
在第 $i$ 天得知秘密的人,会在第 $i+\textit{forget}$ 天忘记秘密。
我们计算的是第 $n$ 天结束时,还没有忘记秘密的人数。
方法一:差分数组
在第 $1$ 天,有 $1$ 个人得知了秘密。对于闭区间 $[1+\textit{delay}, \textit{forget}]$ 中的每个整数 $j$,都会新增 $1$ 个「恰好在第 $j$ 天得知秘密的人」。
考虑维护一个数组 $\textit{known}$,其中 $\textit{known}[i]$ 表示恰好在第 $i$ 天得知秘密的人数。为什么是恰好?如果不这样定义,第 $i$ 天各种人(刚知道秘密,昨天知道秘密,前天知道秘密,……)混在一起,不好处理。
根据题意,恰好在第 $i$ 天得知秘密的人,会在第 $j = i+\textit{delay},i+\textit{delay}+1,\dots,i+\textit{forget}-1$ 天分享秘密,也就是把 $\textit{known}[j]$ 增加 $\textit{known}[i]$。
初始值 $\textit{known}[1] = 1$。
答案为第 $n$ 天没有忘记秘密的人数。这要求 $i+\textit{forget}-1 \ge n$,解得 $i \ge n-\textit{forget}+1$。所以答案为 $\textit{known}$ 中的 $[\max(n-\textit{forget}+1,1),n]$ 的元素和。
注意取模。为什么可以在中途取模?见 模运算的世界:当加减乘除遇上取模。
优化前(不用差分数组)
class Solution:
def peopleAwareOfSecret(self, n: int, delay: int, forget: int) -> int:
MOD = 1_000_000_007
# known[i] 表示恰好在第 i 天得知秘密的人数
known = [0] * (n + 1)
known[1] = 1
for i in range(1, n + 1):
known[i] %= MOD
# 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
for j in range(i + delay, min(i + forget, n + 1)):
known[j] += known[i]
# 统计在第 n 天没有忘记秘密的人数
# 这要求 i+forget-1 >= n,解得 i >= n-forget+1
return sum(known[-forget:]) % MOD
class Solution {
public int peopleAwareOfSecret(int n, int delay, int forget) {
final int MOD = 1_000_000_007;
// known[i] 表示恰好在第 i 天得知秘密的人数
int[] known = new int[n + 1];
known[1] = 1;
long ans = 0;
for (int i = 1; i <= n; i++) {
// 统计在第 n 天没有忘记秘密的人数
// 这要求 i+forget-1 >= n,解得 i >= n-forget+1
if (i >= n - forget + 1) {
ans += known[i];
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
for (int j = i + delay; j <= Math.min(i + forget - 1, n); j++) {
known[j] = (known[j] + known[i]) % MOD; // known[j] += known[i]
}
}
return (int) (ans % MOD);
}
}
class Solution {
public:
int peopleAwareOfSecret(int n, int delay, int forget) {
const int MOD = 1'000'000'007;
// known[i] 表示恰好在第 i 天得知秘密的人数
vector<int> known(n + 1);
known[1] = 1;
long long ans = 0;
for (int i = 1; i <= n; i++) {
// 统计在第 n 天没有忘记秘密的人数
// 这要求 i+forget-1 >= n,解得 i >= n-forget+1
if (i >= n - forget + 1) {
ans += known[i];
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
for (int j = i + delay; j <= min(i + forget - 1, n); j++) {
known[j] = (known[j] + known[i]) % MOD; // known[j] += known[i]
}
}
return ans % MOD;
}
};
#define MOD 1000000007
#define MIN(a, b) ((b) < (a) ? (b) : (a))
int peopleAwareOfSecret(int n, int delay, int forget){
// known[i] 表示恰好在第 i 天得知秘密的人数
int* known = calloc(n + 1, sizeof(int));
known[1] = 1;
long long ans = 0;
for (int i = 1; i <= n; i++) {
// 统计在第 n 天没有忘记秘密的人数
// 这要求 i+forget-1 >= n,解得 i >= n-forget+1
if (i >= n - forget + 1) {
ans += known[i];
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
for (int j = i + delay; j <= MIN(i + forget - 1, n); j++) {
known[j] = (known[j] + known[i]) % MOD; // known[j] += known[i]
}
}
free(known);
return (int) (ans % MOD);
}
func peopleAwareOfSecret(n, delay, forget int) (ans int) {
const mod = 1_000_000_007
// known[i] 表示恰好在第 i 天得知秘密的人数
known := make([]int, n+1)
known[1] = 1
for i := 1; i <= n; i++ {
known[i] %= mod
// 统计在第 n 天没有忘记秘密的人数
// 这要求 i+forget-1 >= n,解得 i >= n-forget+1
if i >= n-forget+1 {
ans += known[i]
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
for j := i + delay; j <= min(i+forget-1, n); j++ {
known[j] += known[i]
}
}
return ans % mod
}
var peopleAwareOfSecret = function(n, delay, forget) {
const MOD = 1_000_000_007;
// known[i] 表示恰好在第 i 天得知秘密的人数
const known = Array(n + 1).fill(0);
known[1] = 1;
let ans = 0;
for (let i = 1; i <= n; i++) {
known[i] %= MOD;
// 统计在第 n 天没有忘记秘密的人数
// 这要求 i+forget-1 >= n,解得 i >= n-forget+1
if (i >= n - forget + 1) {
ans += known[i];
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
for (let j = i + delay; j <= Math.min(i + forget - 1, n); j++) {
known[j] += known[i];
}
}
return ans % MOD;
};
impl Solution {
pub fn people_aware_of_secret(n: i32, delay: i32, forget: i32) -> i32 {
const MOD: i32 = 1_000_000_007;
let n = n as usize;
let delay = delay as usize;
let forget = forget as usize;
// known[i] 表示恰好在第 i 天得知秘密的人数
let mut known = vec![0; n + 1];
known[1] = 1;
let mut ans = 0;
for i in 1..=n {
// 统计在第 n 天没有忘记秘密的人数
// 这要求 i+forget-1 >= n,解得 i >= n-forget+1
if i >= n - forget + 1 {
ans = (ans + known[i]) % MOD;
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
for j in i + delay..=n.min(i + forget - 1) {
known[j] = (known[j] + known[i]) % MOD; // known[j] += known[i]
}
}
ans
}
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n(\textit{forget} - \textit{delay}))$。
- 空间复杂度:$\mathcal{O}(n)$。
优化
上面代码有一个「把子数组的每个元素都增加 $\textit{known}[i]$」的逻辑,这可以用差分数组优化,请看 原理讲解。
$\textit{diff}$ 数组是 $\textit{known}$ 数组的差分数组。
对于初始值 $\textit{known}[1] = 1$,对应到差分数组上,就是 $\textit{diff}[1] = 1$ 以及 $\textit{diff}[2] = -1$。
class Solution:
def peopleAwareOfSecret(self, n: int, delay: int, forget: int) -> int:
MOD = 1_000_000_007
diff = [0] * (n + 2)
diff[1] = 1
diff[2] = -1
ans = known = 0
for i in range(1, n + 1):
# 加上 diff[i] 后,known 表示恰好在第 i 天得知秘密的人数
known = (known + diff[i]) % MOD
# 统计在第 n 天没有忘记秘密的人数
if i >= n - forget + 1:
ans += known
# 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
diff[min(i + delay, n + 1)] += known
diff[min(i + forget, n + 1)] -= known
return ans % MOD
class Solution {
public int peopleAwareOfSecret(int n, int delay, int forget) {
final int MOD = 1_000_000_007;
int[] diff = new int[n + 1];
diff[1] = 1;
diff[2] = -1;
int known = 0;
long ans = 0;
for (int i = 1; i <= n; i++) {
// 加上 diff[i] 后,known 表示恰好在第 i 天得知秘密的人数
known = (known + diff[i]) % MOD;
// 统计在第 n 天没有忘记秘密的人数
if (i >= n - forget + 1) {
ans += known;
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
if (i + delay <= n) {
diff[i + delay] = (diff[i + delay] + known) % MOD;
}
if (i + forget <= n) {
diff[i + forget] = (diff[i + forget] - known + MOD) % MOD; // +MOD 保证结果非负
}
}
return (int) (ans % MOD);
}
}
class Solution {
public:
int peopleAwareOfSecret(int n, int delay, int forget) {
const int MOD = 1'000'000'007;
vector<int> diff(n + 1);
diff[1] = 1;
diff[2] = -1;
int known = 0;
long long ans = 0;
for (int i = 1; i <= n; i++) {
// 加上 diff[i] 后,known 表示恰好在第 i 天得知秘密的人数
known = (known + diff[i]) % MOD;
// 统计在第 n 天没有忘记秘密的人数
if (i >= n - forget + 1) {
ans += known;
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
if (i + delay <= n) {
diff[i + delay] = (diff[i + delay] + known) % MOD;
}
if (i + forget <= n) {
diff[i + forget] = (diff[i + forget] - known + MOD) % MOD; // +MOD 保证结果非负
}
}
return ans % MOD;
}
};
#define MOD 1000000007
int peopleAwareOfSecret(int n, int delay, int forget) {
int* diff = calloc(n + 1, sizeof(int));
diff[1] = 1;
diff[2] = -1;
int known = 0;
long long ans = 0;
for (int i = 1; i <= n; i++) {
// 加上 diff[i] 后,known 表示恰好在第 i 天得知秘密的人数
known = (known + diff[i]) % MOD;
// 统计在第 n 天没有忘记秘密的人数
if (i >= n - forget + 1) {
ans = (ans + known) % MOD;
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
if (i + delay <= n) {
diff[i + delay] = (diff[i + delay] + known) % MOD;
}
if (i + forget <= n) {
diff[i + forget] = (diff[i + forget] - known + MOD) % MOD; // +MOD 保证结果非负
}
}
free(diff);
return ans % MOD;
}
func peopleAwareOfSecret(n, delay, forget int) (ans int) {
const mod = 1_000_000_007
diff := make([]int, n+2)
diff[1] = 1
diff[2] = -1
known := 0
for i := 1; i <= n; i++ {
// 加上 diff[i] 后,known 表示恰好在第 i 天得知秘密的人数
known = (known + diff[i]) % mod
// 统计在第 n 天没有忘记秘密的人数
if i >= n-forget+1 {
ans += known
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
diff[min(i+delay, n+1)] += known
diff[min(i+forget, n+1)] -= known // 注意这里有减法,这会导致上面累加 diff[i] 时,known 可能是负数
}
return (ans%mod + mod) % mod // 保证答案非负
}
var peopleAwareOfSecret = function(n, delay, forget) {
const MOD = 1_000_000_007;
const diff = Array(n + 2).fill(0);
diff[1] = 1;
diff[2] = -1;
let known = 0;
let ans = 0;
for (let i = 1; i <= n; i++) {
// 加上 diff[i] 后,known 表示恰好在第 i 天得知秘密的人数
known = (known + diff[i]) % MOD;
// 统计在第 n 天没有忘记秘密的人数
if (i >= n - forget + 1) {
ans += known;
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
diff[Math.min(i + delay, n + 1)] += known;
diff[Math.min(i + forget, n + 1)] -= known; // 注意这里有减法,这会导致上面累加 diff[i] 时,known 可能是负数
}
return (ans % MOD + MOD) % MOD; // 保证答案非负
};
impl Solution {
pub fn people_aware_of_secret(n: i32, delay: i32, forget: i32) -> i32 {
const MOD: i32 = 1_000_000_007;
let n = n as usize;
let delay = delay as usize;
let forget = forget as usize;
let mut diff = vec![0; n + 1];
diff[1] = 1;
diff[2] = -1;
let mut known = 0;
let mut ans = 0;
for i in 1..=n {
// 加上 diff[i] 后,known 表示恰好在第 i 天得知秘密的人数
known = (known + diff[i]) % MOD;
// 统计在第 n 天没有忘记秘密的人数
if i >= n - forget + 1 {
ans = (ans + known) % MOD;
}
// 恰好在第 i 天得知秘密的人,会在第 [i+delay, i+forget-1] 天分享秘密
if i + delay <= n {
diff[i + delay] = (diff[i + delay] + known) % MOD;
}
if i + forget <= n {
diff[i + forget] = (diff[i + forget] - known + MOD) % MOD; // +MOD 保证结果非负
}
}
ans
}
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n)$。
- 空间复杂度:$\mathcal{O}(n)$。
方法二:前缀和
横看成岭侧成峰,对于 $\textit{known}[j]$ 来说,它会被哪些 $\textit{known}[i]$ 更新?
根据 $i$ 和 $j$ 的关系
$$
i+\textit{delay}\le j\le i+\textit{forget}-1
$$
解得
$$
j-\textit{forget} +1 \le i\le j - \textit{delay}
$$
那么计算 $[j-\textit{forget} + 1,j - \textit{delay}]$ 的子数组和,就能直接得到 $\textit{known}[j]$。
子数组和可以用前缀和优化,请看 原理讲解。
class Solution:
def peopleAwareOfSecret(self, n: int, delay: int, forget: int) -> int:
MOD = 1_000_000_007
s = [0] * (n + 1) # known 数组的前缀和
s[1] = 1
for j in range(2, n + 1):
known = s[max(j - delay, 0)] - s[max(j - forget, 0)]
s[j] = (s[j - 1] + known) % MOD
return (s[n] - s[max(n - forget, 0)]) % MOD
class Solution {
public int peopleAwareOfSecret(int n, int delay, int forget) {
final int MOD = 1_000_000_007;
int[] sum = new int[n + 1]; // known 数组的前缀和
sum[1] = 1;
for (int j = 2; j <= n; j++) {
int known = (sum[Math.max(j - delay, 0)] - sum[Math.max(j - forget, 0)]) % MOD;
sum[j] = (sum[j - 1] + known) % MOD;
}
int ans = sum[n] - sum[Math.max(n - forget, 0)];
return (ans % MOD + MOD) % MOD; // 保证答案非负
}
}
class Solution {
public:
int peopleAwareOfSecret(int n, int delay, int forget) {
const int MOD = 1'000'000'007;
vector<int> sum(n + 1); // known 数组的前缀和
sum[1] = 1;
for (int j = 2; j <= n; j++) {
int known = (sum[max(j - delay, 0)] - sum[max(j - forget, 0)]) % MOD;
sum[j] = (sum[j - 1] + known) % MOD;
}
int ans = sum[n] - sum[max(n - forget, 0)];
return (ans % MOD + MOD) % MOD; // 保证答案非负
}
};
#define MOD 1000000007
#define MAX(a, b) ((b) > (a) ? (b) : (a))
int peopleAwareOfSecret(int n, int delay, int forget){
int* sum = malloc((n + 1) * sizeof(int)); // known 数组的前缀和
sum[0] = 0;
sum[1] = 1;
for (int j = 2; j <= n; j++){
int known = (sum[MAX(j - delay, 0)] - sum[MAX(j - forget, 0)]) % MOD;
sum[j] = (sum[j - 1] + known) % MOD;
}
int ans = sum[n] - sum[MAX(n - forget, 0)];
free(sum);
return (ans % MOD + MOD) % MOD; // 保证答案非负
}
func peopleAwareOfSecret(n, delay, forget int) int {
const mod = 1_000_000_007
sum := make([]int, n+1) // known 数组的前缀和
sum[1] = 1
for j := 2; j <= n; j++ {
known := sum[max(j-delay, 0)] - sum[max(j-forget, 0)]
sum[j] = (sum[j-1] + known) % mod
}
ans := sum[n] - sum[max(n-forget, 0)]
return (ans%mod + mod) % mod // 保证答案非负
}
var peopleAwareOfSecret = function(n, delay, forget) {
const MOD = 1_000_000_007;
const sum = Array(n + 1).fill(0); // known 数组的前缀和
sum[1] = 1;
for (let j = 2; j <= n; j++) {
const known = sum[Math.max(j - delay, 0)] - sum[Math.max(j - forget, 0)];
sum[j] = (sum[j - 1] + known) % MOD;
}
const ans = sum[n] - sum[Math.max(n - forget, 0)];
return (ans % MOD + MOD) % MOD; // 保证答案非负
};
impl Solution {
pub fn people_aware_of_secret(n: i32, delay: i32, forget: i32) -> i32 {
const MOD: i32 = 1_000_000_007;
let n = n as usize;
let delay = delay as usize;
let forget = forget as usize;
let mut sum = vec![0; n + 1]; // known 数组的前缀和
sum[1] = 1;
for j in 2..=n {
let known = (sum[j.saturating_sub(delay)] - sum[j.saturating_sub(forget)]) % MOD;
sum[j] = (sum[j - 1] + known) % MOD;
}
let ans = sum[n] - sum[n.saturating_sub(forget)];
(ans % MOD + MOD) % MOD // 保证答案非负
}
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n)$。
- 空间复杂度:$\mathcal{O}(n)$。
专题训练
- 数据结构题单的「一、前缀和」和「§2.1 一维差分」。
- 动态规划题单的「§11.1 前缀和优化 DP」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府