【题解】 P7077 [CSP-S2020] 函数调用(dp,拓扑排序)

【题解】 P7077 [CSP-S2020] 函数调用

好题!

结合了 topsort 和线段树 lazylazy 标记的思想!

(所以这题跟 DP 有什么关系?)

题目链接

P7077 [CSP-S2020] 函数调用 - 洛谷

题意概述

给定一个长度为 nn 的序列,有以下三种操作:

  • 单点加;

  • 全局乘;

  • 以一定顺序调用其他操作,保证不直接或间接调用自身

思路分析

首先刚开始我先打了个暴力。(雾)

得到了 45pts 的高分。

然后观察了一下数据范围发现:

  • 数据点 7,107,10 没有操作 33

  • 数据点 5,6,12,135,6,12,13 没有操作 11 或操作 22

往往当我们不会一道题的时候,数据范围总能成为突破口。——aqx

考虑一下如何处理这几个测试点。

首先考虑:对于操作 33,我们将所有输入该函数 ii 都与其调用的函数 gig_i 连一条有向边,那么可以知道,对于每一个函数属性为 TiT_i 的函数,相当于形成了一张 DAG。那么似乎就可以在这张 DAG 上进行 topsort 或是记忆化搜索。这一点在求解下面的问题时,极其重要。

  • 当不含操作 11

    只有全局乘这一个操作,很容易想到利用线段树懒标记的思想,维护一个 lazylazy 标记,表示全局乘了多少,最后直接输出 ai×lazya_i \times lazy 即可;

  • 当不含操作 22

    发现可以在从 QQ 个询问出发,每个询问构建一张 DAG,然后从起点跑一遍 topsort,递推出每个函数的调用次数 cnticnt_i,也就求出来了每个函数要调用多少次,然后最后对于每个操作 11,输出 api+addi×cntia_{p_i}+add_i \times cnt_i 即可。(其中 pip_i 是每次操作 11 要进行单点乘的下标,addiadd_i 表示进行一次单点乘要加的值是多少。)

    在这里从每个起点跑一遍,事实上也可以建立一个虚点 00,然后将 00 与所有 QQ 个询问的起点连一条有向边。整个题目的条件就变成了一张 DAG。直接以 00 为起点进行 topsort 即可。

  • 当不含操作 33

    可以类比线段树 2,我们先举个例子:假如要对一个元素执行以下操作:+1,×3,+2,×2+1,\times 3,+2,\times 2。那么假如我们维护两个标记:加法标记 addadd乘法标记 mulmul

    那么第一次操作 +1+1add+1add+1mulmul 不变;

    第二次操作 ×3\times 3add×3add \times 3mul×3mul \times 3

    第三次操作:add+2add+2mulmul 不变;

    第四次操作 ×2\times 2add×2add \times 2mulmul 不变。

    可以发现,每次乘操作,会使得 mulmuladdadd 乘上对应值,而每次加操作,只会使得 addadd 加上对应值,而不会使得 mulmul 发生任何变化。

    那么我们就有了一个较为清晰的思路:

    我们首先可以像线段树 2 一样规定“先乘后除”。

    可以对于每个点 ii 维护一个 mulimul_i,一个 addiadd_i,加法标记就是当前调用的加法属性的函数对应的每次要加的值,乘法标记记录的是当前已经乘上了多大的值。然后维护一个全局乘标记 lazylazy。这里可能有点抽象,先不急,往下看:

    那么对于每次加操作,对于 aia_i 首先要乘上 lazy/mulilazy/mul_i,因为 mulimul_i 表示你当前已经乘上了多大的值,而 lazylazy 表示的是全局的乘法标记,所以你还需要乘上 lazy/mulilazy/mul_i。然后再加上加法标记 addiadd_i。每次这样的操作之后,aia_i 就会变成当前最新值。然后清空 mulimul_i。(注意 mulimul_i 要清空为 11 而不是 00

    对于每次乘操作,直接给全局乘标记乘上对应值即可。

    最后输出每个 ai×(lazy/muli)a_i \times (lazy/mul_i) 即可。

那么解决了上述这几个特殊性质的问题之后,我们就会顺利拿到 60pts 的高分。

而同时,上述这些特殊性质,也为我们想出正解提供了极大的帮助。

结合上述的性质我们便可以很容易想出此题正解。

首先对于所有的函数属性为 33 的操作,将 ii 与所有 gig_i 连一条有向边。然后对于 QQ 个操作构成的操作序列,将虚点 00 与所有的操作 ii 连一条有向边。

然后对于 mm 个操作,每个操作 ii 记录一个加法标记 addiadd_i乘法标记 mulimul_i 以及调用次数 cnticnt_i

接下来我们来看一张图:

44mulmul3377mulmul1188mulmul2266mulmul33,那么 11mulmul 就为 mul4×mul5×mul6=mul4×mul7×mul8×mul6=3×1×2×3=18mul_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

我们发现,一个节点的 mulmul 值答案等于它各个子节点的 mulmul 的乘积。

那么我们可以用一个 topsort 求出 1m1-m 中所有数的 mulmul

需要注意的是,这次的 topsort 的反向建图的。(因为是子节点更新父节点嘛。)

然后我们再看一张图:

108_2.png

(选自洛谷)

假如 11 的操作次数是 xx,那么 +2+2 的操作次数应该增加 3x3x+1+1 的操作应该增加 12x12x

所以下传 cntcnt 时,假设一个点 xx 的操作次数 cntcnt,它的儿子是 y1,y2,yky_1​,y_2​,⋯y_k​, 那么 yiy_i​ 的 cntcnt 就应该增加 cntcnt 乘上 yi+1yky_i+1​∼y_k​ 的 mulmul 之积。

那么只需要正向建图再跑一遍 topsort,求出每个节点的 cntcnt,注意枚举一个点的儿子时是倒序枚举

那么最后只需要将 ai×mul0a_i\times mul_0,然后对应所有的操作 11 都让 api+addi×cntia_{p_i}+add_i\times cnt_i 即可。

然后就结束了。

梳理一下整个求解思路:

对于所有的操作 33,将 iigig_i 分别正反向建图 \rightarrow 跑两遍 topsort 求出 mulmulcntcnt \rightarrow 求出所有操作 11 后的 aia_i

易错点

  • 对于所有操作属性为 22 的点初始化 mulmul 为输入的值,其它两种操作属性的点都初始化为 11

  • cnt0cnt_0 要初始化为 11

  • 第一次 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*/
posted @   向日葵Reta  阅读(159)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示