P11765 「KFCOI Round #1」回首 解题报告
P11765 「KFCOI Round #1」回首 解题报告
题意简述
我们有一张包含 \(n\) 个节点和 \(m\) 条边的有向图。每个节点代表一条“回忆”,每条边 \(u \to v\) 代表回忆 \(u\) 恰好发生在回忆 \(v\) 之前。
所有节点(回忆)的初始“重要度”都是 0。我们需要进行 \(T\) 次“回想”操作。
在第 \(t\) 次操作(\(t\) 从 1 开始)中,会发生两件事:
- 相乘:每个节点 \(i\) 的当前重要度 \(A_i\) 变为 \(A_i \times k_i\)。
- 相加:
- 对于每个节点 \(i\),找到所有指向它的节点(即它的“前置回忆”)。将这些前置回忆在本次操作相乘前的重要度全部加到节点 \(i\) 的新重要度上。
- 如果一个节点 \(i\) 没有任何前置回忆(即没有边指向它,入度为0),那么它的新重要度就加上当前的操作次数 \(t\)。
任务是计算经过 \(T\) 次操作后,每个节点的最终重要度,并对 \(998244353\) 取模。
思路分析
Part 1: 直接模拟 (适用于 \(T\) 较小的情况,约 40分)
当 \(T\) 的值很小(例如 \(T \le 10^5\))时,我们可以完全按照题目描述的步骤来模拟。
我们用一个数组 A[1...n]
来存储每个节点的重要度。然后我们进行一个大循环,从 t = 1
到 t = T
。
在每次循环 t
中,我们需要注意一个关键细节:相加步骤使用的是相乘前的重要度。这意味着我们不能直接在原数组 A
上修改。一个简单的处理方法是,在循环开始时,我们先用一个临时数组 A_old
存下当前 A
数组的所有值。
第 t 次操作的具体流程:
- 创建一个临时数组
A_old
,A_old[i] = A[i]
fori = 1 to n
。 - 相乘:
A[i] = A_old[i] * k[i]
fori = 1 to n
。 - 相加:
- 对于每一个节点 \(i\) (从 1 到 \(n\)):
- 如果节点 \(i\) 有前置回忆(入度 > 0),假设其前置回忆集合为
Pred(i)
。那么A[i] += sum(A_old[p])
for allp
inPred(i)
。 - 如果节点 \(i\) 没有前置回忆(入度 = 0),那么
A[i] += t
。
- 如果节点 \(i\) 有前置回忆(入度 > 0),假设其前置回忆集合为
- 对于每一个节点 \(i\) (从 1 到 \(n\)):
- 记得所有计算都要对 \(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\)。
- 我们还发现,更新规则中出现了变量
t
。t
在每次操作后会变成t+1
。这个变化的量也需要被包含进我们的状态里。 - 为了处理常数项的加法(比如
+t
),我们通常会在状态向量中增加一个常数1
。
综上,我们可以定义一个 \((n+2)\) 维的状态向量,它在第 \(t-1\) 次操作结束后的状态是:
这是一个 \(1 \times (n+2)\) 的行向量。我们的目标是找到一个 \((n+2) \times (n+2)\) 的转移矩阵 M
,使得:
2. 构造转移矩阵 M
现在我们来推导这个神奇的转移矩阵 M
。M
的第 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
。
- 第
- Case A: 节点 \(j\) 有前置回忆。
-
对于新的操作次数 \(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
。
- 第
- 新的 \(t\) 就是旧的
-
对于常数 1 (矩阵的第 n+2 列)
- 常数项
1
保持不变。\(1_{new} = 1_{old} \times 1\)。 - 所以在
M
的第n+2
列:- 第
n+2
行是1
。 - 其他行是
0
。
- 第
- 常数项
3. 最终计算
-
初始状态:在第 1 次操作之前(可以看作是第 0 次操作结束时),所有重要度为
0
,时间t
也可以看作是0
。所以初始状态向量是:\[\text{State}_0 = [0, 0, \dots, 0, 0, 1] \](最后一位是常数1)
-
构建转移矩阵
M
:按照上面推导的规则,构建一个 \((n+2) \times (n+2)\) 的矩阵。 -
矩阵快速幂:计算
M^T
。这个可以用二分法(类似整数快速幂)在 \(O((n+2)^3 \log T)\) 的时间内完成。 -
最终结果:经过
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\) 次的复杂问题,转化成了一次矩阵乘法和一次矩阵快速幂,成功地在规定时间内解决了问题。