第十五届蓝桥杯软件赛国赛C++ B组 题解。
 
A. 合法密码 按题意模拟即可。
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;void  solve ()     string s = "kfdhtshmrw4nxg#f44ehlbn33ccto#mwfn2waebry#3qd1ubwyhcyuavuajb#vyecsycuzsmwp31ipzah#catatja3kaqbcss2th" ;          int  res = 0 ;     for (int  i = 0 ; i < s.size (); i++){         for (int  j = 8 ; j <= 16  && i + j - 1  < s.size (); j++){             string tmp = s.substr (i, j);             int  dig = 0 ;             int  chr = 0 ;             for (auto  &i : tmp){                 dig += isdigit (i);                 chr += ('a'  <= i && i <= 'z' ) || ('A'  <= i && i <= 'Z' );             }             res += (dig && dig + chr != j);         }     }     cout << res; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
B. 蚂蚁开会 由于题目提到:不保证任意两条线段之间交点有限,所以求交点是肯定不可行的。
观察数据范围,不妨直接标记每条线段所经过的整点,在所有被标记过的整点中寻找被标记次数大于 1 的即可。
标记线段经过的整点时,可以将线段分为 “两个方向长度的gcd” 个段,这样每个段的两个端点都是整点,且只有这些端点是整点。
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; cin >> n;          unordered_map<int , unordered_map<int , int >> mp;     for (int  i = 0 ; i < n; i++){         int  a, b, c, d; cin >> a >> b >> c >> d;         int  g = gcd (c - a, d - b);         int  x = a, y = b;         int  dx = (c - a) / g, dy = (d - b) / g;         mp[x][y] ++;         do {             x += dx, y += dy;             mp[x][y] ++;         } while (!(x == c && y == d));     }     int  res = 0 ;     for (auto  &[_, l] : mp){         for (auto  &[_, cnt] : l){             res += cnt > 1 ;         }     }     cout << res << '\n' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
C. 选数概率 $$
于是不难得到:
(这里可以验证一下是否满足给出的等式,同时不难发现,由于 $ab$,$C(a+b+c,2)$ 都是二次的,因此其值只与 $a,b,c$ 比例有关,如果验证成功,那么这样取值就是最小的了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <bits/stdc++.h>  #define  int long long using  namespace  std;void  solve ()     int  i = 517  * 5 , j = 2632 , k = 308  * 5 ;     int  a = i * k, b = i * j, c = j * k;     int  g = gcd (gcd (a, b), c);     a /= g, b /= g, c /= g;     if (2091  * a * b != 517  * (a + b + c) * (a + b + c - 1 ) / 2 ){         cout << "No"  << '\n' ;         return ;     }     cout << a << ','  << b << ','  << c << '\n' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
D. 立定跳远 增加检查点 $=$ 在检查点间多跳跃一次
使用爆发技能 $=$ 在检查点间多跳跃一次
所以其实爆发技能和一个检查点的效果是等价的,也就是说,相当于现在我们可以多跳跃 $m+1$ 次。
接下来二分单次跳跃的距离 $L$,看我们需要多跳跃的次数是否在 $m+1$ 以内即可。
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;void  solve ()     int  n, m; cin >> n >> m;     m++;     vector<int > a (n + 1 )  ;     for (int  i = 1 ; i <= n; i++) cin >> a[i];     auto  check = [&](int  x){         int  cnt = 0 ;         for (int  i = 1 ; i <= n; i++){             if (a[i] == a[i - 1 ]) continue ;             cnt += (a[i] - a[i - 1 ] + x - 1 ) / x - 1 ;         }         return  cnt <= m;     };     int  l = 0 , r = a[n] - a[0 ];     while (l + 1  != r){         int  mid = l + r >> 1 ;         if (check (mid)) r = mid;         else  l = mid;     }     cout << r << '\n' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
E. 最小字符串 贪心插入显然是最优的。
我们可以先将插入串做一个排序,然后用两个指针分别指向被插入串和插入串。
只要这里选择插入串中的字符是更优的,就选择插入串,否则选择原串。
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;void  solve ()     int  n, m; cin >> n >> m;     string a, b; cin >> a >> b;     sort (b.begin (), b.end ());     for (int  i = 0 , j = 0 ; i < n; i++){         while (j != m && b[j] < a[i]){             cout << b[j++];         }         cout << a[i];         if (i == n - 1 ){             while (j != m){                 cout << b[j++];             }         }     } } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
F. 数位翻转 由于数据范围比较小,可以直接考虑处理出翻转后的序列 $b$,然后进行一个 $O(n^2)$ 的 $dp$ 来选择哪些段进行翻转。
处理翻转就先得到二进制 01 串然后反着倒回十进制就好了。
$dp[i][j][0/1]$:表示前 i 个字符,至此翻转过 j 个段,第 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 42 43 44 45 46 47 48 49 #include <bits/stdc++.h>  #define  int long long using  namespace  std;constexpr  int  inf = 1e18 ;void  solve ()     int  n, m; cin >> n >> m;     vector<int > a (n + 1 ) , b (n + 1 )  ;     for (int  i = 1 ; i <= n; i++){         int  x; cin >> x; a[i] = x;                  string s;         while (x){             s += (x & 1 ) + '0' ;             x >>= 1 ;         }         for (auto  &c : s){             b[i] <<= 1 ;             b[i] += c - '0' ;         }     }     vector<vector<vector<int >>> dp (n + 1 , vector<vector<int >>(m + 1 , vector <int >(2 , -inf)));     dp[0 ][0 ][0 ] = 0 ;          for (int  i = 1 ; i <= n; i++){         dp[i][0 ][0 ] = dp[i - 1 ][0 ][0 ] + a[i];         for (int  j = 1 ; j <= m; j++){             dp[i][j][0 ] = max (dp[i - 1 ][j][0 ], dp[i - 1 ][j][1 ]) + a[i];             dp[i][j][1 ] = max (dp[i - 1 ][j - 1 ][0 ], dp[i - 1 ][j][1 ]) + b[i];         }     }     int  res = 0 ;     for (int  j = 0 ; j <= m; j++){         res = max (res, dp[n][j][0 ]);         res = max (res, dp[n][j][1 ]);     }     cout << res << '\n' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
G. 数星星 一个 $|V_G|$ 的星星,就是在中间的点选择了它的 $|V_G|-1$ 的邻点。
那么其实图中有用的信息就只有度数,我们把每个点的度数都记录下来。
那么最后的答案就是:
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;constexpr  int  mod = 1e9  + 7 ;constexpr  int  N = 1e5  + 1 ;int  qmi (int  a, int  k, int  p)     int  res = 1 ;     while (k){         if (k & 1 ) res = res * a % p;         a = a * a % p;         k >>= 1 ;     }     return  res; } int  inv (int  a)     return  qmi (a, mod - 2 , mod); } vector<int > fact (N)  ;void  init ()     fact[0 ] = 1 ;     for (int  i = 1 ; i < N; i++){         fact[i] = fact[i - 1 ] * i % mod;     } } int  C (int  a, int  b)     if (a < b) return  0 ;     return  fact[a] * inv (fact[b] * fact[a - b] % mod) % mod; } void  solve ()     int  n; cin >> n;     vector<int > d (n)  ;     for (int  i = 1 ; i < n; i++){         int  u, v; cin >> u >> v;         u--, v--;         d[u] ++, d[v] ++;     }     int  L, R; cin >> L >> R;     int  res = 0 ;     unordered_map<int , int > mp;     for (int  i = 0 ; i < n; i++){         if (mp.count (d[i])){             res += mp[d[i]];             continue ;         }         int  sum = 0 ;         for (int  j = L; j <= R; j++){             sum += C (d[i], j - 1 );             sum %= mod;         }         res += (mp[d[i]] = sum);            res %= mod;     }     cout << res << '\n' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     init ();     solve ();     return  0 ; } 
H. 套手镯 我们可以先考虑一维的情况:
给出一堆区间,找到一个框框 $[L,R]$ 使得完全包含的区间最多。
将区间按右端点排列,然后用一个优先队列保存左端点,每次加入一个新区间,将左端点不能满足的全部弹出即可。这个过程中,优先队列大小的最大值就是框框能包含的最多区间数了。
那么对于这个二维的情况,我们一维一维的处理就好了,先在 $y$ 轴上按上述手段处理一遍,然后提取出所有在当前无限长框框($y=down,y=up$)中的圆,再在 $x$ 轴上处理一遍,就能求出一次最多能包含的圆的个数了。
注意最后要把 $w,h$ 反过来再做一遍。
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;struct  cir {    int  x, y, r;     bool  operator > (const  cir &o) const {         return  y - r > o.y - o.r;     } }; struct  seg {    int  l, r;     seg (int  l = 0 , int  r = 0 ) : l (l), r (r) {}     bool  operator > (const  seg &o) const {         return  l > o.l;     } }; void  solve ()     int  n, w, h; cin >> n >> w >> h;     vector<cir> p (n)  ;     for (int  i = 0 ; i < n; i++){         int  x, y, r; cin >> x >> y >> r;         p[i] = {x, y, r};     }     sort (p.begin (), p.end (), [](const  cir &a, const  cir &b){         return  a.y + a.r < b.y + b.r;     });     int  res = 0 ;     auto  dodo = [&]{         priority_queue<cir, vector<cir>, greater<cir>> Q;          for (int  i = 0 ; i < n; i++){         	Q.push (p[i]);         	auto  u = Q.top ();         	while (p[i].y + p[i].r - h > u.y - u.r){         		Q.pop ();         		if (Q.empty ()) break ;         		u = Q.top ();         	}         	if (Q.empty ()) continue ;         	auto  tmp = Q;         	vector<seg> segs;         	while (!tmp.empty ()){         		auto  v = tmp.top ();         		tmp.pop ();         		segs.emplace_back (v.x - v.r, v.x + v.r);         	}         	sort (segs.begin (), segs.end (), [](const  seg &a, const  seg &b){         		return  a.r < b.r;         	});         	priority_queue<seg, vector<seg>, greater<seg>> S;         	for (int  j = 0 ; j < segs.size (); j++){         		S.push (segs[j]);         		auto  s = S.top ();         		while (segs[j].r - w > s.l){         			S.pop ();         			if (S.empty ()) break ;         			s = S.top ();         		}         		int  sz = S.size ();         		res = max (res, sz);         	}         }     };     dodo ();     swap (w, h);     dodo ();     cout << res << '\n' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
I. 跳石头 不难发现:
对于这种转移,用 $bitset$ 可以非常高效的实现(或运算)。
统计集合的最大元素个数即可。
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;void  solve ()     int  n; cin >> n;     vector<int > c (n + 1 )  ;     for (int  i = 1 ; i <= n; i++){         cin >> c[i];     }     int  res = 0 ;     vector<bitset<40001>> uni (n + 1 );     for (int  i = n; i >= 1 ; i--){         uni[i][c[i]] = 1 ;         if (i + c[i] <= n) uni[i] |= uni[i + c[i]];         if (i + i <= n) uni[i] |= uni[i + i];         res = max (res, (int )uni[i].count ());     }     cout << res << '\n' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; } 
J. 最长回文前后缀 第一步,找出当前就存在的公共前后缀,统计个数后删去。
对于剩下的部分,我们一定是删去某个前缀或某个后缀(否则就没有回文前后缀了),然后考虑得到最长的回文前后缀。
这一部分可以用 KMP 实现:
先考虑删去后缀,对一个字符串 $s = c_1c_2\cdots c_n$,我们希望找到一个 $c_1c_2…=c_rc_{r-1}…$ 的最长回文前后缀。我们记 $t$ 是 $s$ 的逆序。
做拼接:str=s+#+t,这样,我们只需要利用 KMP 求出 str 中的 “最长相同前后缀” 即可,实现是 $O(n)$ 的。
再详细一些:str = c_1c_2...c_n#c_nc_{n-1}...c_1,在 $str$ 中 字符 # 后的最长相同前后缀,就等价于 $s$ 的前缀 与 删去一段后缀后的回文后缀。
同样的,考虑删除前缀的话,就做拼接 str=t+#+s 就可以。
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 #include <bits/stdc++.h>  #define  int long long using  namespace  std;void  solve ()     string s; cin >> s;     int  cnt = 0 ;          {         int  i = 0 , j = s.size () - 1 ;         while (s[i] == s[j]){             i++, j--, cnt++;         }         s = s.substr (i, s.size () - 2  * cnt);     }     auto  get_f = [](string s){         int  n = s.size ();         vector<int > f (n + 1 )  ;         for (int  i = 1 , j = 0 ; i < n; i++){             while (j && s[i] != s[j]){                 j = f[j];             }             j += (s[i] == s[j]);             f[i + 1 ] = j;         }         return  f;     };     int  res = 0 ;     auto  dodo = [&](string a, string b){         auto  f = get_f (a + "#"  + b);         for (int  i = a.size () + 2 ; i < f.size (); i++){             if (i + f[i] < f.size ()) res = max (res, f[i]);         }     };     string t = s;     reverse (t.begin (), t.end ());     dodo (s, t);     dodo (t, s);     cout << cnt + res << '\n' ; } signed  main ()     ios::sync_with_stdio (0 );     cin.tie (0 );     solve ();     return  0 ; }