YsOI2023 小记

D2T1

签。

#include <cstdio>
using namespace std;
int read(){/*...*/}
typedef long long ll;
void solve(){
	ll n=read()-1,x=read();
	ll y=x;
	while(~y&1) y>>=1;
	for(int i=1;i<n;++i) y<<=1;
	if(x>y) y=x;
	printf("%lld\n",y);
}
int main(){
	int tc=read();
	while(tc--) solve();
	return 0;
}

D2T2

诈骗。发现翻转一个区间一定不会增加新的灵异区间,直接统计初始值即可。

#include <cstdio>
#include <unordered_map>
using namespace std;
int read(){/*...*/}
typedef long long ll;
const int N=100003;
int n,a[N];
int main(){
	n=read();
	unordered_map<int,int> mp;
	long long res=0;
	++mp[0];
	for(int i=1;i<=n;++i)
		res+=mp[a[i]=read()^a[i-1]]++;
	printf("%lld\n",res);
	return 0;
}

D1T1

首先由于 bfs 树分层,我们只需要对于跨层边考虑,发现限制相当于对于连着同一个节点一条树边和一条非树边,求出这两条边的另一个端点的 LCA,那么对于这个 LCA 分别往这两个端点延伸的树边有一个偏序的限制,拓扑排序即可。注意重边。

#include <cstdio>
#include <vector>
using namespace std;
int read(){/*...*/}
typedef long long ll;
const int N=200003,M=400003;
vector<int> vec[N];
int n,m;
int eu[M],ev[M];
namespace D{
	const int N=::M;
	const int M=1000003;
	int hd[N],ver[M],nxt[M],tot;
	int deg[N];
	void add(int u,int v){
		++deg[v];nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;
	}
	int que[N],tl;
	void topo(){
		for(int i=1;i<=m;++i) if(!deg[i]) que[++tl]=i;
		for(int pos=1;pos<=tl;++pos){
			int u=que[pos];
			printf("%d %d\n",eu[u],ev[u]);
			for(int i=hd[u];i;i=nxt[i]){
				int v=ver[i];
				if(!--deg[v]) que[++tl]=v;
			}
		}
	}
}
namespace G{
	int hd[N],ver[M<<1],nxt[M<<1],tot=1;
	void add(int u,int v){
		nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;
	}
}
namespace T{
	int hd[N],ver[N<<1],nxt[N<<1],tot;
	void add(int u,int v){
		nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;
	}
	int de[N];
	void dfs(int u){
		vec[de[u]].emplace_back(u);
		for(int i=hd[u];i;i=nxt[i]){
			int v=ver[i];
			de[v]=de[u]+1;
			dfs(v);
		}
	}
}
using T::de;
int p[N],bel[N],f[18][N];
int main(){
	n=read();m=read();
	for(int i=1;i<=m;++i){
		int u=eu[i]=read();
		int v=ev[i]=read();
		G::add(u,v);G::add(v,u);
	}
	int rt=0;
	for(int i=1;i<=n;++i){
		f[0][i]=p[i]=read();
		if(!p[i]) rt=i;
		else T::add(p[i],i);
	}
	for(int t=1;t<18;++t)
		for(int i=1;i<=n;++i) f[t][i]=f[t-1][f[t-1][i]];
	T::dfs(rt);
	for(int d=1;d<=n;++d)
		for(int x:vec[d]){
			for(int i=G::hd[x];i;i=G::nxt[i]){
				int v=G::ver[i];
				if(v==p[x]) bel[x]=i>>1;
			}
			for(int i=G::hd[x];i;i=G::nxt[i]){
				int v=G::ver[i];
				if((i>>1)==bel[x]) continue;
				if(de[v]==d-1){
					int u=p[x];
					for(int t=17;~t;--t)
						if(f[t][u]!=f[t][v]){u=f[t][u];v=f[t][v];}
					if(u!=v) D::add(bel[u],bel[v]);
					else D::add(bel[x],i>>1);
				}
			}
		}
	D::topo();
	return 0;
}

D1T2

\(m=n-1\) 为 Prufer 序板子,\(m=n\) 考虑建出仙人掌圆方树,由于只有一个方点相当于做 \(n+1\) 个点的树的情况。

\(m=n+1\) 依旧考虑建圆方树,对于有两个非平凡点双的情况相当于做 \(n+2\) 个点的树,但是要容斥去两个方点相邻的情况。

对于只有一个点双,发现点双形态一定是杏仁,简单计算即可。

#include <cstdio>
using namespace std;
int read(){/*...*/}
typedef long long ll;
const int N=100003,P=998244353;
int fac[N],fiv[N];
int qp(int a,int b=P-2){/*...*/}
int n,m;
int a[N];
namespace tree{
	void solve(){
		int sum=0;
		int res=fac[n-2];
		for(int i=1;i<=n;++i){
			res=(ll)res*fiv[a[i]-1]%P;
			sum+=a[i]-1;
		}
		if(sum!=n-2) puts("0");
		else printf("%d\n",res);
	}
}
namespace cir{
	void solve(){
		int sum=0;
		int res=fac[n-1];
		for(int i=1;i<=n;++i){
			res=(ll)res*fiv[a[i]-1]%P;
			sum+=a[i]-1;
		}
		if(sum>n-2) puts("0");
		else{
			if(res&1) res+=P;
			res>>=1;
			printf("%d\n",res);
		}
	}
}
int C(int n,int k){return (ll)fac[n]*fiv[k]%P*fiv[n-k]%P;}
namespace sol{
	int ss;
	int calc(){
		int res=fac[n-1],sum=0;
		for(int i=1;i<=n;++i){
			res=(ll)res*fiv[a[i]-1]%P;
			sum+=a[i]-1;
		}
		if(sum>n-1) return 0;
		res=(ll)res*fiv[n-1-sum]%P;
		ss=sum;
		return res;
	}
	int tmp;
	int sub1(){
		int k=n-ss;
		if(k<=3) return 0;
		int coe=((ll)k*(k-1)>>1)%P;
		coe=(ll)coe*(C(k,2)+P-3)%P*fac[k-2]%P;
		coe=(ll)coe*qp(6)%P;
		coe=(ll)coe*tmp%P;
		return coe;
	}
	const int ivs=qp(4);
	int sub2(){
		int res=fac[n],sum=0;
		for(int i=1;i<=n;++i){
			res=(ll)res*fiv[a[i]-1]%P;
			sum+=a[i]-1;
		}
		if(sum>n) return 0;
		int cur=0;
		for(int i=0;i<=n-sum;++i){
			int coe=(ll)res*fiv[i]%P*fiv[n-sum-i]%P;
			coe=(coe-(ll)tmp*C(n-sum,i))%P;
			if(coe<0) coe+=P;
			if(i>1&&n-sum-i>1)
				cur=(cur+(ll)fac[i]*fac[n-sum-i]%P*coe%P*ivs)%P;
		}
		if(cur&1) cur+=P;
		return cur>>1;
	}
	void solve(){
		tmp=calc();
		int res=sub1()+sub2();
		if(res>=P) res-=P;
		printf("%d\n",res);
	}
}
int main(){
	n=read();m=read();
	for(int i=1;i<=n;++i) a[i]=read();
	fac[0]=1;
	for(int i=1;i<=n;++i) fac[i]=(ll)fac[i-1]*i%P;
	fiv[n]=qp(fac[n]);
	for(int i=n;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
	if(m==n-1){
		tree::solve();
		return 0;
	}
	if(m==n){
		cir::solve();
		return 0;
	}
	sol::solve();
	return 0;
}

D1T3

考虑如何形式化 Prufer 序的过程,相当于维护一个当前的叶子点集 \(S\) 和已经删除的点集 \(T\),每次移除叶子点集中最小的点到删除点集,然后看当前点是否能加入叶子点集。

由于这道题 Prufer 序整体形态不确定,我们可以先枚举初始的叶子点集(相当于在 DP 数组初始化时将这些点置为 1),然后每次遇到序列中的一个点决策其是否加入子序列中/是否加入叶子点集。只要最后 \(|S|=1,|T|=n-2\) 那么就说明中途的 Prufer 序其实合法。

现在考虑统计距离期望。我们把 DP 倒过来,每次对于每个不在 \(T\) 中的点 \(x\) 记录所有可能距离总和及其方案数。相当于设 \(DP_{S,T,x}\) 如果当前最小的叶子就是 \(x\),那么将它连上 \(a_t\) 的父亲时它的值要从 \(DP_{*,*,a_t}\) 处继承。

考虑优化掉 \(3^n\)。注意到 \(S,T\) 实际上的对数不是很多,事实上,结合 Prufer 序的 \(O(n)\) 转化法,我们发现 \(S\) 大概是 \(S\cup T\) 的一段后缀,但除了一个新加的点例外。所以它的实际总数是 \(O(2^n n^2)\) 的。然而这还不足以通过。

我们发现当前 \(S\) 中最小的点很快就要被删掉了,我们其实并不关心它具体是多少,只用知道它是不是 \(x\)。所以只记录一个 \(0/1\) 状态,总复杂度 \(O(2^n n^2 m)\)

由于鬼畜的数据范围全程 vector,但是注意不要每次将数组滚动清空,否则会 T。

#include <cstdio>
#include <array>
#include <vector>
#include <algorithm>
#define fi first
#define se second
typedef long long ll;
using namespace std;
int read(){/*...*/}
typedef pair<int,int> pii;
typedef vector<int> vi;
typedef array<pii,2> ar;
typedef vector<ar> va;
typedef vector<va> vva;
typedef vector<vva> vvva;
const int P=1000000007;
void inc(int &x,int v){if((x+=v)>=P) x-=P;}
int n,m,_n,len;
int jump(int s,int x){
	++x;
	if(!(s>>x)) return _n;
	return __builtin_ctz(s>>x)+x;
}
int qp(int a,int b=P-2){/*...*/}
int main(){
	n=read();m=read();_n=n-1;len=(1<<_n);
	vi arr(m),res(_n);
	for(int i=0;i<m;++i) arr[m-i-1]=read()-1;
	vvva f(len,vva(n,va(n)));
	for(int i=0;i<n;++i){
		f[len-1][_n][i][0]=pii(0,1);
		f[len-1][_n][i][1]=pii(1,1);
	}
	vi fac(m+1),fiv(m+1);
	fac[0]=1;
	for(int i=1;i<=m;++i) fac[i]=(ll)fac[i-1]*i%P;
	fiv[m]=qp(fac[m]);
	for(int i=m;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
	for(int x=0;x<m;++x){
		for(int s=1;s<len;++s){
			if(s>>arr[x]&1) continue;
			for(int lim=0;lim<n;++lim)
				if(lim==_n||(s>>lim&1))
					for(int t=0;t<n;++t){
						if(arr[x]<_n){
							int _s=s|(1<<arr[x]);
							int _lim=jump(_s,lim);
							if(arr[x]<lim){
								inc(f[s][lim][t][0].fi,f[_s][lim][t][arr[x]==t].fi);
								inc(f[s][lim][t][0].se,f[_s][lim][t][arr[x]==t].se);
								inc(f[s][lim][t][1].fi,f[_s][lim][arr[x]][1].fi);
								inc(f[s][lim][t][1].se,f[_s][lim][arr[x]][1].se);
								inc(f[s][lim][t][1].fi,f[_s][lim][arr[x]][1].se);
							}
							else{
								inc(f[s][lim][t][0].fi,f[_s][_lim][t][lim==t].fi);
								inc(f[s][lim][t][0].se,f[_s][_lim][t][lim==t].se);
								inc(f[s][lim][t][1].fi,f[_s][_lim][arr[x]][lim==arr[x]].fi);
								inc(f[s][lim][t][1].se,f[_s][_lim][arr[x]][lim==arr[x]].se);
								inc(f[s][lim][t][1].fi,f[_s][_lim][arr[x]][lim==arr[x]].se);
							}
						}
						if(lim<_n){
							int _lim=jump(s,lim);
							inc(f[s][lim][t][0].fi,f[s][_lim][t][lim==t].fi);
							inc(f[s][lim][t][0].se,f[s][_lim][t][lim==t].se);
							inc(f[s][lim][t][1].fi,f[s][_lim][arr[x]][lim==arr[x]].fi);
							inc(f[s][lim][t][1].se,f[s][_lim][arr[x]][lim==arr[x]].se);
							inc(f[s][lim][t][1].fi,f[s][_lim][arr[x]][lim==arr[x]].se);
						}
					}
		}
	}
	int iv=(ll)fac[n-2]*fac[m-n+2]%P*fiv[m]%P;
	for(int s=1;s<len;++s){
		int lb=__builtin_ctz(s);
		int p=jump(s,lb);
		for(int i=0;i<_n;++i) inc(res[i],f[s][p][i][lb==i].fi);
	}
	for(int i=0;i<_n;++i) res[i]=(ll)res[i]*iv%P;
	for(int i=0;i<_n;++i) printf("%d ",res[i]);
	putchar('\n');
	return 0;
}

D1T4

好题。对于一个有限的正整数集 \(S\)\(F(S)=\{x-y|x,y\in S,x>y\}\) 有一个大小下界 \(|S|-1\),而这个大小下界仅在等差数列处取到。

回顾题目中的博弈过程,我们发现轮到某一个固定的人操作时两个人数集大小的差是一样的。而当正在操作的人的数集大小比另一个人至少大二,那么显然这个人永远有办法给对方插入一个数。此时由于游戏必然有限轮内结束这个人必胜。

设先手 \(n\) 个数,后手 \(m\) 个数,当 \(m<n-1\) 时先手必胜,\(m>n\) 时后手必胜。

注意到 \(|F(S)|\) 取到 \(|S|-1\) 的条件是很苛刻的,绝大多数情况下我们可以认为 \(m=n\) 后手胜,\(m=n-1\) 先手胜。

考虑何时出现“以少胜多”的反例。结束局面时输家有一个 \(t+1\) 项的等差数列 \(\{a,a+d,a+2d,\dots,a+td\}\),而赢家正好有 \(\{d,2d,\dots,td\}\)

如果 \(m=n-1\) 一开始就是这种局面先手就输了!否则我们发现输家至少有一个值是赢家给的,所以 \(d|a\)

在最极端的情况下,输家恰好拥有 \(\{d,2d,\dots td,(t+1)d\}\)。此时至少 \(td,(t+1)d\) 是输家一开始就有的,而只要赢家的最大值不超过 \(td\),可以证明一定合法。

接下来考虑计数。

需要我们特殊计数的三个情况分别是:

  • 先手 \(n+1\) 数,后手 \(n\) 数,先手是首项任意公差为 \(d\) 的等差数列,后手是首项为 \(d\) 公差为 \(d\) 的等差数列。后手胜。

  • 先手 \(n+1\) 数,后手 \(n\) 数,先手最大两数为 \((t+1)d,td\),后手所有数不超过 \(td\),且所有数都是 \(d\) 倍数。后手胜。

  • 先手 \(n\) 数,后手 \(n\) 数,后手最大两数为 \((t+1)d,td\),先手所有数不超过 \(td\),且所有数都是 \(d\) 倍数。先手胜。

对于第一种可以直接 bitset 统计。枚举公差 \(d\),然后每次 bs&=bs<<d。一共 \(O(a\ln a)\)bitset 操作,复杂度 \(O(\frac{a^2\ln a}{\omega})\) 正好卡在 \(10^8\) 左右,实际上非常快。

对于第二、三种情况出题人题解没有讲,自己推了一下。依旧枚举公差 \(d\),这时候需要考虑的数的总量是 \(O(a\ln a)\) 级别的。我们可以枚举合法的 \(td,(t+1)d\),然后设在给定集合中 \(\leq td\) 的个数为 \(c\)。这里以第二种情况为例,我们枚举后手选的数的个数 \(p\)。方案数就是:

\[\sum_{p=1}^c {c \choose p} {c-1\choose p-1} \]

这个式子看起来很像经典问题“组合数平方和”,我们可以用范德蒙德卷积化:

\[\begin{aligned} &\sum_{p=1}^c {c \choose p} {c-1\choose p-1}\\ &=\sum_{p=1}^c {c \choose c-p} {c-1\choose p-1}\\ &={2c-1\choose c-1} \end{aligned} \]

第三种情况同理。

还有一个小问题,第一种和第二种情况会算重一点点。但这样先手和后手分别只会拿一段连续等差数列的前缀和后缀,可以放在一起简单统计。

int main(){
	n=read();
	for(int i=1;i<=n;++i) msk.set(a[i]=read());
	int mx=a[n];
	fac[0]=1;
	for(int i=1;i<=mx+mx;++i) fac[i]=(ll)fac[i-1]*i%P;
	fiv[mx+mx]=qp(fac[mx+mx]);
	for(int i=mx+mx;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
	int tmp=0,res=0;
	for(int i=0;i<=n;++i){
		int cur=C(n,i);
		if(i>1) res=(res+(ll)cur*tmp)%P;
		inc(tmp,cur);
	}
	for(int t=1;t<=mx;++t){
		bs=msk;
		for(int i=t;i<=mx;i+=t)
			if(msk[i]){
				bs&=(bs<<t);
				int cc=bs.count();
				if(!cc) break;
				res-=cc;
			}
			else break;
		while(res<0) res+=P;
	}
	for(int t=1;t<=mx;++t){
		int cnt=0,cont=0,las=0;
		bool fl=1;
		for(int i=t;i<=mx;i+=t){
			if(msk[i]) ++cnt,++las;
			else fl=0,las=0;
			if(fl) ++cont;
			if(i+t<=mx&&msk[i]&&msk[i+t]){
				res=(res-C(cnt+cnt-1,cnt-1))%P;
				if(cnt>1) res=(res+C(cnt+cnt-1,cnt-2))%P;
				res=(res+min(cont,las))%P;
			}
		}
	}
	while(res<0) res+=P;
	printf("%d\n",res);
	return 0;
}
posted @ 2023-08-15 09:47  yyyyxh  阅读(57)  评论(0编辑  收藏  举报