ZJOI2022 题解
ZJOI2022 部分题目题解
D1T1 [ZJOI2022] 树
题意
按照如下方式生成两棵树:
- 第一棵树:节点 \(1\) 作为树的根,\(\forall i\in[2,n]\),从 \([1,i-1]\) 中选取一个点作为 \(i\) 的父亲。
- 第二棵树:节点 \(n\) 作为树的根,\(\forall i\in[1,n-1]\),从 \([i+1,n]\) 中选取一个点作为 \(i\) 的父亲。
要求对于所有节点 \(i\),\(i\) 恰好是两棵树中的一个的叶子节点。一个节点被称为叶子当且仅当没有节点的父亲是它。两种方案不同当且仅当存在一棵树中的一个节点 \(i\),两种方案中它的父亲不同。
对于 \(n\in[2,N]\),计算方案数 \(\bmod m\)。
数据范围:\(n\le 500\)。
solution
设 \(f(S)\) 表示第一棵树中,叶子集合恰好为 \(S\) 的方案数,\(g(S)\) 表示第二棵树中,叶子集合恰好为 \(S\) 的方案数。为了方便容斥,设 \(f'(S)\) 表示第一棵树中,叶子集合为 \(S\) 的超集的方案数,\(g'(S)\) 表示第二棵树中,叶子集合为 \(S\) 的超集的方案数。
那么
答案是(设 \(U\) 为全集 \(1\sim n\))
最后一步转化是 \((-1)^{|S|-|s|}(-1)^{|S|-|t|}=(-1)^{|s|-|t|}\),合法的 \(S\) 共 \(2^{|s|-|t|}\) 个。
考虑 DP 维护转移系数。\(i\) 对 \(f'(s)\) 的贡献是 \(j\in[1,i-1],j\not \in s\) 的 \(j\) 的数量,\(i\) 对 \(g'(U\setminus s)\) 的贡献是 \(j\in[i+1,n],j\in s\) 的 \(j\) 的数量。那么设 DP 状态 \(f_{i,x,y}\) 表示考虑完前 \(i\) 个点,已经放在 \(s\) 中的点的数量为 \(x\),钦定后面有 \(y\) 个点在 \(t\) 集合内的 \(\sum_{s}f'(s)\sum_{t\subseteq s}g'(U\setminus t)(-2)^{|s|-|t|}\)。
转移有:
分别表示 不属于 \(s\)、属于 \(s\) 也属于 \(t\),只属于 \(s\) 不属于 \(t\)。空间可以滚动一下。
复杂度 \(O(n^3)\)。
view code
#include <bits/stdc++.h>
using namespace std;
const int N=505;
int mod;
int f[2][N][N],n;
int main(){
cin>>n>>mod;
int pre=0,cur=1;
for(int i=1;i<=n;++i)f[0][0][i]=1;
for(int i=1;i<=n;pre^=1,cur^=1,++i){
memset(f[cur],0,sizeof(f[cur]));
int ans=0;
for(int j=0;j<i;++j){
for(int k=0;k<=n-i+1;++k){
int l=(i==1)?1:i-1-j;
f[cur][j][k]=(f[cur][j][k]+1ll*f[pre][j][k]*l*k)%mod;
if(k==1)ans=(ans+1ll*f[pre][j][k]*l)%mod;
if(i>1){
if(k)f[cur][j+1][k-1]=(f[cur][j+1][k-1]+1ll*f[pre][j][k]*l*(k-1))%mod;
f[cur][j+1][k]=(f[cur][j+1][k]-2ll*f[pre][j][k]*l*k)%mod;
}
}
}
if(i>1)printf("%d\n",(ans+mod)%mod);
}
return 0;
}
D1T2 [ZJOI2022] 众数
题意
给你序列 \(a_1,a_2,\cdots a_n\)。你可以进行一次操作,选择一个区间 \([l,r](1\le l\le r\le n)\) 和一个整数 \(k\),将区间 \([l,r]\) 的数 \(+k\)。询问操作之后众数的出现次数的最大值,并输出这个众数的所有可能取值。
数据范围:\(T\) 组数据,\(T\le 20,2\le n\le 2\times 10^5,\sum n\le 5\times 10^5,1\le a_i\le 10^9,\) 所有 \(a_i\) 不全相等。
solution
我们最终的答案一定是选择一个有序对 \((x,y)\),然后选择一个区间,把这个区间内的 \(y\) 全变成 \(x\)(此时这个区间内的 \(x\) 会变成一个 \(\neq x\) 的数),让众数变成 \(x\)。
考虑根号分治。
- 若 \((x,y)\) 中 \(x\) 的出现次数 \(\ge \sqrt{n}\) 或 \(y\) 的出现次数 \(\ge \sqrt n\)
枚举所有出现次数 \(\ge \sqrt{n}\) 的 \(i\)。现在我们要对所有 \(j\) 求出以下两个东西:
- 原序列中,\(i\) 的权值为 \(1\),\(j\) 的权值为 \(-1\),其它位置权值为 \(0\) 的最大子段和(此时众数为 \(j\))。
- 原序列中,\(i\) 的权值为 \(-1\),\(j\) 的权值为 \(1\),其它位置权值为 \(0\) 的最大子段和(此时众数为 \(i\))。
这两个东西都可以 \(O(n)\) 扫一遍,维护每个不同的 \(j\) 的前缀和的最小值求出。
因为出现次数 \(\ge \sqrt{n}\) 的数的数量 \(\le \sqrt{n}\),这一部分的复杂度是 \(O(n\sqrt{n})\)。
- \((x,y)\) 中 \(x,y\) 的出现次数均 \(\le \sqrt{n}\)
我们要求的还是:原序列中,\(i\) 的权值为 \(-1\),\(j\) 的权值为 \(1\),其它位置权值为 \(0\) 的最大子段和(此时众数为 \(j\))。
因为 \(i\) 的出现次数 \(\le \sqrt{n}\),我们找出 \(i\) 的所有区间(这样我们知道里面有多少个 \(-1\),这样的区间最多 \(O(n\sqrt{n})\) 个),现在的问题是 \(O(n\sqrt{n})\) 个区间的区间众数。
直接求区间众数没法快速求出,注意到这个区间众数一定 \(\le \sqrt{n}\)(因为 \(y\) 的出现次数 \(\le\sqrt{n}\)),我们暴力枚举这个区间众数是多少,设为 \(p\),那么我们对每个右端点 \(r\),双指针找到一个最靠右的左端点 \(l\),满足 \([l,r]\) 的区间众数为 \(p\)。我们就能回答所有右端点为 \(r\),左端点 \(\le l\) 的所有询问了。
这一部分的复杂度也是 \(O(n\sqrt{n})\)。
总复杂度 \(O(n\sqrt{n})\)。
view code
#include <bits/stdc++.h>
using namespace std;
inline int read(){
int s=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9')s=(s<<3)+(s<<1)+ch-'0',ch=getchar();
return s*f;
}
const int N=2e5+5;
int a[N],b[N],n,B,T,mx[N],s[N],tcnt[N],tot;
vector<int> ans;
template<typename T>inline void Max(T&a,T b){if(a<b)a=b;}
template<typename T>inline void Min(T&a,T b){if(a>b)a=b;}
namespace sub1{
int buc[N],t,s[N],mn[N],mn1[N];
bool tag[N];
inline void work(int c){
t=0;
for(int i=1;i<=tot;++i)tag[i]=1,mn[i]=0,mn1[i]=0,s[i]=0,buc[i]=i;
t=tot;
int cnt=0;
for(int i=1;i<=n;++i){
if(a[i]==c){
for(int j=1;j<=t;++j)tag[buc[j]]=0;
++cnt;t=0;
}else{
const int col=a[i];
++s[col];
if(!tag[col]){
Min(mn[col],s[col]-1-cnt);
tag[col]=1;
buc[++t]=col;
}
Max(mx[col],cnt-s[col]+1-mn1[col]+tcnt[col]);
Max(mx[c],s[col]-cnt-mn[col]+tcnt[c]);
Min(mn[col],s[col]-cnt);
Min(mn1[col],cnt-s[col]);
}
}
for(int i=1;i<=tot;++i)if(i!=c)Max(mx[i],cnt-s[i]-mn1[i]+tcnt[i]);
}
}
int curmx;
namespace sub2{
vector<int> buc[N];
int cnt[N],ccnt[N],app;
inline void insert(int x){
--ccnt[cnt[x]];
++ccnt[++cnt[x]];
Max(app,cnt[x]);
}
inline void del(int x){
--ccnt[cnt[x]];
++ccnt[--cnt[x]];
if(!ccnt[app])--app;
}
int L[N],rk[N],L1[N];
inline void work(){
for(int i=B;i>=1&&i+B>=curmx;--i){
int r=0,l=1;
app=0;
memset(cnt+1,0,tot<<2);
memset(ccnt+1,0,n<<2);
ccnt[0]=tot;
while(r<=n&&app<i)
insert(a[++r]);
if(app<i)continue;
del(a[r]);
for(;r<=n;++r){
insert(a[r]);
while(app>=i)del(a[l++]);
insert(a[--l]);
if(r==n){
for(int c=1;c<=tot;++c){
if(tcnt[c]>=B)continue;
while(L1[c]<=tcnt[c]&&buc[c][L1[c]]<=l){
Max(mx[c],app-(tcnt[c]-L1[c])+tcnt[c]);
Max(curmx,mx[c]);
++L1[c];
}
}
break;
}
int c=a[r+1];
if(tcnt[c]>=B)continue;
while(L[r+1]<rk[r+1]&&buc[c][L[r+1]]<=l){
Max(mx[c],app-(rk[r+1]-L[r+1]-1)+tcnt[c]);
Max(curmx,mx[c]);
++L[r+1];
}
}
}
}
inline void init(){
for(int i=1;i<=tot;++i){
buc[i].clear();
buc[i].push_back(0);
L1[i]=0;
}
for(int i=1;i<=n;++i)L[i]=0,buc[a[i]].push_back(i),rk[i]=buc[a[i]].size()-1;
}
}
int main(){
T=read();
while(T--){
n=read();
for(int i=1;i<=n;++i)a[i]=read(),b[i]=a[i];
tot=n;curmx=0;
sort(b+1,b+1+tot);
tot=unique(b+1,b+1+tot)-b-1;
for(int i=1;i<=n;++i)
a[i]=lower_bound(b+1,b+1+tot,a[i])-b;
for(int i=1;i<=tot;++i)mx[i]=0,tcnt[i]=0;
for(int i=1;i<=n;++i)++tcnt[a[i]];
B=sqrt(n);
for(int i=1;i<=tot;++i)if(tcnt[i]>=B)sub1::work(i);
for(int i=1;i<=tot;++i)Max(curmx,mx[i]);
sub2::init();
sub2::work();
int maxx=0;
for(int i=1;i<=tot;++i)Max(maxx,mx[i]);
printf("%d\n",maxx);
for(int i=1;i<=tot;++i)
if(mx[i]==maxx)printf("%d\n",b[i]);
}
return 0;
}
D2T2 [ZJOI2022] 计算几何
题意
(写不动简要题意了)
九条可怜是一个喜欢计算几何的女孩子,她画了一个特别的平面坐标系,其中 xx 轴正半轴与 yy 轴正半轴夹角为 \(60\) 度。
从中,她取出所有横纵坐标不全为偶数,且满足 \(-2 a + 1 \le x \le 2 a - 1\),\(-2 b + 1 \le y \le 2 b - 1\),\(-2 c + 1 \le x + y \le 2 c - 1\) 的整点。
可怜想将其中一些点染色,但相邻的点不能同时染色。具体地,对于点 \((x,y)\),它和 \((x, y + 1), (x, y - 1), (x + 1, y), (x - 1, y), (x + 1, y - 1), (x - 1, y + 1)\) 六个点相邻,可结合样例解释理解。
可怜想知道在这个规则下最多能将多少点染色,以及染最多点的染色方案数。由于后者值可能很大,对于染色方案数,你只需要输出对 998244353 取模后的结果。注意不需要将最多染色点数取模。
(其中 A,Q,H 三点是被删除的,不能被选择)
数据范围:\(1\le a,b,c\le 10^6\),\(T\) 组数据,\(1\le T\le 10\)
solution
把所有横纵坐标都是偶数的点连出变成为 \(2\) 的等边三角形,容易发现,所有没被删除的点都是一个等边三角形某一边的中点。把两个有公共边的等边三角形连边,这条边就表示这个边上的中点选不选
原问题中,两个点不能相邻的条件相当于新的图上,两条被选择的边不能有公共点。即一个最大匹配计数。
容易发现,这个题转化之后与 [Cnoi2021]六边形战士。其中原文题的 \(A=\max(0,a+b-c),B=\max(0,a+c-b),C=\max(0,b+c-a)\)。ZJOI 这题还需要求最大匹配的大小,这个最大匹配是完美匹配,所以最大匹配是 \(A\times B+B\times C+A\times C\)。
view code
#include <bits/stdc++.h>
using namespace std;
const int N=1e7+5,mod=998244353;
int T;
inline int Inv(int a){
int ret=1,b=mod-2;
for(;b;b>>=1,a=1ll*a*a%mod)if(b&1)ret=1ll*ret*a%mod;
return ret;
}
int fac[N],h[N];
inline int calc(int a,int b,int c){
int n=a+b+c;
return 1ll*h[a]*h[b]%mod*h[c]%mod*h[n]%mod*Inv(1ll*h[a+b]*h[b+c]%mod*h[a+c]%mod)%mod;
}
int a[100],b[100],c[100],mx;
inline void init(int n){
fac[0]=1;h[0]=1;
for(int i=1;i<=n;++i)fac[i]=1ll*fac[i-1]*i%mod,h[i]=1ll*h[i-1]*fac[i-1]%mod;
}
int main(){
cin>>T;
for(int i=1;i<=T;++i){
cin>>a[i]>>b[i]>>c[i];
mx=max(mx,a[i]+b[i]+c[i]);
}
init(mx);
for(int i=1;i<=T;++i){
a[i]=min(a[i],b[i]+c[i]);
b[i]=min(b[i],a[i]+c[i]);
c[i]=min(c[i],a[i]+b[i]);
int a1=a[i]+b[i]-c[i],b1=a[i]+c[i]-b[i],c1=b[i]+c[i]-a[i];
printf("%lld %d\n",1ll*a1*b1+1ll*b1*c1+1ll*a1*c1,calc(a1,b1,c1));
}
return 0;
}