【2023.11.14】NOIP2023模拟试题-34

T1

个人认为 T1 比 T2 难。

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

AP 的子序列串的数量显然是 Cnk×(nk)!=n!k! .

而考虑 fK(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,22,3 之间。由于 3 是最后一个 <4 的数,3 之后就不能放 4 了,否则 P 串就可以被字典序更小的 4 顶替掉一个位置。

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

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

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

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

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

ans×=(aprd+(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+sizi1]

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

#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;
}

说在后面

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

posted @   DZhearMins  阅读(34)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示