把函数调用关系看成 DAG,先反向求每个函数整体乘法效果,再正向统计每个加法函数最终会被乘上的系数。
OJ: luogu
题目 ID: P7077
难度:提高+/省选-
标签:图论拓扑排序动态规划数学
日期: 2026-06-19 23:42
题意
有一个长度为 n 的数组,以及 m 个函数。
函数分三类:
- 给某个位置加上一个值;
- 把整个数组都乘上一个值;
- 按顺序调用若干个函数。
最后还会给出一个总调用序列,要求你把这些函数依次执行完,输出最终数组。
函数之间不会递归,也就是函数调用关系是一张 DAG。
样例图
这张图展示样例 1 中的函数调用关系:
digraph G {
rankdir=LR;
F3["f3"] -> F1["f1: a1 += 1"];
F3 -> F2["f2: all *= 2"];
Q["query"] -> F2;
Q -> F3;
}
f3 会先执行 f1,再执行 f2。
而总调用序列又是先执行 f2,再执行 f3。
这说明某个加法到底最后会给答案带来多大贡献,不只看它自己加了多少,还要看它后面还会被多少个乘法放大。
思路
先看一个最直接的小数据暴力:
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
const int MAXM = 25;
const int MOD = 998244353;
int n;
int a[MAXN];
int m;
int typ[MAXM];
int pos_[MAXM];
int val_[MAXM];
vector<int> calls[MAXM];
int qnum;
vector<int> queries;
void exec_func(int id) {
if (typ[id] == 1) {
a[pos_[id]] += val_[id];
a[pos_[id]] %= MOD;
} else if (typ[id] == 2) {
for (int i = 1; i <= n; i++) {
a[i] = 1LL * a[i] * val_[id] % MOD;
}
} else {
for (int v : calls[id]) {
exec_func(v);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> typ[i];
calls[i].clear();
if (typ[i] == 1) {
cin >> pos_[i] >> val_[i];
val_[i] %= MOD;
} else if (typ[i] == 2) {
cin >> val_[i];
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;
queries.resize(qnum);
for (int i = 0; i < qnum; i++) {
cin >> queries[i];
}
for (int x : queries) {
exec_func(x);
}
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << a[i];
}
cout << '\n';
return 0;
}暴力就是递归模拟函数调用:
- 遇到 1 类函数就直接加
- 遇到 2 类函数就整段乘
- 遇到 3 类函数就按顺序递归调用
这个写法适合小数据验证,但大数据下会重复展开很多次同一个函数。
关键观察有两个。
第一步,先看“一个函数整体执行完之后,会让整个数组额外乘上多少”。
- 1 类函数不会改变整体乘法效果,所以是
1 - 2 类函数的整体乘法效果就是自己的乘数
- 3 类函数按顺序调用若干个子函数,所以整体乘法效果就是这些子函数整体乘法效果的乘积
记这个量为 mul[f]。
因为 mul[f] 依赖于子函数,所以可以在调用 DAG 上按逆拓扑序计算。
第二步,考虑一个 1 类加法函数到底会被执行多少次、并且它加上的值最后还要再乘上多少。
我们定义 coef[f]:
- 表示函数
f里的加法,放到最终答案里时,还要再乘上的系数
如果一个父函数按顺序调用:
g1, g2, ..., gk
那么:
gk后面没有兄弟函数,所以它直接继承父函数的系数g(k-1)后面还会再执行一次gk,所以它的系数要再乘上mul[gk]- 更前面的函数同理
因此对一段调用序列,我们应该从后往前传播系数。
实现上可以新建一个超级源点 0,把总调用序列也看成一次 3 类函数调用。然后:
- 先拓扑排序;
- 逆拓扑序求出每个函数的
mul; - 再按拓扑序,从超级源点开始,把
coef从父函数往子函数传播; - 最后把所有 1 类函数的贡献加到对应位置上。
最终答案为:
- 原数组每个元素先乘上总调用序列的整体乘法
mul[0] - 再加上所有 1 类函数留下的贡献
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const int MAXM = 100005;
const int MOD = 998244353;
int n;
int a[MAXN];
int m;
int typ[MAXM]; // typ[i] : 第 i 个函数的类型
int pos_[MAXM]; // type1 修改的位置
int val_[MAXM]; // type1 / type2 的参数
vector<int> calls[MAXM]; // type3 按顺序调用的函数列表
int qnum;
vector<int> queries;
vector<int> graph[MAXM];
int indeg[MAXM];
int mul[MAXM]; // mul[i] : 执行完函数 i 后,对整个数组的整体乘法效果
int coef[MAXM]; // coef[i] : 函数 i 中 type1 加法最终要乘上的系数
int add_sum[MAXN]; // add_sum[p] : 所有对位置 p 的加法总贡献
void read_input() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cin >> m;
for (int i = 0; i <= m; i++) {
graph[i].clear();
indeg[i] = 0;
calls[i].clear();
mul[i] = 1;
coef[i] = 0;
}
for (int i = 1; i <= n; i++) {
add_sum[i] = 0;
}
for (int i = 1; i <= m; i++) {
cin >> typ[i];
if (typ[i] == 1) {
cin >> pos_[i] >> val_[i];
val_[i] %= MOD;
} else if (typ[i] == 2) {
cin >> val_[i];
val_[i] %= MOD;
} else {
int c;
cin >> c;
calls[i].resize(c);
for (int j = 0; j < c; j++) {
cin >> calls[i][j];
graph[i].push_back(calls[i][j]);
indeg[calls[i][j]]++;
}
}
}
cin >> qnum;
queries.resize(qnum);
for (int i = 0; i < qnum; i++) {
cin >> queries[i];
graph[0].push_back(queries[i]);
indeg[queries[i]]++;
}
}
vector<int> get_topological_order() {
queue<int> q;
vector<int> deg(m + 1);
vector<int> order;
for (int i = 0; i <= m; i++) {
deg[i] = indeg[i];
if (deg[i] == 0) {
q.push(i);
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
order.push_back(u);
for (int v : graph[u]) {
deg[v]--;
if (deg[v] == 0) {
q.push(v);
}
}
}
return order;
}
void calc_mul(const vector<int> &order) {
for (int idx = (int) order.size() - 1; idx >= 0; idx--) {
int u = order[idx];
if (u == 0) {
long long ret = 1;
for (int v : queries) {
ret = ret * mul[v] % MOD;
}
mul[u] = ret;
} else if (typ[u] == 1) {
mul[u] = 1;
} else if (typ[u] == 2) {
mul[u] = val_[u];
} else {
long long ret = 1;
for (int v : calls[u]) {
ret = ret * mul[v] % MOD;
}
mul[u] = ret;
}
}
}
void propagate_coef(const vector<int> &order) {
coef[0] = 1;
for (int u : order) {
long long cur = coef[u];
if (u == 0) {
for (int i = qnum - 1; i >= 0; i--) {
int v = queries[i];
coef[v] = (coef[v] + cur) % MOD;
cur = cur * mul[v] % MOD;
}
} else if (typ[u] == 3) {
for (int i = (int) calls[u].size() - 1; i >= 0; i--) {
int v = calls[u][i];
coef[v] = (coef[v] + cur) % MOD;
cur = cur * mul[v] % MOD;
}
}
}
}
void solve() {
vector<int> order = get_topological_order();
calc_mul(order);
propagate_coef(order);
for (int i = 1; i <= m; i++) {
if (typ[i] == 1) {
int p = pos_[i];
add_sum[p] = (add_sum[p] + 1LL * coef[i] * val_[i]) % MOD;
}
}
for (int i = 1; i <= n; i++) {
a[i] = (1LL * a[i] * mul[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;
}复杂度
设函数调用边总数为 E。
- 拓扑排序
- 反向计算
mul一次 - 正向传播
coef一次
总时间复杂度
总结
这题最难的不是模拟,而是把“顺序调用”拆成两个可复用的量:
- 一个是整体乘法效果
mul - 一个是加法最终系数
coef
一旦把这两个量分开,问题就落成了一张函数调用 DAG 上的两次拓扑处理。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
