知道秘密的人数 图解题解
这道题到底在问什么
- 输入
- n=6, delay=2, forget=4
- 输出
- 5
- 输入
- n=4, delay=1, forget=3
- 输出
- 6
最优解:为什么这么做
一句话答案:LeetCode 2327 知道秘密的人数用 DP:dp[d] 记第 d 天新学会的人数,等于窗口 [d-forget+1, d-delay] 内 dp 之和;答案是最近 forget 天之和取模,滑动窗口和维护,时间、空间均 O(n)。
第 1 天只有 1 个人,第 6 天怎么就 5 个了
第 1 天有 1 个人知道秘密;每个学会秘密的人满 delay 天后开始每天分享给 1 个新人,学会满 forget 天彻底忘记。求第 n 天结束时还知道秘密的总人数,对 10^9+7 取模(取模只留除后余数,防装不下)。题面示例 n=6、delay=2、forget=4 时答案是 5。
一人一张档案挨个模拟,为什么撑不到第 n 天
逐个人头模拟——每人一张学会日档案,每天翻一遍看谁开讲、谁忘记。新人过 delay 天又拉新人,档案成倍往上堆,天数一多就翻不完。可档案里有用的只有学会日:学会日相同的人,开讲、忘记日全一样。
同一天学会的人,为什么能捆成一格记
按学会日分桶,动态规划(DP,把每天新学会几人存进表,用时直接查)里定义 dp[d] 为第 d 天新学会秘密的人数,这一格就是本题的状态。第 1 天只有最初那人,dp[1]=1 是种子。
窗口两端 d-forget+1 和 d-delay 从哪来
第 d 天新学会几人,等于当天正在开讲的人数(每人当天恰好教会 1 个新人)。谁在第 d 天开讲?学会日 j 得同时满足:首次分享日 j+delay ≤ d,即 j ≤ d-delay(右端);还没忘 d ≤ j+forget-1,即 j ≥ d-forget+1(左端)。能开讲的人学会日恰好落在窗口 [d-forget+1, d-delay] 里。
转移(由前面的格子推出当前格的规则)就一句:dp[d] 等于窗口内所有 dp[j] 之和。窗口两端各错一格,数字立刻不对。
n=6、delay=2、forget=4,答案 5 怎么冒出来
dp[1]=1。第 2 天窗口 [2-4+1, 2-2]=[-1, 0],全在第 1 天前,dp[2]=0。第 3 天窗口 [0, 1] 只有 dp[1]=1,dp[3]=1。第 4 天窗口 [1, 2],dp[1]+dp[2]=1+0=1,dp[4]=1。第 5 天窗口 [2, 3],dp[2]+dp[3]=0+1=1,dp[5]=1。第 6 天窗口 [3, 4],dp[3]+dp[4]=1+1=2,dp[6]=2。
收答案:第 6 天记得秘密的,学会日落在 [6-4+1, 6]=[3, 6],dp[3]+dp[4]+dp[5]+dp[6]=1+1+1+2=5,正是示例 1 的答案。
第 2 天窗口是 [-1, 0],负下标一取会出什么事
每天把窗口里 forget-delay 个数相加是 O(n·(forget-delay));窗口每天右移一格,改成维护一条滑动窗口和(右移时加新格、减掉滑出的格)可降到 O(n)。空间 O(n),存整条 dp。
负下标要防:前几天窗口会伸到第 1 天之前,像第 2 天的 [-1, 0],求和下界卡在 1、负下标按 0 算。取模也别漏:C++ 和 Java 每步取模、减法先补 mod 再取余,免得减出负数。答案那段用题面示例 2 复核:n=4、delay=1、forget=3 时 dp[2]+dp[3]+dp[4]=1+2+3=6(都按窗口和算:dp[2]=dp[1]=1、dp[3]=dp[1]+dp[2]=2、dp[4]=dp[2]+dp[3]=3)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住两句:新学会 dp[i] = 窗口 [i-forget+1, i-delay] 的新增之和;最终答案 = 最近 forget 天新学会的人数之和。下面每帧都在套它。
- 4开局:第 1 天有 1 个人知道秘密,所以 dp[1]=1(绿色)。下标 0 是占位守卫(灰),恒为 0。从第 2 天起,每天的新增都要靠前面几天推出来。
- 5推 第 2 天 的新增 dp[2]。窗口 [-1, 0] 整个落在第 1 天之前,还没有人到了能分享的日子,所以今天没有新学会的人。
- 6把结果填进去:dp[2]=0(绿色)。这一格从此定死,作为后面更晚那些天的贡献来源。
- 7推 第 3 天 的新增 dp[3]。能在今天教新人的,是学会日落在第 1 到 1 天(黄框)的人:更早的已经忘、更晚的还没到分享日。
- 8把黄框里这几天的新增加起来:dp[1]=1,一共 1 人。也就是说 第 3 天 会新学会 1 个人。
- 9把结果填进去:dp[3]=1(绿色)。这一格从此定死,作为后面更晚那些天的贡献来源。
- 10推 第 4 天 的新增 dp[4]。能在今天教新人的,是学会日落在第 1 到 2 天(黄框)的人:更早的已经忘、更晚的还没到分享日。
- 11把黄框里这几天的新增加起来:dp[1]=1,dp[2]=0,一共 1 人。也就是说 第 4 天 会新学会 1 个人。
- 12把结果填进去:dp[4]=1(绿色)。这一格从此定死,作为后面更晚那些天的贡献来源。
- 13推 第 5 天 的新增 dp[5]。能在今天教新人的,是学会日落在第 2 到 3 天(黄框)的人:更早的已经忘、更晚的还没到分享日。
- 14把黄框里这几天的新增加起来:dp[2]=0,dp[3]=1,一共 1 人。也就是说 第 5 天 会新学会 1 个人。
- 15把结果填进去:dp[5]=1(绿色)。这一格从此定死,作为后面更晚那些天的贡献来源。
- 16推 第 6 天 的新增 dp[6]。能在今天教新人的,是学会日落在第 3 到 4 天(黄框)的人:更早的已经忘、更晚的还没到分享日。
- 17把黄框里这几天的新增加起来:dp[3]=1,dp[4]=1,一共 2 人。也就是说 第 6 天 会新学会 2 个人。
- 18把结果填进去:dp[6]=2(绿色)。这一格从此定死,作为后面更晚那些天的贡献来源。
- 19到这里,1 到 6 天每天新学会多少人都推完了。但注意 dp 是「当天新学会」的人数,不是总人数。第 6 天还知道秘密的,只有最近 forget=4 天里学会、还没忘的人。
- 20第 6 天结束时,只有第 3 到 6 天(黄框)学会的人还没忘、仍知道秘密。更早学会的(第 1、2 天)满 forget 天已经忘了。把黄框里的 dp 加起来即答案。
- 21加上第 3 天新学会的 dp[3]=1 人,running 小计到 1。绿色是已经数进答案的天。
- 22加上第 4 天新学会的 dp[4]=1 人,running 小计到 2。绿色是已经数进答案的天。
- 23加上第 5 天新学会的 dp[5]=1 人,running 小计到 3。绿色是已经数进答案的天。
- 24加上第 6 天新学会的 dp[6]=2 人,running 小计到 5。绿色是已经数进答案的天。
- 25最近 4 天学会的人加满,一共 5 人,这就是第 6 天知道秘密的总人数,答案 5。和题面示例 1 的 5 完全对上。
⚠️ 容易写错的地方
✗ 错:窗口右端写成 i-delay+1
✓ 对:右端是 i-delay
第 delay 天之后才分享,学会日最晚为 i-delay 的人今天正好够格
✗ 错:窗口左端把忘记那天算进来
✓ 对:左端是 i-forget+1
从学会日 j 起记得 forget 天(j..j+forget-1),第 j+forget 天才忘记不分享,所以窗口左端最早只能是 i-forget+1
✗ 错:最后直接返回 dp[n]
✓ 对:返回最近 forget 天 dp 之和
dp[n] 只是第 n 天新学会的人,还知道秘密的是最近 forget 天所有新增的总和
✗ 错:C++/Java 差分做减法后忘了补 mod
✓ 对:减完 +mod 再取模
定长整型下会出现负数,取模结果错
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def peopleAwareOfSecret(self, n: int, delay: int, forget: int) -> int:
m = (n << 1) + 10
d = [0] * m
cnt = [0] * m
cnt[1] = 1
for i in range(1, n + 1):
if cnt[i]:
d[i] += cnt[i]
d[i + forget] -= cnt[i]
nxt = i + delay
while nxt < i + forget:
cnt[nxt] += cnt[i]
nxt += 1
mod = 10**9 + 7
return sum(d[: n + 1]) % modC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
int peopleAwareOfSecret(int n, int delay, int forget) {
const int mod = 1e9 + 7;
int m = (n << 1) + 10;
vector<long long> d(m), cnt(m);
cnt[1] = 1;
for (int i = 1; i <= n; i++) {
if (cnt[i]) {
d[i] = (d[i] + cnt[i]) % mod;
d[i + forget] = (d[i + forget] - cnt[i] + mod) % mod;
int nxt = i + delay;
while (nxt < i + forget) {
cnt[nxt] = (cnt[nxt] + cnt[i]) % mod;
nxt++;
}
}
}
long long ans = 0;
for (int i = 0; i <= n; i++) {
ans += d[i];
}
return ans % mod;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int peopleAwareOfSecret(int n, int delay, int forget) {
final int mod = (int) 1e9 + 7;
int m = (n << 1) + 10;
long[] d = new long[m];
long[] cnt = new long[m];
cnt[1] = 1;
for (int i = 1; i <= n; ++i) {
if (cnt[i] > 0) {
d[i] = (d[i] + cnt[i]) % mod;
d[i + forget] = (d[i + forget] - cnt[i] + mod) % mod;
int nxt = i + delay;
while (nxt < i + forget) {
cnt[nxt] = (cnt[nxt] + cnt[i]) % mod;
++nxt;
}
}
}
long ans = 0;
for (int i = 1; i <= n; ++i) {
ans = (ans + d[i]) % mod;
}
return (int) ans;
}
}复杂度
时间
O(n·(forget−delay))
每天要把窗口内 forget−delay 个数相加;用滑动窗口维护跑动和可降到 O(n)
空间
O(n)
开长度约 2n 的 dp/差分数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 知道秘密的人数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么最后不能直接返回 dp[n],也不能把全部 dp 加起来?+
dp[n] 只是第 n 天当天新学会的人数;把 1 到 n 全加,又把已忘记的人算了进去。学会日为 j 的人从第 j+forget 天起忘记,第 n 天还记得的条件是 j ≥ n-forget+1,所以答案是 dp[n-forget+1] 一路加到 dp[n] 的这段和。示例 1 里第 1 天学会的人第 5 天就忘了,第 6 天收答案时不再计入。
窗口右端为什么是 d-delay,写成 d-delay+1 会怎样?+
学会日为 j 的人首次分享在第 j+delay 天,要在第 d 天开口必须 j+delay ≤ d,解出 j ≤ d-delay。右端放宽到 d-delay+1,等于让还差一天才到分享日的人提前开讲:示例 1 里第 4 天的窗口会从 [1, 2] 错成 [1, 3],多算进 dp[3]=1,dp[4] 变成 2,后面的格子全跟着错。
参考代码里的差分数组在干什么,和窗口求和是一回事吗?+
是同一件事的两个方向。正文是向前看:第 d 天回头把窗口里的新增加起来。参考代码是向后撒播:算出第 i 天的新增 cnt[i] 后,把它加到未来能开讲的那些天(第 i+delay 到 i+forget-1 天)上;再用差分数组(只在区间两端记一加一减,最后做一遍前缀和还原每天的量)记下这批人从第 i 天记到第 i+forget-1 天,前缀和推到第 n 天就是还在场的人数。两种写法每天算出的新增完全一致。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 知道秘密的人数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。