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

本文详细解析了P7077[CSP-S2020]函数调用的算法思路,涉及topsort、线段树懒标记和DAG的构建。通过分析数据范围和特殊性质,逐步优化暴力解法,最终提出正解。重点讲解了不含特定操作时的处理方法,并给出了关键代码实现。
摘要由CSDN通过智能技术生成

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

好题!

结合了 topsort 和线段树 \(lazy\) 标记的思想!

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

题目链接

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

题意概述

给定一个长度为 \(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 的反向建图的。(因为是子节点更新父节点嘛。)

然后我们再看一张图:

108_2.png

(选自洛谷)

假如 \(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*/
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值