【一看就会一写就废 指间算法】知道秘密的人 —— 动态规划/前缀和/差分数组

64 阅读4分钟

指尖划过的轨迹,藏着最细腻的答案~

题目:

在第 1 天,有一个人发现了一个秘密。

给你一个整数 delay ,表示每个人会在发现秘密后的 delay 天之后,每天 给一个新的人 分享 秘密。同时给你一个整数 forget ,表示每个人在发现秘密 forget 天之后会 忘记 这个秘密。一个人 不能 在忘记秘密那一天及之后的日子里分享秘密。

给你一个整数 n ,请你返回在第 n 天结束时,知道秘密的人数。由于答案可能会很大,请你将结果对 109 + 7 取余 后返回。

示例 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 个新的人。(六个人知道秘密)

提示:

2 <= n <= 1000
1 <= delay < forget <= n

分析一:

对于一个知道秘密的人i来说其会在[i + delay, i + forget - 1]的每一天中分享秘密给另一个新的人,题目要求最终输出知道秘密的人数。

我们可以维护一个数组knowns[i],表示在第i天新增的知道秘密的人数,其可以在j = i + delay, i + delay + 1, ...,i + forget - 1中的每一天分享秘密给一个新的人,即knowns[j]加上knowns[i]

初始化knowns[1] = 1表示第一知道秘密的人。

答案为第n天没有忘记秘密的人,需要满足,i + forget - 1 > n,即i > n - forget + 1,所以答案我knowns中的[max(n - forget + 1, 1), n]的元素和。

class Solution {
    const int MOD = 1'000'000'007;
public:
    int peopleAwareOfSecret(int n, int delay, int forget) {
        vector<int> knowns(n + 1);
        knowns[1] = 1;
        long long ans = 0;

        for (int i = 1; i <= n; i++) {
            if (i >= n - forget + 1) {
                ans += knowns[i];
            }
            for (int j = i + delay; j <= min(i + forget - 1, n); j++) {
                knowns[j] = (knowns[i] + knowns[j]) % MOD;
            }
        }

        return ans % MOD;
    }
};

复杂度分析

  • 时间复杂度:O(n(forget−delay))。
  • 空间复杂度:O(n)。

差分数组优化

差分数组定义与性质:

对于数组a,定义其差分数组d[i]为: d[i]={a[0], i=0a[i]a[i1],i>0d[i] = \begin{cases} a[0],\ i = 0 \\a[i] - a[i-1], i > 0 \end{cases}

其有如下两条性质:

  • 从左到右累加差分数组可还原为数组a
  • 对数组a从i到j累加num等价于将差分数组的d[i] + num,将d[j + 1] - num

使用性质二,我们可以在O(1)的时间对a的子数组进行操作,然后使用性质1还原数组a。

本题优化:

对于本题来说,j = [i + delay, i + forget - 1]都会增加一个knowns[i],这可以使用差分数组优化。

具体的,对于初始的knowns[1] = 1,翻译为差分数组为:diff[1] = 1; diff[2] = -1;

class Solution {
    const int MOD = 1'000'000'007;
public:
    int peopleAwareOfSecret(int n, int delay, int forget) {
        vector<int> diff(n + 1);
        diff[1] = 1;
        diff[2] = -1;
        long long ans = 0;
        int known = 0;

        for (int i = 1; i <= n; i++) {
            known = (known + diff[i]) % MOD;
            if (i >= n - forget + 1) {
                ans += known;
            }

            if (i + delay <= n) { // 差分数组开始增加num
                diff[i + delay] = (diff[i + delay] + known) % MOD;
            }
            if (i + forget <= n) { // 差分数组结束减少num
                diff[i + forget] = (diff[i + forget] - known + MOD) % MOD;
            }
        }

        return ans % MOD;
    }
};

前缀和优化:

对于knowns[j]来说其会被那些knows[i]更新呢? i+delay<=j<=i+forget1i + delay <= j <= i + forget -1 解得: jforget+1<=i<=jdelayj - forget + 1 <= i <= j - delay

那么计算[j - forget + 1, j - delay]的子数组和,即可得到knowns[j]的值,而子数组和可以使用前缀和。

class Solution {
    const int MOD = 1'000'000'007;
public:
    int peopleAwareOfSecret(int n, int delay, int forget) {
        vector<int> sum(n + 1);
        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; // 保证结果非负
    }
};