第十三届蓝桥杯软件赛省赛C++ A组 题解。
 
    
     
以下代码均通过测试,可放心食用。
本期知识点:离线处理,数学期望(推公式),二分答案,树状数组,计算几何,欧拉筛,带权并查集。
填空题 1. 裁纸刀(5分) 一通计算发现结果需要裁纸的次数为 m * n + 3,输出 443  即可。
2. 灭鼠先锋(5分) 对于只有一行 OOOO 的情况,手推搜索一下,不难发现先手的一定会最终填满本行。
也就是说,第二行先手的必负,因此将第一行先填满者胜。
情况一,乔走至 XOXO,那么乔一定先填满 
情况二,乔走至 XXXX,那么乔一定先填满 
情况三,乔走至 OXXO,那么乔一定先填满 
情况四,一定是蓝先填满 
 
故这四场结果为 LLLV  。
3. 求和(10分) 意义不明的题,会等差数列和开 long long 即可,输出 204634714038436  。
程序题 4. 选数异或(10分) 考虑离线处理。用 map 维护 <数, 下标> ,遍历至 a[i] 时先看 map 中是否有与其异或为 x 的数,如果有,更新最大可取的左端点。处理所有询问右端点为 i 的区间。
时间复杂度 $O(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 41 #include <bits/stdc++.h>  #define  int long long  using  namespace  std;void  solve ()     int  n, m, x; cin >> n >> m >> x;     vector<int >a (n + 1 );     for (int  i = 1 ; i <= n; i++) cin >> a[i];     vector<vector<pair<int , int >>>q (1e5  + 1 );          vector<int >res (m);     for (int  i = 0 ; i < m; i++){         int  l, r; cin >> l >> r;         q[r].emplace_back (l, i);     }     unordered_map<int , int >mp;          int  l_max = -1 ;     for (int  i = 1 ; i <= n; i++){         int  aim = a[i] ^ x;         if (mp[aim]) l_max = max (l_max, mp[aim]);         mp[a[i]] = i;         for (auto  &[l, idx] : q[i]){             if (l_max != -1  && l_max >= l) res[idx] = 1 ;         }     }     for (auto  &i : res){         if (i) cout << "yes"  << '\n' ;         else  cout << "no"  << '\n' ;     } } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
5. 爬树的甲壳虫(15分) 对这个问题,本人用了近一面的草稿纸,构建递推公式 -> 配置系数并累加 -> 解出一个长相些许狰狞的结果。大概长这样:($p_i$ 表示爬向高度 $i$ 时,掉下树根的概率)
于是本着专业事要交给专业人的理念,我请教了一位数学系朋友,他轻描淡写地丢给了我下面这一行方程,然后轻松又极具技巧性地速通了这个问题(一整个降维打击)。我们来看看这个思路:($p_i$ 表示爬向高度 $i$ 时,成功向上爬的概率,$x$ 表示目标期望)
将 目标期望 x 做了如下拆解:
首次爬升失败发生在 第1次 爬升,对应的期望爬升次数为 $(x+1)(1-p_1)$ 
首次爬升失败发生在 第2次 爬升,对应的期望爬升次数为 $(x+2)p_1(1-p_2)$ 
…… 
首次爬升失败发生在 第n次 爬升,对应的期望爬升次数为 $(x+n)p_1p_2…p_{n-1}(1-p_n)$ 
没有爬升失败,对应的期望爬升次数为 $np_1p_2…p_n$ 
 
公式左侧即为这些拆解项之和,右侧为目标期望 x。
利用公式求解即可,时间复杂度 $O(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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <bits/stdc++.h>  #define  int long long using  namespace  std;constexpr  int  mod = 998244353 ;int  qmi (int  a, int  k)     int  res = 1 ;     while (k){         if (k & 1 ) res = res * a % mod;         k >>= 1 ;         a = a * a % mod;     }     return  res; } int  inv (int  x)     return  qmi (x, mod - 2 ); } struct  modint {    int  v;     modint (int  x = 0 ) : v ((x % mod + mod) % mod) {}     modint operator +(const  modint &o){         return  modint (v + o.v);     }     modint operator -(const  modint &o){         return  modint (v - o.v);     }     modint operator *(const  modint &o){         return  modint (v * o.v);     }     modint operator /(const  modint &o){         return  modint (v * inv (o.v));     } }; using  Z = modint;void  solve ()     int  n; cin >> n;     vector<Z>p (n + 1 );     for (int  i = 1 ; i <= n; i++){         int  x, y; cin >> x >> y;         p[i] = Z (y - x) / Z (y);     }          vector<Z>pi (n + 1 );     pi[0 ] = Z (1 );     for (int  i = 1 ; i <= n; i++){         pi[i] = pi[i - 1 ] * p[i];     }          cout << (accumulate (pi.begin (), pi.begin () + n, Z (0 )) / pi[n]).v; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
6. 青蛙过河(15分) 本题重点在于发现以下性质:
充分性:已知 $2x$ 只青蛙能过河,那么每个长度为 $y$ 的区间,都会被踩到至少 $2x$ 次,充分性得证。
必要性:已知每个长度为 $y$ 的区间,石头高度之和大于等于 $2x$,那么考虑,让 $2x$ 只青蛙同时起跳,第一次全部跳到 [1, y] 区间内,分两种情况讨论:
a[1] <= a[y + 1],那么 1 位置可以全部跳到 y + 1位置。a[1] > a[y + 1],那么 将 1 位置的尽可能跳到 y + 1 位置后,剩下的可以全部跳到 [2, y] 位置内(因为 a[2] + a[3] + ... + a[y + 1] <= 2x) 
换言之,这 $2x$ 只青蛙可以全部从 [1, y] 转移到 [2, y + 1] 内。这样,继续转移下去,青蛙可以全部到 [n - y, n - 1] 内,并全部跳到对岸,必要性得证。
得出该性质后,只需二分 y,并维护前缀和来 check 即可。时间复杂度 $O(n\log 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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;void  solve ()     int  n, x; cin >> n >> x;     vector<int >a (n), pre (n);     for (int  i = 1 ; i < n; i++){         cin >> a[i];         pre[i] = pre[i - 1 ] + a[i];     }          auto  check = [&](int  y){         for (int  i = 1 ; i + y - 1  < n; i++){             if (pre[i + y - 1 ] - pre[i - 1 ] < 2  * x){                 return  0 ;             }         }         return  1 ;     };          int  l = 0 , r = n;     while (l + 1  != r){         int  mid = l + r >> 1 ;         if (check (mid)) r = mid;         else  l = mid;     };     cout << r; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
7. 最长不下降子序列(20分) 首先用 L[i], R[i] 分别维护以 a[i] 为 结尾 / 开头 的最长不下降子序列长度。
接下来遍历,改变哪 k 个元素。当我们改变 [r - k, r - 1] 范围内的元素时,整个序列被分为了三段:
[1, r - k - 1] 与 [r - k, r - 1] 与 [r, n],我们现在需要得到整个新序列的最长不下降子序列长度。
令修改的所有元素等于 a[r](这点的可行性在后面给出证明)。这样,我们要在 [1, r - k - 1] 找出满足 a[u] <= a[r] 的 u 中最大的 L[u],这个操作用树状数组维护前缀最大值完成。那么新序列最长不下降子序列长度为 L[u] + k + R[r]。
Tip1:为什么将这 k 个元素修改为 a[r] 是可行的。a[r] 为左端点的最长不下降子序列,因为即使让它去配合 a[r] 更后面的数 a[rr] 去形成最长不降序列,那他也不会有更优答案,因为他不会比 a[rr - k, rr - 1] 更优,那时 L[u] 的选择更广。
Tip2:如何用树状数组维护前缀最大值。
逐个加入 L[i] 时,在数轴上,下标为 a[i] 的位置放上数值 L[i],这样,查询满足 a[u] <= a[r] 的 u 中最大的 L[u] 时,只需要在数轴上 <= a[r] 的部分查询最大值即可。可以用线段树来维护区间最大值,但既然只涉及到前缀最大值,可以用树状数组完成更为方便。
Tip3:这里 a[i] 的范围有限,只有 1e6,建立这么大的树状数组可行,但当数据更大时,需要先进行离散化处理。
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 74 75 76 #include <bits/stdc++.h>  #define  int long long using  namespace  std;class  FenwickTree {public :    int  n;     vector<int >bit;     FenwickTree (int  n) : n (n), bit (n + 1 , 0 ) {}     void  update (int  p, int  v)          while (p <= n){             bit[p] = max (bit[p], v);             p += p & -p;         }     }     int  query (int  p)          int  res = 0 ;         while (p > 0 ){             res = max (res, bit[p]);             p -= p & -p;         }         return  res;     } }; void  solve ()     int  n, k; cin >> n >> k;     vector<int >a (n + 1 );     for (int  i = 1 ; i <= n; i++) cin >> a[i];     vector<int >L (n + 2 ), R (n + 2 );               vector<int >tmp;     for (int  i = 1 ; i <= n; i++){         int  pos = upper_bound (tmp.begin (), tmp.end (), a[i]) - tmp.begin ();         if (pos == tmp.size ()){             tmp.emplace_back (a[i]);         }         else  {             tmp[pos] = a[i];         }         L[i] = pos + 1 ;     }     tmp.clear ();     for (int  i = n; i >= 1 ; i--){         int  pos = upper_bound (tmp.begin (), tmp.end (), a[i], greater<>()) - tmp.begin ();         if (pos == tmp.size ()) {             tmp.emplace_back (a[i]);         }         else {             tmp[pos] = a[i];	         }          R[i] = pos + 1 ;     }     FenwickTree mx_bit (1e6 )  ;     int  res = k + R[k + 1 ];     for (int  r = k + 2 ; r <= n + 1 ; r++){         mx_bit.update (a[r - k - 1 ], L[r - k - 1 ]);         res = max (res, mx_bit.query (a[r]) + k + R[r]);     }     cout << res; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
8. 扫描游戏(20分) 几何处理:
先将整个图像按 $y=x$ 直线翻转,这样回到我们熟悉的从 $x$ 轴正半轴出发,逆时针旋转,这一步骤在读入时交换 $x$ 与 $y$ 的读入顺序即可完成。 
根据上一步操作,将每个点的角度定义为从 $x$ 轴正半轴逆时针旋转到原点与该点直线的角度。为方便比较角度大小且不引入 double 类型,记录每个点的如下性质:象限,在该象限内的 $x$, $y$ 坐标(即两者始终为正)。这样比较两者角度时,先比较象限值,再比较两者再各自象限内斜率 $y_1/x_1$ 与 $y_2/x_2$,也即比较 $y_1\times x_2$ 与 $y_2\times x_1$。 
 
整体上采用如下思路:
先将所有点按照距离原点的距离排序。
将当前长度可以扫到的点加入一个小根堆(按角度从小到大)中。
为了保证小根堆中的顺序就是我们吃到点的顺序,不难发现,只有当前这一圈还未扫到的角度范围内的点才能直接加入小根堆,而当前已经扫过的角度范围内的点,是不能加入小根堆的,他们只能在下一圈内被吃到。因此,额外维护一个数组,存入只有在下一圈内才可以被吃到的点。这样,每当扫完一圈,我们将数组内的元素全部重新压入小根堆中,这样就保证了小根堆中弹出元素的顺序就是我们吃点的顺序。
 
细节处理:
如何保证同时吃掉的点的编号相同呢?只要让这次吃的点和上次吃的点比较角度大小,如果相同,那么将这个点的编号设置会和上一个相同即可。 
如何处理 $(0, 0)$ 这个毒瘤数据呢?直接将其改到 $(1, 0)$,这不影响任何事情,读者自证。 
 
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 #include <bits/stdc++.h>  #define  int long long using  namespace  std;class  point {public :    int  x, y, quadrant, value, idx;     void  setp (int  x, int  y, int  v, int  i)          if (x == 0  && y == 0 ){             this ->x = 1 , this ->y = 0 , quadrant = 1 ;         }         else  if (x > 0  && y >= 0 ) {             quadrant = 1 ;             this ->x = x;             this ->y = y;         }         else  if (y > 0  && x <= 0 ){             quadrant = 2 ;             this ->x = y;             this ->y = -x;			         }         else  if (x < 0  && y <= 0 ){             quadrant = 3 ;             this ->x = -x;             this ->y = -y;         }         else {             quadrant = 4 ;             this ->x = -y;             this ->y = x;         }         value = v, idx = i;     }     int  len ()  const           return  x * x + y * y;      }          bool  operator  > (const  point &o) const  {         if (quadrant != o.quadrant) return  quadrant > o.quadrant;         return  y * o.x > x * o.y;     }          bool  operator  >= (const  point &o) const  {         if (quadrant != o.quadrant) return  quadrant > o.quadrant;         return  y * o.x >= x * o.y;     }          bool  operator  == (const  point &o) const  {         return  quadrant == o.quadrant && y * o.x == x * o.y;     } }; void  solve ()      int  n, l; cin >> n >> l;      __int128 L = l;      vector<point>p (n);      for (int  i = 0 ; i < n; i++){          int  x, y, v; cin >> y >> x >> v;          p[i].setp (x, y, v, i);      }      sort (p.begin (), p.end (), [](const  point &a, const  point &b){          return  a.len () < b.len ();      });      priority_queue<point, vector<point>, greater<point>>Q;      int  label = 0 , run = 0 ;      vector<int > res (n, -1 )  ;            point last = {0 , 0 , -1 , 0 , 0 };     vector<point>next_round;     while (run < n && p[run].len () <= L * L){         Q.push (p[run++]);     }     while (!Q.empty () || !next_round.empty ()){         if (Q.empty () && !next_round.empty ()){             for (auto  &u : next_round) Q.push (u);             next_round.clear ();         }         auto  u = Q.top (); Q.pop ();         L += u.value;         res[u.idx] = ++label;         if (u == last) res[u.idx] = res[last.idx];         last = u;         while (run < n && p[run].len () <= L * L){             if (p[run] >= u){                 Q.push (p[run++]);             }             else {                 next_round.push_back (p[run++]);             }         }      }      for (auto  &i : res) cout << i << ' ' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
9. 数的拆分(25分) 首先考虑将 a 质因数分解,$a=p_1^{q_1}p_2^{q_2}\cdots p_n^{q_n}$,如果存在某个 $q_i = 1$ ,一定不满足题意。
假设现在所有 $q_i\ge 2$ ,那么分为以下三种情况:
$q_i\mod 3=0$,可以写作 $q_i=3k$ 
$q_i\mod 3=1$,可以写作 $q_i=4+3k$ 
$q_i\mod 3=2$,可以写作 $q_i=2+3k$ 
 
即,所有 $q_i$ 都可写作 $2m+3n$ 的形式,于是 $p_i^{q_i}=(p_i^m)^2(p_i^n)^3$,那么 $a$ 就可以表示成 $u^2v^3$ 的形式了。
综上,只要不存在某个 $q_i = 1$,$a$ 就是满足题设的。
为了在合适的时间复杂度内完成,做如下放缩:$1e18\ge a=u^2v^3\ge (\min(u, v))^5$ 
得到 $\min(u,v)\le 10^{\frac{18}{5}}<4000$,这样,把 $4000$ 以下的 $p_i$ 全部筛掉(即 $u$ 或 $v$ 至少有一者被筛干净),最后只能剩下平方根或立方根。
$T=1e5$,遍历 4000 个数去筛也不太合适,因此先做一个线性筛,得到 4000 内质数,再用这些质数去筛。
(这题还有个恶心的点,是 sqrt 和 cbrt 的精度问题,会导致偏差,需要自行处理)
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;class  Euler {public :    vector<int >primes;     vector<bool >comp;     Euler (int  n){         comp.resize (n + 1 );         for (int  i = 2 ; i <= n; i++){             if (!comp[i]) primes.emplace_back (i);             for (int  j = 0 ; i * primes[j] <= n; j++){                 comp[i * primes[j]] = true ;                 if (!(i % primes[j])) break ;             }         }     } }; void  solve ()     Euler sieve (4000 )  ;     auto  check = [&](int  n) -> bool {         for (auto  &u : sieve.primes){             if (n % u == 0 ){                 int  q = 0 ;                 while (n % u == 0 ){                     n /= u, q++;                 }                 if (q == 1 ) return  0 ;             }         }         int  sq = sqrt (n), cb = cbrt (n);         if ((sq + 1 ) * (sq + 1 ) <= n) sq++;    	         if ((cb + 1 ) * (cb + 1 ) * (cb + 1 ) <= n) cb++;         return  sq * sq == n || cb * cb * cb == n;     };     int  t; cin >> t;     while (t--){         int  n; cin >> n;         if (check (n)) cout << "yes"  << '\n' ;         else  cout << "no"  << '\n' ;	     } } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
10. 推导部分和(25分) 可以先去做这题:AT_ABC238_E Range Sums - 洛谷 。
本题与上题的唯一区别就是,我们需要在并查集上维护每两个前缀和的差。
记 $S$ 为前缀和数组,那么每给出一个 [l, r] 的和为 s ,等价于给出 $S_r-S_{l-1}=s$。
这样,我们建立带权并查集,每个结点维护到达根节点的距离($dis_i=S_根-S_i$)。
此带权并查集的路径压缩、合并、查询操作如图:
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;void  solve ()     int  n, m, q; cin >> n >> m >> q;          vector<int >p (n + 1 ), dis (n + 1 );     for (int  i = 1 ; i <= n; i++) p[i] = i;     function<int (int )>find = [&](int  x){         if (x == p[x]) return  x;         int  px = p[x];         p[x] = find (p[x]);         dis[x] += dis[px];         return  p[x];     };     auto  merge = [&](int  x, int  y, int  w){         int  fx = find (x), fy = find (y);         if (fx == fy) return ;         p[fx] = fy;         dis[fx] = dis[y] - dis[x] + w;     };     for (int  i = 1 ; i <= m; i++){         int  l, r, s; cin >> l >> r >> s;         merge (l - 1 , r, s);     }          while (q--){         int  l, r; cin >> l >> r;         if (find (l - 1 ) != find (r)) cout << "UNKNOWN"  << '\n' ;         else  cout << dis[l - 1 ] - dis[r] << '\n' ;     } } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; }