都给我去看创世纪的超神番超时空辉夜姬
突然发现小标题太多就无法伸缩了,所以又开了新的一篇。
算法基础集训营第四场 2.09
赛题:https://ac.nowcoder.com/acm/contest/120564
出题人题解(出题人这次没在牛客写,不过有相应的文档):文档链接
这场神了。。。看标题就知道了。不过由于刚开的时候还在外面吃饭,所以给了2小时,后面点到家之后才开始打。。整体难度还是有的,基本全是构造。
A - 本场比赛灵感来源于树状数组出题组
签到题,数据比较小,可以暴力解决,不过要注意 是除了这个数本身之外的其他数的 。(没注意就了。)
思路:双循环遍历,用记录每次遍历之后满足小于等于的总数量,循环结束后再判断是否满足入A组的条件,满足就加入。
:这里在判断 的时候用了十字相乘的方法规避小数乘法精度的问题。
#include <bits/stdc++.h>
using namespace std;using ll = long long;int a[1010];void solved(){ int n; cin >> n; for (int i = 1; i <= n;i++){ cin >> a[i]; } int sum = 0; for (int i = 1; i <= n;i++){ int cnt = 0; for (int j = 1; j <= n;j++){ if(a[j] <= a[i] && i != j){ cnt += 1; } } if(5 * cnt >= 4 * (n - 1)){ sum += a[i]; } } cout << sum;}int main(){ solved(); return 0;}B - 构造部落
这题主要是要理顺时间线的关系,取决于你怎么判断首领最后一年的年份(取前还是取后,我是取前了)。
思路:分析完题目你会发现,输出的文物的年份需要根据前任首领的在位年份进行求和,所以我们需要知道每一个首领的前几任的一共在位多少年,也就是前缀和。我这里的想法是把第一年减再加年表示元年,可以保证后面每次用数组表示的前缀和都是前一任首领在位的最后一年,方便计算。不过这种方法输出第一位首领的数据要特判一下。
#include <bits/stdc++.h>
using namespace std;using ll = long long;ll a[200010];ll b[200010];void solved(){ ll n, q; ll s; cin >> n >> q >> s; cin >> a[1]; a[1] = a[1] - 1 + s; b[1] = b[0] + a[1]; for (ll i = 2; i <= n;i++){ cin >> a[i]; b[i] = b[i - 1] + a[i]; //cout << b[i] << '\n'; } while(q--){ ll x, y; cin >> x >> y; if(x == 1) cout << s - 1 + y << '\n'; else cout << b[x - 1] + y<< '\n'; }}int main(){ solved(); return 0;}C - 墨提斯的排列
依旧神奇构造。。后来查资料的时候发现居然是格雷码,一种单步变换的二进制码。
思路:我们想要让每相邻的两个数异或值最小,就让它们之间不同的位数最小(只有一位不相同),我们这里可以手动推导演示一下。
构造:: 相差一位,和为。
: 每相邻的一位都是相差一位,和为 。
: 和为
这里基本上就可以看出规律了,然后可以通过 来构造符合条件的排列。(也是格雷码的转换方式)通过错位异或的方式构造出只有一位不相同的排列。
#include <bits/stdc++.h>
using namespace std;using ll = long long;void solved(){ int n; cin >> n; for(int i = 0; i < (1 << n); i++){ cout << (i ^ (i >> 1)) << " "; } cout << '\n';}int main(){ solved(); return 0;}G - 真白的幻觉
这题场上的时候没啥时间想了。。不过应该也是有规律的构造,所以赛后稍微补了一下。
思路:题目很好理解,就是在内找一个能让最大的值(执行次数最大),那么我们可以发现数字中有和都是不合适的,因为这会快速导致数位出现, 的话不会影响最终结果。而像 这些数则相对持久,在的倍数中我们可以尽量选择 ,尽量选择 ,如果数位不足以构造或,就向下构造或者。最终找出数位最大的那两个就可以了。(题目要求不相等)
可以暴力枚举,也可以自己造。
NOTE是文献中著名的最小的持久性的数, 是另一个在 内的等价构造,在 范围内能达到最大“乘数持久性”,,使得 达到最大。
#include <bits/stdc++.h>
using namespace std;using ll = long long;void solved(){ cout << "27777789999999999 277777788888899";}int main(){ solved(); return 0;}I - 初华的扭蛋机
这纯高中数学吧,数学期望这一块。。考验高中数学还记得多少的雷了。
思路:我们首先来分析颜色出现的概率,每个颜色出现的概率为 ,然后有次独立抽取,设某种颜色出现的概率为,则有 (这下看懂了)。我们可以算出对应的概率。
,,
然后我们可以算出数学期望。
也就是每次在一个颜色下注后的期望是 ,所以最好的选择就是次都保留原来的筹码 。
#include <bits/stdc++.h>
using namespace std;using ll = long long;void solved(){ cout << "######" << endl;}int main(){ solved(); return 0;}算法基础集训营第五场 2.11
赛题:https://ac.nowcoder.com/acm/contest/120565
出题人题解:题解链接
这场的基础题目就比较偏简单模拟和模板一点,简单的挺简单,有一些难度的就可能需要学过类似的解法才好做。。
B - 智乃的瓷砖
这题是经典的生成字符矩阵,我觉得算是基础编程能力。
思路:直接根据行列和列的奇偶性分成四种情况,打表输出,这里要注意输出要用多用一个\来转义。
#include <bits/stdc++.h>
using namespace std;using ll = long long;void solved(){ int n, m; cin >> n >> m; for (int i = 1; i <= n;i++){ for (int j = 1; j <= m; j++){ if(i % 2 == 1 && j % 2 == 1){ cout << '/'; } else if(i % 2 == 1 && j % 2 == 0){ cout << '\\'; } else if(i % 2 == 0 && j % 2 == 1){ cout << '\\'; } else if(i % 2 == 0 && j % 2 == 0){ cout << '/'; } } cout << '\n'; }}int main(){ solved(); return 0;}D - 智乃的果子
这题其实是哈夫曼树(最小生成树)的一个构建原理(可以了解一下),用优先队列直接模拟过程,但是会超时。。所以需要优化。
思路:想要最终的代价最小,其实就是要在每一次合并操作之后再找出最小的两个堆继续合并,这样不断操作到只剩最后一个堆时的代价就是最小的了。不过这题就阴在相同权重的果子并不唯一,最终一个一个模拟过程就会超时,我们需要对重复的堆进行合并。也就是当目前权重最小的堆的数量大于时,我们就取整半 来合并, 加入总代价,然后加入新的权值为的新堆。如果等于1,我们就按正常流程将它与次小权值的堆合并,生成新堆,为就删除该堆,一直操作即可。
这里我是用自动排序和迭代器实现了优先队列(因为方便映射对应同权值的堆的数量)。
#include <bits/stdc++.h>
using namespace std;using ll = long long;#define mod 1000000007LLvoid solved(){ int n; cin >> n; map<ll, ll> mp; for (int i = 1; i <= n;i++){ ll c, w; cin >> c >> w; mp[w] += c; } ll ans = 0; while(mp.size() > 1 || (mp.size() == 1 && mp.begin()->second > 1)){ auto it = mp.begin(); ll w1 = it->first; ll c1 = it->second; if(c1 >= 2){ ll p = c1 / 2; ans = (ans + (p % mod) * ((2 * w1) % mod) % mod) % mod; ll w2 = w1 * 2; ll d = p; c1 %= 2; if(c1 == 0){ mp.erase(it); } else{ it->second = c1; } mp[w2] += d; } else{ auto it2 = next(it); ll w2 = it2->first; ll c2 = it2->second; ans = (ans + (w1 % mod) + (w2 % mod)) % mod; ll w3 = w1 + w2; mp.erase(it); c2--; if(c2 == 0){ mp.erase(it2); } else{ it2->second = c2; } mp[w3]++; } } cout << ans << '\n';}int main(){ solved(); return 0;}F - 智乃的算法竞赛群友
这题有点逆向思维内味了,通过反向枚举来将判断所有的情况,然后取出最优的(奈何场上的时候根本想不到)。
思路:这题其实子串所有可能出现的情况只有,,一共三种情况,而最先的想法肯定是尽可能地取,八个字符串双倍收益,也是贪心。而对于剩下不能再组成个字符的则枚举全为或者全为的情况,取其中一种快乐值较高的。而除此之外还有两种情况,一种是个比个高收益,另一种是个比个高收益,都进行一次反向枚举判断即可。
一共四种枚举情况,不过的最大值在这题中刚好取都能过(亲测),一般来说还是尽可能往大的取,不超时就行。
#include <bits/stdc++.h>
using namespace std;using ll = long long;void solved(){ int t; cin >> t; while(t--){ ll n, a, b; cin >> n >> a >> b; ll ans = 0; for (int i = 0; i <= 60 &&i * 7 <= n;i++){ ans = max(ans, i * a + (n - i * 7) / 8 * (a + b)); } for (int i = 0; i <= 60 &&i * 2 <= n;i++){ ans = max(ans, i * b + (n - i * 2) / 8 * (a + b)); } for (int i = 0; i <= 60 &&i * 8 <= n;i++){ ans = max(ans, i * (a + b) + (n - i * 8) / 7 * a); } for (int i = 0; i <= 60 &&i * 8 <= n;i++){ ans = max(ans, i * (a + b) + (n - i * 8) / 2 * b); } cout << ans << '\n'; }}int main(){ solved(); return 0;}G - 智乃的箭头魔术
这题就纯粹的模拟,至少我是选择把全部情况列出来然后判断的。。
思路:根据题意描述,最初是状态是 ,然后可以通过种操作改变状态,让我们输出每一次改变后这个玩具的状态。其实实际上就是去根据每一种情况来判断与继承状态,我们可以把 种情况全部列出然后对每个字符串进行判断即可。
这里可能需要一点空间想象能力,如果感觉烧脑的话就自己做一张简单的小卡片来玩一下就好了。
#include <bits/stdc++.h>
using namespace std;using ll = long long;void solved(){ char t = '0'; string s = "0112233445142015320125410214530214510214102302142025101203201451451522302514203214510021454101002532"; for(char c:s){ if(t == '0' && c == '0'){t = '3';cout << t;} else if(t == '0' && c == '1'){t = '0';cout << t;} else if(t == '0' && c == '2'){t = '1';cout << t;} else if(t == '0' && c == '3'){t = '2';cout << t;} else if(t == '0' && c == '4'){t = '1';cout << t;} else if(t == '0' && c == '5'){t = '3';cout << t;} else if(t == '1' && c == '0'){t = '2';cout << t;} else if(t == '1' && c == '1'){t = '3';cout << t;} else if(t == '1' && c == '2'){t = '0';cout << t;} else if(t == '1' && c == '3'){t = '1';cout << t;} else if(t == '1' && c == '4'){t = '2';cout << t;} else if(t == '1' && c == '5'){t = '0';cout << t;} else if(t == '2' && c == '0'){t = '1';cout << t;} else if(t == '2' && c == '1'){t = '2';cout << t;} else if(t == '2' && c == '2'){t = '3';cout << t;} else if(t == '2' && c == '3'){t = '0';cout << t;} else if(t == '2' && c == '4'){t = '3';cout << t;} else if(t == '2' && c == '5'){t = '1';cout << t;} else if(t == '3' && c == '0'){t = '0';cout << t;} else if(t == '3' && c == '1'){t = '1';cout << t;} else if(t == '3' && c == '2'){t = '2';cout << t;} else if(t == '3' && c == '3'){t = '3';cout << t;} else if(t == '3' && c == '4'){t = '0';cout << t;} else if(t == '3' && c == '5'){t = '2';cout << t;} }}int main(){ solved(); return 0;}J - 智乃的幻方
依旧简单模拟,直接判断。
思路:直接根据题意判断所有条件是否符合,注意不要漏掉 是否只出现一次这个条件。
#include <bits/stdc++.h>
using namespace std;using ll = long long;int a[4][4];map<int, int> mp;void solved(){ for (int i = 1; i <= 3;i++){ for (int j = 1; j <= 3;j++){ cin >> a[i][j]; mp[a[i][j]]++; } } for(auto&it :mp){ if(it.second > 1){ cout << "No"; return; } } int sum1 = a[1][1] + a[1][2] + a[1][3]; int sum2 = a[2][1] + a[2][2] + a[2][3]; int sum3 = a[3][1] + a[3][2] + a[3][3]; int sum4 = a[1][1] + a[2][1] + a[3][1]; int sum5 = a[1][2] + a[2][2] + a[3][2]; int sum6 = a[1][3] + a[2][3] + a[3][3]; int sum7 = a[1][1] + a[2][2] + a[3][3]; int sum8 = a[1][3] + a[2][2] + a[3][1]; if(sum1 == sum2 && sum2 == sum3 && sum3 == sum4 && sum4 == sum5 && sum5 == sum6 && sum6 == sum7 && sum7 == sum8){ cout << "Yes"; } else{ cout << "No"; }}int main(){ solved(); return 0;}算法基础集训营第六场 2.13
赛题:https://ac.nowcoder.com/acm/contest/120566
出题人题解(这场也是只有文档):文档链接
难难难。。第二道题就已经开始二分和简单的了,后面更是烟斗不带烟,做不了一点。。
A - 小L的三角尺
这题看出来是贪心,不过没推出来规律,思路是错的。。整体也比较难,后续简单补了一下这题。
思路:我们可以看看每次打磨之后斜边的减少情况是怎么样的,这里可以数学推导一下:
设剩余长度为,初始时,。
。
我们可以发现随着减少, 严格递减,也就是磨得越多,收益越小。那么我们就可以知道,想要每次磨得收益最大化,就要将每次斜边最大的尺子磨掉一个单位,最终的收益就可以最大化,这才是真正要贪心的地方。我们可以用优先队列模拟这个过程。
实现:这里由于数据非常多且杂乱,所以用了一个结构体来记录对应尺子的所有数据并计算,代表磨掉的单位长度,内置函数代表计算对应的收益。(方便每一步的计算)然后我们使用了二元组记录收益和对应尺子的下标,定义比较器来按收益排列的优先队列,以方便我们的操作。
第一次操作的时候,我们把每一个不为的尺子都计算收益然后加入队列中。然后我们开始循环执行打磨操作,用记录操作的次数,当达到或者队列为空的时候停止循环。每一次循环我们都取出堆顶的元素进行操作,如果收益为就终止,否则就定位到相应的数组下标的元素,对内置加一(打磨一个单位),记录斜边总长的减去这个收益,然后计算新的收益存入这个数组元素,重新入堆(队列)排列。
NOTE
auto cmp = [](const pair<double, int> &p1, const pair<double, int> &p2){return p1.first < p2.first;};是一种的表达式来定义比较器,指定了p2的优先级高于p1,所以在进入队列之后,p2就会排在p1前面。
decltype(cmp)由于cmp是,所以需要decltype获取其比较器类型。
pq(cmp)构造函数,将cmp传入队列,让其使用该比较器。
#include <bits/stdc++.h>
using namespace std;using ll = long long;struct Ruler{ ll x, y; int t; double g() const{ ll r = y - t; if(r == 0) return 0.0; double a = sqrt(x * x + r * r); double b = sqrt(x * x + (r - 1) * (r - 1)); return a - b; }};void solved(){ ll n, w; cin >> n >> w; vector<Ruler> r(n); double sum = 0.0; auto cmp = [](const pair<double, int> &p1, const pair<double, int> &p2) { return p1.first < p2.first; }; priority_queue < pair<double, int>, vector<pair<double, int>>, decltype(cmp)> pq(cmp); for (int i = 0; i < n;i++){ ll x, y; cin >> x >> y; r[i] = {x, y, 0}; sum += sqrt(x * x + y * y); if(y > 0){ double g = r[i].g(); pq.emplace(g, i); } } int p = 0; while(p < w && !pq.empty()){ auto [g, x] = pq.top(); pq.pop(); if(g <= 0) break; auto &r1 = r[x]; r1.t++; sum -= g; p++; if(r1.y - r1.t > 0){ double g1 = r1.g(); pq.emplace(g1, x); } } printf("%.10f", sum);}int main(){ solved(); return 0;}G - 小L的散步
这题很好理解,我们每次都对小附近的石块进行判断是否踩中缝隙就好了。
思路:我们首先要用前缀和记录每块石块距离起点的长度,方便我们每次根据对应走出的总距离判断是否踩中缝隙。然后我们要找到离小最近的裂缝进行判断是否踩到。这里的查询我使用了二分查找(函数),在数组中找到第一个比总步数大的数,然后判断是否踩中了缝隙。
这里注意要特判是否还没开始走就踩缝隙了,因为后续的判断都是走了一步的。
#include <bits/stdc++.h>
using namespace std;using ll = long long;ll a[200010];ll b[200010];void solved(){ ll n, m, l; cin >> n >> m >> l; ll ans = 0; for (int i = 1; i <= n;i++){ cin >> a[i]; b[i] = b[i - 1] + a[i]; } if(b[1] < ans + l){ cout << "YES" << '\n'; return; } for (int i = 1; i <= m;i++){ ll x; cin >> x; ans += x; auto it = lower_bound(b + 1, b + 1 + n, ans + 1); if(it != b + n + 1 && *it < ans + l){ cout << "YES" << '\n'; return; } } cout << "NO" << '\n';}int main(){ solved(); return 0;}H - 小L的数组
得亏于数据只开到,如果你意识到事实上结果也就最大取到那就好做了。
思路:因为两个数组最大的数只能取到,而无论是减法操作还是两数异或都不会让这个数更大了(不会超过),那么你就只需要记录在这个范围内,哪些数是取过的,然后遍历找出最大的就行了。我这里开辟了两个数组和用来记录这种状态。然后用嵌套循环分别对和数组中每一个能取到的数进行记录,而取到的数在新的内层循环可以继续取新的数(其实就是),由此可以把所有可能的结果都记录到中。
这里提一下:只用数组更新状态会导致本来只能执行一次的操作内执行了多次(反复增加新状态),所以要把下次要变换的状态用数组先储存,等这一轮的所有可能操作结束后再更新到数组里面。
#include <bits/stdc++.h>
using namespace std;using ll = long long;int a[2055];int b[2055];vector<bool> c(2055,0);void solved(){ int n; cin >> n; for (int i = 0; i < n;i++){ cin >> a[i]; } for (int i = 0; i < n;i++){ cin >> b[i]; } c[0] = 1; for (int i = 0; i < n;i++){ vector<bool> d(2055,0); for (int j = 0; j < 2048;j++){ if(c[j] == 0){ continue; } if(j - a[i] >= 0){ d[j - a[i]] = 1; } else{ d[0] = 1; } } for (int j = 0; j < 2048;j++){ if(c[j] > 0){ d[j ^ b[i]] = 1; } } c.swap(d); } int ans = 0; for (int i = 2047; i >= 0;i--){ if(c[i] > 0){ ans = i; break; } } cout << ans << '\n';}int main(){ solved(); return 0;}K - 小L的游戏1
签到题,不过刚开始打没睡醒。用了嵌套去模拟,结果超时了。。
思路:对取模,然后判断取模后的数落在哪个区间。在 区间内小最后一手,输出,在 中, 是最后一手,输出 。
#include <bits/stdc++.h>
using namespace std;using ll = long long;void solved(){ int t; cin >> t; while(t--){ ll x = 0; ll i = 1; ll m, n, z; cin >> m >> n >> z; ll x1 = z % (m + n); if((x1 > m && x1 < m + n) || x1 == 0){ cout << 1; } else if(x1 <= m && x1 > 0){ cout << 0; } }}int main(){ solved(); return 0;}后记
随着最后一场比赛结束,寒假的算法学习就暂告一段落了。至少后续过年这段时间不会再学了。。不过新年期间应该还会写篇有关拜年祭的杂谈来练一下笔。