【2023.11.14】NOIP2023模拟试题-34

发布时间 2023-11-14 20:41:39作者: DZhearMins

T1

个人认为 T1 比 T2 难。

首先我们可以把答案转化成 \(A\)\(P\) 的子序列串的数量 减去 \(f_K(P)=A\) 的串数量。

\(A\)\(P\) 的子序列串的数量显然是 \(C_n^k\times(n-k)\;!=\frac{n!}{k!}\) .

而考虑 \(f_K(P)=A\) 的串的数量,先考虑当 \(P\) 是升序序列的情况。

对于串 \(P=\) 2 3 5 7 8 ,考虑按顺序插入 \(1\) , \(4\) , \(6\) , \(9\) , \(10\) 得到长度为 \(10\) 的排列 \(A\)

显然 \(1\) 只能插在 \(2\) 之前得到 1 2 3 5 7 8

\(4\) 可以插在 \(1\) 之前,也可以插在 \(1,2、2,3\) 之间。由于 \(3\) 是最后一个 \(<4\) 的数,\(3\) 之后就不能放 \(4\) 了,否则 \(P\) 串就可以被字典序更小的 4 顶替掉一个位置。

\(6\) 可以插在 \(1\) 之前,也可以插在 \(2,3,4\) 之前一共 \(4\) 个空位里。

\(9\)\(P\) 排列的所有数都更大,可以插进 \(1,2,3,4,5,6,7,8\) 之前的 \(8\) 个空位里,也可以插进序列末尾,一共 \(9\) 个选择。

\(10\) 由于多了一个 \(9\) 之前的空位,一共有 \(10\) 个可以插的地方。

因此,满足 \(f_K(P)=A\) 的长度为 \(10\) 的序列一共有 \(1\times 3 \times 4 \times 9 \times 10 = 1080\) 个。因此答案是 \(29160\) .

我们总结规律得出,设缺失的数为 \(i\),假设 \(i\) 以前的数都填了,填了 \(aprd\) 个,那么

\[ans\times=(aprd+ (\text{upper\_bound}(i)-1)) \]

由于 upper_bound 查询的结果一定随着 \(l\) 的单调递增而单调递增,所以我们可以用双指针优化,用 \(las\) 存储当前指针。

#define fi(l,r) for(int i=l;i<=r;++i)
	fi(1,n){
		if(apr[i]==0){
			while(las<kk&&a[las+1]<i)++las;
			sub=sub*(las+aprd)%P;
			++aprd;
		}
	}

至于如果序列不是升序,我们只用去前面最长的连续一节升序序列就行了,后面的序列没卵用。为什么?因为无论插什么数都不能插在后面数之后的空位里,比如这个例子:

2 3 8 5 7

8 后面 \(1,4,6,9,10\) 都不能插,不然就给了前半部分序列字典序更小的选择。

所以 8 5 7 一定会放在末尾。

参考代码

#include<bits/stdc++.h>
using namespace std;
#define fi(l,r) for(int i=l;i<=r;++i)
#define ff(i,l,r) for(int i=l;i<=r;++i)
#define ll long long
#define P 998244353
#define N 1000005
ll qpow(ll a,ll b){
	ll c=1;
	while(b){
		if(b&1)c=c*a%P;
		b>>=1;
		a=a*a%P;
	}
	return c;
}
ll inv(ll x){return qpow(x,P-2);}
ll fac[N],ifac[N],ans=1,sub=1;
bool apr[N]={0};
#define C(n,m) (fac[n]*ifac[m]%P*ifac[n-m]%P)
int n,kk,a[N],last=1,aprd,las;
int main(){
	freopen("ordinary.in","r",stdin);
	freopen("ordinary.out","w",stdout);
	scanf("%d %d",&n,&kk);
	fi(1,kk){
		scanf("%d",&a[i]);
		apr[a[i]]=1;
		if(a[last]>a[last-1])++last;
	}
	fac[0]=1;
	fi(1,n)fac[i]=fac[i-1]*i%P;
	ifac[n]=inv(fac[n]);
	for(int i=n-1;i>=1;--i)ifac[i]=ifac[i+1]*(i+1)%P;
	ans=fac[n]*ifac[kk]%P;
	fi(1,n){
		if(apr[i]==0){
			while(las<last&&a[las+1]<i)++las;
			sub=sub*(las+aprd)%P;
			++aprd;
		}
	}
	printf("%lld\n",(ans%P-sub%P+P)%P);
	return 0;
}

T2

这不比 T1 简单?

先建括号树,节点的编号 \(i\) 对应着原序列从左往右数第 \(i\) 个左括号(及其对应的右括号)。

对于括号序列

可爱的括号

我们分析一下如何操作才能得出序列 ()()()()()()()()()

可爱的动画

有几步转换不止一个操作。

我们发现只需要 \(8\) 步就行了。

而且仔细观察动画,我们发现将 \(D\) 序列转换为 \(F\) 序列就是把链变成菊花图。

\(F\) 转换为 \(D\) 就是把菊花图转化为链。

更细致地观察,我们发现:一个菊花带上父节点需要 \(2\) 步操作才能让父节点也变进菊花里面。

一个链带上父节点需要 \(1\) 步操作可以把父节点和链变成一朵菊花。

所以我们大胆推出结论:原括号树中一个菊花对应需要 \(2\) 步操作,一个链对应需要 \(1\) 步操作,只需要把菊花和链的个数加起来就是变成大菊花图的步数,即:把每个点的贡献加起来:

#define fi(l,r) for(int i=l;i<=r;++i)
void work(int t){
	fi(1,n){
		if(scnt[t][i]>1)ans[t]+=2;//菊花图:ans+2
		else if(scnt[t][i]==0&&scnt[t][fah[t][i]]==1)++ans[t];//链:ans+1
	}
}

接下来我们考虑两个括号序列之间的转化:

3

通法当然可以将两棵树都转化为 ()()()()()()()()() ,但这并不保证最优解,我们考虑哪些地方是重复操作的。

注意到当两颗树的 \(5\) 子树操作都完成以后,\(5\) 子树就不用动了,不需要转化为平凡序列,因为两颗子树已经相同。

同样的, \(2\) 子树也不需要进一步地操作。

即:我们可以保留 \(1,2,5\) 的结构不动,不计算这三个点的贡献。

显然,当子树包含相同的节点的时候才会保留,那么如何 \(O(n)\) 求出所有节点是否保留呢?(考试的时候就是在这里寄掉了)

其实我们只需要统计子树大小就行了,因为这棵树的节点编号与 dfs 序是一致的,所以任意子树的节点形成的有序序列一定完全等价于升序序列 \([i,i+siz_i-1]\)

对了,还有一个条件就是父亲要相同而且父亲也要是相等的节点,不然子树也会被父亲尝试趋同的操作而被打乱。

#define fi(l,r) for(int i=l;i<=r;++i)
#define ff(i,l,r) for(int i=l;i<=r;++i)
#define fab(sth) ff(t,0,1)
for(int i=n;i>=1;--i)
	fab()
		siz[t][fah[t][i]]+=siz[t][i];
fi(1,n)
	if(fah[0][i]==fah[1][i]&&sam[fah[0][i]]==1&&siz[0][i]==siz[1][i])sam[i]=1;

参考代码

查看代码
#include<bits/stdc++.h>
using namespace std;
#define fi(l,r) for(int i=l;i<=r;++i)
#define ff(i,l,r) for(int i=l;i<=r;++i)
#define ll long long
#define N 500005
#define M 1000005
int fah[2][N],n,L,pcnt,stk[N],stl,posofp[M],posof[M],scnt[2][N],ans[3],siz[2][N];//ans2
bool sam[N]={0};
#define fab(sth) ff(t,0,1)
void nothing(int t){
	char ch=getchar();
	while(ch!='(')ch=getchar();
	fi(1,L){
		stk[++stl]=(ch=='(');
		posof[stl]=i;
		if(ch=='(')posofp[i]=++pcnt;
		else while(stk[stl-1]==1&&stk[stl]==0){
			fah[t][posofp[posof[stl-1]]]=posofp[posof[stl-2]];
			++scnt[t][posofp[posof[stl-2]]];
			stl-=2;//解析括号序列并建树:将栈顶的 fah 指向下一个元素
		}
		ch=getchar();
	}
	if(t==1){
		for(int i=n;i>=1;--i)fab()siz[t][fah[t][i]]+=siz[t][i];
		fi(1,n)if(fah[0][i]==fah[1][i]&&sam[fah[0][i]]==1&&siz[0][i]==siz[1][i])sam[i]=1;
	}
	fi(1,n){
		if(sam[i]==1){
			if(scnt[0][i]>1)ans[2]+=2;//菊花图:ans+2
			else if(scnt[0][i]==0&&scnt[0][fah[0][i]]==1)++ans[2];//链:ans+1
		}else{
			if(scnt[t][i]>1)ans[t]+=2;//菊花图:ans+2
			else if(scnt[t][i]==0&&scnt[t][fah[t][i]]==1)++ans[t];//链:ans+1
		}
	}
}
int main(){
	freopen("miracle.in","r",stdin);
	freopen("miracle.out","w",stdout);
	scanf("%d",&L);
	n=L>>1;sam[0]=1;
	fi(1,n)siz[0][i]=siz[1][i]=1;
	nothing(0);
	stl=pcnt=0;
	nothing(1);
	printf("%d\n",ans[0]+ans[1]-ans[2]);
	return 0;
}

说在后面

先谈一谈我对巴以战争的看法吧