AtCoder-ABC-405 题解

比赛速览

  • A. if-else
  • B. 暴力模拟
  • C. 前缀和优化(不可暴力)
  • D. BFS + 打印路径 + 逆向思维
  • E. 思维 + 枚举 + 组合数
  • F. 问题转化->LCA (区间数据结构做法欢迎投稿)
  • G. 莫队 + 分块暴力

A - Is it rated?

题意

ARC比赛分为两个组别Div. 1 1 1 和Div. 2 2 2, 给定选手的当前分数 R R R与参赛组别Div. X X X,判断选手是否参与积分排名。参与积分排名的规则是:

  1. 对于Div. 1 1 1 比赛,分数在 [ 1600 , 2999 ] [1600, 2999] [1600,2999]区间内。
  2. 对于Div. 2 2 2 比赛,分数在 [ 1200 , 2399 ] [1200, 2399] [1200,2399]区间内。

参考程序

#include<cstdio>
int r,x;
int main(){
	scanf("%d%d",&r,&x);
	printf((x==1&&1600<=r&&r<=2999)||(x==2&&1200<=r&&r<=2399)?"Yes":"No");
	return 0;
}

B - Not All

给定一个长度为N的整数序列A,和一个正整数M。你可以做如下的操作任意多次:
操作:删除A序列的最后一个元素。
求最少的操作次数,使得序列A不满足:A中包含 1 ∼ M 1\sim M 1M中的每个数字。

  • 1 ≤ M ≤ N ≤ 100 1≤M≤N≤100 1MN100
  • 1 ≤ A i ≤ M 1≤A_i≤M 1AiM

由于N比较小,可以暴力Check此时A中是否包含 1 ∼ M 1\sim M 1M中的每个数字。也可以用桶维护。

参考程序

#include<iostream>
using namespace std;
int a[105],b[105];
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i = 1;i<=n;i++)
	{
		cin>>a[i];
		b[a[i]]++;
	}
	for(int i = 1;i<=m;i++)
	{
		if(b[i]==0)
		{
			cout<<'0';
			return 0;
		}
	}
	int c = 0;
	for(int i = n;i>=1;i--)
	{
		c++;
		b[a[i]]--;
		if(b[a[i]]==0)
		{
			break;
		}
	}
	cout<<c;
	return 0;
}

C - Sum of Product

给定一个长度为N的整数序列A, 求 ∑ 1 ≤ i < j ≤ N A i A j \sum_{1\leq i < j \leq N} {A_iA_j} 1i<jNAiAj

  • 2 ≤ N ≤ 3 × 10 5 2≤N≤3×10^5 2N3×105
  • 1 ≤ A i ≤ 10 4 1≤A_i≤10^4 1Ai104

由于N很大,不能直接写两层循环模拟,但是我们发现,结果中是每个 A i A_i Ai都会乘上自己后面每个数字,因此直接求后缀和与 A i A_i Ai相乘即可。反过来对于每个 A j A_j Aj都是乘上自己的前面,即前缀和。

参考程序

#include <bits/stdc++.h>
using namespace std;
int n;
long long a[300005];
long long s[300005];
long long ans;
int main() {
	cin >> n;
	for(int i = 1; i <= n; i++) {
		cin >> a[i];
		s[i] = s[i-1] + a[i]; 
	}
	for(int i = 1; i < n; i++) {
		ans += a[i] * (s[n]-s[i]);
	}
	cout << ans;
	return 0;
}

D - Escape Route

给定一个 H H H W W W列的地图,其中.表示空地可以通过,#表示障碍无法通过,E表示出口。可以上下左右移动,请你求出每个空地到距离最近的出口的路线。
输出每个位置应该向哪个方向移动才能到达最近的出口,方向是(^, >, v, <)

  • 1 ≤ H , W ≤ 1000 1 \leq H, W \leq 1000 1H,W1000

本题与白老师讲过的BFS题目《小明和他的朋友们》非常相似,只是多了一个打印路径。

  1. 首先不能从每个空地开始做BFS,因为这样复杂度过高,每个点都要跑一次是 O ( ( H W ) 2 ) O((HW)^2) O((HW)2)。可以选择从所有的E出发(即将所有的E在一开始都丢到队列中作为起点),每次搜到一个空地点就是这个空地点最近能到达的E。
  2. 第二个问题就是:如何记录路径?
    我们直接在BFS的时候记录搜索到这个点时的方向即可。因为我们等于从终点E搜索到起点空地,那么只要记录每个点的来的方向,就可以一路走到E。

参考程序

#include <iostream>
#include <queue>
#include <string>
using namespace std;

int main() {
  int H, W;
  cin >> H >> W;
  vector<string> S(H);
  for (auto& s : S) cin >> s;

  queue<pair<int, int>> Q;
  for (int i = 0; i < H; i++) {
    for (int j = 0; j < W; j++) {
      if (S[i][j] == 'E') Q.emplace(i, j);
    }
  }
  int dx[] = {1, 0, -1, 0};
  int dy[] = {0, 1, 0, -1};
  auto ok = [&](int i, int j) { return 0 <= i and i < H and 0 <= j and j < W; };
  string A = "^<v>";
  while (!Q.empty()) {
    auto [i, j] = Q.front();
    Q.pop();
    for (int k = 0; k < 4; k++) {
      int ni = i + dx[k];
      int nj = j + dy[k];
      if (!ok(ni, nj)) continue;
      if (S[ni][nj] != '.') continue;
      S[ni][nj] = A[k];
      Q.emplace(ni, nj);
    }
  }

  for (auto& s : S) cout << s << "\n";
}

E - Fruit Lineup

给定四种水果 a a a b b b c c c d d d各有 A A A B B B C C C D D D个,我们需要排列这些水果满足:

  • a a a水果全在 c c c的左边
  • a a a水果全在 d d d的左边
  • b b b水果全在 d d d的左边

同类的水果是完全一致的,没有差异。

  • 1 ≤ A , B , C , D ≤ 10 6 1 \leq A, B, C, D \leq 10^6 1A,B,C,D106

对于这种有 4 4 4个变量的题目,经验告诉我们只要考虑 2 2 2个应该就可以了。 a a a的条件比较多,可以从它下手。

  1. 所有的 a a a最靠右只能放到 A + B A+B A+B位置,因为后面的 C + D C+D C+D至少要保证,否则违反 1 1 1 2 2 2两条规则。
  2. 如果最靠右侧的 a a a 我们确定在位置 i i i,那么 c c c只能在这个位置的右侧 , a ,a ,a只能在这个位置的左侧。
  3. 如果 a a a c c c的位置都确定了,剩余的空位置只能是先放所有的 b b b,再放所有的 d d d。因此只要确定了 a a a c c c的方案就可以了。 b b b d d d不用考虑。

综上,我们可以枚举最后一个 a a a的位置为 i i i,并在 i i i左侧的 i − 1 i-1 i1个位置中选择 A − 1 A-1 A1个位置放 a a a,在右侧 N − i N-i Ni个位置中选择 C C C个位置放 c c c即可。

A n s = ∑ i = A A + B C ( i − 1 , A − 1 ) C ( N − i , C ) Ans = \sum_{i = A}^{A+B} C(i-1, A-1)C(N-i, C) Ans=i=AA+BC(i1,A1)C(Ni,C)

参考程序

#include<bits/stdc++.h>
#define int long long
#define N 1000010
#define tN 3000000
#define mod 998244353
using namespace std;
int a,b,c,d,ans,fac[3*N],inv[3*N];
int qp(int a,int n){
	int fac=1;
	while(n){
		if(n&1) fac=fac*a%mod;
		a=a*a%mod,n>>=1;
	}
	return fac;
}
int C(int n,int m){return fac[n]*inv[m]%mod*inv[n-m]%mod;}
signed main(){
	cin>>a>>b>>c>>d;
	fac[0]=1;
	for(int i=1;i<=tN;i++) fac[i]=fac[i-1]*i%mod;
	inv[tN]=qp(fac[tN],mod-2);
	for(int i=tN;i;i--) inv[i-1]=inv[i]*i%mod;
	for(int x=0;x<=b;x++) (ans+=C(a-1+b-x,a-1)*C(c+d+x,d+x)%mod)%=mod;
	cout<<ans<<"\n";
	return 0;
}

F - Chord Crossing

有2N个点编号为 1 ∼ 2 N 1\sim 2N 12N在一个圆环上顺时针均匀分布,给出M条连线,每条连线连通一组偶数编号的点对 A i A_i Ai B i B_i Bi, 保证这些边互不交叉, 且 A i ≠ B i A_i \neq B_i Ai=Bi

接下来有 Q Q Q个查询,每次查询给出一组奇数编号的点对 C i C_i Ci D i D_i Di并画上连线, 请你求出在给出的M条连线中,与新增连线之间一共会有多少个交点。每次查询给出的连线不会影响下一次查询,即查询新增的连线不会留在图中。

  • 2 ≤ N ≤ 10 6 2 \leq N \leq 10^6 2N106
  • 1 ≤ Q ≤ 10 5 1 \leq Q \leq 10^5 1Q105

样例输入

4 2
2 4
6 8
3
1 3
3 7
1 5

样例输出

1
2
0

如图:

image.png

重要的问题转化基础

圆上点之间的两条连线(a, b) 与(c, d)之间如果有交点,则必须满足a < c < b < d。(令a<b, c<d)

本题圆上问题,我们可以考虑在一条直线上,问题可以完全等价成:

在一条长度为 2 N 2N 2N的线段上,均匀分布 2 N 2N 2N个点,存在 M M M个区间,区间的首尾都是偶数,且这些区间之间要么是分离关系,要么是包含关系,不存在交叉关系。

Q Q Q次查询,每次给出一个新的区间,求新区间与原M个区间中多少区间存在交叉关系。

ABC-405-G.png

对于这个新问题,着重看其中的包含关系。还可以继续转化模型成为一个树模型。

M M M条线段看作是 M M M个点
如果线段 i i i与线段 j j j存在直接包含关系,即不存在区间 k k k满足: i i i包含 k k k, 且 k k k包含 j j j,则从 i i i j j j画一条有向边。

在本题中保证了所有 M M M条区间互不相交,因此包含关系是一个严格偏序关系,以此方法连边的到的一定是树结构(无环,无多个父亲),由此我们可以获得一个森林,额外创建一个虚点 0 0 0, 使得其构建成一棵树结构。

那么对于一个查询区间 [ C i , D i ] [C_i, D_i] [Ci,Di],我们找出直接包含点 C i C_i Ci的区间 u u u, 和直接包含点 D i D_i Di的区间 v v v,那么 u u u v v v是与查询区间相交的,并且 u u u v v v的父亲也可能是和查询区间相交的,可以一直向上找他们的父亲,直到某节点所代表的区间包含了查询区间,很明显,这是一个 L C A LCA LCA的问题。

G - Range Shuffle Query

给定一个长度为 N N N的序列 A , Q A,Q A,Q个查询,每次查询给出 [ L , R , X ] [L, R, X] [L,R,X], 要求将 A A A序列 [ L , R ] [L, R] [L,R]的子段中 < X <X <X的数字都取出来,并做排列,有多少种不同的排列。

例如:对于子段 [ 1 , 2 , 3 , 3 , 1 ] [1,2,3,3,1] [1,2,3,3,1], 当 X = 3 X=3 X=3的时候剩余 [ 1 , 1 , 2 ] [1,1,2] [1,1,2] 排列共有 3 3 3种: [ 1 , 1 , 2 ] , [ 1 , 2 , 1 ] , [ 2 , 1 , 1 ] . [1,1,2], [1, 2, 1], [2, 1, 1]. [1,1,2],[1,2,1],[2,1,1].

  • 1 ≤ N , Q ≤ 2.5 × 10 5 1 \leq N, Q \leq 2.5\times 10^5 1N,Q2.5×105
  • 1 ≤ A i ≤ N 1 \leq A_i \leq N 1AiN

对于多重集组合数,单纯求一次的复杂度是 O ( N ) O(N) O(N), 所以明显复杂度不支持每次查询单独处理,因此需要考虑查询之间的变化量,所以是个莫队算法。

接下来就是如何快速获知两次查询之间每一种数字数量变化了多少,那么就可以在上一次的结果里乘乘除除搞定。

查询不变化不仅仅是个区间的变化,还有一个数字 X X X的变化。如果仅仅是区间的变化则是一个标准的莫队算法。对于 X X X的变化,我们需要用到分块的思想来解决。

  1. 很明显我们需要维护一个桶,用来记录每个数字出现的次数。当区间变化的时候,莫队算法可以很容易维护桶的变化。

  2. 区间变化完成之后 , X ,X ,X的变化会导致原本需要纳入考虑的数字现在不需要计入了( X X X变小了),或者反之;那么当 X X X的变化幅度较大的时候,我们扫描的代价就变大到 O ( N ) O(N) O(N)了,因此我们可以对桶做分块维护,将复杂度降低到 O ( N ) O(\sqrt N) O(N )

  3. 由此我们单次计算多重集排列数的复杂度为 O ( N ) O(\sqrt N) O(N ), 总的计算复杂度为 O ( Q N ) O(Q\sqrt N) O(QN ).

  4. 莫队维护区间变化的总复杂度为 O ( N N ) O(N\sqrt N) O(NN )

  5. 综上总复杂度为 O ( N N ) O(N\sqrt N) O(NN )

参考程序

自行参考Atcoder提交记录。


赛后交流

WX: jikecheng11,赛后大家可以一起交流做题思路,分享做题技巧,欢迎大家的加入。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值