Bootstrap

07/16/24 快乐赛总结&题解

一、总体情况

  • 考试一共有四道题
  • 前一个小时左右做完了 A A A B B B D D D 三题,后两个小时开始乱考 C C C 题读完一遍后根本不会做,也没怎么仔细看

二、题解

A题 顺时针围栏

题目描述

围绕 Farmer John 最大的草地的栅栏已经损坏了,如今他终于决定要换一个新的栅栏。

不幸的是,当 Farmer John 在铺设新栅栏时,一只巨大的蜜蜂突然出现,在他的草地上追着他跑,导致最后栅栏被沿着一条相当不规则的路径铺设。栅栏可以用一个字符串表示,每个字符为 N(north,北)、E(east,东)、S(south,南)、W(west,西)之一。每个字符表示一米长的一段栅栏。举例来说,如果字符串为 NESW,这表示栅栏从起点开始向北延伸 1 1 1 米,然后向东延伸 1 1 1 米,然后向南延伸 1 1 1 米,然后向西延伸 1 1 1 米,回到栅栏的起点。

栅栏的结束位置与开始位置相同,而这是栅栏的路径上唯一会被到达多次的位置(从而起始位置是唯一会被再次到达的位置,在栅栏结束之时)。结果,栅栏确实围起了一个草地上连通的区域,尽管这个区域可能形状十分奇特。

Farmer John 想要知道他铺设栅栏的路径是顺时针(当按字符串表示的顺序沿着栅栏的路径行走时被围起的区域位于右侧)还是逆时针(被围起的区域位于左侧)。

题目分析

这道题确定了每个转弯的度数都是固定的 90 90 90 度,所以可以通过判断左转的弯的数量和右转弯的数量那个多,若左弯更多,则是逆时针,否则输出顺时针

代码
#include <iostream>
#include <string>
#include <vector>

using namespace std;

int t;
string s;
vector<char> cmp = {'N', 'E', 'S', 'W', 'N'};

bool is_clockwise(char start, char finish) {
  for (int i = 1; i < int(cmp.size()); i++)
    if (start == cmp[i - 1] && finish == cmp[i]) return true;
  return false;
}

int main() {
  cin >> t;
  while (t--) {
    cin >> s;
    int cnt_clockwise = 0;
    int cnt_counterclockwise = 0;
    for (int i = 1; i < int(s.size()); i++) {
      if (s[i] == s[i - 1]) {
        continue;
      }
      if (is_clockwise(s[i - 1], s[i])) {
        cnt_clockwise++;
      } else {
        cnt_counterclockwise++;
      }
    }
    if (is_clockwise(s.front(), s.back())) {
      cnt_clockwise++;
    } else {
      cnt_counterclockwise++;
    }
    if (cnt_clockwise > cnt_counterclockwise) {
      // clockwise
      cout << "CW" << endl;
    } else {
      // counterclockwise
      cout << "CCW" << endl;
    }
  }
  return 0;
}

B题 传送门

题目描述

高桥王国右 N N N 个城镇,编号从 1 1 1 N N N

每个城镇有一个传送门。在 i i i 号城镇的传送门可以到达城市 A i A_{i} Ai

国王高桥喜欢正整数 K K K。自私的国王想要知道他从第 1 1 1 号城镇出发,用 K K K 次传送门可以到达的位置。

请帮国王写一个程序完成这个问题。

思路分析

这道题的 K K K 的范围是 K ≤ 1 0 18 K\le 10^{18} K1018 K K K 很大,所以暴力枚举肯定是不行的,而这道题 N N N 的范围是 N ≤ 2 ∗ 1 0 5 N\le 2 * 10^{5} N2105 N N N 不算大。由此可知,这道题中的图肯定有环,所以我们必须先把这个环找出来。

找出环后,有两种情况

  • 因为环不一定是以城市 1 1 1 为起点,所以有可能在传送 K K K 次以后,还没有进到环,就可以得出答案
  • 否则,答案一定在这个环里面,只需要用 K K K 对环的长度取模,再输出对应的值即可
代码
#include <iostream>
#include <vector>

using namespace std;

const int MAX_N = 2e5 + 5;

int n;
long long k;
int to[MAX_N];
bool vis[MAX_N];
vector<int> path;
long long cnt_path = 0;
int start_to_path = 0;

void dfs(int now) {
  if (vis[now]) {
    int p = 1;
    while (p != now) {
      start_to_path++;
      p = to[p];
    }

    path.push_back(now);
    cnt_path++;
    p = to[now];
    while (p != now) {
      path.push_back(p);
      cnt_path++;
      p = to[p];
    }
    return;
  }

  vis[now] = true;
  dfs(to[now]);
}

int main() {
  cin >> n >> k;
  for (int i = 1; i <= n; i++) cin >> to[i];
  dfs(1);
  if (k < start_to_path) {
    int res = 1;
    for (int i = 1; i <= k; i++) {
      res = to[res];
    }
    cout << res << endl;
  } else {
    cout << path[(k - start_to_path) % cnt_path] << endl;
  }
  return 0;
}

C题 涂画

Bessie 最近收到了一套颜料, 她想要给她的牧草地一端的栅栏上色。栅栏由 N N N 个 1 米长的小段组成 1 ≤ N ≤ 1 0 5 1\le N\le 10^{5} 1N105 。 Bessie 可以使用 26 种不同的颜色,她将这些颜色由浅到深用字母 'A''Z' 标号('A' 是很浅的颜色, 'Z' 是很深的颜色)。从而她可以用一个长为 N N N 且每个字符均为字母的字符串来描述她想要给栅栏的每一小段涂上的颜色。

初始时,所有栅栏小段均末被上色。Bessie 一笔可以给任意连续若干小段涂上同一种颜色,只要她不会在较深的颜色之上涂上较浅的颜色(她只能用较深的颜色覆盖较浅的颜色)。

例如, 一段长为 4 的末被涂色的栅栏可以按如下方式上色: BBB. BBLL BQQL 由于时间紧迫, Bessie 认为她可能需要放弃为栅栏上某个连续的区间上色!现在, 她正在考虑 Q Q Q 个候选的区间 1 ≤ Q ≤ 1 0 5 1\le Q \le 10^{5} 1Q105, 每个区间用满足 1 ≤ a ≤ b ≤ N 1\le a\le b\le N 1abN 的两个整数 ( a , b ) (a, b) (a,b) 表示, 为需要不上色的小段 a . . . b a...b a...b 的两端点位置。

对于每个候选区间, 将所有区间外的栅栏小段都涂上所希望的颜色, 并且区间内的栅栏小段均不涂色, 最少需要涂多少笔? 注意在 这个过程中 Bessie 并没有真正进行任何的涂色,所以对于每个候选区间的回答是独立的。

思路分析

这道题在询问的时候问的是一个区间 [ a , b ] [a,b] [a,b] 不涂色,其它涂色,所以我们可以把每次询问的答案转化为 L a − 1 + R b + 1 L_{a-1}+R_{b+1} La1+Rb+1 L L L 数组中, L i L_{i} Li 表示区间 [ 1 , i ] [1, i] [1,i] 涂好色的最少操作次数。 R R R 数组中, R i R_{i} Ri 表示区间 [ 1 , i ] [1, i] [1,i] 涂好色的最少操作次数。

所以,这道题的关键就是 L L L 数组和 R R R 数组怎么求。

L L L 数组为例,设字符串为 S S S,假设当前遍历到了字母 S i S_{i} Si,则分以下几种情况:

  • 若字母 S i S_{i} Si 之前没有出现过,那 S i S_{i} Si 这种颜色肯定要单独涂一遍,则 L i = L i − 1 + 1 L_{i}=L_{i-1}+1 Li=Li1+1
  • 若字母 S i S_{i} Si 之前出现过,且之前的字母 S i ′ S_{i'} Si 到现在的 S i S_{i} Si 之间的所有字母都大于等于 S i S_{i} Si,则在涂 S i ′ S_{i'} Si 时,就可以顺带一起把 S i S_{i} Si 涂了, L i = L i − 1 L_{i}=L_{i-1} Li=Li1
  • 若字母 S i S_{i} Si 之前出现过,但之前的字母 S i ′ S_{i'} Si 到现在的 S i S_{i} Si 之间有字母小于 S i S_{i} Si,则涂 S i ′ S_{i'} Si 时就不能涂 S i S_{i} Si 了,不然中间小于 S i S_{i} Si 的颜色就没办法涂。所以, S i S_{i} Si 要单独涂色, L i = L i − 1 + 1 L_{i}=L_{i-1}+1 Li=Li1+1

以上所有信息都可以用下面代码中的 f l a g flag flag 数组维护。

代码
#include <iostream>
#include <map>
#include <string>
#include <vector>

using namespace std;

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);

  int n, k;
  string s;
  cin >> n >> k >> s;
  vector<int> l(n);
  vector<int> r(n);
  map<char, bool> flag;

  l[0] = 1;
  flag[s[0]] = true;
  for (int i = 1; i < n; i++) {
    if (flag[s[i]]) {
      l[i] = l[i - 1];
    } else {
      l[i] = l[i - 1] + 1;
    }
    flag[s[i]] = true;
    for (char ch = s[i] + 1; ch <= 'Z'; ch++) {
      flag[ch] = false;
    }
  }

  flag.clear();

  r[n - 1] = 1;
  flag[s[n - 1]] = true;
  for (int i = n - 2; i >= 0; i--) {
    if (flag[s[i]]) {
      r[i] = r[i + 1];
    } else {
      r[i] = r[i + 1] + 1;
    }
    flag[s[i]] = true;
    for (char ch = s[i] + 1; ch <= 'Z'; ch++) {
      flag[ch] = false;
    }
  }

  while (k--) {
    int first, last;
    cin >> first >> last;
    first--;
    last--;
    int res = 0;
    if (first - 1 >= 0) res += l[first - 1];
    if (last + 1 < n) res += r[last + 1];
    cout << res << endl;
  }

  return 0;
}

D题 跳跃

题目描述

N N N 个单元格排成一行,编号从左到右为 1 , 2 , . . . N 1,2,...N 1,2,...N

Tak生活在这些单元格中,目前在单元格 1 1 1。他试图通过以下描述的步骤到达单元格 N N N

给定一个整数 K K K,它小于或等于 10 10 10,以及 K K K 个不相交的段 [ L 1 , R 1 ] , [ L 2 , R 2 ] . . . [ L k , R k ] [L_{1},R_{1}],[L_{2},R_{2}]...[L_{k},R_{k}] [L1,R1],[L2,R2]...[Lk,Rk]。令 S S S 为这些 K K K 段的并集。这里,段 [ l , r ] [l,r] [l,r] 表示满足 l ≤ i ≤ r l\le i\le r lir 的所有整数 i i i 的集合。

  • 当你在单元格 i i i 时,从 S S S 中选择一个整数 d d d 并移动到单元格 i + d i+d i+d。你不能移出单元格。

为了帮助Tak,找出能到达单元格 N N N 的方式数,对 998244353 998244353 998244353 取模。

思路分析

这道题跳到每个单元格的状态数都是确定的,并且后面如何选择跳的长度不会影响前面的答案,满足无后效性,可以使用 D P DP DP

状态定义: f [ i ] f[i] f[i]

  • i i i:前 i i i 个单元格中
  • f [ i ] f[i] f[i]:前 i i i 个单元格中的状态数(答案)

状态转移

我们知道,每个单元格可以从在集合 S S S 中的距离远的地方跳过来。并且,由于是同一件事情有多种做法,总方法数可以使用加法原理,转化成式子就是:
f [ i ] = ∑ j = 1 K ∑ k = L j R j f [ max ⁡ ( i − k , 0 ) ] f[i]=\sum_{j=1}^{K}\sum_{k=L_{j}}^{R_{j}}f[\max(i-k,0)] f[i]=j=1Kk=LjRjf[max(ik,0)]
但这样的话,时间复杂度就是外层的 i i i 乘上内层的 j j j,是一个 O ( N 2 ) O(N^{2}) O(N2) 的复杂度,交上去会超时。那怎么优化这个转移方程呢?

首先,我们发现:每个 f [ i ] f[i] f[i] 都是若干段连续 f f f 值的和,于是,问题就变成了就变成了 f f f 数组的单点修改+区间查询,很容易就能想到用树状数组优化,优化后时间复杂度 O ( N log ⁡ 2 N ) O(N\log_{2}N) O(Nlog2N),可以 A C AC AC,我考试时也是写得这种解法。

但是还有一种复杂度 O ( N K ) O(NK) O(NK) 的解法:

为后续方便,令 a ( x ) = max ⁡ ( x , 0 ) a(x)=\max(x, 0) a(x)=max(x,0)

我们可以将上述式子展开,得:
f [ i ] = ∑ j = 1 K ( f [ a ( i − L j ) ] + f [ a ( i − L j − 1 ) ] + . . . + f [ a ( i − R j ) ] ) f[i]=\sum_{j=1}^{K}(f[a(i-L_{j})]+f[a(i-L_{j}-1)]+...+f[a(i-R_{j})]) f[i]=j=1K(f[a(iLj)]+f[a(iLj1)]+...+f[a(iRj)])
同理可得:
f [ i − 1 ] = ∑ j = 1 K ( f [ a ( i − L j − 1 ) ] + f [ a ( i − L j − 2 ) ] + . . . + f [ a ( i − R j − 1 ) ] ) f[i-1]=\sum_{j=1}^{K}(f[a(i-L_{j}-1)]+f[a(i-L_{j}-2)]+...+f[a(i-R_{j}-1)]) f[i1]=j=1K(f[a(iLj1)]+f[a(iLj2)]+...+f[a(iRj1)])
两式相减得:
f [ i ] = f [ i − 1 ] − ∑ j = 1 K f [ a ( i − R j − 1 ) ] + ∑ j = 1 K f [ a ( i − L j ) ] f[i]=f[i-1]-\sum_{j=1}^{K}f[a(i-R_{j}-1)]+\sum_{j=1}^{K}f[a(i-L_{j})] f[i]=f[i1]j=1Kf[a(iRj1)]+j=1Kf[a(iLj)]
f [ i ] = f [ i − 1 ] + ∑ j = 1 K ( f [ a ( i − L j ) ] − f [ a ( i − R j − 1 ) ] ) f[i]=f[i-1]+\sum_{j=1}^{K}(f[a(i-L_{j})]-f[a(i-R_{j}-1)]) f[i]=f[i1]+j=1K(f[a(iLj)]f[a(iRj1)])
而最终得答案就是 f [ N ] − f [ N − 1 ] f[N]-f[N-1] f[N]f[N1],以为现在的每个 f f f 值都变成了之前 f f f 值的和,就像是前缀和数组一样,原来的 f [ N ] f[N] f[N] 就等于现在的 f [ N ] − f [ N − 1 ] f[N]-f[N-1] f[N]f[N1]

记得在过程中取模,并让结果最终一直加上模数,直到变为正数。

代码
#include <iostream>
#include <vector>

using namespace std;

#define MOD 998244353

struct Range {
  int l, r;
};

const int N = 2e5 + 5;

int n, k;
vector<Range> vec;
long long f[N];

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  cin >> n >> k;
  for (int i = 1; i <= k; i++) {
    vec.push_back({0, 0});
    cin >> vec.back().l >> vec.back().r;
    for (int j = vec.back().l; j <= vec.back().r; j++) {
      f[1 + j]++;
    }
  }
  f[1] = 1;
  for (int i = 2; i <= n; i++) {
    f[i] = f[i - 1];
    for (auto j : vec) {
      f[i] += (f[max(i - j.l, 0)] - f[max(i - j.r - 1, 0)]);
      f[i] %= MOD;
    }
  }
  long long ans = f[n] - f[n - 1];
  while (ans < 0) {
    ans += MOD;
  }
  cout << ans << endl;
  return 0;
}

三、考试中存在的一些问题

在还有整整两个小时的时间的情况下, C C C 题我都没仔细想,后来得知,这道题只需要打出暴力再简单优化一下就能拿一半分,我当时但凡打个暴力都还能多拿50分

四、要做的一些事

  • 还是每天坚持打USACO的铜组的题,提升码力
  • 补atcoder的题的时候,有时间的话尽量写下题解,因为我发现这样真的能非常提升对题目做法的理解
  • 下次考试的时候合理安排时间,不要害怕难题,因为很多题的暴力分还是很香的,要敢于去深入的思考。并且,在练模拟题的基础上,可以花点时间练一点 D P DP DP 和贪心的题,提升思维

五、谢谢大家阅读

;