[2021-09-04 AtCoder Beginner Contest 217] 题解

8 篇文章 0 订阅


网址链接

A - Lexicographic Order

签到题

#include <cstdio>
#include <iostream>
using namespace std;
int main() {
	string s, t;
	cin >> s >> t;
	if( s < t ) printf( "Yes\n" );
	else printf( "No\n" );
	return 0;
}

B - AtCoder Quiz

签到题

#include <cstdio>
#include <iostream>
using namespace std;
char s1[10], s2[10], s3[10];

int main() {
	scanf( "%s %s %s", s1, s2, s3 );
	if( 'R' != s1[1] and 'R' != s2[1] and 'R' != s3[1] )
		printf( "ARC\n" );
	if( 'B' != s1[1] and 'B' != s2[1] and 'B' != s3[1] )
		printf( "ABC\n" );
	if( 'H' != s1[1] and 'H' != s2[1] and 'H' != s3[1] )
		printf( "AHC\n" );
	if( 'G' != s1[1] and 'G' != s2[1] and 'G' != s3[1] )
		printf( "AGC\n" );
	return 0;
}

C - Inverse of Permutation

签到题

#include <cstdio>
#define maxn 200005
int n;
int p[maxn], ans[maxn];

int main() {
	scanf( "%d", &n );
	for( int i = 1;i <= n;i ++ )
		scanf( "%d", &p[i] ), ans[p[i]] = i;
	for( int i = 1;i <= n;i ++ )
		printf( "%d ", ans[i] );
	return 0;
}

D - Cutting Woods

签到题

刚开始想歪了,打算不思考直接暴力线段树cao

后来一看1e9的长度,打扰了

再一想,直接set不也可以做到线段树的功能吗

直接把操作点扔进去,找左右最近的分割点即可

#include <set>
#include <cstdio>
using namespace std;
set < int > s;
int n, Q, c, x;

int main() {
	scanf( "%d %d", &n, &Q );
	s.insert( 0 ), s.insert( n );
	while( Q -- ) {
		scanf( "%d %d", &c, &x );
		if( c == 1 ) s.insert( x );
		else {
			auto l = s.lower_bound( x );
			auto r = l; l --;
			printf( "%d\n", *r - *l );
		}
	}
	return 0;
}

E - Sorting Queries

签到题

可能会被排序操作吓到TLE

实际上转个弯就知道序列一定是前面有序后面无序的状态

那么分开存进队列即可,遇到排序就把无序的全都丢进有序队列

每个点最多被操作入队两次

复杂度只有有序队列的log罢了

这都是表面上吓唬人的

#include <queue>
#include <cstdio>
using namespace std;
priority_queue < int, vector < int >, greater < int > > q;
queue < int > New;
int Q, opt, x;

int main() {
	scanf( "%d", &Q );
	while( Q -- ) {
		scanf( "%d", &opt );
		switch( opt ) {
			case 1 : {
				scanf( "%d", &x );
				New.push( x );
				break;
			}
			case 2 : {
				if( ! q.empty() )
					printf( "%d\n", q.top() ), q.pop();
				else 
					printf( "%d\n", New.front() ), New.pop();
				break;
			}
			case 3 : {
				while( ! New.empty() )
					q.push( New.front() ), New.pop();
				break;
			}
		}
	}
}

F - Make Pair

中档题

必须要两人是好关系,并且相邻才行

果断区间 d p dp dp,根据范围 200 200 200果断猜测时间复杂度应为 O ( N 3 ) / O ( N 3 log ⁡ N ) O(N^3)/O(N^3\log N) O(N3)/O(N3logN)

d p i , j : [ i , j ] dp_{i,j}:[i,j] dpi,j:[i,j]区间的人都成功配对离开的方案数,答案自然为 d p 1 , 2 n dp_{1,2n} dp1,2n

考虑枚举与 i i i配对的是 k k k,则 k k k将区间 [ i , j ] [i,j] [i,j]隔绝成两个互相独立的子问题, d p i + 1 , k − 1 ; ; d p k + 1 , r dp_{i+1,k-1};;dp_{k+1,r} dpi+1,k1;;dpk+1,r

定义 d p i + 1 , i = 1 dp_{i+1,i}=1 dpi+1,i=1

假设 [ i + 1 , k − 1 ] [i+1,k-1] [i+1,k1]配对个数为 x ( k − 1 − ( i + 1 ) + 1 = k − i − 1 ) x(k-1-(i+1)+1=k-i-1) x(k1(i+1)+1=ki1) [ k + 1 , r ] [k+1,r] [k+1,r]的配对个数为 y ( r − ( k + 1 ) + 1 = r − k ) y(r-(k+1)+1=r-k) y(r(k+1)+1=rk)

d p i , j = ∑ k = i + 1 r d p i + 1 , k − 1 ∗ d p k + 1 , r ∗ ( x + y + 1 y ) dp_{i,j}=\sum_{k=i+1}^rdp_{i+1,k-1}*dp_{k+1,r}*\binom{x+y+1}{y} dpi,j=k=i+1rdpi+1,k1dpk+1,r(yx+y+1)

( x + y + 1 y ) \binom{x+y+1}{y} (yx+y+1):区间 [ i , j ] [i,j] [i,j]的配对次数为 x + y + 1 x+y+1 x+y+1( i , k i,k i,k的一次配对),从中选择 y y y次操作右子区间

为什么不是从中选择 x x x次操作左子区间?

因为左子区间的操作事关 i , k i,k i,k的相邻问题,而右子区间无关

必须左子区间都操作完了才能操作 ( i , k ) (i,k) (i,k)配对

所以说就不可能出现最后 x x x次操作全都是操作左子区间,反而先操作 ( i , k ) (i,k) (i,k)的配对,这样是错误的,那么选 x x x的组合数 ( x + y + 1 x ) \binom{x+y+1}{x} (xx+y+1)计算的方案数就是错误的

选择右子区间操作的轮数后,剩下的 x + 1 x+1 x+1位置就固定了,最后一个一定是 ( i , k ) (i,k) (i,k)的配对

#include <cstdio>
#define int long long
#define mod 998244353
#define maxn 405
int fac[maxn], inv[maxn];
bool good[maxn][maxn];
bool vis[maxn][maxn];
int dp[maxn][maxn];
int n, m;

int qkpow( int x, int y ) {
	int ans = 1;
	while( y ) {
		if( y & 1 ) ans = ans * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return ans;
}

void init() {
	fac[0] = inv[0] = 1;
	for( int i = 1;i < maxn;i ++ ) 
		fac[i] = fac[i - 1] * i % mod;
	inv[maxn - 1] = qkpow( fac[maxn - 1], mod - 2 );
	for( int i = maxn - 2;i;i -- )
		inv[i] = inv[i + 1] * ( i + 1 ) % mod;
}

int C( int n, int m ) {
	return fac[n] * inv[m] % mod * inv[n - m] % mod;
}

int dfs( int l, int r ) {
	if( vis[l][r] ) return dp[l][r];
	if( l > r ) return vis[l][r] = dp[l][r] = 1;
	int &ans = dp[l][r];
	for( int i = l + 1;i <= r;i += 2 ) {
		if( ! good[l][i] ) continue;
		int x = i - l - 1 >> 1, y = r - i >> 1;
		ans = ( ans + dfs( l + 1, i - 1 ) * dfs( i + 1, r ) % mod * C( x + y + 1, y ) ) % mod; 
	}
	vis[l][r] = 1;
	return ans;
}

signed main() {
	init();
	scanf( "%lld %lld", &n, &m );
	for( int i = 1, a, b;i <= m;i ++ ) {
		scanf( "%lld %lld", &a, &b );
		good[a][b] = good[b][a] = 1;
	}
	n <<= 1;
	printf( "%lld\n", dfs( 1, n ) );
	return 0;
} 

G - Groups

简单题

d p i , j : dp_{i,j}: dpi,j: i i i个数一共使用了 j j j个非空集合

那么转移只会分为两种

  • i i i单独成一个新集合
    • d p i , j + = d p i − 1 , j − 1 dp_{i,j}+=dp_{i-1,j-1} dpi,j+=dpi1,j1
  • i i i放进前 j j j个集合中的某一个
    • 集合无差别
    • 每一个取模 m m m相同的数都在不同的集合
    • 前面与 i i i同余的个数显然为 ⌊ i − 1 m ⌋ \lfloor\frac{i-1}{m}\rfloor mi1
    • 不能放那些数所在的集合,剩下的集合都是可以放的
    • d p i , j + = d p i − 1 , j ∗ max ⁡ ( 0 , j − ⌊ i − 1 m ⌋ ) dp_{i,j}+=dp_{i-1,j}*\max(0,j-\lfloor\frac{i-1}{m}\rfloor) dpi,j+=dpi1,jmax(0,jmi1)
#include <cstdio>
#include <iostream>
using namespace std;
#define int long long
#define mod 998244353
#define maxn 5005
int n, m;
int dp[maxn][maxn];

signed main() {
	scanf( "%lld %lld", &n, &m );
	dp[0][0] = 1;
	for( int i = 1;i <= n;i ++ )
		for( int j = 1;j <= i;j ++ )
			dp[i][j] = ( dp[i - 1][j - 1] + dp[i - 1][j] * max( 0ll, j - ( i - 1 ) / m ) ) % mod;
	for( int i = 1;i <= n;i ++ )
		printf( "%lld\n", dp[n][i] );
	return 0;
}

说实话,我觉得F G的题目分值给反了,前面竟然全是签到水题,后面的经典DP还是可以给个好评的

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值