CSP-S2020 T3 函式呼叫 題解
題目描述
函式是各種程式語言中一項重要的概念,藉助函式,我們總可以將複雜的任務分解成一個個相對簡單的子任務,直到細化為十分簡單的基礎操作,從而使程式碼的組織更加嚴密、更加有條理。然而,過多的函式呼叫也會導致額外的開銷,影響程式的執行效率。
某資料庫應用程式提供了若干函式用以維護資料。已知這些函式的功能可分為三類:
1.將資料中的指定元素加上一個值;
2.將資料中的每一個元素乘以一個相同值;
3.依次執行若干次函式呼叫,保證不會出現遞迴(即不會直接或間接地呼叫本身)。
在使用該資料庫應用時,使用者可一次性輸入要呼叫的函式序列(一個函式可能被呼叫多次),在依次執行完序列中的函式後,系統中的資料被加以更新。某一天,小 A 在應用該資料庫程式處理資料時遇到了困難:由於頻繁而低效的函式呼叫,系統在執行操作時進入了無響應的狀態,他只好強制結束了資料庫程式。為了計算出正確資料,小 A 查閱了軟體的文件,瞭解到每個函式的具體功能資訊,現在他想請你根據這些資訊幫他計算出更新後的資料應該是多少。答案對 998244353 取模
騙分思路
看到這道題我的第一感覺就是線段樹。事實上,第三類函式可以拆分成若干個前兩類函式輪流作用的效果。再加上原本就有的前兩類函式,就有大量的區間、單點操作。由於是整體乘,我們只需線上段樹的根節點打上標記就行,等到單點修改或求值時再下傳。這竟然也能騙到70分!
#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
int n,m,p[100001],c[100001],t[100001],g[1000001],cnt,q,f;
ll a[400001],mod=998244353,v[100001];
void build(int k,int l,int r){ //建樹
if(l==r){
scanf("%lld",&a[k]);
return;
}
int mid=(l+r)/2;
build(k*2,l,mid); build(k*2+1,mid+1,r);
a[k]=1;
return;
}
void pushdown(int k){ //標記下傳
if(a[k]==1) return;
a[k*2]=(a[k*2]*a[k])%mod;
a[k*2+1]=(a[k*2+1]*a[k])%mod;
a[k]=1;
return;
}
void add(int k,int l,int r,int x,ll y){ //單點修改
if(l==r&&r==x){
a[k]=(a[k]+y);
return;
}
pushdown(k);
int mid=(l+r)/2;
if(mid>=x) add(k*2,l,mid,x,y);
if(mid+1<=x) add(k*2+1,mid+1,r,x,y);
return;
}
void print(int k,int l,int r){ //求解,輸出
if(l==r){
printf("%lld ",a[k]);
return;
}
pushdown(k);
int mid=(l+r)/2;
print(k*2,l,mid); print(k*2+1,mid+1,r);
return;
}
void deal(int x){ //處理單個函式
if(t[x]==1) add(1,1,n,p[x],v[x]);
if(t[x]==2) a[1]=(a[1]*v[x])%mod; //區間修改
if(t[x]==3){
for(int i=p[x];i<=p[x]+c[x]-1;i+=1){
deal(g[i]);
}
}
return;
}
int main(){
// freopen("call.in","r",stdin);
// freopen("call.out","w",stdout);
scanf("%d",&n);
build(1,1,n);
scanf("%d",&m);
for(int j=1;j<=m;j+=1){
scanf("%d",&t[j]);
if(t[j]==1) scanf("%d%d",&p[j],&v[j]);
if(t[j]==2) scanf("%d",&v[j]);
if(t[j]==3){
scanf("%d",&c[j]); p[j]=cnt;
for(int i=cnt;i<=c[j]+cnt-1;i+=1) scanf("%d",&g[i]);
cnt+=c[j];
}
}
scanf("%d",&q);
while(q--){
scanf("%d",&f);
deal(f);
}
print(1,1,n);printf("\n");
// fclose(stdin);
// fclose(stdout);
return 0;
}
滿分思路
如果將函式的呼叫關係看成有向邊,再將操作序列看成一個第三類函式m+1,並將m+1與所有操作序列中的函式連邊,再由“保證不會出現遞迴”可知所有邊構成有向無環圖DAG,但我們先把它看作一棵樹。
程式呼叫了函式m+1後,序列中的每一個值由a[i]變成了b*a[i]+d[i]。其中b為所有呼叫的第二類函式中的v值的乘積,而c[i]的值與每個作用於i上的第一類函式及這個函式後面所有第二類函式乘積有關,也就是說
d
[
i
]
=
∑
x
∣
t
[
x
]
=
1
且
p
[
x
]
=
i
k
[
x
]
∗
v
[
x
]
d[i]=\sum_{x|t[x]=1且p[x]=i}{k[x]*v[x]}
d[i]=x∣t[x]=1且p[x]=i∑k[x]∗v[x]其中的k[x]表示函式x的貢獻被“放大”的倍數,它只跟後面呼叫的有關。比方說現在要求k[6]值,則它與函式9、8、3有關。若以f[i][j]表示函式i呼叫的第j個函式給序列乘的值,則f[i][j]可以通過一遍深搜得到,函式9的影響可以通過f陣列傳到函式7,函式3的影響也傳到了k[5]。k[6]=k[5]*f[5][2]*f[5][3]。若有函式u呼叫函式v,且v在函式u中的下標為j,則有
k
[
v
]
=
k
[
u
]
∏
a
=
j
+
1
c
[
u
]
f
[
u
]
[
a
]
k[v]=k[u]\prod_{a=j+1}^{c[u]}{f[u][a]}
k[v]=k[u]a=j+1∏c[u]f[u][a]時間主要耗費在求乘積上,因此我們可以用字尾積的方式優化。如果f’[i][j]為舊陣列,那麼重新定義
f
[
i
]
[
j
]
=
∏
a
=
j
+
1
c
[
i
]
f
′
[
i
]
[
a
]
f[i][j]=\prod_{a=j+1}^{c[i]}{f'[i][a]}
f[i][j]=a=j+1∏c[i]f′[i][a]注意到那並不是一棵樹,而是一張有向無環圖,也就是說同一個函式會被呼叫多次,這需要我們把上式修正一下。假設現在呼叫關係長這樣
那麼k[6]就能夠被兩種途徑分別得到。假設某個函式i由兩種路徑分別得到的是k和k’,則有如下三種情況:
- t[i]=1,則它對p[i]的兩次貢獻分別為v[i]*k和v[i]*k’,總貢獻為v[i]*k+v[i]k’=v[i](k+k’),故可以令k[i]=k+k’;
- t[i]=2,則k[i]本身並無多大意義,也同樣賦值k[i]=k+k’;
- t[i]=3,則它可以轉移到它呼叫的一個函式j,即k[j]=k*f[i][j]+k’*f[i][j]=(k+k’)*f[i][j],同樣k[i]=k+k’。
對於有兩條以上的路徑,也做類似討論。因此最終得出
k
[
v
]
=
∑
k
[
u
]
∗
f
[
u
]
[
j
]
k[v]=\sum{k[u]*f[u][j]}
k[v]=∑k[u]∗f[u][j]即k陣列由父節點轉向子節點,結合有向無環圖,可以用拓撲排序求解k陣列。
深搜和拓撲排序的時間複雜度均小於
O
(
m
+
∑
c
j
)
O(m+\sum c_{j})
O(m+∑cj)。
#include<iostream>
#include<cstdio>
#include<vector>
#define ll long long
using namespace std;int xhx;
const ll mod=998244353;
const int N=100005;
ll a[N],v[N],k[N];
int n,m,q,g;
int t[N],p[N],c;
int deg[N],vi[N];
int top,stk[N];
vector<int>to[N];
vector<ll>f[N];
void add(int x,int y){
to[x].push_back(y);
return;
}
void dfs(int x){ //深搜求解f[i][j]
vi[x]=1;
if(t[x]==1){
f[x].push_back(1ll);
return;
}
if(t[x]==2){
f[x].push_back(v[x]);
return;
}
int y,s=to[x].size();
if(!s){
f[x].push_back(1ll);
return;
}
f[x].resize(to[x].size());
for(int i=s-1;i>=0;i-=1){
y=to[x][i];
deg[y]+=1; //入度在這裡處理
if(!vi[y]) dfs(y);
f[x][i]=(f[y][0]*(i==s-1?1ll:f[x][i+1])%mod)%mod;
}
return;
}
void deal(int x){
top-=1;
int y,s=to[x].size();
for(int i=0;i<s;i+=1){
y=to[x][i];
deg[y]-=1;
if(deg[y]==0) stk[++top]=y;
k[y]=(k[y]+k[x]*(i==s-1?1ll:f[x][i+1])%mod)%mod; //求解k[i]
}
return;
}
int main(){
// freopen("call.in","r",stdin);
// freopen("call.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i+=1) scanf("%lld",&a[i]);
scanf("%d",&m);
for(int i=1;i<=m;i+=1){
scanf("%d",&t[i]);
if(t[i]==1) scanf("%d%lld",&p[i],&v[i]);
if(t[i]==2) scanf("%lld",&v[i]);
if(t[i]==3){
scanf("%d",&c);
while(c--){
scanf("%d",&g);
add(i,g);
}
}
}
scanf("%d",&q);
for(int i=1;i<=q;i+=1){
scanf("%d",&g);
add(m+1,g);
}
dfs(m+1);
stk[++top]=m+1; //拓撲排序
k[m+1]=1;
while(top){
deal(stk[top]);
}
for(int i=1;i<=n;i+=1) a[i]=(a[i]*f[m+1][0])%mod; //b=f[m+1][0]
for(int i=1;i<=m;i+=1){
if(t[i]==1){
a[p[i]]=(a[p[i]]+k[i]*v[i]%mod)%mod; //+d[i]
}
}
for(int i=1;i<=n;i+=1) printf("%lld ",a[i]);
printf("\n");
// fclose(stdin);
// fclose(stdout);
return 0;
}
謝謝觀看
相關文章
- 函式呼叫棧的問題函式
- 詳細講解函式呼叫原理函式
- 類的解構函式自動呼叫函式
- 詳解 JS 中 new 呼叫函式原理JS函式
- 函式呼叫棧函式
- 子函式呼叫函式
- 外部函式的呼叫函式
- gdb 如何呼叫函式?函式
- C程式函式呼叫&系統呼叫C程式函式
- 普通函式與函式模板呼叫規則函式
- PostgreSQL函式裡呼叫函式(SETOF + RETURN QUERY)SQL函式
- 普通函式與函式模板呼叫規則2函式
- Qt 子執行緒呼叫connect/QMetaObject::invokeMethod 不呼叫槽函式問題QT執行緒Object函式
- 2024.10.17 模擬賽T3 題解
- httprunner yml 呼叫外部函式HTTP函式
- 函式呼叫引數變數傳值的問題函式變數
- 如何使用函式指標呼叫類中的函式和普通函式函式指標
- 核心函式 系統呼叫 系統命令 庫函式函式
- python同異級目錄下的函式呼叫問題Python函式
- vue跨頁面呼叫函式Vue函式
- zip-zip(子函式呼叫)函式
- MySQL 儲存函式及呼叫MySql儲存函式
- C語言函式呼叫棧C語言函式
- 虛擬函式的呼叫原理函式
- 函式呼叫與空間分配函式
- 函式棧幀(呼叫過程)函式
- .Net7 CLR的呼叫函式和編譯函式函式編譯
- vue在一個函式中呼叫另外一個函式Vue函式
- 什麼是IIFE(立即呼叫函式表示式)?函式
- 第 8 節:函式-函式巢狀呼叫與返回值函式巢狀
- JavaScript 之有趣的函式(函式宣告、呼叫、預解析、作用域)JavaScript函式
- 運用inlinehook主動呼叫函式inlineHook函式
- [20231123]函式與bash shell呼叫.txt函式
- 函式的呼叫方式和引數函式
- js 使用 DotNetObjectReference 呼叫 c# 函式JSObjectC#函式
- JS的五種函式呼叫方式JS函式
- MySQL:MySQL層比較函式呼叫MySql函式
- Python入門之函式呼叫(二)Python函式