题意
题目给定了一个长度为\(n\)序列\(a\)与\(m\)个操作,操作一共有3种:
1.给定\(x,y\),使\(a_x\)增加\(y\)。
2.给定\(x\),使\(a\)中所有数全部乘上\(x\)。
3.给出k个数\(c_1,c_2,...,c_k\),表示这个操作的任务是按照先后顺序执行编号为\(c_1,c_2,...,c_k\)的\(k\)的操作。
最后,题目相当于给出了一个编号为0的3操作,要求求出将操作执行完之后,变化完的\(a\)序列。
题目保证了对于每一个3操作不会回到自己。
分析
一开始看到几种操作,就会让人往数据结构方向上想,有点类似于线段树2,不过复杂度显然爆炸,只能另寻他法。
我们看到“不会回到自己”,就可以联想到可以构建一个DAG(有向无环图)。但是题目仍然复杂,于是我们可以开始先尝试一下将操作种数减少。
1.只有第一种:很明显,直接加上就完事了。
2.只有第一,三种:我们发现,在一个有向无环图上,我们可以轻松地将一个操作执行的次数\(cnt\)通过拓扑排序递推出来,最后对于每一个\(a_i\),我们把作用于第\(i\)位上的所有操作\(j\)的增加值乘上\(cnt\),再加回\(a_i\)即可。
3.只有第二,三种:此时,我们的任务就是把整个序列乘上的次数求出,其实也相当于对每个2操作求出操作次数\(cnt\),再将所有的\(x^{cnt}\)全部乘起来,最后将原序列的\(a_i\)乘上就可以了。
4.有第一,二种:现在每一种操作都只会执行一次,我们会看到,对于一个数\(a_i\),它最后的值会乘上所有二操作的总乘积再加上一操作的贡献,而每个一操作的贡献就会为它后面所有二操作的总乘积乘上它的\(y\),从操作序列倒推即可。
最后,通过一系列的分类讨论,我们终于要将三种操作同时讨论起来了。首先我们会看到,一个操作节点\(i\)的操作次数\(cnt\)会由到达它的节点决定。具体地说,现在在操作\(i\),它的子节点有\(c_1,c_2,....,c_k\),现在我们判断其对\(c_j\)的贡献,那么\(cnt_{c_j}\)会加上\(cnt_i\)乘上\(c_{j+1},c_{j+2},...,c_k\)之中会执行的操作二的总乘积,原因是此时,\(cnt_i\)决定了到达所有\(c_1,c_2,...,c_k\)的次数,它已经包含了\(cnt_i\)之外二操作的影响,而在\(i\)之中,\(j\)之后执行二操作填补了剩下对\(j\)的影响。
其实也就是说,我们对于每一个操作节点\(i\)可以维护两个量\(cnt_i,mul_i\),分别表示实际操作次数与其中二操作总乘积。
而现在\(i\)节点对子节点\(c_j\)的贡献可以写成\(cnt_{c_j}+=cnt_i*\prod_{x=j+1}^
k mul_{c_x}\)。
最后一步我们仿照第二种讨论处理方式就可以将原问题解决了。
实现
我们首先将图建出来,建议使用vector存图,方便后面对顺序的控制。
然后跑一编记忆化搜索,将每一个节点的\(mul\)仿照第三种处理方式求出。
ll dfs(int now) {
if(bj[now]) return mul[now];
bj[now]=1;
if(A[now].ti==2) mul[now]=A[now].vi;
else mul[now]=1;
for(vector<int> :: iterator it=g[now].begin();it!=g[now].end();it++) {
int st=*it;
mul[now]=mul[now]*dfs(st)%mod;
}
return mul[now];
}
接下来我们将每个节点的入度\(in\)求出,因为有一些操作并不需要使用,所以不可以一开始存图时处理,当我们处理完\(bj\)后,我们就会知道哪些点需要使用,很方便就完成了\(in\)的求值。
int in[maxn];
void build() {
for(int i=0;i<=m;i++) {
if(bj[i]) for(vector<int> :: iterator it=g[i].begin();it!=g[i].end();it++) in[*it]++;
}
}
然后我们就可以开始愉快地进行拓扑排序了,根据上述提到的转移方式,同时我们遍历时应从后往前遍历,方便把后缀积求出。
void top_sort() {
queue<int> que; que.push(0);
cnt[0]=1;
while(!que.empty()) {
int now=que.front(); que.pop();
ll mull=1;
if(g[now].empty()) continue;
for(vector<int> :: iterator it=--g[now].end();;it--) {
int st=*it;
cnt[st]=(cnt[st]+cnt[now]*mull%mod)%mod;
mull=mull*mul[st]%mod;
in[st]--; if(!in[st]) que.push(st);
if(it==g[now].begin()) break;
}
}
}
最后的最后只需要轻松地求和就可以了,下面把AC代码一并奉上:
#include<algorithm>
#include<stdio.h>
#include<vector>
#include<queue>
#define maxn 100005
#define ll long long
using namespace std;
int n,m,q;
ll a[maxn],mul[maxn],bj[maxn];
ll mod=998244353;
struct ope {
int ti,pi;
ll vi;
} A[maxn];
vector<int> g[maxn];
ll dfs(int now) {
if(bj[now]) return mul[now];
bj[now]=1;
if(A[now].ti==2) mul[now]=A[now].vi;
else mul[now]=1;
for(vector<int> :: iterator it=g[now].begin();it!=g[now].end();it++) {
int st=*it;
mul[now]=mul[now]*dfs(st)%mod;
}
return mul[now];
}
int in[maxn];
void build() {
for(int i=0;i<=m;i++) {
if(bj[i]) for(vector<int> :: iterator it=g[i].begin();it!=g[i].end();it++) in[*it]++;
}
}
ll cnt[maxn];
void top_sort() {
queue<int> que; que.push(0);
cnt[0]=1;
while(!que.empty()) {
int now=que.front(); que.pop();
ll mull=1;
if(g[now].empty()) continue;
for(vector<int> :: iterator it=--g[now].end();;it--) {
int st=*it;
cnt[st]=(cnt[st]+cnt[now]*mull%mod)%mod;
mull=mull*mul[st]%mod;
in[st]--; if(!in[st]) que.push(st);
if(it==g[now].begin()) break;
}
}
}
void fix() {
for(int i=1;i<=n;i++) a[i]=a[i]*mul[0]%mod;
for(int i=1;i<=m;i++) {
if(A[i].ti==1) a[A[i].pi]=(a[A[i].pi]+A[i].vi*cnt[i]%mod)%mod;
}
for(int i=1;i<=n;i++) printf("%lld ",a[i]);
}
int main() {
freopen("P7077.in","r",stdin);
freopen("P7077.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
scanf("%d",&m);
for(int i=1;i<=m;i++) {
scanf("%d",&A[i].ti);
if(A[i].ti==1) scanf("%d%lld",&A[i].pi,&A[i].vi);
else if(A[i].ti==2) scanf("%lld",&A[i].vi);
else {
int c; scanf("%d",&c);
for(int j=1;j<=c;j++) {
int v; scanf("%d",&v);
g[i].push_back(v);
}
}
}
scanf("%d",&q);
for(int i=1;i<=q;i++) {
int v; scanf("%d",&v);
g[0].push_back(v);
}
dfs(0);
build();
top_sort();
fix();
return 0;
}
总结
一道好题,想了大概有两天了,发现其实只需要慢慢地分类讨论,正解也不是那么复杂。