把函数调用关系看成 DAG,逆拓扑序求每个函数的整体乘法倍数,再正拓扑序从后往前传播加法的最终系数。
OJ: luogu
题目 ID: P7077
难度:提高+/省选-
标签:图论拓扑排序动态规划数学
创建: 2026-06-19 23:42
更新: 2026-10-24 00:10
形式化题目
给定长度为
- 单点加法:
; - 整体乘法:对所有
, ; - 顺序调用:依次执行给定的若干函数
。
再给定长度
函数调用关系不递归,即构成一张有向无环图。所有运算都在模
样例
digraph G {
rankdir=LR;
F0["超级源点 0"] -> F2["f2: 全体 *2"];
F0 -> F3["f3"];
F3 -> F1["f1: a1 += 1"];
F3 -> F2;
}
图中 f3 先调用 f1 再调用 f2,而总调用序列是先 f2 后 f3。可以看到 f1 加上的那个 f2 放大成 f2 在序列里出现两次,最终原数组被整体放大了
暴力解法
思路
最直接的做法就是照着题意递归模拟:遇到
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 20:59
* update_at: 2026-10-01 21:09
*/
// brute.cpp:小数据暴力解,直接递归模拟每个函数的效果,用来理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 105; // 暴力只服务小数据,n 取到 100 左右
const int MAXM = 105; // 函数个数同样只到小规模
const ll MOD = 998244353;
int n, m, qnum;
ll a[MAXN]; // a[i]:下标 i 的数据值,模拟过程中不断更新
int func_type[MAXM]; // func_type[f]:函数 f 的类型 1/2/3
int add_pos[MAXM]; // add_pos[f]:1 类函数要加的位置
ll func_val[MAXM]; // func_val[f]:1 类的加数 / 2 类的乘数
vector<int> calls[MAXM]; // calls[f]:3 类函数按顺序调用的函数编号
int query_list[MAXM]; // query_list[i]:总调用序列里的第 i 个函数
// 递归执行函数 id 的全部效果:1 类单点加,2 类整体乘,3 类按顺序递归子函数。
void exec_func(int id) {
if (func_type[id] == 1) {
a[add_pos[id]] = (a[add_pos[id]] + func_val[id]) % MOD;
} else if (func_type[id] == 2) {
for (int i = 1; i <= n; i++) {
a[i] = a[i] * func_val[id] % MOD;
}
} else {
int c = calls[id].size();
for (int i = 0; i < c; i++) {
exec_func(calls[id][i]);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i] %= MOD;
}
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> func_type[i];
if (func_type[i] == 1) {
cin >> add_pos[i] >> func_val[i];
func_val[i] %= MOD;
} else if (func_type[i] == 2) {
cin >> func_val[i];
func_val[i] %= MOD;
} else {
int c;
cin >> c;
calls[i].resize(c);
for (int j = 0; j < c; j++) {
cin >> calls[i][j];
}
}
}
cin >> qnum;
for (int i = 0; i < qnum; i++) {
cin >> query_list[i];
}
// 照题目要求依次执行整个调用序列,直接得到答案。
for (int i = 0; i < qnum; i++) {
exec_func(query_list[i]);
}
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << a[i];
}
cout << '\n';
return 0;
}复杂度
设
瓶颈
这个做法在测试点
真正的问题是:我们其实并不需要知道每个函数被执行的过程,只需要知道两件事——它让整个数组乘了几倍,以及它里面的加法最后被乘了几倍。
正解
思路
第一个量:整体乘法倍数 mul_all
先只关心乘法。对每个函数
类函数不改变整体倍数, ; 类函数就是它自己的乘数, ; 类函数按顺序执行 ,整体倍数就是各子函数倍数的乘积:
因为
第二个量:加法的最终系数 coef
光有 mul_all 还不够。以样例 f1 加的是 f3 里排在 f2 前面,会被 f2 放大成 f2 在总序列里又出现一次,最终贡献是
设
超级源点
考虑一个父函数按顺序调用
后面没有兄弟函数,直接继承 ; 后面还要执行 ,所以继承 ; - 更靠前的
要再乘上它后面所有兄弟的倍数。
因此从后往前扫描子函数列表,维护一个后缀积 suffix,就能把系数发下去:
suffix = C
for i = k down to 1:
coef[g_i] += suffix
suffix *= mul_all[g_i]这里每个子函数接收到的都是“父函数系数 × 自己后面所有兄弟的倍数”,正是它执行完之后还会被放大多少次。
为什么必须先拓扑排序
注意函数编号和拓扑序没有关系:题面只保证调用关系无环,编号小的函数完全可能调用编号大的函数。所以不能简单地按 order[]:
- 逆着
order[]扫,求mul_all; - 顺着
order[]扫,把coef从父传给子。
汇总答案
把所有
最后
样例推演
样例 a1 += 1,all *= 2,
| 阶段 | 对象 | 结果 |
|---|---|---|
mul_all |
||
mul_all |
||
mul_all |
||
mul_all |
源点 |
|
coef |
源点 |
|
coef |
从 |
|
coef |
源点列表里 |
|
coef |
||
add_sum |
位置 |
|
| 答案 |
与样例输出 6 8 12 一致。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 20:59
* update_at: 2026-10-01 21:09
*/
// main.cpp:把函数调用关系看成 DAG 做两轮处理:
// 逆拓扑序求出每个函数执行完后的整体乘法倍数 mul_all,
// 再正拓扑序把加法系数 coef 从父函数传给子函数。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005; // n <= 1e5,下标用 int 足够
const int MAXM = 100005; // m, Q <= 1e5,函数编号用 int 足够
const ll MOD = 998244353;
int n, m, qnum;
ll a[MAXN]; // a[i]:下标 i 的数据值,最后原地变成答案
int func_type[MAXM]; // func_type[f]:函数 f 的类型 1/2/3,取值很小但仍用 int 便于读
int add_pos[MAXM]; // add_pos[f]:1 类函数要加的位置
ll func_val[MAXM]; // func_val[f]:1 类的加数 / 2 类的乘数,均已取模
// calls[f]:3 类函数按顺序调用的函数编号;
// calls[0] 是超级源点,存放题目给出的总调用序列。
vector<int> calls[MAXM];
int indeg[MAXM]; // indeg[f]:调用 DAG 上的入度,用来做拓扑排序
int work_deg[MAXM]; // work_deg[f]:拓扑排序时的工作副本,避免破坏 indeg
int order[MAXM + 1]; // order[0..order_cnt-1]:整个调用 DAG 的拓扑序
int order_cnt;
ll mul_all[MAXM]; // mul_all[f]:执行完函数 f 后整个数组额外乘上的倍数
ll coef[MAXM]; // coef[f]:函数 f 里的加法落到最终答案时还要再乘的系数
ll add_sum[MAXN]; // add_sum[p]:位置 p 收到的全部加法贡献
// 读入数据,并把总调用序列挂到超级源点 0 上。
void read_input() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i] %= MOD;
}
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> func_type[i];
if (func_type[i] == 1) {
cin >> add_pos[i] >> func_val[i];
func_val[i] %= MOD;
} else if (func_type[i] == 2) {
cin >> func_val[i];
func_val[i] %= MOD;
} else {
int c;
cin >> c;
calls[i].resize(c);
for (int j = 0; j < c; j++) {
cin >> calls[i][j];
indeg[calls[i][j]]++;
}
}
}
cin >> qnum;
calls[0].resize(qnum); // 超级源点 0 的调用序列就是题目给的总调用序列
for (int i = 0; i < qnum; i++) {
cin >> calls[0][i];
indeg[calls[0][i]]++;
}
}
// 对所有函数做一次拓扑排序,结果存入 order[]。
// 调用关系保证无环,但子函数编号可能比父函数大,所以不能只按编号顺序处理。
void build_topological_order() {
queue<int> que;
for (int i = 0; i <= m; i++) {
work_deg[i] = indeg[i];
if (work_deg[i] == 0) {
que.push(i);
}
}
order_cnt = 0;
while (!que.empty()) {
int u = que.front();
que.pop();
order[order_cnt++] = u;
int c = calls[u].size();
for (int i = 0; i < c; i++) {
int v = calls[u][i];
work_deg[v]--;
if (work_deg[v] == 0) {
que.push(v);
}
}
}
}
// 逆拓扑序求 mul_all:函数 f 执行完后,整个数组会额外乘上的倍数。
void calc_mul_all() {
for (int idx = order_cnt - 1; idx >= 0; idx--) {
int u = order[idx];
// 3 类函数的倍数就是全部子函数倍数的乘积;超级源点同理。
ll product = 1;
int c = calls[u].size();
for (int i = 0; i < c; i++) {
product = product * mul_all[calls[u][i]] % MOD;
}
if (u == 0) {
mul_all[u] = product; // 总调用序列的整体倍数
} else if (func_type[u] == 1) {
mul_all[u] = 1; // 加法不改变整体倍数
} else if (func_type[u] == 2) {
mul_all[u] = func_val[u]; // 乘法函数的倍数就是自己的乘数
} else {
mul_all[u] = product;
}
}
}
// 正拓扑序传播 coef:父函数从后往前把系数分给子函数。
// 某个子函数后面还会执行的兄弟函数越多,它能继承到的系数就越小。
void propagate_coef() {
coef[0] = 1;
for (int idx = 0; idx < order_cnt; idx++) {
int u = order[idx];
if (u != 0 && func_type[u] != 3) {
continue; // 只有超级源点和 3 类函数会继续往下调用
}
// suffix 是当前子函数能继承的系数:它后面所有兄弟函数倍数的乘积。
ll suffix = coef[u];
int c = calls[u].size();
for (int i = c - 1; i >= 0; i--) {
int v = calls[u][i];
coef[v] = (coef[v] + suffix) % MOD;
suffix = suffix * mul_all[v] % MOD;
}
}
}
void solve() {
build_topological_order();
calc_mul_all();
propagate_coef();
// 把每个 1 类函数的加法按最终系数汇总到具体位置上。
for (int f = 1; f <= m; f++) {
if (func_type[f] == 1) {
int p = add_pos[f];
add_sum[p] = (add_sum[p] + coef[f] * func_val[f]) % MOD;
}
}
// 原数组先整体乘上总调用序列的倍数,再加上所有加法贡献。
for (int i = 1; i <= n; i++) {
a[i] = (a[i] * mul_all[0] + add_sum[i]) % MOD;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << a[i];
}
cout << '\n';
return 0;
}复杂度
设
- 拓扑排序
; - 逆拓扑序求
mul_all一次; - 正拓扑序传播
coef一次; - 汇总答案
。
总时间复杂度
总结
这题不是模拟题,而是把“顺序调用”拆成两个可以复用的量:
mul_all[f]:函数对整体数组的乘法倍数,逆拓扑序求; coef[f]:函数中加法落到答案上的最终系数,正拓扑序从后往前传播。
一旦把这两个量分开,每个函数就只需要被处理常数次,问题落成调用 DAG 上的两次线性扫描。最容易踩的坑是函数编号不等于拓扑序,必须先做一次拓扑排序。
本文的 main.cpp 已用递归模拟的 brute.cpp 对拍 in1 / out1(样例 1)与 in2 / out2(样例 2)。题面列出的样例 3 是 call/call3.in 外部附件,本题目录未收录。
图示解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
