【题解】 P7077 [CSP-S2020] 函数调用
好题!
结合了 topsort 和线段树 \(lazy\) 标记的思想!
(所以这题跟 DP 有什么关系?)
题目链接
题意概述
给定一个长度为 \(n\) 的序列,有以下三种操作:
-
单点加;
-
全局乘;
-
以一定顺序调用其他操作,保证不直接或间接调用自身。
思路分析
首先刚开始我先打了个暴力。(雾)
得到了 45pts 的高分。
然后观察了一下数据范围发现:
-
数据点 \(7,10\) 没有操作 \(3\);
-
数据点 \(5,6,12,13\) 没有操作 \(1\) 或操作 \(2\);
往往当我们不会一道题的时候,数据范围总能成为突破口。——aqx
考虑一下如何处理这几个测试点。
首先考虑:对于操作 \(3\),我们将所有输入该函数 \(i\) 都与其调用的函数 \(g_i\) 连一条有向边,那么可以知道,对于每一个函数属性为 \(T_i\) 的函数,相当于形成了一张 DAG。那么似乎就可以在这张 DAG 上进行 topsort 或是记忆化搜索。这一点在求解下面的问题时,极其重要。
-
当不含操作 \(1\):
只有全局乘这一个操作,很容易想到利用线段树懒标记的思想,维护一个 \(lazy\) 标记,表示全局乘了多少,最后直接输出 \(a_i \times lazy\) 即可;
-
当不含操作 \(2\):
发现可以在从 \(Q\) 个询问出发,每个询问构建一张 DAG,然后从起点跑一遍 topsort,递推出每个函数的调用次数 \(cnt_i\),也就求出来了每个函数要调用多少次,然后最后对于每个操作 \(1\),输出 \(a_{p_i}+add_i \times cnt_i\) 即可。(其中 \(p_i\) 是每次操作 \(1\) 要进行单点乘的下标,\(add_i\) 表示进行一次单点乘要加的值是多少。)
在这里从每个起点跑一遍,事实上也可以建立一个虚点 \(0\),然后将 \(0\) 与所有 \(Q\) 个询问的起点连一条有向边。整个题目的条件就变成了一张 DAG。直接以 \(0\) 为起点进行 topsort 即可。
-
当不含操作 \(3\):
可以类比线段树 2,我们先举个例子:假如要对一个元素执行以下操作:\(+1,\times 3,+2,\times 2\)。那么假如我们维护两个标记:加法标记 \(add\) 和乘法标记 \(mul\)。
那么第一次操作 \(+1\):\(add+1\),\(mul\) 不变;
第二次操作 \(\times 3\):\(add \times 3\),\(mul \times 3\);
第三次操作:\(add+2\),\(mul\) 不变;
第四次操作 \(\times 2\):\(add \times 2\),\(mul\) 不变。
可以发现,每次乘操作,会使得 \(mul\) 和 \(add\) 乘上对应值,而每次加操作,只会使得 \(add\) 加上对应值,而不会使得 \(mul\) 发生任何变化。
那么我们就有了一个较为清晰的思路:
我们首先可以像线段树 2 一样规定“先乘后除”。
可以对于每个点 \(i\) 维护一个 \(mul_i\),一个 \(add_i\),加法标记就是当前调用的加法属性的函数对应的每次要加的值,乘法标记记录的是当前已经乘上了多大的值。然后维护一个全局乘标记 \(lazy\)。这里可能有点抽象,先不急,往下看:
那么对于每次加操作,对于 \(a_i\) 首先要乘上 \(lazy/mul_i\),因为 \(mul_i\) 表示你当前已经乘上了多大的值,而 \(lazy\) 表示的是全局的乘法标记,所以你还需要乘上 \(lazy/mul_i\)。然后再加上加法标记 \(add_i\)。每次这样的操作之后,\(a_i\) 就会变成当前最新值。然后清空 \(mul_i\)。(注意 \(mul_i\) 要清空为 \(1\) 而不是 \(0\))
对于每次乘操作,直接给全局乘标记乘上对应值即可。
最后输出每个 \(a_i \times (lazy/mul_i)\) 即可。
那么解决了上述这几个特殊性质的问题之后,我们就会顺利拿到 60pts 的高分。
而同时,上述这些特殊性质,也为我们想出正解提供了极大的帮助。
结合上述的性质我们便可以很容易想出此题正解。
首先对于所有的函数属性为 \(3\) 的操作,将 \(i\) 与所有 \(g_i\) 连一条有向边。然后对于 \(Q\) 个操作构成的操作序列,将虚点 \(0\) 与所有的操作 \(i\) 连一条有向边。
然后对于 \(m\) 个操作,每个操作 \(i\) 记录一个加法标记 \(add_i\),乘法标记 \(mul_i\) 以及调用次数 \(cnt_i\)。
接下来我们来看一张图:
若 \(4\) 的 \(mul\) 为 \(3\),\(7\) 的 \(mul\) 为 \(1\),\(8\) 的 \(mul\) 为 \(2\),\(6\) 的 \(mul\) 为 \(3\),那么 \(1\) 的 \(mul\) 就为 \(mul_4 \times mul_5\times mul_6=mul_4\times mul_7\times mul_8\times mul_6=3\times 1\times 2 \times 3=18\)。
我们发现,一个节点的 \(mul\) 值答案等于它各个子节点的 \(mul\) 的乘积。
那么我们可以用一个 topsort 求出 \(1-m\) 中所有数的 \(mul\)。
需要注意的是,这次的 topsort 的反向建图的。(因为是子节点更新父节点嘛。)
然后我们再看一张图:
(选自洛谷)
假如 \(1\) 的操作次数是 \(x\),那么 \(+2\) 的操作次数应该增加 \(3x\),\(+1\) 的操作应该增加 \(12x\)。
所以下传 \(cnt\) 时,假设一个点 \(x\) 的操作次数 \(cnt\),它的儿子是 \(y_1,y_2,⋯y_k\), 那么 \(y_i\) 的 \(cnt\) 就应该增加 \(cnt\) 乘上 \(y_i+1∼y_k\) 的 \(mul\) 之积。
那么只需要正向建图再跑一遍 topsort,求出每个节点的 \(cnt\),注意枚举一个点的儿子时是倒序枚举。
那么最后只需要将 \(a_i\times mul_0\),然后对应所有的操作 \(1\) 都让 \(a_{p_i}+add_i\times cnt_i\) 即可。
然后就结束了。
梳理一下整个求解思路:
对于所有的操作 \(3\),将 \(i\) 个 \(g_i\) 分别正反向建图 \(\rightarrow\) 跑两遍 topsort 求出 \(mul\) 和 \(cnt\) \(\rightarrow\) 求出所有操作 \(1\) 后的 \(a_i\)。
易错点
-
对于所有操作属性为 \(2\) 的点初始化 \(mul\) 为输入的值,其它两种操作属性的点都初始化为 \(1\)。
-
\(cnt_0\) 要初始化为 \(1\);
-
第一次 topsort 要反向建图;
-
第二次 topsort 要倒序枚举每个子节点。
经验
-
一道难题一眼看不出正解时,不妨考虑打暴力,然后再优化暴力;
-
一定要多关注数据范围以及部分分和特殊性质,这些往往能成为你突破和解决问题的关键!
代码实现
//luoguP7077
//正解
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<queue>
#define int long long
using namespace std;
const int maxn=1e5+10;
const int mod=998244353;
int n,m;
int in1[maxn],in2[maxn];
int a[maxn],opt[maxn],p[maxn],v[maxn],add[maxn],mul[maxn],cnt[maxn];
basic_string<int>edge1[maxn],edge2[maxn];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void topsort1()
{
queue<int>q;
for(int i=0;i<=m;i++)
{
if(in1[i]==0)q.push(i);
}
while(!q.empty())
{
int now=q.front();
q.pop();
for(int nxt:edge1[now])
{
(mul[nxt]*=mul[now])%=mod;
in1[nxt]--;
if(!in1[nxt])q.push(nxt);
}
}
return ;
}
void topsort2()
{
queue<int>q;
for(int i=0;i<=m;i++)
{
if(!in2[i])q.push(i);
}
while(!q.empty())
{
int now=q.front();
q.pop();
int Mul=1;
for(int i=edge2[now].size()-1;i>=0;i--)
{
int nxt=edge2[now][i];
cnt[nxt]=(cnt[nxt]+cnt[now]*Mul%mod)%mod;
(Mul*=mul[nxt])%=mod;
in2[nxt]--;
if(!in2[nxt])q.push(nxt);
}
}
return ;
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)a[i]=read();
m=read();
mul[0]=1;
for(int i=1;i<=m;i++)
{
opt[i]=read();
if(opt[i]==1)
{
p[i]=read();add[i]=read();
mul[i]=1;
}
else if(opt[i]==2) mul[i]=read();
else
{
v[i]=read();mul[i]=1;
for(int j=1;j<=v[i];j++)
{
int x=read();
edge2[i]+=x;edge1[x]+=i;
in2[x]++;in1[i]++;
}
}
}
int q=read();
cnt[0]=1;
for(int i=1;i<=q;i++)
{
int x=read();
int tt=0;
edge2[0]+=x;
edge1[x]+=tt;
in2[x]++;in1[0]++;
}
topsort1();
topsort2();
for(int i=1;i<=n;i++)(a[i]*=mul[0])%=mod;
for(int i=1;i<=m;i++)
{
if(opt[i]==1)(a[p[i]]=a[p[i]]+add[i]*cnt[i]%mod)%=mod;
}
for(int i=1;i<=n;i++)cout<<a[i]<<" ";
cout<<'\n';
return 0;
}
/*3
1 2 3
3
1 1 1
2 2
3 2 1 2
2
2 3*/