AtCoder Regular Contest 162

【题解】AtCoder Regular Contest 162

A.Ekiden Race

题目描述:

nn 个人参加了往返赛跑,每个人有一个编号 11nn。已知以下信息:

  • 如果按照往路的成绩排序,那么任何两个人的成绩都不相同。同时第 ii 个人在往路中排名第 ii
  • 如果按照往返的成绩排序,那么任何两个人的成绩都不相同。同时第 ii 个人在往返中排名第 pip_i
  • 如果復路的成绩最快,那么获得復路奖的人是復路成绩最快的人。

请计算有多少人可能得到了復路奖项。

TT 个测试用例,请对于每个测试用例输出答案。

(翻译者注:往路指来回的第一段路程,復路指来回的第二段路程。)

2n1032 \le n \le 10^3

题目分析:

如果 aa 在第一段路快于 bb,而经过了第二段路之后 aa 慢于 bb,则必然有 aa 第二段路慢于 bb
否则无法确定 a,ba,b 的第二段路的快慢,也就是可以直接认为 a,ba,b 都有可能。
直接这样判就可以了。

代码:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int a[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%d",&T);
while(T--){
int n;scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
int ans = n+1;
int tot = 0;
for(int i=n; i>=1; i--){
if(a[i] < ans) ++tot;
ans = min(ans,a[i]);
}
printf("%d\n",tot);
}
return 0;
}

B.Insertion Sort 2

题目描述:

给定 1,2,...,N1,2,...,N 的排列 P=(P1,P2,...,PN)P=(P_1,P_2,...,P_N)

最多进行 2×1032\times 10^3 次操作,每次操作满足 1iN1,0jN21\le i\le N-1, 0\le j\le N-2,选取整数 i,ji,j,从 PP 中取出 (Pi,Pi+1)(P_i,P_{i+1}) 得到序列 Q=(Q1,Q2,...,QN2)Q=(Q_1,Q_2,...,Q_{N-2}),则将 PP(Pi,Pi+1)(P_i,P_{i+1}) 替换成序列 QQjjj+1j+1 位置上的数,得到新的排列 PP'

判断是否能够通过这样的操作使 PP 变成升序排列,如果可以,给出操作步骤。
2  N  1032\ \leq\ N\ \leq\ 10^3

题目分析:

一个显然的想法就是一个个数地确定,也就是先把 11 通过操作放到第一个,然后 22 放到第二个然后依次类推。
但是现在的有的一个问题就是如果我们现在要放 xx,但是 xx 是序列中的最后一个数就做不了这个操作了,可是我们可以做类似如下的操作:(n2,n1,n)(n-2,n-1,n) \to (n1,n,n2)(n-1,n,n-2) 这样 PnP_n 就不在最后了,我们就可以操作它了。

代码:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<bits/stdc++.h>
using namespace std;
const int N = 2e3+5;
vector<pair<int,int> > v;
int n,a[N],b[N];
void solve(int pos1,int pos2){
if(!(1 <= pos1 && pos1 <= n-1)) return;
if(!(0 <= pos2 && pos2 <= n-2)) return;
v.push_back({pos1,pos2});
int tot = 0;
if(tot == pos2) b[++tot] = a[pos1],b[++tot] = a[pos1+1];
for(int i=1; i<=n; i++){
if(i == pos1 || i == pos1 + 1) continue;
b[++tot] = a[i];
if(tot == pos2) b[++tot] = a[pos1],b[++tot] = a[pos1+1];
}
for(int i=1; i<=n; i++) a[i] = b[i];
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
for(int i=1; i<=n; i++){
if(a[i] == i) continue;
if(a[n] == i) solve(n-1,n-3);
int pos = i;
for(int j=i+1; j<=n; j++){
if(a[j] == i) pos = j;
}
if(pos > i) solve(pos,i-1);
}
if(a[n] != n) printf("No\n");
else{
printf("Yes\n");
printf("%d\n",(int)v.size());
for(auto i : v) printf("%d %d\n",i.first,i.second);
}
return 0;
}

C.Mex Game on Tree

题目描述:

有一棵 nn 个节点且以 11 为根的数。

每个节点上有一个数表示颜色。

  • 如果为 1-1,表示没有填颜色。
  • 否则,表示填的颜色。

Alice\text{Alice}Bob\text{Bob} 轮流对没填颜色的节点填上 00nnAlice\text{Alice} 先手)。

填完后,如果某个点和它的子树颜色的 mex\mathrm{mex}kkAlice\text{Alice} 胜,否则为 Bob\text{Bob}

2  N  1032\ \leq\ N\ \leq\ 10^3

题目分析:

BoB 的策略十分显然,就是填 kk,所以只要让 Bob 走一步,那么 Alice 之前的所有布局就全没用了。
因为是考虑某一个子树,所以不妨对每一棵子树都求一下是不是可以使得它的 mex\mathrm{mex}kk
因为我们对每一棵子树都考虑了,所以若 Alice 走两步可以让某个子树变成合法,要么就是那棵子树走一步就可以合法,要么就是受到前一步的影响,而第二种情况显然 Bob 可以直接堵掉 Alice 的第二步,就结束了。

代码:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+5;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[2 * N];
int n,k,cnt,head[N],a[N];
bool vis[N],ans;
vector<int> v;
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
void get_val(int now,int fath){
v.push_back(a[now]);
for(int i=head[now]; i; i=e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
get_val(to,now);
}
}
void dfs(int now,int fath){
for(int i=head[now]; i; i=e[i].nxt){
int to = e[i].to;
if(to == fath) continue;
dfs(to,now);
}

get_val(now,fath);
int flag = 0;
for(auto i : v){
if(i == -1){
flag++;
continue;
}
vis[i] = true;
}
int tmp = 0;
while(vis[tmp]) ++tmp;
if(flag && tmp < k){
++tmp;
while(vis[tmp]) ++tmp;
}
if(tmp == k && flag <= 1) ans = true;
for(auto i : v){
if(i != -1) vis[i] = false;
}
v.clear();
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&k);
for(int i=2; i<=n; i++){
int fa;scanf("%d",&fa);
add_edge(fa,i);add_edge(i,fa);
}
for(int i=1; i<=n; i++) scanf("%d",&a[i]);
dfs(1,0);
if(ans) printf("Alice\n");
else printf("Bob\n");
cnt = 0;
for(int i=1; i<=n; i++) head[i] = 0;
ans = false;
}
return 0;
}

D.Smallest Vertices

题目描述:

在本问题中,当我们提到有根有向树时,我们指的是所有边都指向从根到叶子的有根树。

给定一个使得其总和为 N1N-1 的非负整数序列 d=(d1,d2,,dN)d=(d_1,d_2,\ldots,d_N)

对于带编号从 11NN 的顶点,假设 11 是根,我们将其点度数定义为 did_i

我们称满足以下条件的根付有向树为好树

  • ii 的出度是 did_i

此外,对于好树的顶点 vv,定义 f(v)f(v) 为“包含顶点 vv 的子树中的顶点(包括 vv)的顶点编号的最小值”。我们将满足 f(v)=vf(v)=v 的顶点称为好顶点

求好树中所有好顶点的总数,将其对 998244353998244353 取模后的余数。

  • 2  N  5002\ \leq\ N\ \leq\ 500
  • 0  di  N10\ \leq\ d_i\ \leq\ N-1
  • d1  1d_1\ \geq\ 1
  • i=1N di = N1\sum_{i=1}^N\ d_i\ =\ N-1

题目分析:

显然可以考虑对于每一个点作为好节点的方案数求和就是答案。

v=1v = 1dv=0d_v = 0,则任意一个好树都是合法的,这个时候根据 pruferprufer 序列,显然方案数就是:

(n2)!u(d1,u1)!\dfrac{(n-2)!}{\prod_{u} (d_{1,u}-1)!}

其中 di,jd_{i,j} 表示以 ii 为根的时候 jj 的度数。
否则的话,考虑以 vv 为根的子树只能包含 [v,n][v,n] 这些点,但是好像除了这个就发现不了什么式子了,就不大能做。
所以不妨设 SSvv 子树内的点的点集,看看这个时候的方案数是什么:

(S2)!uS(dv,u1)!×(nS+12)!u∉S(d1,u1)!=(S2)!(nS1)!d1dvu(du!)\dfrac{(|S|-2)!}{\prod\limits_{u\in S} (d_{v,u}-1)!} \times \dfrac{(n-|S|+1-2)!}{\prod\limits_{u\not\in S}(d_{1,u}-1)!} = \dfrac{(|S|-2)!(n-|S|-1)!d_1d_v}{\prod\limits_u (d_u!)}

第一个式子就是考虑 vv 的子树的方案数,乘以其余点构成一棵树的方案数,其中在其余点构成的树中 vv 所在子树充当一个叶子。
第二个式子就是考虑如下的事实:

di,j={dji=jdj+1ijd_{i,j} = \begin{cases} d_j & i = j \\ d_j+1 & i \not= j \end{cases}

所以带入就得到了第二个式子。
注意为了可以组成一棵树,一个隐形的限制就是:uSdu=S1\sum\limits_{u \in S} d_u = |S| - 1
所以此时 dpdp 状态就相当显然了,dp[i][j][k]dp[i][j][k] 表示大于等于 ii 的点,选择了 jj 个点,dud_u 之和为 kk 的方案数。
这样就可以直接统计方案数,然后乘以上面的系数就可以得到答案。

代码:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 505;
const int MOD = 998244353;
int f[N][N][N],d[N],fac[N],inv[N];
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
void add(int &a,int b){
a = (a + b) % MOD;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;scanf("%lld",&n);
fac[0] = 1;
for(int i=1; i<=n; i++) fac[i] = fac[i-1] * i % MOD;
inv[n] = power(fac[n],MOD-2);
for(int i=n-1; i>=0; i--) inv[i] = inv[i+1] * (i+1) % MOD;
for(int i=1; i<=n; i++) scanf("%lld",&d[i]);
f[n+1][0][0] = 1;
for(int i=n; i>=1; i--){
for(int j=0; j<=n; j++){
for(int k=0; k<=n; k++){
add(f[i][j][k],f[i+1][j][k]);
if(j-d[i]>=0&&k-1>=0) add(f[i][j][k],f[i+1][j-d[i]][k-1]);
}
}
}
int tmp = fac[n-2];
for(int i=1; i<=n; i++){
if(i == 1) tmp = tmp * inv[d[i]-1] % MOD;
else tmp = tmp * inv[d[i]] % MOD;
}
int mul = 1,ans = 0;
for(int i=1; i<=n; i++) mul = mul * inv[d[i]] % MOD;
for(int i=1; i<=n; i++){ //统计 i 为好节点的贡献
if(i == 1 || d[i] == 0){
add(ans,tmp);
// printf("%lld %lld\n",i,tmp);
continue;
}
for(int j=1; j<=n; j++){ //枚举 [i,n] 选择了多少个节点
int sum_d = j - 1;
if(sum_d-d[i]>=0 && j-1>=0 && j-2>=0 && n-j-1>=0)
add(ans,f[i+1][sum_d-d[i]][j-1] * d[1] %MOD* d[i] %MOD* fac[j-2] %MOD* fac[n-j-1]%MOD * mul%MOD);
}
// printf("%lld %lld\n",i,ans);
// ans = 0;
}
printf("%lld\n",ans);
return 0;
}

E.Strange Constraints

题目描述:

给定长度为 nn 的序列 AA,求序列 BB 的个数模 998244353998244353,满足以下条件:

  • 值域 [1,n][1, n]
  • ii 的个数不超过 AiA_i
  • BiB_i 的个数不超过 AiA_i

1n5001 \le n \le 500

题目分析:

考虑设 did_i 表示数 ii 出现的次数,则题目条件可以转化为:

  • diAid_i \le A_i
  • ii 可以放在位置 jj,则必然满足 diAjd_i \le A_j

ci=j=1n[iAj]c_i = \sum_{j=1}^n [i \le A_j],则 cic_i 就代表出现次数为 ii 的数可选的种类数,以及出现次数为 ii 的数在 BB 中可以放的位置数。
所以这个东西直接 dpdp 去做就很简单了,也就是设 dp[i][j][k]dp[i][j][k] 表示考虑了出现次数大于等于 ii 的数,总共选择了 jj 种数,放在 BBkk 个位置上。
转移就是枚举出现次数等于 ii 的数有 xx 个:

dp[i+1][j][k]×(cijx)×(cik)!(i!)x(cikix)!dp[i][j+x][k+ix]dp[i+1][j][k] \times \binom{c_i-j}{x} \times \dfrac{(c_i-k)!}{(i!)^x(c_i-k-ix)!} \to dp[i][j+x][k+ix]

转移的两个系数:第一个就是选择哪些数,第二个就是这些数放在哪些位置。

代码:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 505;
const int MOD = 998244353;
int f[N][N][N],fac[N],inv[N],a[N],c[N];
int binom(int n,int m){
if(n < m || n < 0 || m < 0) return 0;
return fac[n] * inv[m] % MOD * inv[n-m]%MOD;
}
int power(int a,int b){
int res = 1;
while(b){
if(b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
void add(int &a,int b){
a = (a + b)%MOD;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n;scanf("%lld",&n);
fac[0] = 1;
for(int i=1; i<=n; i++) fac[i] = fac[i-1] * i % MOD;
inv[n] = power(fac[n],MOD-2);
for(int i=n-1; i>=0; i--) inv[i] = inv[i+1] * (i+1) % MOD;
for(int i=1; i<=n; i++) scanf("%lld",&a[i]);
for(int i=0; i<=n; i++){
for(int j=1; j<=n; j++){
if(a[j] >= i) c[i]++;
}
}
f[n+1][0][0] = 1;
for(int i=n; i>=1; i--){
for(int j=0; j*i<=n; j++){
if(j > c[i]) break;
for(int k=0; k<=n; k++){
if(k > c[i]) break;
for(int x=0; x*i<=n; x++){
if(k + i * x > c[i]) break;
if(j+x<=n && k+i*x<=n && c[i]-j>=0 && c[i]-k>=0 && c[i]-k-i*x>=0)
add(f[i][j+x][k+i*x],f[i+1][j][k] * binom(c[i]-j,x) %MOD* fac[c[i]-k] %MOD* power(inv[i],x) %MOD* inv[c[i]-k-i*x]%MOD);
}
}
}
}
int ans = 0;
for(int i=1; i<=n; i++) add(ans,f[1][i][n]);
printf("%lld\n",ans);
return 0;
}
作者

linyihdfj

发布于

2023-09-08

更新于

2023-09-14

许可协议