do_while_true

一言(ヒトコト)

2022 CCPC Henan Provincial Collegiate Programming Contest (2022年CCPC河南省赛)解题报告(确信)

比赛地址

Difficulty 后面跟的是我认为的难度,如果和官方题解不一样那么也会给出官方题解的难度。

整体难度比预想中的要简单,除了计算几何和大模拟选择跳过,防 AK 的 poly 题没做出来以外,其他题都做出来了。虽然有的题复杂度 / 实现方式 / 做法比官方题解更劣,姑且还是都写了题解。

我觉得 idea 不错的题:BCFHJK.

并不是很好的题(太过简单 / 几乎是板子 / 无趣的套路):AEL.

从正常比赛来看,由于我对这场比赛的期望难度比实际难度要高,做完才发现基本上都是中低档题。虽然有 CK 作为过渡,但高档题和中档题之间仍然存在 gap 过大的问题。中档题基本上出的都很不错,因为不会计算几何 D 不做过多评价,而 L 是能够通过基本的机械化处理技巧来完成的套路 poly 题,做到了防 AK,但是总感觉有些无聊。

上面是我从不看最终结果的初步印象,但是从榜单来看,似乎打比赛的队伍平均水平并没有想象中的很强,所以区分度仍然很高,这套比赛在这个位置上已经出到了很好。

所以做这些点评有什么用呢我又不打ACM

A

Difficulty: Check-in.

签到题,\(n\leq 10\) 输出 \(1023456789\) 的前 \(n\) 位,\(n>10\) 无解。

string s="1023456789";
signed main(){
	int n;read(n);
	if(n>10)puts("-1");
	else for(int i=0;i<n;i++)putchar(s[i]);
	return 0;
}

B

Difficulty: Medium.

官方 Difficulty: Medium-Hard.

我认为做出来的难度应该是 Medium,结论还是比较好猜的。

\(31^7>998244353\),所以猜一个每段长度一定 \(\leq 20\),令其为 \(k\).于是即有 dp,枚举开头所在的段的起点,然后作 dp \(f_{i}\) 表示考虑到 \(i\),从 \(i\) 断开的最优答案是多少。枚举起点 \(\mathcal{O}(k)\),dp 每次暴力枚举上一个断点是 \(\mathcal{O}(k)\),所以总复杂度就是 \(\mathcal{O}(nk^2)\)

官方题解给出证明 \(k=12\),事实上只要能观察出 \(31\) 乘几次就乘爆了,就不难想到每段长度不是很长。

const int mod=998244353;
inline void cadd(int &x,int y){x=(x+y>=mod)?(x+y-mod):(x+y);}
inline void cdel(int &x,int y){x=(x-y<0)?(x-y+mod):(x-y);}
inline int add(int x,int y){return (x+y>=mod)?(x+y-mod):(x+y);}
inline int del(int x,int y){return (x-y<0)?(x-y+mod):(x-y);}
const int N=200010;
const int K=20;
int n,k,a[N];
int has[N],bas[N];
int w(int l,int r){
	return l>r?0:del(has[r],1ll*has[l-1]*bas[r-l+1]%mod);
}
char s[N];
ll f[N],ans;
signed main(){
	scanf("%s",s+1);
	k=min(n=strlen(s+1),K);
	bas[0]=1;
	for(int i=1;i<=n;i++){
		if(s[i]=='a')a[i]=1;
		if(s[i]=='e')a[i]=2;
		if(s[i]=='h')a[i]=3;
		if(s[i]=='n')a[i]=4;
		bas[i]=bas[i-1]*31ll%mod;
		has[i]=add(has[i-1]*31ll%mod,a[i]);
	}
	for(int o=0;o<k;o++){
		int t=w(n-o+1,n);
		for(int i=1;i<=k;i++){
			t=add(t*31ll%mod,a[i]);
			f[i]=t;
		}
		for(int i=2;i<=n-o;i++){
			if(i>k)f[i]=0;
			for(int j=i-1;j>=max(1,i-k);j--){
				cmax(f[i],f[j]+w(j+1,i));
			}
		}
		cmax(ans,f[n-o]);
	}
	cout << ans << '\n';
	return 0;
}

C

Difficulty: Medium-Hard.

下面是我口胡的解法:

先看怎么查询,一个想法是 dp,但是并不好把它放在区间的结构上。观察一下性质。

考虑所有极长上升段(形如 ABCD, AC, CD, BCD 这种,而且还要满足前面和后面都不能再接:

  • 如果长度为 \(1\),那么它只能 \(+1\) 道题,方案数 \(\times 1\)
  • 如果长度为 \(2\),可以不劈开,或者从中间劈开,\(+1\) 或者 \(+2\) 道题,方案数 \(\times 1\) 个方案;
  • 如果长度为 \(3\),不劈 \(+1\) 题,方案数 \(1\) 个方案,劈一刀,\(+2\) 道题,方案数 \(\times 3\),劈两刀,\(+3\) 道题,方案数 \(\times 1\)
  • 如果长度为 \(4\) 也是类似的。

写成生成函数,那么上面四种情况分别是 \(x,x+x^2,x+2x^2+x^3,x+3x^2+3x^3+x^4\),因式分解一下就是 \(x,x(1+x),x(1+x)^2,x(1+x)^3\),询问就是询问所有连续段的生成函数卷起来,第 \(k\) 项的答案。那么只需要维护区间 \(x\) 的次幂以及 \((1+x)\) 的次幂即可,知道这两个次幂,答案就是一个组合数。

如果没有修改的话,就用线段树维护一个分治信息,线段树上的每个节点要记录的信息有:\(x\) 的个数,\((1+x)\) 的个数,最靠左的段是哪几个字符,最靠右的段是哪几个字符。在 pushup 的时候需要看一下左儿子的最靠右段和右儿子的最靠左段是否会产生合并。

然后还有一个区间字符循环位移的东西,实际上的映射无非只是 \(4\) 种,对于区间内每四种映射都处理出那个分治信息即可。时间复杂度是 \(\mathcal{O}(n\log n)\),常数可能大一些。

题解的做法考虑的东西比我的更简单。大概就是直接考虑插板,\(S_i\geq S_{i+1}\) 那么这中间一定要放一个板,\(S_i<S_{i+1}\) 那么中间可以插一个板。那么只需要支持查询区间中有多少个 \(S_i<S_{i+1}\) 即可,线段树维护的分治信息就是区间里的 \(S_i<S_{i+1}\) 个数,最左侧和最右侧的字符转了几次。对于修改操作同样是把四种映射全部处理出来。

反正怎么写都能过,甚至直接记录每个区间内 \(c_{x,y}\) 表示 \(S_i=x,S_{i+1}=y\) 的个数都行。这样常数只不过从 \(4\) 变成了 \(16\)

const int N=200010;
int n,q,a[N],fac[N],inv[N];
int trnt,ls[N],rs[N],tag[N],cl[N],cr[N],cnt[N][4][4];
char s[N];
int C(int x,int y){
	return (x<y||y<0)?0:1ll*fac[x]*inv[y]%mod*inv[x-y]%mod;
}
inline void upd(int x,int c){
	static int t[4][4];
	tag[x]=(tag[x]+c)&3;cl[x]=(cl[x]+c)&3;cr[x]=(cr[x]+c)&3;
	for(int i=0;i<4;i++)
		for(int j=0;j<4;j++)
			t[(i+c)&3][(j+c)&3]=cnt[x][i][j];
	for(int i=0;i<4;i++)
		for(int j=0;j<4;j++)
			cnt[x][i][j]=t[i][j];
}
inline void pushdown(int x){
	if(tag[x]){
		upd(ls[x],tag[x]);upd(rs[x],tag[x]);
		tag[x]=0;
	}
}
inline void pushup(int x,int mid){
	for(int i=0;i<4;i++)
		for(int j=0;j<4;j++)
			cnt[x][i][j]=cnt[ls[x]][i][j]+cnt[rs[x]][i][j];
	cnt[x][(a[mid]+cr[ls[x]])&3][(a[mid+1]+cl[rs[x]])&3]++;
	cl[x]=cl[ls[x]];cr[x]=cr[rs[x]];
}
int build(int l,int r){
	int x=++trnt,mid=(l+r)>>1;
	if(l==r)return x;
	ls[x]=build(l,mid);rs[x]=build(mid+1,r);
	pushup(x,mid);
	return x;
}
void modify(int x,int tl,int tr,int l,int r){
	if(tl>=l&&tr<=r)return upd(x,1);
	int mid=(tl+tr)>>1;pushdown(x);
	if(l<=mid)modify(ls[x],tl,mid,l,r);
	if(mid<r)modify(rs[x],mid+1,tr,l,r);
	pushup(x,mid);
}
void query(int x,int tl,int tr,int l,int r,int c[4][4]){
	if(tl>=l&&tr<=r){
		for(int i=0;i<4;i++)
			for(int j=0;j<4;j++)
				c[i][j]+=cnt[x][i][j];
		return ;
	}
	int mid=(tl+tr)>>1;pushdown(x);
	if(l<=mid)query(ls[x],tl,mid,l,r,c);
	if(mid<r)query(rs[x],mid+1,tr,l,r,c);
	if(l<=mid&&mid<r)c[(a[mid]+cr[ls[x]])&3][(a[mid+1]+cl[rs[x]])&3]++;
	pushup(x,mid);
}
int c[4][4];
signed main(){
	read(n,q);
	fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
	inv[n]=qpow(fac[n],mod-2);for(int i=n-1;~i;--i)inv[i]=1ll*inv[i+1]*(i+1)%mod;
	scanf("%s",s+1);
	for(int i=1;i<=n;i++)a[i]=s[i]-'A';
	build(1,n);
	while(q--){
		int op,l,r,k;read(op,l,r);
		if(op==1){
			modify(1,1,n,l,r);
		}
		else{
			read(k);
			memset(c,0,sizeof(c));
			query(1,1,n,l,r,c);
			int x=0;
			for(int i=0;i<4;i++)
				for(int j=0;j<=i;j++)
					x+=c[i][j];
			cout << C(r-l-x,k-1-x) << '\n';
		}
	}
	return 0;
}

D

计算几何,开摆!

E

Difficulty: Check-in.

官方题解给的 Difficulty 是 Easy,其实我也不太懂简单题的难度,看起来都很简单的,但是这个 E 我还是写 WA 了两发

从前往后找,直到第一次有一个字符 \(a\) 出现了五遍。

然后从后往前找,直到第一次有一个字符 \(c\) 出现了五遍。

再从中间剩下没遍历到的地方找是否有字符 \(b\) 出现了七遍。

const int N=1000010;
int n,v[30],l,r;
char s[N],a,b,c;
signed main(){
	read(n);
	scanf("%s",s+1);
	for(int i=1;i<=n;i++)
		if(++v[s[i]-'a'+1]==5){
			a=s[l=i];
			break;
		}
	memset(v,0,sizeof(v));
	for(int i=n;i>=1;i--)
		if(++v[s[i]-'a'+1]==5){
			c=s[r=i];
			break;
		}
	memset(v,0,sizeof(v));
	for(int i=l+1;i<r;i++)
		if(++v[s[i]-'a'+1]==7){
			b=s[i];
			break;
		}
	if(!b)puts("none");
	else{
		for(int i=1;i<=5;i++)putchar(a);
		for(int i=1;i<=7;i++)putchar(b);
		for(int i=1;i<=5;i++)putchar(c);
	}
	return 0;
}

F

Difficulty: Easy.

乱试就能试出来的构造,但是我需要在非常安静的情况下才能专注思考构造,感觉需要提升一下对环境噪音的抗性。

\(A=\{1,2,3,\cdots,x\}\)\(A+A=\{2,\cdots,2x\},|A+A|=2x-1\),要求 \(x\geq 1\)

\(A=\{0,2,3,4,\cdots,x\}\)\(A+A=\{0,2,3,4,\cdots 2x\},|A+A|=2x\),要求 \(x\geq 3\)

\(n=1\)\(A=\{0\}\)

容易证明 \(n=2,4\) 无解(只要对 \(|A|\) 分类讨论)。

signed main(){
	int n;read(n);
	if(n==1)puts("1\n0");
	else if(n==2||n==4)puts("-1");
	else if(n&1){
		cout << (n+1)/2 << '\n';
		for(int i=1;i<=(n+1)/2;i++)cout << i << ' ';
	}
	else{
		cout << n/2 << '\n' << "0 ";
		for(int i=2;i<=n/2;i++)cout << i << ' ';
	}
	return 0;
}

G

Difficulty: Easy.

首先每一位可以分开来看,然后发现如果初始全是 \(1\),那么无论怎么操作最终按位与仍然是 \(1\);初始有 \(0\),那么无论操作最终按位与一定是 \(0\).所以直接统计有多少位所有串在这一位上都是 \(1\)

int n,m,f[N];
string s;
signed main(){
	read(n,m);
	for(int i=0;i<m;i++)f[i]=1;
	for(int i=1;i<=n;i++){
		cin>>s;
		for(int i=0;i<m;i++)f[i]&=s[i]=='1';
	}
	cout << accumulate(f,f+m,0) << '\n';
	return 0;
}

H

Difficulty: Medium

先考虑 \(x=1,y=m\),水流不会往左流,所以令 \(f_{i,j}\) 为水流是否能从 \((i,j-1)\) 流到 \((i,j)\)

不失一般性,令 \(x\leq y\).考虑什么从 \(x\) 进去的流时候会往左流,那一定是一开始就往左流,然后再流到 \((2,x)\).同理,如果从 \(y\) 右侧流来的话,一定是从 \((1,y)\) 往右再往下再往左流到 \(y\).然后中间部分依然可以用上面那个 dp 解决。

官方题解的做法是,直接进行模拟,然后 dfs 即可,根据上面的分析也不难得到一个点最多会被 dfs 到两次。

时间复杂度 \(\mathcal{O}(m)\)

#define Win do{puts("YES");return ;}while(0)
#define Lose do{puts("NO");return ;}while(0)
const int N=500010;
int n,x,y,f[2][N];
char s[2][N];
void solve(){
	read(n,x,y);
	scanf("%s%s",s[0]+1,s[1]+1);
	for(int i=1;i<=n;i++)f[0][i]=f[1][i]=0;
	if(x>y)x=n-x+1,y=n-y+1,reverse(s[0]+1,s[0]+n+1),reverse(s[1]+1,s[1]+n+1);
	if(x==y){
		if(s[0][x]!=s[1][x])Lose;
		if(s[0][x]=='I')Win;
		for(int i=x-1;i>=1;i--){
			if(s[0][i]!=s[1][i])break;
			if(s[0][i]=='L')Win;
		}
		for(int i=x+1;i<=n;i++){
			if(s[0][i]!=s[1][i])break;
			if(s[0][i]=='L')Win;
		}
		Lose;
	}
	if(s[0][x]=='I'){
		if(s[1][x]=='I')Lose;
		f[1][x+1]=1;
	}
	else{
		f[0][x+1]=1;
		if(s[1][x]=='I')
			for(int i=x-1;i>=1;i--){
				if(s[0][i]!=s[1][i])break;
				if(s[0][i]=='L'){
					f[1][x+1]=1;
					break;
				}
			}
	}
	for(int i=x+1;i<y;i++){
		if(f[0][i]){
			if(s[0][i]=='I')f[0][i+1]=1;
			else if(s[1][i]=='L')f[1][i+1]=1;
		}
		if(f[1][i]){
			if(s[1][i]=='I')f[1][i+1]=1;
			else if(s[0][i]=='L')f[0][i+1]=1;
		}
	}
	if(f[0][y]){
		if(s[0][y]=='L'&&s[1][y]=='I')Win;
		if(s[0][y]=='I'&&s[1][y]=='L'){
			for(int i=y+1;i<=n;i++){
				if(s[0][i]!=s[1][i])break;
				if(s[0][i]=='L')Win;
			}
		}
	}
	if(f[1][y]){
		if(s[1][y]=='L')Win;
	}
	Lose;
}
signed main(){
	int T;read(T);
	while(T--)solve();
	return 0;
}

I

模拟题,skip.

J

Difficulty: Easy or Medium.

官方题解给的是 Medium,不过这个题确实比较简单。

回顾一下遇到 \(\text{mex}\) 的几个思路:

  • 从小到大考虑 \(\text{mex}\),就是这个题的第一步;
  • 从大到小考虑 \(\text{mex}\),有哪个题是这么做的我还没有想到;
  • \(\text{mex}\) 计数类问题,差分成 \(\text{mex}\geq i\),这样限制就变成了 \(<i\) 的必须出现,而 \(\geq i\) 的没有限制(不需要强制钦定 \(i\) 不出现),例题是 CF1527D.

从小到大考虑计算 \(\text{mex}=k\) 的答案,假设 \(a_x=k\),首先是要求 \(x\) 不能出现在连通块中,那么这样就把树分成了 \(x\) 的若干子树,以及 \(x\) 的子树补。

然后看 \(<k\) 的点是否全部出现在某个子树/子树补中,这个可以在 dfs 序上用树状数组维护区间中已经出现了的点的个数,判断点是否出现在 \(x\) 的子树补中就是判断 \(x\) 的子树中是否一个 \(<k\) 的也没有出现。

时间复杂度是 \(\mathcal{O}(n\log n)\)\(k=n\) 需要特判掉,答案显然为 \(n\)

官方题解给出的 \(\mathcal{O}(n)\) 做法是,实际上上面那个过程只需要特殊考虑 \(\text{mex}=0\) 的情况,那么就是将 \(a_x=0\)\(x\) 删掉之后剩余子树的最大大小。为了方便处理令这个 \(x\) 为根,那么对于一个 \(\text{mex}=k\)\(a_y=k\),就要求 \(y\) 子树中不能出现 \(<k\) 的,这样答案就是 \(n-size_y\),否则答案是 \(0\)

const int N=1000010;
inline int lowbit(int x){return x&(-x);}
int n,l[N],r[N],ct,vis[N],tree[N],fa[N],siz[N];
vi eg[N];
void modify(int x){for(;x<=n;x+=lowbit(x))tree[x]++;}
int query(int x){int s=0;for(;x;x-=lowbit(x))s+=tree[x];return s;}
int query(int l,int r){return query(r)-query(l-1);}
void dfs(int x){
	l[x]=++ct;siz[x]=1;
	for(auto v:eg[x])dfs(v),siz[x]+=siz[v];
	r[x]=ct;
}
signed main(){
	read(n);
	for(int i=1,x;i<=n;i++)vis[read(x)]=i;
	for(int i=2;i<=n;i++)eg[read(fa[i])].pb(i);
	dfs(1);
	for(int i=0;i<n;i++){
		int x=vis[i],ans=0;
		for(auto v:eg[x])
			if(query(l[v],r[v])==i)
				cmax(ans,siz[v]);
		if(!query(l[x],r[x]))
			cmax(ans,n-siz[x]);
		cout << ans << ' ';
		modify(l[x]);
	}
	cout << n << '\n';
	return 0;
}

K

Difficulty:Hard

\(f(x)\) 看作 \(x\to f(x)\) 的一条边,那么 \(n\) 个点,每个点出度都是 \(1\),就把这 \(n\) 个点连成了一个内向基环树森林,询问即为问有多少个 \(x\) 满足其在图上走 \(a\) 步和走 \(b\) 步走到的点相同。

先考虑给定 \(x,a,b\),并假设 \(a\neq b\)\(a=b\) 的话所有 \(x\) 都合法),判断是否有 \(f^a(x)=f^b(x)\).先预处理出 \(x\) 第一次走到环上的步数 \(c_x\),以及环长 \(l_x\),那么即有 \(f^a(x)=f^b(x)\Longleftrightarrow a\geq c_x\wedge b\geq c_x\wedge (a-c_x)\equiv (b-c_x)\pmod {l_x}\)

最后那个式子化简得到 \(a-b\equiv 0\pmod {l_x}\)\(l\mid |a-b|\)

将询问离线,按 \(\min(a,b)\) 从小到大作扫描线,那么问题就变成插入一个 \(l\),或者询问有多少个 \(l\) 满足 \(|a-b|\)\(l\) 的倍数。

由于所有环长的和为 \(n\),所以不同的 \(l\) 只有根号个。

总复杂度就是 \(\mathcal{O}(n\log n+q(\sqrt n+\log a))\)

ll gcd(ll a,ll b){return !b?a:gcd(b,a%b);}
const int N=100010;
int n,q,in[N],fa[N],h[N],vis[N];
int l[N],c[N],ans[N],pos[N],v[N],len;
int tl[N],tot;
struct Node{
	ll a,b;
	int i;
}p[N];
vi eg[N];
void dfs(int x,int t){
	if(vis[x])return ;
	vis[x]=1;c[x]=t;l[x]=len;
	for(auto v:eg[x])dfs(v,t+1-h[v]);
}
signed main(){
	read(n);
	for(int i=1;i<=n;i++)eg[read(fa[i])].pb(i),in[fa[i]]++,pos[i]=i;
	{queue<int>q;
	for(int i=1;i<=n;i++)if(!in[i])q.push(i);
	while(!q.empty()){
		int x=q.front();q.pop();
		in[fa[x]]--;
		if(!in[fa[x]])q.push(fa[x]);
	}}
	for(int i=1;i<=n;i++){
		if(!vis[i]&&in[i]){
			int x=fa[i];len=h[fa[i]]=1;
			while(x!=i)++len,h[x=fa[x]]=1;
			dfs(i,0);
			tl[++tot]=len;
		}
	}
	sort(tl+1,tl+tot+1);
	tot=unique(tl+1,tl+tot+1)-tl-1;
	read(q);
	for(int i=1;i<=q;i++)read(p[i].a,p[i].b),p[i].i=i;
	sort(p+1,p+q+1,[](const Node &x,const Node &y){return min(x.a,x.b)<min(y.a,y.b);});
	sort(pos+1,pos+n+1,[](const int &x,const int &y){return c[x]<c[y];});
	int r=1;
	for(int i=1;i<=q;i++){
		while(r<=n&&c[pos[r]]<=min(p[i].a,p[i].b)){
			++v[l[pos[r]]];
			++r;
		}
		ll x=abs(p[i].a-p[i].b);
		if(!x)ans[p[i].i]=n;
		else for(int j=1;j<=tot;j++)
			if(x%tl[j]==0)
				ans[p[i].i]+=v[tl[j]];
	}
	for(int i=1;i<=q;i++)cout << ans[i] << '\n';
	return 0;
}

L

在 DFA 上硬上科技的拼接题。

首先考虑一个 \(f\) 如何计算,可以简单地由调整证明能分到前面就尽量分到前面,使得除了最后一个串,前面分出来的子串都不能再往后添下一个字符。

对所有 \(s\) 建立广义 SAM,求出出现次数 \(\geq m\) 的导出子图,那么在这个 DFA 上跑 dp,即可得到一个 \(\mathcal{O}(nr)\) 的做法。

可以写成线性递推的形式,提高组做法是 \(\mathcal{O}(n^3\log r)\) 的矩阵快速幂。

再 DFA 上暴力 dp 算出来前 \(\mathcal{O}(n)\) 项的答案,用 BM 求出最短递推式,再用多项式科技 \(\mathcal{O}(n\log n\log r)\) 求解。总时间复杂度就是 \(\mathcal{O}(n^2+n\log n\log r)\)

你说的对,但是我不选择写这题的代码,因为我是 poly 小萌新。

posted @ 2022-10-11 20:16  do_while_true  阅读(629)  评论(0编辑  收藏  举报