NOI 2019 题目选做

斗主地

题目描述

点此看题

解法

首先考虑 \(30\) 分的做法,我们可以设计 \(f[i][j]\) 表示前 \(i\) 轮第 \(j\) 个位置的期望分数,\(g[i][j]\) 表示对于现在这一轮的 \(a\),第一堆取走了 \(i\) 个,第二堆取走了 \(j\) 个的概率,转移很容易写。

结论是:一次函数洗牌之后的期望仍然是一次函数,二次函数洗牌后的期望仍然是二次函数由于我的数学功底太差,所以并不能证明这个结论。知道这个结论以后我们用 \(dp\) 维护前三项然后插值即可,时间复杂度 \(O(n+9\cdot m)\)

#include <cstdio>
#include <cstring>
const int M = 105;
#define int long long
const int MOD = 998244353;
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,m,q,w,tp,A,B,C,f[5],l[5],r[5],g[5][5],inv[10000005];
void add(int &x,int y) {x=(x+y)%MOD;}
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
int ask(int x)
{
	return (A*x%MOD*x+B*x+C)%MOD;
}
signed main()
{
	n=read();m=read();tp=read();
	if(tp==1) A=0,B=1,C=0;
	if(tp==2) A=1,B=0,C=0;
	inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++)
		inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=1;i<=m;i++)
	{
		int x=read(),y=n-x;w^=1;
		for(int j=1;j<=3;j++)
			l[j]=ask(j),r[j]=ask(j+x);
		memset(f,0,sizeof f);
		memset(g,0,sizeof g);g[0][0]=1;
		for(int j=0;j<=x && j<=3;j++)
			for(int k=0;k<=y && k<=3;k++)
			{
				int v=inv[x-j+y-k];
				if(j<x) add(g[j+1][k],g[j][k]*v%MOD*(x-j));
				if(k<y) add(g[j][k+1],g[j][k]*v%MOD*(y-k));
			}
		for(int j=1;j<=n && j<=3;j++)
			for(int k=1;k<=j;k++)
			{
				int v=inv[n-j+1];
				if(k<=x) add(f[j],l[k]*g[k-1][j-k]%MOD*(x-k+1)%MOD*v);
				if(k<=y) add(f[j],r[k]*g[j-k][k-1]%MOD*(y-k+1)%MOD*v);
			}
		A=((f[3]-2*f[2]+f[1])*inv[2]%MOD+MOD)%MOD;
		B=((8*f[2]-5*f[1]-3*f[3])*inv[2]%MOD+MOD)%MOD;
		C=((3*f[1]-3*f[2]+f[3])%MOD+MOD)%MOD;
	}
	q=read();
	while(q--) printf("%lld\n",ask(read()));
}

机器人

题目描述

点此看题

解法

这道题基本上自己做出来了,话说后面的优化和今年的联合省选一模一样啊

首先考虑表达限制,很容易把问题联系到笛卡尔树,那么我们借助笛卡尔树的结构来考虑问题。那么就是对于笛卡尔树上的每个点,左儿子区间长度和右儿子区间长度差的绝对值 \(\leq 2\)

那么设计区间 \(dp\),设 \(f[l][r][v]\) 表示区间 \([l,r]\) 的最大值是 \(v\),并且子树内满足条件的方案数,\(g[l][r][v]\) 表示最大值 \(\leq v\) 的方案数,那么转移可以先枚举最大值的位置 \(x\)(最多有 \(3\) 个可能的取值),然后划分成两个区间:

\[f[l][r][v]\leftarrow g[l][x-1][v]\cdot g[x+1][r][x-1] \]

时间复杂度 \(O(n^2B)\),可以得到 \(35\) 分的高分。可以发现有效的区间个数只有 \(m=2000\) 个左右,所以把可能的区间预处理出来,然后 \(dp\) 可以做到 \(O(mB)\),得到了 \(50\) 分的高分!

可以用归纳法证明 \(f,g\) 数组都是不超过 \(n\) 次的分段多项式,按照 \(a_i\)\(b_i+1\) 可以分成若干个左闭右开的区间 \([c_i,c_{i+1})\),我们依次处理每个区间,先计算 \(n+1\) 个点值,然后插值计算出在 \(c_{i+1}-1\) 的点值,因为用来描述函数的点的编号是连续的,所以单次插值可以优化到 \(O(n)\),那么总时间复杂度 \(O(mn^2)\)

由于被卡常了只获得了 \(95\) 分。


还有复杂度更为优秀的做法,我们把区间 \([l,r]\) 计算的点值个数限制在 \(r-l\) 级别,然后如果要用就在线插值,具体见 这篇博客

那么复杂度可以做到 \(O(n\cdot (n^2+(n/2)^2\cdot 3^1+(n/4)^2\cdot 3^2+(n/8)^2\cdot 3^3...))=O(n^3)\)

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 305;
const int N = 2605;
const int MOD = 1e9+7;
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,m,k,a[M],b[M],c[M<<1],f[N][M],id[M][M],vis[M][M];
int T,L,inv[M],tmp[M];
int Abs(int x) {return x>0?x:-x;}
void add(int &x,int y) {x=(x+y)%MOD;}
int mul(int x,int y) {return 1ll*x*y%MOD;}
void init(int l,int r)
{
	if(id[l][r] || l>r) return ;id[l][r]=++k;
	for(int i=l;i<=r;i++) if(Abs(r-i-(i-l))<=2)
		init(l,i-1),init(i+1,r);
}
void work(int l,int r)
{
	if(vis[l][r] || l>r) return ;
	int u=id[l][r];vis[l][r]=1;
	for(int i=l;i<=r;i++)
		if(Abs(r-i-(i-l))<=2 && a[i]<=c[T] && c[T]<b[i])
		{
			work(l,i-1);work(i+1,r);
			int x=id[l][i-1],y=id[i+1][r];
			for(int v=1;v<=L;v++)
				add(f[u][v],1ll*f[x][v]*f[y][v-1]%MOD);
		}
	for(int v=1;v<=L;v++)
		add(f[u][v],f[u][v-1]);
}
void lagrange(int l,int r)
{
	if(l+n>=r)
	{
		for(int u=1;u<=k;u++)
			f[u][0]=f[u][r-l+1];
		return ;
	}
	tmp[n+1]=1;
	for(int i=n;i>=1;i--) tmp[i]=mul(tmp[i+1],r-l-i);
	for(int u=1;u<=k;u++) f[u][0]=0;
	for(int i=l,zxy=1;i<=l+n;i++)
	{
		int xs=mul(tmp[i-l+1],zxy);
		int dw=mul(inv[l+n-i],inv[i-l]);
		if((l+n-i)&1) dw=MOD-dw;xs=mul(xs,dw);
		for(int u=1;u<=k;u++)
			add(f[u][0],1ll*f[u][i-l+1]*xs%MOD);
		zxy=mul(zxy,r-i);
	}
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();b[i]=read()+1;
		c[++m]=a[i];c[++m]=b[i];
	}
	for(int i=0;i<=n+1;i++) f[0][i]=1;
	sort(c+1,c+1+m);m=unique(c+1,c+1+m)-c-1;
	init(1,n);inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++)
		inv[i]=mul(inv[MOD%i],MOD-MOD/i);
	for(int i=2;i<=n;i++)
		inv[i]=mul(inv[i],inv[i-1]);
	for(T=1;T<m;T++)
	{
		L=min(c[T+1]-c[T],n+1);
		memset(vis,0,sizeof vis);
		for(int l=1;l<=n;l++) for(int r=1;r<=n;r++)
			if(id[l][r]) work(l,r);
		lagrange(c[T],c[T+1]-1);
		for(int u=1;u<=k;u++) for(int v=1;v<=L;v++)
			f[u][v]=0;
	}
	printf("%d\n",f[id[1][n]][0]);
}

I 君的探险

题目描述

点此看题

1-5

可以把所有点依次 modify 一遍,然后如果某个点的亮暗状态改变,就说明这两个点之间有边,只检测编号比当前点大的点的亮暗状态可以获得 \(\frac{1}{2}\) 的常数。

6-9

可以依次确定所连点二进制的每一位来做到 \(O(n\log n)\) 的复杂度。具体来说,假设现在考虑到了第 \(w\) 位,就把包含 \(2^w\) 的点给取出来点亮,那么一个点和它的相连点在这一位相等就等价于状态是暗,一个点和它的相连点在这一位不同就等价于状态是亮,那么我们把所有点的亮暗状态都检测一遍即可。

这种构造方法特别常见,要注意积累一下。

10-11

由于每个点只会编号比它小的点连边,那么对于单个点我们可以二分一个前缀,使得点亮这个前缀能改变这个点的亮暗状态。使用整体二分的技巧就可以让复杂度为 \(O(n\log n)\)

12-17

拓展 6-9 的方法,我们发现二进制处理之后,每个点得到的值 \(p[u]\) 就表示和 \(u\) 有边的所有点的编号异或和。

由于是树我们可以从叶子往上构造,我们维护一个队列,初始把 1,2...n 都加入队列里。我们取出队首然后判断它和它的 \(p\) 是否有连边(modify+query+modify 的单次检测方法),如果有边就说明是叶子,把它删除,更改父亲的 \(p\),把父亲加入队列中即可。那么最后达到的效果就是从叶子往上删完了整棵树。

18-25

拓展 10-11 的方法,我们发现如果确定了编号的顺序,那么按照同样的方法,向前缀连边数量奇数的点(简称为奇数点),一定会至少确定它的一条边(奇数点也可能确定多条边,偶数点也可能确定边)

主观上感受奇数点的数量有一半,根据题解所说有 \(n/3\),反正一次整体二分能确定很多边。注意我们要把已经确定的边看作"删除"之后再进行以后的整体二分,以防反复确定同样的边(modify 的时候需要手动改状态)

那么就是循环操作,先随机化一个排列,然后跑整体二分,最后把已经连满的点通过 check 操作去除即可。

不会说明正确性,但是主观上特别有道理

实现

需要数据分治,封装三个 subtask,分别实现 1-510-11(可以顺便处理 6-9),18-25 的方法即可。

void modify(int x);
int query(int x);
void report(int x, int y);
int check(int x);
#include <cstdio>
#include <vector>
#include <iostream>
#include <ctime>
using namespace std;
const int M = 200005;
#define pb push_back
int n,m;
namespace s1
{
	int s[M];
	void main()
	{
		for(int i=0;i<n-1;i++)
		{
			modify(i);
			for(int j=i+1;j<n;j++)
			{
				bool res=query(j);
				if(s[j]^res) report(i,j);
				s[j]=res;
			}
		}
	}
}
namespace s2
{
	void solve(int l,int r,vector<int> p)
	{
		if(l==r)
		{
			for(int x:p) report(x,l);
			return ;
		}
		int mid=(l+r)>>1;
		vector<int> pl,pr;
		for(int i=l;i<=mid;i++) modify(i);
		for(int i=mid+1;i<=r;i++)
			if(query(i)) pl.pb(i);
		for(int x:p)
		{
			if(query(x)) pl.pb(x);
			else pr.pb(x);
		}
		for(int i=l;i<=mid;i++) modify(i);
		solve(l,mid,pl);solve(mid+1,r,pr);
	}
	void main()
	{
		solve(0,n-1,vector<int>());
	}
}
namespace s3
{
	vector<int> o,g[M];int s[M];
	void solve(int l,int r,vector<int> p)
	{
		if(l==r)
		{
			for(int x:p)
			{
				report(x,o[l]);
				g[o[l]].pb(x);g[x].pb(o[l]);
			}
			return ;
		}
		int mid=(l+r)/2;
		vector<int> pl,pr;
		for(int i=l;i<=mid;i++)
		{
			modify(o[i]);
			for(int x:g[o[i]]) s[x]^=1;
		}
		for(int i=mid+1;i<=r;i++)
			if(s[o[i]]^query(o[i])) pl.pb(o[i]);
		for(int x:p)
		{
			if(s[x]^query(x)) pl.pb(x);
			else pr.pb(x);
		}
		for(int i=l;i<=mid;i++)
		{
			modify(o[i]);
			for(int x:g[o[i]]) s[x]^=1;
		}
		solve(l,mid,pl);solve(mid+1,r,pr);
	}
	void main()
	{
		srand(time(0));
		for(int i=0;i<n;i++) o.pb(i);
		while(!o.empty())
		{
			random_shuffle(o.begin(),o.end());
			solve(0,o.size()-1,vector<int>());
			vector<int> r;
			for(int x:o) if(!check(x)) r.pb(x);
			o=r;
		}
	}
}
void explore(int N,int M)
{
	n=N;m=M;
	if(n<=500) s1::main();
	else if(n%10==7 || n%10==8) s2::main();
	else s3::main();
}
posted @ 2022-05-08 10:52  C202044zxy  阅读(102)  评论(0编辑  收藏  举报