前言
本场比赛出题周期里,由于在备赛冲击奖牌,就把这场第二届迎新杯的出题工作基本全交给校队未来的栋梁们了。整个出题过程中,几位学弟们认真负责的态度让我非常放心,事实证明也确实带来了一套不错的题👍从去年叱咤赛场的选手身份转变为尽职尽责的出题人,他们做得很棒!
这场出题人比去年还是很善良的(),赛前不断地在降难度。(而且原本Lihg给的两个疑似蓝题+紫题都被ban了
最难的应该算是天际线那道题,思维上有一定的门槛,但也不难想。我勉强算出了这一道题,目的就是想给赛有余力oier们强调一下STL的重要性。
应该说,善良的出题人这回是没有防AK的,其实都是可做题。
非要细分的话,我赛前预估大概4 3 2的梯度(4签到+3简单+2中等,octal的个人看法)
题号
预估难度
知识点
FirstBlood
A
1 ★
简单数学、容斥原理
ェメ
B
2 ★
二分、数学
clhdlg
C
2 ★
DP动态规划
Ovine
D
3 ★
贪心、优先队列
/
E
2 ★
贪心
ェメ
F
1 ★
思维题
翊,
G
1 ★
贪心、简单模拟
翊,
H
1 ★(签到题)
简单数学
learner_164
I
3 ★
组合数学、推式子 打表找规律
翊,
赛后看了一下,虽然简单了,但也是有区分度的,各个水平的选手也都有题可做,为出题人们点赞!👍
另外预告一下:
下学期的校赛将会由YNU现役单体最强、刷题量3000+的Lihg主导出题,大家敬请期待一下!
说明:以下所有题解代码,大家关注main函数就行了,代码头部纯属个人习惯,不用深究。
A 穿越之爆裂魔法与特殊数字
【容斥原理】
首先,我想对于刚高考完的同学来说,整除
这个概念可能有点陌生了。因为大家第一次接触这个词应该是小学初中,高考数学应该是不涉及。
不过大家看样例解释应该就能get到:
a 被 b 整除 <=> a 除 b 余数为0 <=> a 是 b 的倍数
这道题就是相当于求 1 − n 1-n 1 − n 中,有多少个数是 a , b , c a, b, c a , b , c 中至少一个数的倍数。
如果是求 1 − n 1-n 1 − n 中 x x x 的倍数,要怎么求?
是不是只需要 n / x n / x n / x (整除) 就行。
那是不是 n / a + n / b + n / c n / a + n / b + n / c n / a + n / b + n / c ?
no,no,no。因为会有重复的。
所以就需要用到容斥原理,看张图就理解了
代码如下:
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> using namespace std;#define pii pair<int, int> #define pdd pair<double, double> #define pll pair<ll, ll> #define debug(x) cerr << #x << ": " << x << '\n' ; #define endl '\n' typedef long long ll;typedef unsigned long long ull;inline void solve () { int n, a, b, c; cin >> n >> a >> b >> c; ll res = 0 ; res += n/a + n/b + n/c; res -= n/(a*b) + n/(a*c) + n/(b*c); res += n / (a*b*c); cout << res << endl; } int main () { cout << fixed << setprecision (10 ); ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); int t; cin >> t; while (t --) solve (); return 0 ; }
B 机房的神秘灯光
【二分】
其实有经验的选手这道题就是会比较秒杀,因为二分的题基本都比较套路。
k k k 的范围是 1 e 18 1e18 1 e 1 8 ,这也不像是有规律的数学模型,所以第一反应就应该想到二分。
要讲清楚这道题,大概有两个要点:
① 第 i i i 盏灯只会被 i i i 的因数改变状态。(并且 i i i 的因数是小于等于 i i i 的,即一定都会被选择)
② 按奇数次开关,灯关;按偶数次开关,灯才会依然开。
③ 只有平方数的因子个数是奇数,其他数的因子一定是两两不相同的数一组。
那么,实际上就是求满足 [ 1 , n ] [1, n] [ 1 , n ] 中非平方数的个数恰好为 k k k 的最小的 n n n 是多少。考虑到 k k k 范围是 1 e 18 1e18 1 e 1 8 二分求解即可,复杂度为 O ( l o g 2 k ) O(log_2{k}) O ( l o g 2 k ) ,大概在64这个数量级。
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> using namespace std;#define pii pair<int, int> #define pdd pair<double, double> #define pll pair<ll, ll> #define debug(x) cerr << #x << ": " << x << '\n' ; #define endl '\n' typedef long long ll;typedef unsigned long long ull;inline void solve () { ll k; cin >> k; auto check = [&](ll x) { ll t = sqrt (x); return x - t >= k; }; ll l = 1 , r = 4e18 ; while (l < r) { ll mid = l+r >> 1 ; if (check (mid)) r = mid; else l = mid + 1 ; } cout << l << endl; } int main () { cout << fixed << setprecision (10 ); ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); int t; cin >> t; while (t --) solve (); return 0 ; }
C 云山探险记2
【DP】
前言:去年校赛的云山探险记1也是一个比较典的DP,不过思维难度比本题高一些,欢迎有兴趣的同学去钻研一下(PS:去年那题的定位是防AK,所以一时半会儿没想通也别着急)。
题意就是有一个 n n n 个数的序列,每个数可以选或不选。
选到第偶数个数时,可以收获翻倍。问选完n个数后,最大收获是多少。
这道题之所以不能直接贪心,是因为我中途可以放弃选一个数,从而使得后续有个原本是第奇数个选中的数,变为第偶数个数,这个数很大的话收获翻倍
就能弥补放弃的损失。
还有个值得注意的点是,不会有连续的两个数都不被选中。(从贪心的角度上,这个行为是自损而无利的)
DP思路一
解析直接看代码里的注释
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 #include <bits/stdc++.h> using namespace std;#define pii pair<int, int> #define pdd pair<double, double> #define pll pair<ll, ll> #define debug(x) cerr << #x << ": " << x << '\n' ; #define endl '\n' typedef long long ll;typedef unsigned long long ull;int main () { int n; cin >> n; vector<vector<ll>> dp (n+5 , vector <ll>(2 )); vector<int > a (n+1 ) ; for (int i=1 ; i<=n; i++) cin >> a[i]; dp[1 ][1 ] = a[1 ]; dp[1 ][0 ] = -2e18 ; for (int i=2 ; i<=n; i++) { dp[i][0 ] = max (dp[i-1 ][1 ] + a[i]*2 , dp[i-2 ][1 ] + a[i]*2 ); dp[i][1 ] = max (dp[i-1 ][0 ] + a[i], dp[i-2 ][0 ] + a[i]); } cout << max (dp[n][0 ], dp[n][1 ]) << endl; return 0 ; }
DP思路二
解析直接看代码里的注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int main () { int n; cin >> n; vector<vector<ll>> dp (n+5 , vector <ll>(2 )); vector<int > a (n+1 ) ; for (int i=1 ; i<=n; i++) cin >> a[i]; dp[1 ][1 ] = a[1 ]; dp[1 ][0 ] = -2e18 ; for (int i=2 ; i<=n; i++) { dp[i][0 ] = max (dp[i-1 ][0 ], dp[i-1 ][1 ] + a[i]*2 ); dp[i][1 ] = max (dp[i-1 ][1 ], dp[i-1 ][0 ] + a[i]); } cout << max (dp[n][0 ], dp[n][1 ]) << endl; return 0 ; }
D 穿越之我在《都市:天际线》里修路
【贪心】【优先队列】
前段时间看朋友在玩这个游戏,顺手想了个故事。
抛开题目背景,一句话题意其实就是:
有 n n n 个点,每个点有一个权值 a i a_i a i ,每个点的度为 d i d_i d i 。构造一颗树使得 $ \sum_{i=1}^N a_i\times{d_i}^2$ 的值最小。
不知道有没有同学看到这道题的时候,想到了最小生成树🤔
这道题的思想和最小生成树的prim算法很类似哈。
不过只能说学过prim有助于想到正解,其实完全不会prim也是能写的哈。prim算法虽然有名有姓的,但也就是个贪心而已,没那么厉害。
这道题怎么贪呢?
如果一上来就从整体(完全体的树)考虑,根本无从下手。
我们不妨不要那么聪明,别一上来就建模成 "构造一颗树使得 $ \sum_{i=1}^N a_i\times{d_i}^2$ 的值最小“这个问题哈。
不如顺着题目描述,路是一条一条修的,我们就一步步考虑。
我们考虑一个一个城市地扩张连通块,直到全部城市都连进来。
对于题给背景,每修一条路其实就对应着把一个新城市连到当前的连通块里。(一定是这样的,因为是树)
那么新增的城市 i i i 当前只会连着一条公路,即加进来的代价为 $a_i × 1 $ ,是个固定值。
而与城市 i i i 所连接的那个点 j j j 所新增的代价是不固定的,这也就是我们要贪的对象。
原本 j j j 的代价是 a j × d j 2 a_j × d_j^2 a j × d j 2 ,现在要变为 a j × ( d j + 1 ) 2 a_j × (d_j+1)^2 a j × ( d j + 1 ) 2 。
我们要最小化总的代价,其实只需要最小化每一步的 a j × ( d j + 1 ) 2 − a j × d j 2 a_j × (d_j+1)^2 - a_j × d_j^2 a j × ( d j + 1 ) 2 − a j × d j 2 。
至于怎么维护每个 j j j 对应的代价,可能需要用到优先队列 + 重载运算符。(不重载的话也能写,就是麻烦点)
关于重载运算符的方法,可以看一下我以前写过的这篇文章:优先队列的重载运算符 - octal_zhihao - 博客园
代码如下:
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 #include <bits/stdc++.h> using namespace std;#define pii pair<int, int> #define pdd pair<double, double> #define pll pair<ll, ll> #define endl '\n' typedef long long ll;typedef unsigned long long ull;struct node { ll x; ll y; friend bool operator <(node a,node b) { ll ta = a.x * ((a.y+1 )*(a.y+1 ) - a.y*a.y); ll tb = b.x * ((b.y+1 )*(b.y+1 ) - b.y*b.y); return ta > tb; } }; priority_queue<node> q; inline void solve () { int n; cin >> n; vector<ll> a (n+1 ) ; for (int i=1 ; i<=n; i++) cin >> a[i]; sort (a.begin ()+1 , a.end ()); q.push ({a[1 ], 1 }); q.push ({a[2 ], 1 }); ll res = 0 ; res += a[1 ] + a[2 ]; for (int i=3 ; i<=n; i++) { auto [w, d] = q.top (); q.pop (); res += w * ((d+1 )*(d+1 ) - d*d); res += a[i]; q.push ({a[i], 1 }); q.push ({w, d+1 }); } cout << res << endl; } int main () { cout << fixed << setprecision (10 ); ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); solve (); return 0 ; }
E 饥饿之墙
【贪心】
贪心策略:每次摧毁从最左边的尚未被破坏的墙的右端点 R i R_i R i 开始破坏,就能保证每次摧毁的D列尽可能得靠右,也就能尽可能多地覆盖右侧的墙。
换句话说,每次在未摧毁的 ( L , R ) (L, R) ( L , R ) 对中找 R R R 最小的来打。
那么只需要,最开始就按 R R R 来排序,然后从前往后遍历就行。
代码如下,详细过程可看注释:
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 #include <bits/stdc++.h> using namespace std;#define pii pair<int, int> #define pdd pair<double, double> #define pll pair<ll, ll> #define debug(x) cerr << #x << ": " << x << '\n' ; #define endl '\n' typedef long long ll;typedef unsigned long long ull;inline void solve () { int n, d; cin >> n >> d; vector<pii> a; for (int i=1 ; i<=n; i++) { int l, r; cin >> l >> r; a.push_back ({l, r}); } sort (a.begin (), a.end (), [&](pii x, pii y) { return x.second < y.second; }); int R = 0 , res = 0 ; for (auto [l, r]: a) { if (l <= R) continue ; res ++; R = r + d - 1 ; } cout << res << endl; } int main () { cout << fixed << setprecision (10 ); ios::sync_with_stdio (false ); cin.tie (0 );cout.tie (0 ); solve (); return 0 ; }
F Fire_Lime的*3 or /2问题
【思维】【数学】
这种题其实也算初阶的诈骗题,ICPC其实还有很多很智慧的诈骗题(也非常有意思)。
过程搞这么复杂,其实*3是不会增加因数分解后2的个数的。
所以我们只需要求出,这些数里一共有多少个因子2可以来除就ok了。
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 #include <bits/stdc++.h> using namespace std;#define pii pair<int, int> #define pdd pair<double, double> #define pll pair<ll, ll> #define debug(x) cerr << #x << ": " << x << '\n' ; #define endl '\n' typedef long long ll;typedef unsigned long long ull;int main () { int res = 0 ; int n; cin >> n; for (int i=1 ; i<=n; i++) { int a; cin >> a; while (a % 2 == 0 ) { a /= 2 ; res ++; } } cout << res << endl; return 0 ; }
G Fire_Lime的打印机
【贪心】【模拟】
验题的时候目测G的做法应该是比较多的,但本质其实差不多。我也没考虑其他做法,这里就给一种好理解的做法吧:
首先,我们想尽可能地省时 ,那就要尽可能最大化每次右移所打印的数 。
然后规则上,打印机打印了 a a a ,那么下一次就只能打印 a + 1 a + 1 a + 1 ,都没有其他方案可选。那其实最大化每次打印的方案是显然且固定的,就是每次遇到上一个数 + 1 +1 + 1 就打印 ,直到无数可印而返回。
这里有个误区就是考虑到DP,但其实每次转移只能到 a + 1 a+1 a + 1 ,转移结果是固定的,没必要DP。
拓展:这题再加点难度可以改为:“每次打印能打印比上一个数字更大的数”,例如打印出数字 "1"之后,可以打印2,3,4…,n。
这样就是一道家喻户晓的经典DP模型了。
那么回过来,这个逻辑用代码要怎么实现呢?
对于一个数 a a a 而言,只要 a − 1 a - 1 a − 1 在前面出现过,我们在 打印 a − 1 a - 1 a − 1 的那一轮就可以把 a a a 打印了,不需要付出额外的代价。反之,就只能新开一轮,从 a a a 开始打印。
怎么记录 a − 1 a-1 a − 1 是否在 $ a $ 之前呢?一个简单的做法是用 STL
库的 set 容器(不用万能头的话,需要引入set头文件)。
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 #include <bits/stdc++.h> using namespace std;#define pii pair<int, int> #define pdd pair<double, double> #define pll pair<ll, ll> #define debug(x) cerr << #x << ": " << x << '\n' ; #define endl '\n' typedef long long ll;typedef unsigned long long ull;int main () { int n; cin >> n; set<int > st; int res = 0 ; for (int i=1 ; i<=n; i++) { int ai; cin >> ai; if (!st.count (ai-1 )) res ++; st.insert (ai); } cout << res-1 << endl; return 0 ; }
不用set其实也很简单,用一个bool类型的数组代替就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main () { int n; cin >> n; vector<bool > p (n+1 , false ) ; int res = 0 ; for (int i=1 ; i<=n; i++) { int ai; cin >> ai; if (!p[ai-1 ]) res ++; p[ai] = true ; } cout << res-1 << endl; return 0 ; }
H 我爱YNU-ICPC校队
【签到】【模拟】or【数学】
这道题的难点其实就在于有隔天的,如果要比大小,那应该统一参考系。
做法很多哈,为了保证正确性其实可以多写几个if来模拟一下时间。花点时间总能做出来的,所以定位是签到。
介绍一下我的做法:
首先需要明确一点,对一个时间+24是不会改变它本来的意义的。
我们不妨就把 b b b 作为最早的时间,凡是比b小的都加24.
然后看看 a a a 是否在 [ b , c ] [b, c] [ b , c ] 范围内即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <bits/stdc++.h> using namespace std;int main () { int a, b, c; cin >> a >> b >> c; if (c < b) c += 24 ; if (a < b) a += 24 ; if (a >= b && a <= c) cout << "No" << endl; else cout << "I love YNU-ICPC team" << endl; return 0 ; }
I 量子宇宙的迎新赛
【组合数学】【推式子】
考了考高中数学的知识,数竞生应该是能拿捏的
从高中课本上有的两个公式入手
性质1: $ C_{n}^{m} =\frac{n!}{m!(n-m)!} $
性质2: C n 1 + C n 2 + C n 3 + . . . + C n n = 2 n C_{n}^{1}+C_{n}^{2}+ C_{n}^{3}+ ... +C_{n}^{n}= 2^{n} C n 1 + C n 2 + C n 3 + . . . + C n n = 2 n ①
由性质1 可以推出:$m × C_{n}^{m} = n × C_{n-1}^{m-1} $ (展开了约分一下就行)
那么结合性质2可得:1 C n 1 + 2 C n 2 + 3 C n 3 + ⋯ + n C n n = n 2 n − 1 1 C_{n}^{1}+2 C_{n}^{2}+3 C_{n}^{3}+\cdots+n C_{n}^{n}=n 2^{n-1} 1 C n 1 + 2 C n 2 + 3 C n 3 + ⋯ + n C n n = n 2 n − 1 (不过这个式子本题用不到)
又因为 m 2 × C n m = m × n × C n − 1 m − 1 = n × C n − 1 m − 1 + n × ( m − 1 ) × C n − 1 m − 1 m^2 × C_{n}^{m} = m × n × C_{n-1}^{m-1} = n × C_{n-1}^{m-1} + n × (m-1) × C_{n-1}^{m-1} m 2 × C n m = m × n × C n − 1 m − 1 = n × C n − 1 m − 1 + n × ( m − 1 ) × C n − 1 m − 1
1 2 C n 1 + 2 2 C n 2 + 3 2 C n 3 + ⋯ + n 2 C n n = n 2 n − 1 + n × ( n − 1 ) × 2 n − 2 = n ( n + 1 ) 2 n − 2 1^{2} C_{n}^{1}+2^{2} C_{n}^{2}+3^{2} C_{n}^{3}+\cdots+n^{2} C_{n}^{n}= n 2^{n-1} +n ×(n-1)×2^{n-2} =n(n+1) 2^{n-2} 1 2 C n 1 + 2 2 C n 2 + 3 2 C n 3 + ⋯ + n 2 C n n = n 2 n − 1 + n × ( n − 1 ) × 2 n − 2 = n ( n + 1 ) 2 n − 2 ②
以上是结论推导部分
这道题,我们实际需要求的是:
将①②式代入:简化答案为 ( n + 1 ) × n / 4 (n+1)×n / 4 ( n + 1 ) × n / 4 ,O ( 1 ) O(1) O ( 1 ) 可求得。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <bits/stdc++.h> using namespace std;#define pii pair<int, int> #define pdd pair<double, double> #define pll pair<ll, ll> #define debug(x) cerr << #x << ": " << x << '\n' ; #define endl '\n' typedef long long ll;typedef unsigned long long ull;int main () { ll n; cin >> n; ll res = (n+1 )*n / 4 ; cout << res << endl; return 0 ; }