【题解】AtCoder Regular Contest 162
A.Ekiden Race
题目描述:
有 n n n 个人参加了往返赛跑,每个人有一个编号 1 1 1 到 n n n 。已知以下信息:
如果按照往路的成绩排序,那么任何两个人的成绩都不相同。同时第 i i i 个人在往路中排名第 i i i 。
如果按照往返的成绩排序,那么任何两个人的成绩都不相同。同时第 i i i 个人在往返中排名第 p i p_i p i 。
如果復路的成绩最快,那么获得復路奖的人是復路成绩最快的人。
请计算有多少人可能得到了復路奖项。
有 T T T 个测试用例,请对于每个测试用例输出答案。
(翻译者注:往路指来回的第一段路程,復路指来回的第二段路程。)
2 ≤ n ≤ 1 0 3 2 \le n \le 10^3 2 ≤ n ≤ 1 0 3
题目分析:
如果 a a a 在第一段路快于 b b b ,而经过了第二段路之后 a a a 慢于 b b b ,则必然有 a a a 第二段路慢于 b b b 。
否则无法确定 a , b a,b a , b 的第二段路的快慢,也就是可以直接认为 a , b a,b a , 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 , . . . , N 1,2,...,N 1 , 2 , . . . , N 的排列 P = ( P 1 , P 2 , . . . , P N ) P=(P_1,P_2,...,P_N) P = ( P 1 , P 2 , . . . , P N ) 。
最多进行 2 × 1 0 3 2\times 10^3 2 × 1 0 3 次操作,每次操作满足 1 ≤ i ≤ N − 1 , 0 ≤ j ≤ N − 2 1\le i\le N-1, 0\le j\le N-2 1 ≤ i ≤ N − 1 , 0 ≤ j ≤ N − 2 ,选取整数 i , j i,j i , j ,从 P P P 中取出 ( P i , P i + 1 ) (P_i,P_{i+1}) ( P i , P i + 1 ) 得到序列 Q = ( Q 1 , Q 2 , . . . , Q N − 2 ) Q=(Q_1,Q_2,...,Q_{N-2}) Q = ( Q 1 , Q 2 , . . . , Q N − 2 ) ,则将 P P P 中 ( P i , P i + 1 ) (P_i,P_{i+1}) ( P i , P i + 1 ) 替换成序列 Q Q Q 的 j j j 与 j + 1 j+1 j + 1 位置上的数,得到新的排列 P ′ P' P ′ 。
判断是否能够通过这样的操作使 P P P 变成升序排列,如果可以,给出操作步骤。
2 ≤ N ≤ 1 0 3 2\ \leq\ N\ \leq\ 10^3 2 ≤ N ≤ 1 0 3
题目分析:
一个显然的想法就是一个个数地确定,也就是先把 1 1 1 通过操作放到第一个,然后 2 2 2 放到第二个然后依次类推。
但是现在的有的一个问题就是如果我们现在要放 x x x ,但是 x x x 是序列中的最后一个数就做不了这个操作了,可是我们可以做类似如下的操作:( n − 2 , n − 1 , n ) (n-2,n-1,n) ( n − 2 , n − 1 , n ) → \to → ( n − 1 , n , n − 2 ) (n-1,n,n-2) ( n − 1 , n , n − 2 ) 这样 P n P_n P 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
题目描述:
有一棵 n n n 个节点且以 1 1 1 为根的数。
每个节点上有一个数表示颜色。
如果为 − 1 -1 − 1 ,表示没有填颜色。
否则,表示填的颜色。
Alice \text{Alice} Alice 和 Bob \text{Bob} Bob 轮流对没填颜色的节点填上 0 0 0 到 n n n (Alice \text{Alice} Alice 先手)。
填完后,如果某个点和它的子树颜色的 m e x \mathrm{mex} m e x 为 k k k ,Alice \text{Alice} Alice 胜,否则为 Bob \text{Bob} Bob 。
2 ≤ N ≤ 1 0 3 2\ \leq\ N\ \leq\ 10^3 2 ≤ N ≤ 1 0 3
题目分析:
BoB 的策略十分显然,就是填 k k k ,所以只要让 Bob 走一步,那么 Alice 之前的所有布局就全没用了。
因为是考虑某一个子树,所以不妨对每一棵子树都求一下是不是可以使得它的 m e x \mathrm{mex} m e x 为 k k k 。
因为我们对每一棵子树都考虑了,所以若 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
题目描述:
在本问题中,当我们提到有根有向树时,我们指的是所有边都指向从根到叶子的有根树。
给定一个使得其总和为 N − 1 N-1 N − 1 的非负整数序列 d = ( d 1 , d 2 , … , d N ) d=(d_1,d_2,\ldots,d_N) d = ( d 1 , d 2 , … , d N ) 。
对于带编号从 1 1 1 到 N N N 的顶点,假设 1 1 1 是根,我们将其点度数定义为 d i d_i d i 。
我们称满足以下条件的根付有向树为好树 :
此外,对于好树的顶点 v v v ,定义 f ( v ) f(v) f ( v ) 为“包含顶点 v v v 的子树中的顶点(包括 v v v )的顶点编号的最小值”。我们将满足 f ( v ) = v f(v)=v f ( v ) = v 的顶点称为好顶点 。
求好树中所有好顶点的总数,将其对 998244353 998244353 9 9 8 2 4 4 3 5 3 取模后的余数。
2 ≤ N ≤ 500 2\ \leq\ N\ \leq\ 500 2 ≤ N ≤ 5 0 0
0 ≤ d i ≤ N − 1 0\ \leq\ d_i\ \leq\ N-1 0 ≤ d i ≤ N − 1
d 1 ≥ 1 d_1\ \geq\ 1 d 1 ≥ 1
∑ i = 1 N d i = N − 1 \sum_{i=1}^N\ d_i\ =\ N-1 ∑ i = 1 N d i = N − 1
题目分析:
显然可以考虑对于每一个点作为好节点的方案数求和就是答案。
若 v = 1 v = 1 v = 1 或 d v = 0 d_v = 0 d v = 0 ,则任意一个好树都是合法的,这个时候根据 p r u f e r prufer p r u f e r 序列,显然方案数就是:
( n − 2 ) ! ∏ u ( d 1 , u − 1 ) ! \dfrac{(n-2)!}{\prod_{u} (d_{1,u}-1)!}
∏ u ( d 1 , u − 1 ) ! ( n − 2 ) !
其中 d i , j d_{i,j} d i , j 表示以 i i i 为根的时候 j j j 的度数。
否则的话,考虑以 v v v 为根的子树只能包含 [ v , n ] [v,n] [ v , n ] 这些点,但是好像除了这个就发现不了什么式子了,就不大能做。
所以不妨设 S S S 为 v v v 子树内的点的点集,看看这个时候的方案数是什么:
( ∣ S ∣ − 2 ) ! ∏ u ∈ S ( d v , u − 1 ) ! × ( n − ∣ S ∣ + 1 − 2 ) ! ∏ u ∉ S ( d 1 , u − 1 ) ! = ( ∣ S ∣ − 2 ) ! ( n − ∣ S ∣ − 1 ) ! d 1 d v ∏ u ( d u ! ) \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!)}
u ∈ S ∏ ( d v , u − 1 ) ! ( ∣ S ∣ − 2 ) ! × u ∈ S ∏ ( d 1 , u − 1 ) ! ( n − ∣ S ∣ + 1 − 2 ) ! = u ∏ ( d u ! ) ( ∣ S ∣ − 2 ) ! ( n − ∣ S ∣ − 1 ) ! d 1 d v
第一个式子就是考虑 v v v 的子树的方案数,乘以其余点构成一棵树的方案数,其中在其余点构成的树中 v v v 所在子树充当一个叶子。
第二个式子就是考虑如下的事实:
d i , j = { d j i = j d j + 1 i ≠ j d_{i,j} =
\begin{cases}
d_j & i = j \\
d_j+1 & i \not= j
\end{cases}
d i , j = { d j d j + 1 i = j i = j
所以带入就得到了第二个式子。
注意为了可以组成一棵树,一个隐形的限制就是:∑ u ∈ S d u = ∣ S ∣ − 1 \sum\limits_{u \in S} d_u = |S| - 1 u ∈ S ∑ d u = ∣ S ∣ − 1 。
所以此时 d p dp d p 状态就相当显然了,d p [ i ] [ j ] [ k ] dp[i][j][k] d p [ i ] [ j ] [ k ] 表示大于等于 i i i 的点,选择了 j j j 个点,d u d_u d u 之和为 k k k 的方案数。
这样就可以直接统计方案数,然后乘以上面的系数就可以得到答案。
代码:
点击查看代码
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
题目描述:
给定长度为 n n n 的序列 A A A ,求序列 B B B 的个数模 998244353 998244353 9 9 8 2 4 4 3 5 3 ,满足以下条件:
值域 [ 1 , n ] [1, n] [ 1 , n ] 。
i i i 的个数不超过 A i A_i A i 。
B i B_i B i 的个数不超过 A i A_i A i 。
1 ≤ n ≤ 500 1 \le n \le 500 1 ≤ n ≤ 5 0 0 。
题目分析:
考虑设 d i d_i d i 表示数 i i i 出现的次数,则题目条件可以转化为:
d i ≤ A i d_i \le A_i d i ≤ A i
若 i i i 可以放在位置 j j j ,则必然满足 d i ≤ A j d_i \le A_j d i ≤ A j
设 c i = ∑ j = 1 n [ i ≤ A j ] c_i = \sum_{j=1}^n [i \le A_j] c i = ∑ j = 1 n [ i ≤ A j ] ,则 c i c_i c i 就代表出现次数为 i i i 的数可选的种类数,以及出现次数为 i i i 的数在 B B B 中可以放的位置数。
所以这个东西直接 d p dp d p 去做就很简单了,也就是设 d p [ i ] [ j ] [ k ] dp[i][j][k] d p [ i ] [ j ] [ k ] 表示考虑了出现次数大于等于 i i i 的数,总共选择了 j j j 种数,放在 B B B 的 k k k 个位置上。
转移就是枚举出现次数等于 i i i 的数有 x x x 个:
d p [ i + 1 ] [ j ] [ k ] × ( c i − j x ) × ( c i − k ) ! ( i ! ) x ( c i − k − i x ) ! → d p [ i ] [ j + x ] [ k + i x ] 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]
d p [ i + 1 ] [ j ] [ k ] × ( x c i − j ) × ( i ! ) x ( c i − k − i x ) ! ( c i − k ) ! → d p [ i ] [ j + x ] [ k + i x ]
转移的两个系数:第一个就是选择哪些数,第二个就是这些数放在哪些位置。
代码:
点击查看代码
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; }