DAG与拓扑排序

现实生活中我们经常要做一连串事情,这些事情之间有顺序关系或依赖关系,做一件事情之前必须先做另一件事,如安排客人的座位、穿衣服的先后、课程学习的先后等。这些事情可以抽象为图论中的拓扑排序(Topological Sorting)问题。

给定一张有向无环图(在有向图中,从一个节点出发,最终回到它自身的路径被称为“环”,不存在环的有向图即为有向无环图),若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y)xA 中都出现在 y 之前,则称 A 是该有向无环图顶点的一个拓扑序。求解序列 A 的过程就称为拓扑排序。

image

显然,一个图的拓扑序不一定唯一。

另外,只有在有向无环图(DAG)中才有拓扑序,如果图中有环,则没有合法的拓扑序。

例题:P4017 最大食物链计数

给出一个食物网,要求出这个食物网中最大食物链的数量。这里的“最大食物链”,指的是生物学意义上的食物链,即开头是不会捕食其他生物的生产者,结尾是不会被其他生物捕食的消费者。答案可能很大,所以要对 80112002 取模。

分析: 考虑把这个食物网转换为一个图,食物网有什么样的特点呢?

  1. 食物网中的捕食关系一定是单向的(比如猫吃鱼,而不是鱼吃猫)。
  2. 食物网中的捕食关系一定是无环的,不存在 A 捕食 B,B 捕食 C,C 捕食 A 这种情况。

所以可以发现食物网其实就是一个 DAG(有向无环图)。在这道题目中“最大食物链”的定义就是一条从入度为 0 的点开始到出度为 0 的点结束的链,即要计算这样的链的个数。

image

用一个数组 f[x] 表示从任意一个入度为 0 的点到点 x 的食物链计数。那么对于任意一个入度为 0 的点 y,它的 f[y]=1。对于一个入度非 0 的点 z,它的 f[z] 等于能到达点 z 的点 u 的 f[u] 之和。

如点 3,它的食物链计数等于点 1 的食物链计数加上点 10 的,即 f[3]=f[1]+f[10]=1+1=2。而对于点 6,它的食物链计数等于点 2 的食物链计数加上点 3 的,即 f[6]=f[2]+f[3]=1+2=3。这样最后只要对所有出度为 0 的点的事物链计数求和就能求出题目所求的答案了。

在计算 f[x] 的过程中,需要保证对于点 x,所有能到达点 x 的点 y 的 f[y] 已经被计算过了,这样就需要确定一个合适的计算顺序。因此使用 拓扑排序 来控制计算顺序。拓扑排序并不是对一个数列进行排序,而是在 DAG 上对点进行排序,使得在搜到点 x 时所有能到达点 x 的点 y 的结果已经计算完成了。具体流程如下:

  1. 统计每个点的入度,将所有入度为 0 的点加入处理队列。
  2. 将处于队头的点 x 取出,遍历点 x 能到达的所有点 y。
  3. 对于每一个 y,删去从点 x 到点 y 的边。在具体的实现中,只需要让 y 的入度减一即可。在这一步中,顺便可以对点 y 的数据进行维护,在这题中是 f[y]=(f[y]+f[x])%MOD
  4. 如果点 y 的入度减到 0 了,说明所有能到 y 的点都被计算过了,这时将点 y 加入处理队列。
  5. 重复步骤 2 直到处理队列为空。

这样,就保证了在食物链计数这题中求 f[x] 的顺序正确。时间复杂度为 O(n+m)

参考代码
#include <cstdio>
#include <vector>
#include <queue>
using namespace std;
const int N = 5005;
const int MOD = 80112002;
vector<int> graph[N];
int ind[N], f[N];
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int a, b; scanf("%d%d", &a, &b);
graph[a].push_back(b); // 存边
ind[b]++; // 点b的入度+1
}
queue<int> q;
for (int i = 1; i <= n; i++)
if (ind[i] == 0) {
q.push(i); f[i] = 1; // 将入度为0的点加入队列
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : graph[u]) {
f[v] = (f[v] + f[u]) % MOD;
ind[v]--;
if (ind[v] == 0) q.push(v); // 此时点y的依赖都解除了,将点y加入队列
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
if (graph[i].size() == 0) ans = (ans + f[i]) % MOD;
printf("%d\n", ans);
return 0;
}

答案需要对 80112002 取模,在计算 f 时一边加一边取模,以及在对出度为 0 的点的食物链计数求和时一边加一边取模。如果只在输出答案时取模,那么可能在累加的过程中答案超出了数据类型存储的范围而导致答案的错误。

如果原图中有环,则拓扑排序过程中取出的点的数量会小于整个图的点数,有一些点的入度没有降到 0

例题:P6145 [USACO20FEB] Timeline G

分析si 为第 i 次挤奶最早开始的时间,初始值为输入。

在拓扑排序的过程中,设目前考虑的是点 u,它有一条指向 v 的边,则 sv=max(sv,su+w(u,v))

参考代码
#include <cstdio>
#include <utility>
#include <vector>
#include <queue>
#include <algorithm>
using std::vector;
using std::pair;
using std::queue;
using std::max;
using ll = long long;
using edge = pair<int, int>;
const int N = 100005;
ll s[N];
int ind[N];
vector<edge> graph[N];
int main()
{
int n, m, c; scanf("%d%d%d", &n, &m, &c);
for (int i = 1; i <= n; i++) scanf("%lld", &s[i]);
for (int i = 1; i <= c; i++) {
int a, b, x; scanf("%d%d%d", &a, &b, &x);
graph[a].push_back({b, x}); ind[b]++;
}
queue<int> q;
for (int i = 1; i <= n; i++) if (ind[i] == 0) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
for (edge e : graph[u]) {
int v = e.first, w = e.second;
s[v] = max(s[v], s[u] + w);
ind[v]--;
if (ind[v] == 0) q.push(v);
}
}
for (int i = 1; i <= n; i++) printf("%lld\n", s[i]);
return 0;
}

例题:P10480 可达性统计

给定一个 N 个点 M 条边的有向无环图,分别统计从每个点出发能够到达的点的数量。N,M30000

分析:设从点 x 出发能够到达的点构成的集合是 f(x),则显然有:f(x)={x}((x,y)f(y))

也就是说,从 x 出发能够到达的点,是从“x 的各个后继节点 y”出发能够到达的点的并集,再加上点 x 自身。所以,在计算出一个点的所有后继节点的连通集合之后,就可以计算出该点的连通集合。因此可以用反向边建图,则这个图上的拓扑序是可以用来依次计算连通集合。

这里涉及到集合运算,结合 N30000,可以使用 STL 中的 bitset 支持集合相关的高效运算。最终时间复杂度为 O(N(N+M)ω),空间复杂度为 O(N2ω+N+M)

参考代码
#include <cstdio>
#include <vector>
#include <queue>
#include <bitset>
using std::vector;
using std::queue;
using std::bitset;
const int N = 30005;
vector<int> graph[N];
int ind[N];
bitset<N> ans[N];
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y; scanf("%d%d", &x, &y);
graph[y].push_back(x);
ind[x]++;
}
queue<int> q;
for (int i = 1; i <= n; i++) {
if (ind[i] == 0) q.push(i);
ans[i][i] = 1;
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : graph[u]) {
ans[v] |= ans[u];
ind[v]--;
if (ind[v] == 0) q.push(v);
}
}
for (int i = 1; i <= n; i++) printf("%d\n", (int)ans[i].count());
return 0;
}

例题:P1038 [NOIP2003 提高组] 神经网络

解题思路

按拓扑序计算,在向后传递的过程中注意,只有当前的 C 大于 0 才会对之后的点有贡献。

最后要统计有多少没有出度且 C 大于 0 的点,并依次输出。

参考代码
#include <cstdio>
#include <vector>
#include <queue>
#include <utility>
using std::pair;
using std::vector;
using std::queue;
using edge = pair<int, int>;
const int N = 105;
int u[N], c[N], ind[N];
vector<edge> graph[N];
int main()
{
int n, p; scanf("%d%d", &n, &p);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &c[i], &u[i]);
if (c[i] == 0) c[i] -= u[i];
}
for (int i = 1; i <= p; i++) {
int u, v, w; scanf("%d%d%d", &u, &v, &w);
graph[u].push_back({v, w}); ind[v]++;
}
queue<int> q;
for (int i = 1; i <= n; i++) if (ind[i] == 0) q.push(i);
while (!q.empty()) {
int cur = q.front(); q.pop();
for (edge p : graph[cur]) {
int nxt = p.first, w = p.second;
ind[nxt]--;
if (c[cur] > 0) c[nxt] += w * c[cur];
if (ind[nxt] == 0) q.push(nxt);
}
}
bool null = true;
for (int i = 1; i <= n; i++)
if (graph[i].size() == 0 && c[i] > 0) {
null = false;
printf("%d %d\n", i, c[i]);
}
if (null) printf("NULL\n");
return 0;
}

例题:P1983 [NOIP2013 普及组] 车站分级

解题思路

希望分的级别尽量少,也就是最高级别尽量低,如果可以让每个车站取到它的最低等级,那最终的结果就是这些等级的最大值。

这就可以建图了,点显然就是每个车站,边要根据车次来定,没停的站的等级一定低于停的站,所以可以由没停的站向停的站连边,注意不要添加重边,否则边数太多。

leveli 表示车站 i 的最低等级,拓扑排序过程中对于边 (u,v),取 levelv=max(levelv,levelu+1)

最后结果为 level 数组的最大值,极限时间复杂度 O(mn2),但在这题的数据上可过。

参考代码
#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
using std::queue;
using std::vector;
using std::max;
const int N = 1005;
vector<int> graph[N];
int ind[N], level[N];
bool vis[N][N];
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int s; scanf("%d", &s);
vector<int> stop(s);
for (int j = 0; j < s; j++) scanf("%d", &stop[j]);
for (int j = 1; j < s; j++) {
for (int k1 = stop[j - 1] + 1; k1 < stop[j]; k1++) {
for (int k2 : stop) {
if (vis[k1][k2]) continue;
graph[k1].push_back(k2);
ind[k2]++;
vis[k1][k2] = true;
}
}
}
}
queue<int> q;
int ans = 0;
for (int i = 1; i <= n; i++)
if (ind[i] == 0) {
q.push(i); level[i] = 1;
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : graph[u]) {
level[v] = max(level[v], level[u] + 1);
ans = max(ans, level[v]);
ind[v]--;
if (ind[v] == 0) q.push(v);
}
}
printf("%d\n", ans);
return 0;
}

瓶颈在于连边的次数过多,需要减少边数。

这里的建图还可以进一步优化,对于每一趟车次可以增加一个虚拟结点,所有不停靠的车站指向这个虚拟结点,而虚拟结点又指向所有停靠的车站,这样可以将建图的边数由 |S1|×|S2| 降至 |S1|+|S2|,其中 |S1||S2| 分别为停靠车站的数量和不停靠车站的数量。这样的话答案为最长链的边数除以 2 再加 1。这样时间复杂度为 O(nm)

参考代码
#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
using std::queue;
using std::vector;
using std::max;
const int N = 2005;
vector<int> graph[N];
int ind[N], level[N];
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int s; scanf("%d", &s);
// virtual node (n+i)
int pre = 0;
for (int j = 1; j <= s; j++) {
int stop; scanf("%d", &stop);
if (j == 1) pre = stop;
graph[stop].push_back(n + i); ind[n + i]++;
for (int k = pre + 1; k < stop; k++) {
graph[n + i].push_back(k);
ind[k]++;
}
pre = stop;
}
}
queue<int> q;
int ans = 1;
for (int i = 1; i <= n + m; i++)
if (ind[i] == 0) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : graph[u]) {
level[v] = max(level[v], level[u] + 1);
ans = max(ans, level[v]);
ind[v]--;
if (ind[v] == 0) q.push(v);
}
}
printf("%d\n", ans / 2 + 1);
return 0;
}

例题:P7113 [NOIP2020] 排水系统

解题思路

按拓扑序计算,ansi 的初始值为 1,对于拓扑排序过程中连向 v 的边,有 ansv=ansuoutdegu

最后输出出度为 0 的点的答案。

注意结果用分数形式保存,即分子、分母分别存储。

本题的坑点:分母最大可达 6011,如果只用 long long 得分为 6090 分。现在可以使用 __int128

参考代码
#include <cstdio>
#include <vector>
#include <queue>
#include <utility>
using std::vector;
using std::queue;
using std::pair;
using frac = pair<__int128, __int128>; // 分子,分母
const int N = 100005;
vector<int> graph[N];
int ind[N];
frac ans[N];
__int128 gcd(__int128 x, __int128 y) {
return y == 0 ? x : gcd(y, x % y);
}
frac add(frac f1, frac f2) {
frac res;
__int128 lcm = f1.second / gcd(f1.second, f2.second) * f2.second;
res.first = lcm / f1.second * f1.first + lcm / f2.second * f2.first;
res.second = lcm;
__int128 g = gcd(res.first, res.second);
res.first /= g; res.second /= g;
return res;
}
void output(__int128 n) {
if (n > 9) output(n / 10);
putchar(n % 10 + '0');
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
ans[i] = {0, 1};
int d; scanf("%d", &d);
for (int j = 1; j <= d; j++) {
int a; scanf("%d", &a);
graph[i].push_back(a);
ind[a]++;
}
}
queue<int> q;
for (int i = 1; i <= n; i++)
if (ind[i] == 0) {
q.push(i); ans[i] = {1, 1};
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : graph[u]) {
frac f = {ans[u].first, ans[u].second * (__int128)graph[u].size()};
ans[v] = add(ans[v], f);
ind[v]--;
if (ind[v] == 0) q.push(v);
}
}
for (int i = 1; i <= n; i++)
if (graph[i].size() == 0) {
output(ans[i].first); printf(" "); output(ans[i].second); printf("\n");
}
return 0;
}

例题:P1347 排序

解题思路

矛盾就是有环,也就是拓扑排序过程中取出的元素数不够 n

唯一解就是自始至终队列里都只有不超过 1 个元素。

如果队列里有多个备选元素,就是多解。

参考代码
#include <cstdio>
#include <vector>
#include <queue>
using std::vector;
using std::queue;
int main()
{
int n, m; scanf("%d%d", &n, &m);
vector<vector<int>> g(n);
vector<int> ind(n);
for (int i = 1; i <= m; i++) {
char buf[4]; scanf("%s", buf);
int u = buf[0] - 'A', v = buf[2] - 'A';
g[u].push_back(v); ind[v]++;
queue<int> q;
vector<int> tmp = ind;
for (int j = 0; j < n; j++) if (tmp[j] == 0) q.push(j);
bool mul = false;
vector<int> order;
while (!q.empty()) {
if (q.size() > 1) mul = true;
u = q.front(); q.pop(); order.push_back(u);
for (int v : g[u]) {
tmp[v]--;
if (tmp[v] == 0) q.push(v);
}
}
if (order.size() < n) {
printf("Inconsistency found after %d relations.\n", i);
return 0;
} else if (!mul) {
printf("Sorted sequence determined after %d relations: ", i);
for (int x : order) printf("%c", x + 'A');
printf(".\n");
return 0;
}
}
printf("Sorted sequence cannot be determined.\n");
return 0;
}

例题:P4376 [USACO18OPEN] Milking Order G

分析:分两步,第一步求最大的符合条件的 X,第二步求字典序最小的方案。

先看第一步,最朴素的想法是暴力枚举 X,跑拓扑排序判断是否有可行解,这一步时间复杂度为 O((n+m)m)

想要进一步优化需要观察到 X 对结果的影响有单调性,可以二分答案求出最大的 X,这样时间复杂度降为 O((n+m)logm)

第二步,求字典序最小的方案。

首先需要理解拓扑排序过程中队列里的节点的含义,这些点是目前入度为 0 的点(所有前置依赖都已释放),它们之间没有必然顺序,所以实际上不一定非得用队列存储,只是通常会使用队列来实现。

现在要求字典序最小的方案,也就是每次要取出的点是可选的点中编号最小的,因此可以把队列换成优先队列,让编号小的先出队。

这一步时间复杂度为 O((n+m)logm),注意第一步用二分答案计算 X 时,判定是否有可行解应该使用朴素的队列,否则时间复杂度里就会有两个 log

参考代码
#include <cstdio>
#include <vector>
#include <queue>
using std::vector;
using std::queue;
using std::priority_queue;
using std::greater;
const int N = 200005;
vector<int> order[N], graph[N];
int ind[N], res[N];
void init(int n) {
for (int i = 1; i <= n; i++) {
graph[i].clear(); ind[i] = 0;
}
}
bool check(int n, int x) {
init(n);
for (int i = 1; i <= x; i++) {
for (int j = 1; j < order[i].size(); j++) {
int pre = order[i][j - 1], cur = order[i][j];
graph[pre].push_back(cur); ind[cur]++;
}
}
queue<int> q;
for (int i = 1; i <= n; i++) if (ind[i] == 0) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : graph[u]) {
ind[v]--;
if (ind[v] == 0) q.push(v);
}
}
for (int i = 1; i <= n; i++) if (ind[i] != 0) return false;
return true;
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int cnt; scanf("%d", &cnt);
for (int j = 1; j <= cnt; j++) {
int x; scanf("%d", &x); order[i].push_back(x);
}
}
int ans = 0, l = 1, r = m;
while (l <= r) {
int mid = (l + r) / 2;
if (check(n, mid)) {
l = mid + 1; ans = mid;
} else {
r = mid - 1;
}
}
priority_queue<int, vector<int>, greater<int>> q;
init(n);
for (int i = 1; i <= ans; i++) {
for (int j = 1; j < order[i].size(); j++) {
int pre = order[i][j - 1], cur = order[i][j];
graph[pre].push_back(cur); ind[cur]++;
}
}
for (int i = 1; i <= n; i++) if (ind[i] == 0) q.push(i);
int cnt = 0;
while (!q.empty()) {
cnt++; res[cnt] = q.top(); q.pop();
for (int v : graph[res[cnt]]) {
ind[v]--;
if (ind[v] == 0) q.push(v);
}
}
for (int i = 1; i <= n; i++) printf("%d ", res[i]);
return 0;
}

习题:P3243 [HNOI2015] 菜肴制作

解题思路

注意,本题并不是求字典序最小的拓扑序。

例如,4 种菜肴,24 前,31 前。

则如果求字典序最小的拓扑序会求出 2,3,1,4,而本题实际上想要求得的结果是 3,1,2,4

考虑制作的最后一道菜肴(其出度必为 0),这道菜在满足拓扑序的情况下,编号应该尽可能大。如果最后一道菜不是可以作为最后的菜肴中编号最大的,那么将另一道编号更大的菜肴与其交换之后会使得结果更优。

最后一道菜定了之后,可以将它移出考虑范畴了。此时问题演变为剩下 n1 道菜的制作顺序,上面的策略可以复用。

因此本题等价于按反向边建图之后求字典序最大的拓扑序,这个结果倒过来输出就是本题要求的答案。

参考代码
#include <cstdio>
#include <vector>
#include <queue>
using std::vector;
using std::priority_queue;
const int N = 100005;
vector<int> graph[N];
int ind[N], ans[N];
void solve() {
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
graph[i].clear(); ind[i] = 0;
}
for (int i = 1; i <= m; i++) {
int x, y; scanf("%d%d", &x, &y);
graph[y].push_back(x); ind[x]++;
}
priority_queue<int> q;
for (int i = 1; i <= n; i++) if (ind[i] == 0) q.push(i);
int idx = 0;
while (!q.empty()) {
int u = q.top(); q.pop();
ans[++idx] = u;
for (int v : graph[u]) {
ind[v]--;
if (ind[v] == 0) q.push(v);
}
}
if (idx < n) printf("Impossible!");
else for (int i = n; i >= 1; i--) printf("%d ", ans[i]);
printf("\n");
}
int main()
{
int t; scanf("%d", &t);
for (int i = 1; i <= t; i++) {
solve();
}
return 0;
}

例题:P7077 [CSP-S2020] 函数调用

分析:先从简单的操作入手。

如果只有操作 1,直接单点加即可。

如果只有操作 2,用一个变量维护全局乘的数即可。

如果只有操作 1,2,会发现当一个操作 1 后接了若干操作 2 时,该操作 1 加的数会被乘后续操作 2 的乘积,也可以看作把这次操作 1 调用后续操作 2 的乘积次。

如果只有操作 1,3,可以设 cnti 表示函数 i 的调用次数,对调用的关系形成的 DAG 进行拓扑排序可以求得所有的 cnt 值,若 u 调用 v,则有 cntvcntv+cntu

如果只有操作 2,3,同样地,可以设 muli 表示调用函数 i 之后全局将会乘多少,按照调用关系形成的 DAG 进行记忆化搜索(或用反向边建图进行拓扑排序)计算得出所有的 mul,若 u 调用 v,则有 mulumulu×mulv

参考代码
#include <cstdio>
#include <vector>
#include <queue>
using std::vector;
using std::queue;
const int N = 100005;
const int MOD = 998244353;
int n, m, q, a[N], t[N], p[N], add[N], mul[N], f[N];
int ind[N], rev_ind[N], cnt[N];
vector<int> graph[N], rev_graph[N];
void func(int u) {
if (t[u] == 1) {
int idx = p[u];
a[idx] = (a[idx] + add[u]) % MOD;
} else if (t[u] == 2) {
for (int i = 1; i <= n; i++) {
a[i] = 1ll * a[i] * mul[u] % MOD;
}
} else {
for (int g : graph[u]) {
func(g);
}
}
}
bool check_no_3() { // 没有操作3,只有1和2
for (int i = 1; i <= m; i++) {
if (graph[i].size()) return false;
}
return true;
}
void solve_no_3() {
vector<int> add_num(n + 1);
int global_mul = 1;
for (int i = q; i >= 1; i--) {
int u = f[i];
if (t[u] == 1) {
int idx = p[u];
add_num[idx] = (add_num[idx] + 1ll * global_mul * add[u] % MOD) % MOD;
} else {
global_mul = 1ll * global_mul * mul[u] % MOD;
}
}
for (int i = 1; i <= n; i++) {
a[i] = 1ll * a[i] * global_mul % MOD;
a[i] = (a[i] + add_num[i]) % MOD;
}
}
int quickpow(int x, int y) {
int res = 1;
while (y > 0) {
if (y % 2 == 1) res = 1ll * res * x % MOD;
x = 1ll * x * x % MOD;
y /= 2;
}
return res;
}
bool check_no_2() { // 没有操作2,只有1和3
for (int i = 1; i <= m; i++)
if (t[i] == 2) return false;
return true;
}
void solve_no_2() {
for (int i = 1; i <= q; i++) cnt[f[i]]++;
queue<int> que;
for (int i = 1; i <= m; i++) if (ind[i] == 0) que.push(i);
while (!que.empty()) {
int u = que.front(); que.pop();
for (int v : graph[u]) {
cnt[v] = (cnt[v] + cnt[u]) % MOD;
ind[v]--;
if (ind[v] == 0) que.push(v);
}
}
for (int i = 1; i <= m; i++) {
if (t[i] == 1) {
a[p[i]] = (a[p[i]] + 1ll * cnt[i] * add[i] % MOD) % MOD;
}
}
}
bool check_no_1() { // 没有操作1,只有2和3
for (int i = 1; i <= m; i++)
if (t[i] == 1) return false;
return true;
}
void solve_no_1() {
queue<int> que;
for (int i = 1; i <= m; i++) if (rev_ind[i] == 0) que.push(i);
while (!que.empty()) {
int u = que.front(); que.pop();
for (int v : rev_graph[u]) {
mul[v] = 1ll * mul[u] * mul[v] % MOD;
rev_ind[v]--;
if (rev_ind[v] == 0) que.push(v);
}
}
int global_mul = 1;
for (int i = 1; i <= q; i++) global_mul = 1ll * global_mul * mul[f[i]] % MOD;
for (int i = 1; i <= n; i++) a[i] = 1ll * a[i] * global_mul % MOD;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
scanf("%d", &m);
for (int i = 1; i <= m; i++) {
scanf("%d", &t[i]);
if (t[i] == 1) {
scanf("%d%d", &p[i], &add[i]);
mul[i] = 1;
} else if (t[i] == 2) {
scanf("%d", &mul[i]);
} else {
int c; scanf("%d", &c); mul[i] = 1;
for (int j = 1; j <= c; j++) {
int g; scanf("%d", &g);
graph[i].push_back(g); ind[g]++;
rev_graph[g].push_back(i); rev_ind[i]++;
}
}
}
scanf("%d", &q);
for (int i = 1; i <= q; i++) {
scanf("%d", &f[i]);
}
if (check_no_3()) {
solve_no_3();
} else if (check_no_2()) {
solve_no_2();
} else if (check_no_1()) {
solve_no_1();
} else {
for (int i = 1; i <= q; i++) func(f[i]);
}
for (int i = 1; i <= n; i++) printf("%d ", a[i]);
return 0;
}

为了方便,可以建立一个虚点 0,表示“主函数”,也就是由 0 依次调用那 Q 个要执行的函数。

在操作 1,3 的基础上结合 1,2 混合的处理方式。

对于拓扑排序过程中遇到的每个节点 u,维护一个变量 nowmul 表示 u 调用 v 以后在 u 的调用过程中后续还要乘多少,也就等价于在一次 u 的执行过程中 v 要执行 nowmul 次,所以执行次数 cntvcntv+cntu×nowmul,并且将 nowmulnowmul×mulv,因为 u 要调用 v 之前的函数时,v 带来的乘法效果就需要考虑了。

对于初始化,只需要将 cnt0 设为 1 即可,最后答案是先把每个数乘 mul0,再遍历每个函数,如果是操作 1,则将该单点加执行 cnti 次,即 apiapi+vi×cnti

注意遍历 u 指出的边时需要按输入的倒序。

参考代码
#include <cstdio>
#include <vector>
#include <queue>
using std::vector;
using std::queue;
const int N = 100005;
const int MOD = 998244353;
vector<int> g[N], rev_g[N];
int ind[N], rev_ind[N], a[N], t[N], p[N], add[N], mul[N], cnt[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
int m; scanf("%d", &m);
for (int i = 1; i <= m; i++) {
scanf("%d", &t[i]);
if (t[i] == 1) {
scanf("%d%d", &p[i], &add[i]);
mul[i] = 1;
} else if (t[i] == 2) {
scanf("%d", &mul[i]);
} else {
mul[i] = 1;
int c; scanf("%d", &c);
for (int j = 1; j <= c; j++) {
int gid; scanf("%d", &gid);
g[i].push_back(gid); ind[gid]++;
rev_g[gid].push_back(i); rev_ind[i]++;
}
}
}
int q; scanf("%d", &q);
for (int i = 1; i <= q; i++) {
int f; scanf("%d", &f);
g[0].push_back(f); ind[f]++;
rev_g[f].push_back(0); rev_ind[0]++;
}
mul[0] = 1; cnt[0] = 1;
queue<int> que;
for (int i = 0; i <= m; i++) {
if (rev_ind[i] == 0) que.push(i);
}
while (!que.empty()) {
int u = que.front(); que.pop();
for (int v : rev_g[u]) {
mul[v] = 1ll * mul[v] * mul[u] % MOD;
if (--rev_ind[v] == 0) que.push(v);
}
}
for (int i = 1; i <= n; i++) a[i] = 1ll * a[i] * mul[0] % MOD;
for (int i = 0; i <= m; i++) if (ind[i] == 0) que.push(i);
while (!que.empty()) {
int u = que.front(); que.pop();
int nowmul = 1;
if (g[u].size() > 0) {
for (int i = (int)g[u].size() - 1; i >= 0; i--) {
int v = g[u][i];
cnt[v] = (cnt[v] + 1ll * cnt[u] * nowmul % MOD) % MOD;
if (--ind[v] == 0) que.push(v);
nowmul = 1ll * nowmul * mul[v] % MOD;
}
}
}
for (int i = 1; i <= m; i++) {
if (t[i] == 1) {
a[p[i]] = (a[p[i]] + 1ll * add[i] * cnt[i] % MOD) % MOD;
}
}
for (int i = 1; i <= n; i++) printf("%d ", a[i]);
return 0;
}
posted @   RonChen  阅读(206)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示