2022牛客冬令营 第二场 题解

A题 小沙的炉石(数学,思维)

现在我们有 n 张攻击牌和 m 张回蓝牌。攻击牌消耗一格蓝量,并且对对方造成 1 点普通伤害和附带的法伤。回蓝牌则回复一格蓝量(蓝量无上限)。

此外,我们每使用一张牌之后,可以使得我们的法力伤害值增加 1,可叠加(初始值为 0)。

现在有 k 次询问,每次给定敌方血量 x,问我们能否找到一个出牌顺序,使得地方能够恰好被我们斩杀(不要求用完牌)。

1n109,0m109,1k105,看不懂法伤那部分可以看样例。

我们不难找到伤害最大化的方法并计算出来:前面疯狂堆回复牌,然后后面能打多少进攻牌就打多少,值的话就直接等差数列套个公式就行。

接下来,我们看看我们可以构造出哪些攻击方式。

在进行 t 次攻击的情况下,我们至少可以造成 t2 的伤害(1010101这种方式),至多造成 t(2m+t+1)2 的伤害。值得注意的是,这是连续的(临项交换法,将攻击牌往后面交换位置)。

不过显然,我们不能每次一个个求出来区间,然后每次询问的时候都去遍历一遍,那么我们只能看看这些区间能不能合并(显然是可以的)

实践发现,仅存在 t=2,m=1,x=3t=3,m=2,b=8 两种情况是特例(可以自己草稿纸上面推,也可以写个暴力直接莽),其余情况下直接看在不在 [1,Max] 里面即可。

#include<bits/stdc++.h> using namespace std; #define LL long long int main() { LL n, m, k; cin >> n >> m >> k; LL t = min(n, m + 1), Max = t * (2 * m + t + 1) / 2; while (k--) { LL x; cin >> x; if ((t == 2 && m == 1 && x == 3) || (t == 3 && m == 2 && x == 8)) puts("NO"); else puts(x > Max ? "NO" : "YES"); } return 0; }

B题 小沙的魔法(排序,离散化,图上遍历)

给定一个 n 个点的图,每个点有一个权值 ai。现在我们每次可以进行一次操作,使得某个点的权值下降 1,我们的目标是使得所有点的权值变为 0。

不过,单纯这样操作有点慢,所以我们还有 m 个连接器,每个连接器可以将两个点进行连接,之后每次操作可以视为对整个连通块进行一次操作。连接器至多使用 min(5n,m) 次。

1n5105,1mmin(n(n1)2,5106),0ai109

(至多 n1 次连接就可以使得整个图联通,所以连接器的次数限制无意义。)

这题其实和著名的 P1969 [NOIP2013 提高组] 积木大赛 很类似,不过那题是线性,本题搬到了图上面。

先排个序,然后离散化一下,从大到小依次处理。我勉强看得懂题解代码和思路,但有一说一,自己真写不出来。

#include <bits/stdc++.h> using namespace std; #define LL long long const int N = 500010; int n, m, s[N]; vector<int> e[N], ans[N], mp; int F(int x) { return lower_bound(mp.begin(), mp.end(), x) - mp.begin(); } // int fa[N]; int find(int x) { if (x != fa[x]) fa[x] = find(fa[x]); return fa[x]; } bool merge(int x, int y) { x = find(x), y = find(y); if (x != y) fa[x] = y; return x != y; } int main() { scanf("%d%d", &n, &m); mp.resize(n + 1, 0); for (int i = 1; i <= n; i++) { scanf("%d", &s[i]); mp[i] = s[i]; } //离散化 sort(mp.begin(), mp.end()); mp.erase(unique(mp.begin(), mp.end()), mp.end()); for (int i = 1; i <= n; i++) ans[F(s[i])].push_back(i); //图的构建 for (int i = 1; i <= m; i++) { int u, v; scanf("%d%d", &u, &v); e[u].push_back(v), e[v].push_back(u); } //solve for (int i = 1; i <= n; ++i) fa[i] = i; LL res = 0, cnt = 0; for (int i = mp.size() - 1; i >= 1; i--) { for (int x : ans[i]) { cnt++; for (int y : e[x]) if (s[y] >= s[x]) if (merge(x, y)) cnt--; } res += (mp[i] - mp[i - 1]) * cnt; } //output printf("%lld", res); return 0; }

C题 小沙的杀球(贪心)

小沙特别喜欢杀球(羽毛球的一个玩法),但是水平有限,只能杀后场的高远球,杀不了前场的小球。

小沙是有体力限制的,每杀一个球就会消耗 a 体力,但是不杀球的话则会回复 b 体力。体力没有上限,但是体力不可能为负。

现在小沙有体力 x,现在即将有 n 个球打过来,并且知道他们分别是哪种球(按顺序),试求出小沙究竟最多能杀多少球?

0x,a,b109,1n106

按照贪心,尽可能的来杀高远球,如果体力不够或者是小球就休息一下,恢复体力。

这题会卡 int,所以记得开 long long。

#include<bits/stdc++.h> using namespace std; const int N = 1000010; int n; char s[N]; int main() { long long x, a, b; scanf("%lld%lld%lld%s", &x, &a, &b, s + 1); n = strlen(s + 1); int ans = 0; for (int i = 1; i <= n; ++i) if (s[i] == '1' && x >= a)x -= a, ans++; else x += b; printf("%d", ans); return 0; }

D题 小沙的涂色(unsolved)

  • 小思维

  • 细节

  • 中模拟

  • 构造

  • 有许多种构造思路,这里列举出一种

  • 如果边长是3的倍数我们便无法构造出合法方案。因为我们需要填色的格子数一定是3的倍数,所以要满足 (n21)=0mod3

    对于边长mod 3情况下是1的情况,我们可以选择试边界向内缩小6格的方案使他一直维持 mod 3 为 1,最后只剩一个格子停下即可,对于边长 mod 3 为 2 的情况,我们需要让他变成 1,所以我们可以选择向内缩小 4格的方案,从外向内染色。

  • 也可以使用二分的方法染色,解法很多,只要你觉得好写~

  • 时间复杂度O(n2)

E题 小沙的长路(图论推理)

给定一个 n 个点的完全图,现在我们可以任意给这些边加方向,问加上之后这个图的最长路的可能的最小值和最大值。

1n109

  • 最大值

    n 为奇数的时候,直接把图当成一个无向图来走欧拉通路即可(每个点的度都是 n1,偶数,所以存在欧拉通路),然后按照路径来标边的方向,值为 n(n1)2

    n 为偶数的时候,所有点的度数都是奇数,不存在欧拉通路。如果我们将最长路之外的边删掉,意味着这条最长路就是剩下的边所组成的图的欧拉通路。那显然,删掉 n21 条边的时候,可以将 n2 个点的度数减去一(变成偶数),再留下两个奇数点,满足构成欧拉回路的条件,所以答案为 n(n1)2(n21)(也是删边最少的方案)

  • 最小值

    我们逐个添加点,发现每添加一个点,最长路径至少增加 1(可以根据每个点的入度出度来判断)。

    由数学归纳法可知,最小值为 n1

#include<bits/stdc++.h> using namespace std; int main() { long long n; cin >> n; printf("%lld %lld", n - 1, n * (n - 1) / 2 - (n % 2 ? 0 : n / 2 - 1)); return 0; }

F题 小沙的算数 (模拟,逆元)

现在有一个长度为 n,仅含加号和乘号的表达式,每个位置上面有一个初始值。

现在,有 q 次询问,每次询问会要求将位置 x 上面的数改成 y,然后计算一下表达式的值并且输出。

n106,q105,答案对 109+7 取模且题目中给出的所有数值都在 [1,109+7) 范围内。

仅包含加号和乘号,那我们可以将不同位置的数,打上不同的标记(相同标记表示在一个连通块内),一个连通块的值等于等于该标记的值的乘积,表达式的值等于所有连通块之和。

对于每次询问,我们找到位置 x 对应的连通块,然后就是一连串的先除再乘,顺便更新答案即可。因为取模的原因,所以还要写下逆元啥的。

#include<bits/stdc++.h> using namespace std; // #define LL long long const int N = 1000010; const LL mod = 1000000007; LL power(LL a, LL b) { a %= mod; LL res = 1; while (b) { if (b & 1) res = res * a % mod; b >>= 1; a = a * a % mod; } return res; } LL inv(LL x) { return power(x, mod - 2); } // int n, q; char opt[N]; int vis[N]; LL a[N], v[N]; int main() { scanf("%d%d", &n, &q); scanf("%s", opt + 1); // vis[1] = 1; for (int i = 1; i < n; ++i) vis[i + 1] = vis[i] + (opt[i] == '+'); // for (int i = 1; i <= n; ++i) scanf("%lld", &a[i]); for (int i = 1; i <= vis[n]; ++i) v[i] = 1; for (int i = 1; i <= n; ++i) v[vis[i]] = v[vis[i]] * a[i] % mod; // LL ans = 0; for (int i = 1; i <= vis[n]; ++i) ans += v[i]; while (q--) { int x; LL y; scanf("%d%lld", &x, &y); int tag = vis[x]; ans = (ans - v[tag] + mod) % mod; v[tag] = v[tag] * inv(a[x]) % mod * y % mod; ans = (ans + v[tag]) % mod; a[x] = y; printf("%lld\n", ans); } return 0; }

G题 小沙的身法(LCA)

给定一个有 n 个点的联通树,每个点有一个高度 ai

我们可以沿着树上面的边移动,从 x 跳到 y,需要花费 max(ayax,0) 的体力(从高处往低处跳不需要体力,反之则需要耗费高度差的体力)。

现在有 m 次询问,每次询问给定起点和终点,我们从地上跳到起点,然后一路跳到终点后跳回地上,问需要耗费的体力是多少?(地面可以视为高度为 0,但是只有开头和结束可以跳)

1n106,1m105,1ai109

树上路径,多次查询,那显然就是倍增 LCA了。

考虑到具有方向性,所以我们需要构建两颗树,均以 1 为根,然后一个按照从深度低往深度高的方式赋边权,另一棵树反过来,然后每次询问的时候分别查询即可。

#include <bits/stdc++.h> using namespace std; #define LL long long const int N = 1000010; int lg[N]; // int n, m, a[N]; // vector<int> tree[N]; int depth[N], fa[N][23]; LL dis1[N], dis2[N]; void dfs(int x, int f) { depth[x] = depth[f] + 1; fa[x][0] = f; for (int i = 1; (1 << i) <= depth[x]; i++) fa[x][i] = fa[fa[x][i - 1]][i - 1]; for (int y : tree[x]) if (y != f) { dis1[y] = dis1[x] + max(a[x] - a[y], 0); dis2[y] = dis2[x] + max(a[y] - a[x], 0); dfs(y, x); } } int lca(int x, int y) { if (depth[x] < depth[y]) swap(x, y); while (depth[x] > depth[y]) x = fa[x][lg[depth[x] - depth[y]]]; if (x == y) return x; for (int k = lg[depth[x]]; k >= 0; k--) if (fa[x][k] != fa[y][k]) x = fa[x][k], y = fa[y][k]; return fa[x][0]; } int main() { lg[1] = 0; for (int i = 2; i < N; i++) lg[i] = lg[i / 2] + 1; //read & build scanf("%d%d", &n, &m); for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); for (int i = 1; i < n; ++i) { int u, v; scanf("%d%d", &u, &v); tree[u].push_back(v); tree[v].push_back(u); } dfs(1, 0); while (m--) { int u, v; scanf("%d%d", &u, &v); int x = lca(u, v); printf("%lld\n", a[u] + (dis1[u] - dis1[x]) + (dis2[v] - dis2[x])); } return 0; }

H题 小沙的数数(数学)

已知一个长度为 n 的非负整数数列 an,数值和为 m

现在我们想要让数列的异或和最大,尝试找出有几种方案。

1n1018,0ai,m1018,方案数对 109+7 取模。

不是很好证明,异或和的最大值就是 m:感性上讲,就是讲 m 的二进制上面的 1,被均匀分配到 n 个坐标的不同位置上面,能够使得异或和最大。而任何尝试使异或值变得更大的方式,都会使得数值和超过 m

分配方式很奇妙:我们记 m 在二进制下有 t 个 1,那么每个位置的 1 都有 n 个选择,所以答案是 nt

n 的规模比较离谱,所以一开始就得取个模,不然直接炸 long long。

#include<bits/stdc++.h> using namespace std; #define LL long long const LL mod = 1e9 + 7; int main() { LL n, m, ans = 1; cin >> n >> m; n %= mod; for (; m; m >>= 1) if (m % 2) ans = ans * n % mod; cout << ans; return 0; }

I题 小沙的构造(模拟,构造)

直接看代码,别问咋构造的,我 WA 了 12 发才过就离谱。

#include<bits/stdc++.h> #include<bits/stdc++.h> using namespace std; string str1 = "\"!'*+-.08:=^_WTYUIOAHXVM|";//0 1 2 ... 24 string str2 = "<>\\/[]{}()";// 01 23 45 67 89 int n, m; string solve() { deque<char> q1, q2, q3; int cnt = 0, len = 0; for (int i = 0; i < 5 && cnt + 2 < m; ++i) { q1.push_front(str2[2 * i]); q3.push_back (str2[2 * i + 1]); cnt += 2, len += 2; } for (int i = 0; i < 24 && cnt + 1 < m; ++i) { q1.push_front(str1[i]); q3.push_back (str1[i]); cnt += 1, len += 2; } if (n - len <= 0) return "-1"; for (int i = 0; i < n - len; ++i) q2.push_back(str1[24]); string res = ""; for (char c : q1) res.push_back(c); for (char c : q2) res.push_back(c); for (char c : q3) res.push_back(c); return res; } int main() { //read cin >> n >> m; //solve string ans; if (m == 36 || (m > 11 && n < 2 * m - 11) || (m <= 11 && n < m)) ans = "-1"; else ans = solve(); //solve cout << ans; return 0; }

J题 小沙的Dota(unsolved)

  • DDP(所需知识点线段树)

  • 考虑转移方程 dpi,v=min(dpi,v,dpi1,x)

  • 在需要修改的情况下,我们可以采用线段树维护带修改的 DP 过程。

  • 对于节点维护左状态为 V 的情况下变化到 X 所需要的最小代价,预处理各个状态之间的代价转移即可

  • 时间复杂度:O(63(n+m)logn)

  • O(64(n+m)logn)的合并写丑了可能会被卡。

K题 小沙的步伐 (签到)

除了在 5 上面不用动,别的地方直接都是目标位置和 5 都加 1。

#include<bits/stdc++.h> using namespace std; int ans[10]; int main() { string str; cin >> str; for (char c : str) if (c != '5') ans[c - '0']++, ans[5]++; for (int i = 1; i < 10; ++i) printf("%d ", ans[i]); return 0; }

L/M题 小沙的remake(DP,树状数组,排序)

不难写出一个 O(n2) 的 DP:

for (int i = 1; i <= n; ++i) { dp[i] = 1; for (int j = max(i - b[i], 1); j < i; ++j) if (a[i] >= a[j]) dp[i] = (dp[i] + dp[j]) % mod; } //output LL ans = 0; for (int i = 1; i <= n; ++i) ans = (ans + dp[i]) % mod; printf("%lld", ans);

这题是最长上升子序列的一种变形(从最长变成了求方案数,同时加上了范围限制),以前是可以通过树状数组+离散化来优化复杂度,但是对这题似乎不太够。

这题比较特殊,求方案数,所以我们在传统下标上面建立树状数组(用来求和),但是遍历DP的时候按照排序过后的下标来,这样恰好满足了:

  1. 可以限定下标范围(显然的,就相当于上面那个朴素DP的优化)
  2. 根据按照数值排序后的下标顺序来进行状态转移,保证了每次查询之后的值都是合法的,可以直接求和
#include <bits/stdc++.h> #define LL long long namespace GenHelper { int z1, z2, z3, z4, z5, u, res; int get() { z5 = ((z1 << 6) ^ z1) >> 13; z1 = ((int)(z1 & 4294967) << 18) ^ z5; z5 = ((z2 << 2) ^ z2) >> 27; z2 = ((z2 & 4294968) << 2) ^ z5; z5 = ((z3 << 13) ^ z3) >> 21; z3 = ((z3 & 4294967) << 7) ^ z5; z5 = ((z4 << 3) ^ z4) >> 12; z4 = ((z4 & 4294967) << 13) ^ z5; return (z1 ^ z2 ^ z3 ^ z4); } int read(int m) { u = get(); u >>= 1; if (m == 0) res = u; else res = (u / 2345 + 1000054321) % m; return res; } void srand(int x) { z1 = x; z2 = (~x) ^ (0x23333333); z3 = x ^ (0x12345798); z4 = (~x) + 51; u = 0; } } using namespace GenHelper; using namespace std; const int N = 2e6 + 7, mod = 1e9 + 7; int n, seed; int a[N], b[N]; pair<int, int> p[N]; //TreeArray #define lowbit(x) (x & (-x)) LL t[N]; LL query(int x) { LL res = 0; for (; x; x -= lowbit(x)) res = (res + t[x]) % mod; return res; } void add(int x, int val) { for (; x <= n; x += lowbit(x)) t[x] = (t[x] + val) % mod; } //Main int main() { scanf("%d %d", &n, &seed); srand(seed); for (int i = 1; i <= n; i++) { a[i] = read(0), b[i] = read(i); p[i] = {a[i], i}; } sort(p + 1, p + n + 1); LL ans = 0; for (int i = 1; i <= n; i++) { int id = p[i].second; LL val = (query(id) - query(max(0, id - b[id] - 1)) + 1 + mod) % mod; add(id, val); ans = (ans + val) % mod; } cout << ans << endl; return 0; }

__EOF__

本文作者cyhforlight
本文链接https://www.cnblogs.com/cyhforlight/p/15861925.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyhforlight  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
历史上的今天:
2021-02-03 2021牛客寒假算法基础集训营1 解题补题报告
点击右上角即可分享
微信分享提示