• [省选集训2022] 模拟赛16


    小Z与函数

    题目描述

    \(2022/3/24\) 上午,\(\tt zxy\) 看到了一个函数:

    int get(int n)
    {
    	int res=0;
    	for(int i=1;i<=n;i++)
    	{
    		int vs=0;
    		for(int j=i;j<=n;j++) if(a[i]<a[j])
    			swap(a[i],a[j]),res++,vs=1;
    		res+=vs;
    	}
    	return res;
    }
    

    有一个长度为 \(n\) 的序列 \(a\),对于 \(a\) 的每个前缀,将其作为一个序列 \(a\) 所求得的函数值是多少?

    \(T\leq 5,1\leq n\leq 2\cdot 10^5,1\leq a_i\leq n\)

    解法

    社论:我都会做的题一定是垃圾题。我们可以把总次数分成 \(\tt swap\)\(\tt vs\) 两部分分别解决。

    首先考虑 \(\tt swap\) 的次数,其实就是每个点前面比它小的值的个数之和(相同的值要去重),不难证明。

    然后发现这个 \(\tt vs\) 很难做,好像单次计算都要 \(O(n^2)\),那么我们不妨先考虑如何动态插入,达成总时间复杂度 \(O(n^2)\) 的目标。考虑新加入的数对前面的影响,因为选择排序原来是选出一个极长上升子序列,然后做这样的变化:

    上图分别展示了,正常选择排序时的变化,和我们在序列末尾插入(用红点表示)对前面的影响。从插入的角度看,我们是选出一个极长下降子序列(首项为第一个 \(<\) 插入值的数),然后子序列对应的位置都会产生一次交换。

    如果你理解了上面的过程,就不难写出 \(O(n^2)\) 的优秀算法:

    for(int i=1;i<=n;i++)
    	{
    		for(int j=1;j<i;j++) if(a[j]<a[i])
    		{
    			swap(a[j],a[i]);ans++;
    			if(!b[j]) ans++;b[j]|=1;
    		}
    		printf("%d ",ans);
    	}
    

    取出一个极长下降子序列是很难得,但是考虑到每个位置只会被覆盖一次,我们考虑使用势能法,把未覆盖的位置作为势能来让复杂度正确。有一个关键的 \(\tt observation\) 是,如果某个值 \(x\) 在极长下降子序列中去覆盖某个位置,而这个位置先前已经被覆盖过的话,\(x\) 的覆盖以后都不起作用。因为这样一定是存在一个比 \(x\) 小的数先前就把这个位置覆盖过,那么这个较小数一定抢先覆盖了所有 \(x\) 未来所有可能覆盖到的位置。

    所以我们维护一个关于值的 \(\tt set\),每次暴力扫描,用树状数组维护排名,那么要么删除这个值,要么覆盖一个位置。根据势能法,时间复杂度 \(O(n\log n)\),代码实现极为简洁。

    #include <cstdio>
    #include <vector>
    #include <cstring>
    #include <iostream>
    #include <set>
    using namespace std;
    const int M = 200005;
    #define ll long long
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    void write(ll x)
    {
    	if(x>=10) write(x/10);
    	putchar(x%10+'0');
    }
    int T,n,a[M],b[M],use[M];
    struct fenwick
    {
    	int b[M];
    	fenwick() {memset(b,0,sizeof b);}
    	void clear() {memset(b,0,sizeof b);}
    	void add(int x)
    	{
    		for(int i=x;i<=n;i+=i&(-i)) b[i]++;
    	}
    	int ask(int x)
    	{
    		int r=0;
    		for(int i=x;i>0;i-=i&(-i)) r+=b[i];
    		return r;
    	}
    }A,B;
    void work()
    {
    	n=read();long long ans=0;
    	set<int> s;A.clear();B.clear();
    	for(int i=1;i<=n;i++)
    		a[i]=read(),b[i]=use[i]=0;
    	for(int i=1;i<=n;i++)
    	{
    		vector<int> d;
    		for(auto &x:s)
    		{
    			if(x>=a[i]) break;
    			int p=i-B.ask(x);
    			if(!b[p]) b[p]=1,ans++;
    			else if(b[p]) d.push_back(x); 
    		}
    		for(auto &x:d) s.erase(x);
    		int y=a[i];ans+=A.ask(y-1);
    		if(!use[y])
    			s.insert(y),A.add(y),use[y]=1;
    		B.add(y);
    		write(ans),putchar(' ');
    	}
    	puts("");
    }
    signed main()
    {
    	freopen("function.in","r",stdin);
    	freopen("function.out","w",stdout);
    	T=read();
    	while(T--) work();
    }
    

    游戏

    题目描述

    \(n\) 个数的排列,首先进行 \(n\) 次操作,从小到大地把排列中的数插入到 \(a\) 序列的前端或者末尾。然后再进行 \(n\) 次操作,把 \(a\) 序列的前端或者末尾插入到 \(b\) 序列的末尾。

    问有多少种可能被生成的 \(b\) 序列,使得第 \(k\) 位是 \(1\),答案对大质数取模。

    \(k\leq n\leq 5\cdot 10^5\)

    解法

    既然 \(1\) 的位置是关键的,我们按照 \(1\) 的位置把 \(b\) 序列划分成两半。首先我们考虑检验 \(b\) 的前半段是否合法,考虑逆向操作,每次把 \(b\) 的首项插入到 \(a\) 中。

    考虑 \(a\) 的结构一定是从 \(1\) 分开,前面是单调递减,后面是单调递增。那么把 \(b\) 插入时,可以看成对两个独立的单调递减序列在末尾插入。并且这个插入是有最优策略的,如果两边都可以插入,我们贪心地选择值较小的那一边插入,这个插入过程是由唯一策略的,所以可以根据插入过程来计数

    考虑到反序表可以唯一的表示排列,所以我们在规划 \(b\) 时考虑 \(b\) 的反序表 \(d_i\),如果 \(d_i\) 等于 \(0\),那么值小的那一方会接上 \(i\);否则 \(i\) 会接到另一边,我们可以记录下夹在两个末尾中间数的个数,如图(下面是 \(d+1\),对不起标错了):

    \(dp[i][j]\) 表示考虑了前 \(i\) 个数,有 \(j\) 个数夹在中间的方案数,转移:

    \[dp[i][j]=\sum_{k=j-1}^{i-1}dp[i-1][k] \]

    \(suf[i][j]=\sum_{k=j}^i dp[i][k]\),那么把转移写成后缀和的形式:

    \[suf[i][j]=suf[i-1][j-1]+suf[i][j+1] \]

    考虑上式的组合意义,可以对上式进行变换,可以得到卡特兰数的形式:

    \[suf[i][(i-j)]=suf[i-1][(i-j)]-suf[i][(i-j)-1] \]

    那么就是向上走一格或者向右走一格,要求范围在 \([0,i)\) 中的方案数,就是普通的卡特兰数了。这样我们可以求出 \(dp[k-1][i]\),就成功地解决了前半部分,考虑 \(1\) 一定会接在值小的序列末尾,而剩下的数会接在另一个序列末尾。

    根据上面的大小关系我们可以知道选出后半段数的方案是 \({i+(n-k)\choose n-k}\) 的,再乘上任意按按钮的方案数是 \(2^{\max(0,n-k-1)}\)(考虑 \(a\) 生成 \(b\) 的过程),时间复杂度 \(O(n)\)

    #include <cstdio>
    #include <iostream>
    using namespace std;
    const int M = 1000005;
    #define int long long
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,k,MOD,ans,fac[M],inv[M],pw[M];
    void init(int n)
    {
    	fac[0]=inv[0]=inv[1]=pw[0]=1;
    	for(int i=1;i<=n;i++) pw[i]=pw[i-1]*2%MOD;
    	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
    	for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
    	for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%MOD;
    }
    int C(int n,int m)
    {
    	if(n<m || m<0) return 0;
    	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
    }
    int walk(int n,int m)
    {
    	m=n-m;
    	return (C(n+m,n)-C(n+m,n+1)+MOD)%MOD;
    }
    signed main()
    {
    	freopen("game.in","r",stdin);
    	freopen("game.out","w",stdout);
    	n=read();k=read();MOD=read();init(1e6);
    	if(k==1)
    	{
    		printf("%lld\n",pw[max(0ll,n-2)]);
    		return 0;
    	}
    	for(int i=1;i<k;i++)
    	{
    		int dp=(walk(k-1,i)-walk(k-1,i+1)+MOD)%MOD;
    		ans=(ans+dp*C(i+n-k,n-k)%MOD*pw[max(0ll,n-k-1)])%MOD;
    	}
    	printf("%lld\n",ans);
    }
    
  • 相关阅读:
    docker学习笔记及hadoop集群搭建
    Zookeeper+Kafka+Storm+HDFS实践
    zookeeper集群搭建
    scala学习笔记——特质
    scala学习笔记-集合
    scala学习笔记-隐式转换和隐式参数
    RDD 重新分区,排序 repartitionAndSortWithinPartitions
    scala学习笔记——操作符
    JAVA基础系列(一) 概述与相关概念
    网络收藏夹
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/16049898.html
Copyright © 2020-2023  润新知