知道秘密的人数 图解题解
这道题到底在问什么
- 输入
- n=6, delay=2, forget=4
- 输出
- 5
- 输入
- n=4, delay=1, forget=3
- 输出
- 6
最优解:一步一步想明白
- 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++ 跟着练。
面试官可能追问
分享区间为什么是左闭右开 [i+delay, i+forget)?+
第 delay 天之后才分享,所以从 i+delay 那天开始;从学会日 j 起记得 forget 天(j..j+forget-1),第 j+forget 天才忘记且不分享,所以分享到 i+forget-1 为止,写成区间就是 [i+delay, i+forget),右端开。
参考代码为什么用差分数组统计答案,而不是每天遍历一遍?+
一个人「从第 a 天记到第 b 天」这件事,用差分只需在 d[a] 加、d[b+1] 减两个端点操作;最后做一次前缀和就能 O(n) 求出第 n 天在场人数,避免对每天重新扫一遍带来的额外开销。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 知道秘密的人数 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。