CSP-S2020 T3 函式呼叫 題解

徐巨集烯......發表於2020-12-19

題目描述

函式是各種程式語言中一項重要的概念,藉助函式,我們總可以將複雜的任務分解成一個個相對簡單的子任務,直到細化為十分簡單的基礎操作,從而使程式碼的組織更加嚴密、更加有條理。然而,過多的函式呼叫也會導致額外的開銷,影響程式的執行效率。
某資料庫應用程式提供了若干函式用以維護資料。已知這些函式的功能可分為三類:
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]=xt[x]=1p[x]=ik[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+1c[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+1c[i]f[i][a]注意到那並不是一棵樹,而是一張有向無環圖,也就是說同一個函式會被呼叫多次,這需要我們把上式修正一下。假設現在呼叫關係長這樣
在這裡插入圖片描述
那麼k[6]就能夠被兩種途徑分別得到。假設某個函式i由兩種路徑分別得到的是k和k’,則有如下三種情況:

  1. 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’;
  2. t[i]=2,則k[i]本身並無多大意義,也同樣賦值k[i]=k+k’;
  3. 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;
}

謝謝觀看

相關文章