AtCoder Regular Contest 171 (ARC171) 题目翻译 & 全场题解

本文介绍了六道算法题,涵盖国际象棋棋盘棋子放置、排列操作、树的棋子放法、哈希序列构造、网格石头摆放及字符串构造等问题。针对每道题给出题意,并详细阐述了解题思路,涉及贪心算法、建图、树上dp、状压dp等方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

by RabbieWjy qwq


某天 vp 了一下这场比赛,题解拖到了现在()

赛时做出了前三题,第四题写挂了 ovo

A

题意

有一个 N × N N \times N N×N 的国际象棋棋盘,需要在上面放上 A A A 个车和 B B B 个兵,车可以吃同行同列的棋子,兵可以吃在同列且在它上一行的棋子,问是否存在放法,使得这 A + B A + B A+B 个棋子互相不能吃到,有多测。

1 ≤ T ≤ 1 0 5 1 \le T \le 10^5 1T105 1 ≤ N ≤ 1 0 4 1 \le N \le 10^4 1N104 0 ≤ A , B 0 \le A,B 0A,B 1 ≤ A + B ≤ N 2 1 \le A + B \le N ^ 2 1A+BN2

解法

一行一列中最多放一个车,所以车的上界是 N N N

同时,放了车的行和列不能有兵,所以放了兵的行和列上界都为 N − A N - A NA

由于兵的限制只和上一行有关,可以贪心地对于每一行,把能放兵的位置放满。此时下一行就放不了了,所以放兵的行还有一个上界 ⌊ N + 1 2 ⌋ \lfloor \frac{N + 1}{2} \rfloor 2N+1

综合起来,兵的上界就为 min ⁡ ( N − A , ⌊ N + 1 2 ⌋ ) × ( N − A ) \min(N - A,\lfloor \frac {N + 1}{2} \rfloor) \times (N - A) min(NA,2N+1⌋)×(NA)

考虑构造。发现任意两列是可以任意交换的,只考虑行。放兵的行的上一列放不了棋子,考虑尽量让放兵的行和放车的行间隔起来。具体地,先把车放在奇数行,放完再从上到下放偶数行,再把兵放在空出来的行。

考虑证明。若车的数量不足以占领所有奇数行,那么放兵相当于没有限制地放,可以达到上界 ⌊ N + 1 2 ⌋ \lfloor \frac{N + 1}{2} \rfloor 2N+1。若车的数量够占领奇数行,那么剩下的行都可以放车,可以达到上界 N − A N - A NA

B

题意

对于一个 [ 1 , N ] [1,N] [1,N] 的排列 P P P,定义 F ( P ) F(P) F(P) 为这个排列经过以下操作后生成的排列 B B B

  • 排列 B B B 初始为 ( 1 , 2 , ⋯   , N ) (1,2,\cdots,N) (1,2,,N)
    如果有一个最小的数 i i i 满足 B i < P B i B_i < P_{B_i} Bi<PBi,则 B i ← P B i B_i \leftarrow P_{B_i} BiPBi

给定一个长度为 N N N A A A,问有多少个排列 P P P 满足 F ( P ) = A F(P) = A F(P)=A

1 ≤ N ≤ 2 × 1 0 5 1 \le N \le 2 \times 10 ^ 5 1N2×105

解法

先看看操作是什么东西。看到排列,想到建图(?)。建一个有 N N N 个点的有向图, i i i P i P_i Pi 连边。则这个图被分为若干个环。

初始时,每个点上有一个东西。题面中所说的操作,相当于找一个最小编号的点,使得它的编号小于它指向的点,把在它上的东西沿边移动。发现这个“最小编号”其实没什么用,每个东西最终停住的地方,就是它到达的第一个指向的点编号大于自己的点。它经过的点编号单调递减。

将给定的 A A A 中值一样的位置拿出来,除最后一个点外,它们原本连的边一定指向下一个点。由于图是若干个环,每一种值的编号最大的点指向的一定为一个小于自己、且为某种值的编号最小的点。同时,编号最大的点必然为这个值。这样,原问题转化为有多少种将各种值的编号最小(黑)、最大(白)点两两连边的方式。

由于每个白点只能连向比自己小的点,考虑从小到大枚举白点,第 i i i 个点有 ( 比自己小的黑点数 − i + 1 ) (比自己小的黑点数 - i + 1) (比自己小的黑点数i+1) 种选法。乘法原理算出来就是答案。

代码:

#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

const int mod = 998244353;

int n;
vector <int> v[200010];
int a[200010],use[200010],pd[200010];
int ans,res = 1;

int main()
{
	scanf("%d",&n);
	for (int i = 1;i <= n;i ++) scanf("%d",a + i),v[a[i]].push_back(i);
	for (int i = 1;i <= n;i ++)
	{
		if (a[i] != i && v[i].size())
		{
			printf("0\n");
			return 0;
		}
		if (!v[i].size()) continue;
		for (int j : v[i])
			if (j > i)
			{
				printf("0\n");
				return 0;
			}
		pd[i] = 1;
		use[v[i][0]] = 1;
	}
	for (int i = 1;i <= n;i ++)
	{
		if (use[i]) ans ++;
		if (pd[i]) res = 1ll * res * ans % mod,ans --;
	}
	printf("%d\n",res);
}

C

题意

给定一棵有 N N N 个节点的树,编号为 1 1 1 N N N。初始时,每一个点 i i i 上有一个编号为 i i i 的棋子。

你可以进行任意多次(包括 0 0 0 次)如下操作:

  • 选择一条边,交换边的两端节点上的棋子,然后把这条边删掉。

问最后树上的棋子有几种放法。

2 ≤ N ≤ 3000 2 \le N \le 3000 2N3000

解法

发现子树内的棋子的方案数比较独立,只有子树的根和父亲交换可能影响到这棵子树。考虑树上 dp。

手动模拟一下,会发现一棵树最终的形态由操作的相对顺序(指祖先和儿子的关系(?))决定。因此,在 dp 的时候要记录操作数,便于算操作的相对顺序个数。

dp 的转移就是将两棵树的方案合并起来的过程。状态设为 f [ i ] [ j ] f[i][j] f[i][j],表示以 i i i 为根的子树中发生了 j j j 次交换操作的放法数。如果两棵树的根不交换,方案数就是直接乘起来;否则枚举这次交换操作要插进两个操作序列的哪里,再乘起来更新答案。将根为 v v v v ∈ s o n u v \in son_u vsonu)的子树与根为 u u u 的子树合并的转移式:

f ′ [ u ] [ k ] = ∑ i + j = k f [ u ] [ i ] × f [ v ] [ j ] + ∑ i + j = k − 1 f [ u ] [ i ] × f [ v ] [ j ] × ( i + 1 ) × ( j + 1 ) f'[u][k] = \sum \limits_{i + j = k} f[u][i] \times f[v][j] + \sum \limits _{i + j = k - 1} f[u][i] \times f[v][j] \times (i + 1) \times (j + 1) f[u][k]=i+j=kf[u][i]×f[v][j]+i+j=k1f[u][i]×f[v][j]×(i+1)×(j+1)

直接 dp,时间复杂度 O ( n 2 ) O(n ^ 2) O(n2)

D

题意

给定非负整数 P P P B B B,对于一个序列 X X X,定义 hash ⁡ ( X ) = ( ∑ i = 1 ∣ X ∣ X i B ∣ X ∣ − i ) m o d    p \operatorname{hash}(X) = (\sum \limits _{i=1} ^{|X|} X_i B^{|X| - i}) \mod p hash(X)=(i=1XXiBXi)modp。给出 M M M 个区间 [ L i , R i ] [L_i,R_i] [Li,Ri],问是否可能构造序列 A = ( A 1 , A 2 , ⋯   , A N ) A=(A_1,A_2,\cdots,A_N) A=(A1,A2,,AN),使得对于所有 1 ≤ i ≤ M 1 \le i \le M 1iM hash ⁡ ( A L i , A L i + 1 , ⋯   , A R i ) ≠ 0 \operatorname{hash}(A_{L_i},A_{L_i + 1},\cdots,A_{R_i}) \ne 0 hash(ALi,ALi+1,,ARi)=0

2 ≤ P ≤ 1 0 9 2 \le P \le 10^9 2P109 1 ≤ B < P 1 \le B < P 1B<P 1 ≤ N ≤ 16 1 \le N \le 16 1N16 1 ≤ M ≤ N ( N + 1 ) 2 1 \le M \le \frac{N(N + 1)}{2} 1M2N(N+1),区间互不相同。

解法

定义 F ( i ) = hash ⁡ ( A i , A i + 1 , ⋯   , A ∣ X ∣ ) F(i)=\operatorname{hash}(A_i,A_{i + 1},\cdots,A_{|X|}) F(i)=hash(Ai,Ai+1,,AX),根据哈希函数定义可得 hash ⁡ ( A x ∣ x ∈ [ L i , R i ] ) = F ( L i ) − F ( R i + 1 ) B ∣ X ∣ − R i m o d    P \operatorname{hash}({A_x | x \in [L_i,R_i]}) = \frac{F(L_i) - F(R_i + 1)}{B^{|X| - R_i}} \mod P hash(Axx[Li,Ri])=BXRiF(Li)F(Ri+1)modP。由于 B ∣ X ∣ − R i ≠ 0 B^{|X| - R_i} \ne 0 BXRi=0,要使这个值不为 0 0 0,则 F ( L i ) ≠ F ( R i + 1 ) F(L_i) \ne F(R_i + 1) F(Li)=F(Ri+1)。题目给的 M M M 条限制,相当于在图上连了 M M M 条边,边的两端填的数不一样。而 0 ≤ F ( i ) < P 0 \le F(i) < P 0F(i)<P,所以原题有解等价于这个图上没有 ( P + 1 ) (P + 1) (P+1) 阶完全图。

发现 N ≤ 16 N \le 16 N16,考虑用状压 dp 解决。定义 f [ i ] f[i] f[i] 为二进制数 i i i 代表的子图中最大的完全图有几个点,转移方程为

f [ i ] = min ⁡ j ⊕ k = i ( f [ j ] + f [ k ] ) f[i] = \min \limits _{j \oplus k = i} (f[j] + f[k]) f[i]=jk=imin(f[j]+f[k])

注意特判 i i i 代表的子图中没有边的情况。时间复杂度 O ( 2 N N 2 ) O(2 ^ N N ^ 2) O(2NN2)

代码:

#include <iostream>
#include <cstdio>
using namespace std;

int P,B,n,m;
int pd[20][20],f[200010];

int main()
{
	scanf("%d%d%d%d",&P,&B,&n,&m);
	if (P > n)
	{
		printf("Yes\n");
		return 0;
	}
	for (int i = 1;i <= m;i ++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		y ++;
		pd[x][y] = pd[y][x] = 1;
	}
	n ++;
	for (int i = 1;i < (1 << n);i ++)
	{
		bool fl = true;
		for (int j = 1;j <= n;j ++)
		{
			if (!(i >> (j - 1) & 1)) continue;
			for (int k = j + 1;k <= n;k ++)
			{
				if (!(i >> (k - 1) & 1)) continue;
				fl &= (!pd[j][k]);
			}
		}
		if (fl)
		{
			f[i] = 1;
			continue;
		}
		f[i] = n;
		for (int j = (i - 1) & i;j;j = (j - 1) & i)
			f[i] = min(f[i],f[j] + f[i ^ j]);
		if (f[i] >= P + 1)
		{
			printf("No\n");
			return 0;
		}
	}
	printf("Yes\n");
}

E

题意

给定一个 N × N N \times N N×N 的网格, ( i , j ) (i,j) (i,j) 表示第 i i i 行第 j j j 列的格子。现在需要在网格上摆放 1 1 1 个黑色石头和 M M M 个白色石头,且每行每列最多只有一个白色石头,黑色石头在 ( A , B ) (A,B) (A,B) 上。接下来,要进行以下操作,直到不可进行为止:

  • 设黑色石头位于 ( i , j ) (i,j) (i,j)。若上下左右四个方向上有至少一个白色石头,选择其中一个,将黑色石头放到白色石头后面(白色石头所对方向)一格,并拿走这个白色石头。

如果操作结束后,黑色石头重新位于 ( A , B ) (A,B) (A,B),且白色石头全部被拿走,则操作成功。问有多少种白色石头的摆放方式,使得操作成功。

2 ≤ M ≤ N ≤ 2 × 1 0 5 2 \le M \le N \le 2 \times 10^5 2MN2×105

解法

黑色石头走的路线和白色石头的摆放方式是一一对应的,证明略,可以手动模拟感性理解一下()

由于黑色石头的路线是横一下竖一下的,总步数应为偶数。每一次黑色石头进入一行或者一列,相当于把两行或两列标记上不能再次进入,因为这两行或两列的白色石头被拿走了。考虑确定白色石头的位置,每个石头占领两行或两列,且互不相同。行和列可以分开考虑,再乘起来。

观察行的情况,总共 K = ⌊ M 2 ⌋ K = \lfloor \frac {M}{2} \rfloor K=2M 个石头占领两行,其中一定有一个石头,占领的两行包括第 A A A 行。分两种情况考虑,一种是第 A − 1 A - 1 A1 A A A 行,一种是 A A A A + 1 A + 1 A+1 行。先确定占领第 A A A 行的石头是哪个,再算剩下的占领哪些行,两种情况的方案数分别如下:

∑ i i ( A − 2 − i i ) ( N − A − ( K − i − 1 ) K − i − 1 ) \sum \limits _i i{ {A - 2 - i} \choose {i} } { {N - A - (K - i - 1)} \choose {K - i - 1} } ii(iA2i)(Ki1NA(Ki1))

∑ i ( K − i − 1 ) ( A − 1 − i i ) ( N − A − 1 − ( K − i − 1 ) K − i − 1 ) \sum \limits _i (K - i - 1){ {A - 1 - i} \choose i} { {N - A - 1 - (K - i - 1)} \choose {K - i - 1} } i(Ki1)(iA1i)(Ki1NA1(Ki1))

列的情况同理。最后再乘上行选择列的 ( M − 2 ) ! (M - 2)! (M2)!。时间复杂度线性。

F

题意

定义一个字符串 T T T好的,当且仅当它满足下列条件:

  • 存在非空字符串 A A A B B B,使得 A + B = T A + B = T A+B=T,且 A + rev ⁡ ( B ) A + \operatorname{rev}(B) A+rev(B) rev ⁡ ( A ) + B \operatorname{rev}(A) + B rev(A)+B 均为回文串。

A + B A + B A+B 表示将 A A A B B B 拼接得到的字符串, rev ⁡ ( A ) \operatorname{rev}(A) rev(A) 表示将 A A A 首尾反转得到的字符串。

给定一个长为 N N N,由小写英文字母和 ? 组成的字符串 S S S,问有多少种将 ? 改成小写字母的方法,使得 S S S好的字符串。

2 ≤ N ≤ 5 × 1 0 4 2 \le N \le 5 \times 10^4 2N5×104

解法

(这里空着一段官方题解的说明,从最后一段的 This part is also somewhat complex, but we will omit the details. 开始 qwq)

官方题解讲得十分详细,但是实现没有讲。

题解中得到了一个结论,就是把 S S S 划分成 ( A , B ) (A,B) (A,B) 分为两种情况:

  1. S = T n S = T^n S=Tn,其中 T T T 为 primitive root。此时有 n − 1 n - 1 n1 种分法,把 S S S 分为两个回文串。

  2. S S S 不为回文串,那么 A A A B B B 形如一堆 T T T T T T 的一半拼在一起。此时分法是唯一的。

考虑怎么用这个结论和容斥求出原问题的解。

对于第一种情况,直接枚举 T T T 的长度,把相对应位置并起来求方案数就行了。注意 T T T 要求是 primitive root,要把不是的情况容斥掉,即去掉 ∣ T ∣ m o d    ∣ T ′ ∣ = 0 |T| \mod |T'| = 0 TmodT=0 的情况。

对于第二种情况,也是枚举 T T T 的长度,同时枚举 A A A B B B 的断点。把 T T T 看成 X + rev ⁡ ( X ) X + \operatorname{rev} (X) X+rev(X),相当于 X X X 正着接一个,反着接一个,但在断点左右都是正着的。可以先正着和反着预处理正反正反地接到当前节点的方案数,枚举断点的时候左右一拼就行了。

想一想这么做会多算哪些东西。一种是 X X X 也为回文串的情况,需要减掉。另一种是 X X X 不为回文串,但 T T T 为回文串的情况,如 abbaabbaabba。此时,通过观察可以发现,在同一断点上, T T T 比较长的情况包含了比较短的情况。所以,在实际处理中,只需要在同一断点上保留 ∣ T ∣ |T| T 最大时算出来的答案就行了。

代码:

#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

const int mod = 998244353;

int n;
char s[50010],t[50010];
int P[50010];
int pre[50010],suf[50010];
vector <char> prestr[50010],sufstr[50010];
int tot,Ans[50010];

int main()
{
	scanf("%d",&n);
	scanf("%s",s + 1);
	for (int i = 1;i <= n;i ++)
	{
		if (n % i != 0) continue;
		// is a palindrome
		bool fl = true;
		for (int j = 1;j <= i;j ++) t[j] = s[j];
		for (int j = 2;j <= n / i;j ++)
		{
			int l = (j - 1) * i + 1,r = j * i;
			for (int k = l;k <= r;k ++)
				if (t[k - l + 1] == '?' && s[k] != '?')
					t[k - l + 1] = s[k];
				else if (t[k - l + 1] != '?' && s[k] != '?' && t[k - l + 1] != s[k])
					fl = false;
		}
		for (int j = 1;j < i - j + 1;j ++)
			if (t[j] != t[i - j + 1] && t[j] != '?' && t[i - j + 1] != '?')
				fl = false;
		if (fl && i < n)
		{
			int ans = 1;
			for (int j = 1;j <= i - j + 1;j ++)
				if (t[j] == '?' && t[i - j + 1] == '?') ans = 26ll * ans % mod;
			P[i] = ans;
		}
		// is not a palindrome
		if (i * 2 > n || n / i % 2 != 0 || i == 1) continue;
		int tmp = n / i;
		for (int j = 1;j <= i;j ++) t[j] = '?';
		pre[0] = 1;
		for (int j = 1;j <= tmp;j ++)
		{
			int l = (j - 1) * i + 1,r = j * i;
			prestr[j].clear();
			if ((j + 1) & 1)
			{
				pre[j] = 1;
				if (!pre[j - 1]) pre[j] = 0;
				for (int k = 1;k <= i;k ++)
					if (t[k] == '?' && s[r - k + 1] != '?')
						t[k] = s[r - k + 1];
					else if (t[k] != '?' && s[r - k + 1] != '?' && t[k] != s[r - k + 1])
						pre[j] = 0;
			}
			else
			{
				pre[j] = 1;
				if (!pre[j - 1]) pre[j] = 0;
				for (int k = 1;k <= i;k ++)
					if (t[k] == '?' && s[l + k - 1] != '?')
						t[k] = s[l + k - 1];
					else if (t[k] != '?'&& s[l + k - 1] != '?' && t[k] != s[l + k - 1])
						pre[j] = 0;
			}
			for (int k = 1;k <= i;k ++) prestr[j].push_back(t[k]);
		}
		for (int j = 1;j <= i;j ++) t[j] = '?';
		suf[tmp + 1] = 1;
		for (int j = tmp;j >= 1;j --)
		{
			int l = (j - 1) * i + 1,r = j * i;
			sufstr[j].clear();
			if ((tmp - j) & 1)
			{
				suf[j] = 1;
				if (!suf[j + 1]) suf[j] = 0;
				for (int k = 1;k <= i;k ++)
					if (t[k] == '?' && s[r - k + 1] != '?')
						t[k] = s[r - k + 1];
					else if (t[k] != '?' && s[r - k + 1] != '?' && t[k] != s[r - k + 1])
						suf[j] = 0;
			}
			else
			{
				suf[j] = 1;
				if (!suf[j + 1]) suf[j] = 0;
				for (int k = 1;k <= i;k ++)
					if (t[k] == '?' && s[l + k - 1] != '?')
						t[k] = s[l + k - 1];
					else if (t[k] != '?'&& s[l + k - 1] != '?' && t[k] != s[l + k - 1])
						suf[j] = 0;
			}
			for (int k = 1;k <= i;k ++) sufstr[j].push_back(t[k]);
		}
		for (int j = 1;j < tmp;j += 2)
		{
			if (!pre[j] || !suf[j + 1]) continue;
			int ans = 1;
			for (int k = 0;k < i;k ++)
				if (prestr[j][k] == '?' && sufstr[j + 1][k] == '?') ans = 26ll * ans % mod,t[k + 1] = '?';
				else if (prestr[j][k] != '?' && sufstr[j + 1][k] != '?' && prestr[j][k] != sufstr[j + 1][k])
					ans = 0;
				else if (prestr[j][k] != '?') t[k + 1] = prestr[j][k];
				else t[k + 1] = sufstr[j + 1][k];
			if (ans)
			{
				int res = 1;
				for (int k = 1;k <= i - k + 1;k ++)
					if (t[k] == '?' && t[i - k + 1] == '?')
						res = 26ll * res % mod;
					else if (t[k] != '?' && t[i - k + 1] != '?' && t[k] != t[i - k + 1])
						res = 0;
				ans = (ans - res + mod) % mod;
			}
			Ans[j * i] = ans;
		}
	}
	for (int i = 1;i < n;i ++)
		if (n % i == 0 && P[i])
		{
			for (int j = 1;j < i;j ++)
				if (i % j == 0) P[i] = (P[i] - P[j] + mod) % mod;
			tot = (tot + P[i]) % mod;
		}
	for (int i = 1;i <= n;i ++) tot = (tot + Ans[i]) % mod;
	printf("%d\n",tot);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值