[CSP-S 2020] 函数调用

GitHub跳转原题关系图返回列表

把函数调用关系看成 DAG,先反向求每个函数整体乘法效果,再正向统计每个加法函数最终会被乘上的系数。

OJ: luogu

题目 ID: P7077

难度:提高+/省选-

标签:图论拓扑排序动态规划数学

日期: 2026-06-19 23:42

题意

有一个长度为 n 的数组,以及 m 个函数。

函数分三类:

  1. 给某个位置加上一个值;
  2. 把整个数组都乘上一个值;
  3. 按顺序调用若干个函数。

最后还会给出一个总调用序列,要求你把这些函数依次执行完,输出最终数组。

函数之间不会递归,也就是函数调用关系是一张 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。 这说明某个加法到底最后会给答案带来多大贡献,不只看它自己加了多少,还要看它后面还会被多少个乘法放大。

思路

先看一个最直接的小数据暴力:

cpp
// 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 类函数调用。然后:

  1. 先拓扑排序;
  2. 逆拓扑序求出每个函数的 mul
  3. 再按拓扑序,从超级源点开始,把 coef 从父函数往子函数传播;
  4. 最后把所有 1 类函数的贡献加到对应位置上。

最终答案为:

  • 原数组每个元素先乘上总调用序列的整体乘法 mul[0]
  • 再加上所有 1 类函数留下的贡献

代码

cpp
#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

  • 拓扑排序 O(m+E)O(m + E)
  • 反向计算 mul 一次 O(m+E)O(m + E)
  • 正向传播 coef 一次 O(m+E)O(m + E)

总时间复杂度 O(n+m+E)O(n + m + E),空间复杂度 O(n+m+E)O(n + m + E)

总结

这题最难的不是模拟,而是把“顺序调用”拆成两个可复用的量:

  • 一个是整体乘法效果 mul
  • 一个是加法最终系数 coef

一旦把这两个量分开,问题就落成了一张函数调用 DAG 上的两次拓扑处理。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析