UOJ Easy Round #2 题解

A

这是手玩样例 2 2 2 的图:
在这里插入图片描述
不难发现,|| 可以看做一个分界点,需要考虑每一段连续 && 的贡献。

一段长度为 k k k 的 && 会产生 1 1 1 个返回值为 1 1 1 的手机,这个手机会直接不执行后面的所有操作。然后还会产生 k k k 个返回值为 0 0 0 的手机,这些手机会直接开始进行下一个 || 后面的操作。

于是可以记录一个 p r o d prod prod,表示前面所有 && 段产生的返回值为 0 0 0 的手机的乘积,每次遇到一个新段时,令 a n s ans ans 加上 p r o d prod prod(即那个返回值为 1 1 1 的手机的贡献),然后再乘上 k k k 即可。

最后再让 a n s ans ans 加多一个 p r o d prod prod,表示直至最后返回值都是 0 0 0 的手机个数。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define mod 998244353

int n;
char s[5];
int prod=1,now=1,ans=0;

int main()
{
    
    
	scanf("%d",&n);
	for(int i=1;i<n;i++){
    
    
		scanf("%s",s);
		if(s[0]=='&')now++;
		else{
    
    
			ans=(ans+prod)%mod;
			prod=1ll*prod*now%mod;
			now=1;
		}
	}
	ans=(ans+prod)%mod;
	prod=1ll*prod*now%mod;
	
	ans=(ans+prod)%mod;
	printf("%d",ans);
}

B

i i i a i a_i ai 连边,会形成若干个环,显然两两环之间是独立的子问题,他们之间是不可能存在边的,于是下面就只考虑一个环如何求解。

一个环其实就是朋友关系图中的一个连通块,而对于一个连通块,其实只有他的dfs树需要考虑,另外的就是一些返祖边,对于一条 ( i , k ) (i,k) (i,k) 返祖边,且 k k k i i i j j j 这个儿子的子树内,那么当 k > j k>j k>j 时,这条边才能连,并且连不连无所谓,对于一棵有 c c c 条返祖边可以连的dfs树,他的贡献就是 2 c 2^c 2c

下面先不考虑返祖边,单纯考虑dfs树有多少种形态。

假设连通块内编号最小的节点为 m i mi mi,设 p o s i pos_i posi 表示 i i i 在原序列中的位置,从 m i mi mi 开始dfs,将 p o s m i pos_{mi} posmi 加入到一个新数组 s s s 内,然后令 m i mi mi 等于 p o s m i pos_{mi} posmi,重复操作直到走回一开始的 m i mi mi

比如说,当 a = { 2 , 4 , 1 , 3 } a=\{2,4,1,3\} a={ 2,4,1,3} 时,路线为 1 → 3 → 4 → 2 → 1 1\to 3\to 4\to 2\to 1 13421 s = { 3 , 4 , 2 , 1 } s=\{3,4,2,1\} s={ 3,4,2,1}

类比二叉树的后序遍历,可以定义出一个适合于所有树的广义后序遍历,然后求出来的这个 s s s,实际上就是dfs树的广义后序遍历。

原因很简单,一开始的 m i mi mi,其实就是题目中 d f s dfs dfs 的初始位置,然后一路swap,最后 m i mi mi 被放到了 p o s m i pos_{mi} posmi 位置,而原来在 p o s m i pos_{mi} posmi 位置的数,又被一路swap到了他的 p o s pos pos 位置,这个过程手玩一下会清晰很多。所以一路走 p o s pos pos 得到的就是广义后序遍历。

那么问题变成了:给出一个广义后序遍历序列 s s s,求原树的方案数。

首先有一个东西不能不知道: s s s 的最后一位一定是这棵树的根。

然后就可以简单的 dp \text{dp} dp 出来了,设 f i , j f_{i,j} fi,j 表示以 s i s_i si ~ s j s_j sj 这一段作为广义后序遍历的树的个数, g i , j g_{i,j} gi,j 表示将 s i s_i si ~ s j s_j sj 划分为若干段,一个划分的贡献为每一段的 f f f 的乘积,所有划分方案的总和(其实就是分成森林的方案数)。

那么有 f i , j = g i , j − 1 f_{i,j}=g_{i,j-1} fi,j=gi,j1,即将 j j j 看成 g i , j − 1 g_{i,j-1} gi,j1 内所有树的根。

考虑每次往一个森林的后面加一棵树(注意不能往前面加,这样没法比较根节点的编号大小,要注意题目里的dfs是按编号从小到大遍历的,所以每次往后面新加的树的树根编号一定要大于上一棵树的树根), g g g 的转移就是:
g i , j = ∑ k = i j − 1 [ s k < s j ] g i , k × f k + 1 , j g_{i,j}=\sum_{k=i}^{j-1}[s_k<s_j]g_{i,k}\times f_{k+1,j} gi,j=k=ij1[sk<sj]gi,k×fk+1,j

于是就在 O ( ∣ s ∣ 3 ) O(|s|^3) O(s3) 的时间内做完了,总时间复杂度的上限为 O ( n 3 ) O(n^3) O(n3)

但是还有个返祖边的问题,定义 h i , j h_{i,j} hi,j 表示 s [ i , j ] s_{[i,j]} s[i,j] 内有多少个数大于 s j s_j sj,那么其实 h i , j h_{i,j} hi,j 相当于 [ i , j ] [i,j] [i,j] 内可以往 j j j 的父亲那里连多少条返祖边,于是转移变成:
g i , j = ∑ k = i j − 1 [ s k < s j ] g i , k × f k + 1 , j × 2 h k + 1 , j g_{i,j}=\sum_{k=i}^{j-1}[s_k<s_j]g_{i,k}\times f_{k+1,j}\times 2^{h_{k+1,j}} gi,j=k=ij1[sk<sj]gi,k×fk+1,j×2hk+1,j

然后就做完了,将每个连通块的方案数乘起来就是答案。代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 510
#define mod 998244353

int n,p[maxn],pos[maxn],bin[maxn];
int s[maxn],t;
int f[maxn][maxn],g[maxn][maxn],h[maxn][maxn],ans=1;
int calc(int st){
    
    
	int mi=st; t=0;
	for(int i=p[st];i!=mi;i=p[i])mi=min(mi,i);
	for(int i=pos[mi];i!=mi;i=pos[i])s[++t]=i;s[++t]=mi;
	for(int i=1;i<=t;i++)p[s[i]]=0;
	
	#define MS(x) memset(x,0,sizeof(x))
	MS(f);MS(g);MS(h);
	for(int i=1;i<=t;i++){
    
    
		f[i][i]=g[i][i]=1;
		for(int j=i-1;j>=1;j--){
    
    
			h[j][i]=h[j+1][i]+(s[j]>s[i]);
		}
	}
	for(int len=2;len<=t;len++){
    
    
		for(int i=1;i<=t-len+1;i++){
    
    
			f[i][i+len-1]=g[i][i+len-2];
			g[i][i+len-1]=1ll*f[i][i+len-1]*bin[h[i][i+len-1]]%mod;
		}
		for(int i=1;i+len-1<=t;i++){
    
    
			int j=i+len-1;
			for(int k=i;k<j;k++)if(s[k]<s[j]){
    
    
				g[i][j]=(g[i][j]+1ll*g[i][k]*f[k+1][j]%mod*bin[h[k+1][j]]%mod)%mod;
			}
		}
	}
	
	return f[1][t];
}

int main()
{
    
    
	scanf("%d",&n);
	bin[0]=1;for(int i=1;i<=n;i++)bin[i]=2ll*bin[i-1]%mod;
	for(int i=1;i<=n;i++)scanf("%d",&p[i]),pos[p[i]]=i;
	for(int i=1;i<=n;i++)if(p[i])ans=1ll*ans*calc(i)%mod;
	printf("%d",ans);
}

后来看了题解,其实将这个广义后序遍历翻转,就恰好得到官方题解中的逆dfs序,原因也是显然的,手玩一下直接就看明白了。

所以还有另外一个奇怪的收获:一棵树的广义后序遍历的翻转等于这棵树的逆dfs序……

C

膜了一手官方题解。

f i f_i fi i i i 能到达的节点数量,最大值的上界显然是 ∑ f i \sum f_i fi,考虑如何达到这个上界。

首先不难发现这是棵基环内向树,去掉基环先考虑树,考虑树的一个链剖分,将每个点的一个儿子染黑,使自己的守护者为这个黑儿子,然后每次合并一棵子树时,令上一棵子树的剩余叶子节点守护这个新子树的根即可。这样合并完所有子树后,只有根节点没有守护者,以及存在一个叶子结点没有守护别人。

然后考虑环,从某一个位置开始,依次使下一个点的剩余叶子节点成为自己的守护者即可。

这样可以使每个守护者 b i b_i bi 都不能被 i i i 到达,这样就达到了上界。

下界的话考虑 b b b 的逆置换,原来是尽量别让真理捍卫者挡路,现在是尽量让真理捍卫者挡别人的路

下界的答案统计理论上需要分两类讨论,设 j j j i i i 的守护者,一开始的答案为 ∑ f i \sum f_i fi

  1. j j j i i i 的祖先,并且 j j j 不在环上,那么贡献为 − f j + 1 -f_j+1 fj+1
  2. j j j i i i 的祖先,并且 j j j 在环上,那么贡献为 l − s + 1 l-s+1 ls+1,其中 s s s 为环长, l l l i i i 的入环点要走几步才能到 j j j

然而事实上,vfk用了更优秀的方式来统计,这个就看代码吧,也不太好说,需要自己大力手玩。

并且链剖分那部分,实现的时候是每次合并一棵子树给父亲,原理是一样的。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100010

int n,a[maxn],d[maxn];
int q[maxn],t=0;
bool vis[maxn];
int f[maxn],ne[maxn];
int b1[maxn],b2[maxn];
long long sum1=0,sum2=0;

int main()
{
    
    
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),d[a[i]]++;
	for(int i=1;i<=n;i++)if(!d[i])q[++t]=i;
	for(int i=1;i<=t;i++){
    
    
		int x=q[i];d[a[x]]--;
		if(!d[a[x]])q[++t]=a[x];
	}
	for(int i=1;i<=n;i++)if(d[i]&&!vis[i]){
    
    
		int x=i,sz=0;
		do sz++,vis[x]=true,x=a[x];while(x!=i);
		x=i;do f[x]=sz,x=a[x];while(x!=i);
	}
	for(int i=t;i>=1;i--)f[q[i]]=f[a[q[i]]]+1;
	for(int i=1;i<=n;i++)ne[i]=i;
	for(int i=1;i<=t;i++){
    
    
		int x=q[i];
		b1[x]=ne[a[x]];b2[ne[a[x]]]=x;
		ne[a[x]]=ne[x];
	}
	for(int i=1;i<=n;i++)if(d[i]){
    
    
		b1[i]=ne[a[i]];b2[ne[a[i]]]=i;
	}
	for(int i=1;i<=n;i++){
    
    
		if(ne[i]==i){
    
    
			sum1+=d[i]?2:f[i];
		}else sum1++;
		sum2+=f[i];
	}
	printf("%lld\n",sum1);
	for(int i=1;i<=n;i++)printf("%d ",b1[i]);
	printf("\n%lld\n",sum2);
	for(int i=1;i<=n;i++)printf("%d ",b2[i]);
}

猜你喜欢

转载自blog.csdn.net/a_forever_dream/article/details/109626187