P11765 「KFCOI Round #1」回首 解题报告


P11765 「KFCOI Round #1」回首 解题报告

题意简述

我们有一张包含 \(n\) 个节点和 \(m\) 条边的有向图。每个节点代表一条“回忆”,每条边 \(u \to v\) 代表回忆 \(u\) 恰好发生在回忆 \(v\) 之前。

所有节点(回忆)的初始“重要度”都是 0。我们需要进行 \(T\) 次“回想”操作。

在第 \(t\) 次操作(\(t\) 从 1 开始)中,会发生两件事:

  1. 相乘:每个节点 \(i\) 的当前重要度 \(A_i\) 变为 \(A_i \times k_i\)
  2. 相加
    • 对于每个节点 \(i\),找到所有指向它的节点(即它的“前置回忆”)。将这些前置回忆在本次操作相乘前的重要度全部加到节点 \(i\) 的新重要度上。
    • 如果一个节点 \(i\) 没有任何前置回忆(即没有边指向它,入度为0),那么它的新重要度就加上当前的操作次数 \(t\)

任务是计算经过 \(T\) 次操作后,每个节点的最终重要度,并对 \(998244353\) 取模。

思路分析

Part 1: 直接模拟 (适用于 \(T\) 较小的情况,约 40分)

\(T\) 的值很小(例如 \(T \le 10^5\))时,我们可以完全按照题目描述的步骤来模拟。

我们用一个数组 A[1...n] 来存储每个节点的重要度。然后我们进行一个大循环,从 t = 1t = T

在每次循环 t 中,我们需要注意一个关键细节:相加步骤使用的是相乘前的重要度。这意味着我们不能直接在原数组 A 上修改。一个简单的处理方法是,在循环开始时,我们先用一个临时数组 A_old 存下当前 A 数组的所有值。

第 t 次操作的具体流程:

  1. 创建一个临时数组 A_oldA_old[i] = A[i] for i = 1 to n
  2. 相乘A[i] = A_old[i] * k[i] for i = 1 to n
  3. 相加
    • 对于每一个节点 \(i\) (从 1 到 \(n\)):
      • 如果节点 \(i\) 有前置回忆(入度 > 0),假设其前置回忆集合为 Pred(i)。那么 A[i] += sum(A_old[p]) for all p in Pred(i)
      • 如果节点 \(i\) 没有前置回忆(入度 = 0),那么 A[i] += t
  4. 记得所有计算都要对 \(998244353\) 取模。

这个模拟的时间复杂度大约是 \(O(T \times (n+m))\)。当 \(T\) 达到 \(10^{18}\) 时,这种方法显然会超时。

Part 2: 矩阵快速幂 (适用于 \(T\) 巨大的情况,100分)

看到 \(T\) 的范围是 \(10^{18}\),这是一个非常经典的信号,提示我们应该使用矩阵快速幂

矩阵快速幂的核心思想是:如果一个状态的转移过程可以用一个固定的线性变换(即矩阵乘法)来表示,那么 \(T\) 次变换就等价于乘以这个变换矩阵的 \(T\) 次方。而矩阵的 \(T\) 次方可以通过类似整数快速幂的方法在 \(O(\log T)\) 时间内求出。

1. 构造状态向量

首先,我们需要确定在每次操作之间,需要哪些信息才能推导出下一次操作的结果。

  • 显然,我们需要 \(n\) 个节点的重要度 \(A_1, A_2, \dots, A_n\)
  • 我们还发现,更新规则中出现了变量 tt 在每次操作后会变成 t+1。这个变化的量也需要被包含进我们的状态里。
  • 为了处理常数项的加法(比如 +t),我们通常会在状态向量中增加一个常数 1

综上,我们可以定义一个 \((n+2)\) 维的状态向量,它在第 \(t-1\) 次操作结束后的状态是:

\[\text{State}_{t-1} = [A_1, A_2, \dots, A_n, t-1, 1] \]

这是一个 \(1 \times (n+2)\) 的行向量。我们的目标是找到一个 \((n+2) \times (n+2)\)转移矩阵 M,使得:

\[\text{State}_{t} = \text{State}_{t-1} \times M \]

2. 构造转移矩阵 M

现在我们来推导这个神奇的转移矩阵 MM 的第 j 列决定了新状态向量的第 j 个元素是如何由旧状态向量计算出来的。

  • 对于新的重要度 \(A_j\) (矩阵的前 n 列, \(j=1, \dots, n\))

    • Case A: 节点 \(j\) 有前置回忆。
      • \(A_j(t) = A_j(t-1) \times k_j + \sum_{p \to j} A_p(t-1)\)
      • 这表示新的 \(A_j\) 是由旧的 \(A_j\) 乘以 \(k_j\),再加上它所有前置节点 \(p\) 的旧值 \(A_p\) 乘以 \(1\) 得到的。
      • 所以在转移矩阵 M 的第 j 列:
        • j 行的元素是 k_j
        • 对于每个从 p 指向 j 的边,第 p 行的元素是 1
        • 其他行元素为 0
    • Case B: 节点 \(j\) 没有前置回忆。
      • \(A_j(t) = A_j(t-1) \times k_j + t\)
      • 我们知道 \(t = (t-1) + 1\)。所以 \(A_j(t) = A_j(t-1) \times k_j + (t-1) \times 1 + 1 \times 1\)
      • 这表示新的 \(A_j\) 是由旧的 \(A_j\) 乘以 \(k_j\),加上旧的状态 t-1 乘以 1,再加上常数 1 乘以 1 得到的。
      • 所以在转移矩阵 M 的第 j 列:
        • j 行的元素是 k_j
        • n+1 行(对应 t-1)的元素是 1
        • n+2 行(对应常数 1)的元素是 1
  • 对于新的操作次数 \(t\) (矩阵的第 n+1 列)

    • 新的 \(t\) 就是旧的 t-1 加上 1。即 \(t_{new} = (t-1)_{old} \times 1 + 1_{old} \times 1\)
    • 所以在 M 的第 n+1 列:
      • n+1 行是 1
      • n+2 行是 1
      • 其他行是 0
  • 对于常数 1 (矩阵的第 n+2 列)

    • 常数项 1 保持不变。\(1_{new} = 1_{old} \times 1\)
    • 所以在 M 的第 n+2 列:
      • n+2 行是 1
      • 其他行是 0

3. 最终计算

  1. 初始状态:在第 1 次操作之前(可以看作是第 0 次操作结束时),所有重要度为 0,时间 t 也可以看作是 0。所以初始状态向量是:

    \[\text{State}_0 = [0, 0, \dots, 0, 0, 1] \]

    (最后一位是常数1)

  2. 构建转移矩阵 M:按照上面推导的规则,构建一个 \((n+2) \times (n+2)\) 的矩阵。

  3. 矩阵快速幂:计算 M^T。这个可以用二分法(类似整数快速幂)在 \(O((n+2)^3 \log T)\) 的时间内完成。

  4. 最终结果:经过 T 次操作后的最终状态是:

    \[\text{State}_T = \text{State}_0 \times M^T \]

    我们只需要计算这个乘法,得到的向量的前 \(n\) 个元素就是我们要求的答案。

总结与代码对照

对照题解中给出的代码,可以发现其逻辑与我们的推导完全一致。

// 对应我们的推导
// Base 是转移矩阵 M, F 是状态向量
read (n, m, T);

// 设置 M 的对角线为 k_i
// M[i][i] = k_i (对应 A_j(t-1) * k_j)
for (i32 i = 1; i <= n; i++)
    read (Base.mat[i][i]);

// 建立图,并设置 M 中对应 predecessor 的值为 1
// M[p][j] = 1 (对应 sum(A_p(t-1)))
for (i32 i = 1; i <= m; i++){
    i64 x, y; read (x, y);
    G[x].push_back (y);
}
for (i32 i = 1; i <= n; i++) {
    for (auto it : G[i]) 
        in[it]++, Base.mat[i][it] = 1; // i->it, 所以新it的值依赖旧i
}

// 处理入度为 0 的节点
// M[n+1][i] = 1, M[n+2][i] = 1 (对应 t-1 + 1)
for (i32 i = 1; i <= n; i++)
    if (!in[i])
        Base.mat[n + 1][i] = Base.mat[n + 2][i] = 1;

// 设置 t 和 1 的转移规则
// M[n+1][n+1]=1, M[n+2][n+1]=1 (对应 t_new = t_old + 1)
Base.mat[n + 1][n + 1] = Base.mat[n + 2][n + 1] = 1;
// M[n+2][n+2]=1 (对应 1_new = 1_old)
Base.mat[n + 2][n + 2] = 1;

// 设置初始状态向量 State_0 = [0, 0, ..., 0, 0, 1]
// F.mat[1][n+2] = 1;

// 计算 State_T = State_0 * M^T
F = fpow (F, Base, T);

// 输出结果向量的前 n 个元素
for (i32 i = 1; i <= n; i++)
    print (F.mat[1][i]), putchar (' ');

通过这种方式,我们将一个需要迭代 \(T\) 次的复杂问题,转化成了一次矩阵乘法和一次矩阵快速幂,成功地在规定时间内解决了问题。

posted @ 2025-07-23 17:03  surprise_ying  阅读(8)  评论(0)    收藏  举报