2022牛客冬令营 第四场 题解

官方题解

A题 R (二分/双指针)

给定一个长度为 n 的纯大写字母字符串,我们想要知道,这个字符串有多少个连续子串,内部有至少 k 个 R,且不包含 P。

1n2105,1k20

官方题解有两个解法,我们这里用第二个(因为我也是用第二个方法过的)。

我们枚举左端点 L,发现右端点 R 必须满足:

  1. 区间 [L,R] 内 R 的数量要大于等于 k
  2. 区间 [L,R] 内没有P,也就是说 P 的数量要小于等于 1

这两个都具有单调性质,所以可以分别求出来对应的边界,R 只要夹在他俩中间即可。

这个方法不太好写双指针,只能二分,不过改一下的话就可以了:我们将字符串沿着 P 分开,分成若干个子串,这样在这些子串内部求区间(此时要求只剩下了 R 的数量大于等于 k 这一限制),这样就可以愉快的双指针了。

//二分代码 #include<bits/stdc++.h> using namespace std; const int N = 200010; int n, k, a[N], b[N]; char s[N]; int main() { //read scanf("%d%d%s", &n, &k, s + 1); //solve for (int i = 1; i <= n; ++i) if (s[i] == 'R') a[i] = 1; else if (s[i] == 'P') b[i] = 1; for (int i = 1; i <= n; ++i) a[i] += a[i - 1], b[i] += b[i - 1]; long long ans = 0; for (int i = 1; i <= n; ++i) { if (a[n] - a[i - 1] < k || s[i] == 'P') continue; int L = lower_bound(a + 1, a + n + 1, a[i - 1] + k) - a; int R = upper_bound(b + 1, b + n + 1, b[i - 1]) - b - 1; ans += max(0, R - L + 1); } printf("%lld", ans); return 0; }

B题 进制 (线段树)

给定一个长度为 n 的序列 {an},保证任意时刻每个序列上面的值都在 [0,10) 之间。现在,我们有 q 次操作,分两类:

  1. 1 x y,使得 ax=y
  2. 2 x y,求出序列区间 [x,y] 上面这些数按照字符串形式生成的数(例如 [1,2,5,4] 子区间生成的数就是 1254),问使用几进制来表示它可以使得其值最小,并输出这个最小值(对 109+7 取模)。

1n,q105

区间 [l,r] 上面最大值为 P,那么按照 P+1 进制生成的值就是最小的。

显然,我们不可能每次暴力算一遍这个值是多少,加上频繁修改,显然就是使用线段树了。对于每个线段树的节点,维护三个核心值:区间长度,区间最大值(方便查询用几进制),区间数在各进制下所表示的值。

对于查询,我们直接找到这若干个区间,按照正常字符串拼接时候计算对应值的方式来看看他们怎么加即可(例如 123 和 56 在 8 进制下拼接,显然有 val(12356)=val(123)82+val(56),其中 2 是 56 的长度)。

对于修改,我们找到对应的叶子节点,改好之后一路向上 pushup 即可。

综上,复杂度为 O(10nlogn)

#include <bits/stdc++.h> using namespace std; #define LL long long const LL mod = 1e9 + 7; LL power(LL a, LL b) { LL res = 1; while (b) { if (b & 1) res = res * a % mod; b >>= 1; a = a * a % mod; } return res; } // const int N = 100010; int n, q, str2int[N]; char str[N]; struct Node { int l, r, len; LL sum[11]; int Max; } a[N << 2]; int mp[N]; #define ls(x) (x << 1) #define rs(x) (x << 1 | 1) inline void pushup(int x) { Node &L = a[ls(x)], &R = a[rs(x)]; a[x].Max = max(L.Max, R.Max); for (int P = 1; P <= 10; ++P) a[x].sum[P] = (L.sum[P] * power(P, R.len) % mod + R.sum[P]) % mod; } void build(int x, int l, int r) { a[x].l = l, a[x].r = r, a[x].len = r - l + 1; if (l == r) { mp[l] = x; a[x].Max = str2int[l]; for (int P = 1; P <= 10; ++P) a[x].sum[P] = a[x].Max; return; } int mid = (l + r) >> 1; build(ls(x), l, mid); build(rs(x), mid + 1, r); pushup(x); } LL query(int x, int l, int r, int P, int &len) { if (l <= a[x].l && a[x].r <= r) { len = a[x].len; return a[x].sum[P]; } int mid = (a[x].l + a[x].r) >> 1; LL res = 0; len = 0; if (l <= mid) { int len2 = 0; LL L = query(ls(x), l, r, P, len2); res = (res * power(P, len2) % mod + L) % mod; len += len2; } if (r > mid) { int len2 = 0; LL R = query(rs(x), l, r, P, len2); res = (res * power(P, len2) % mod + R) % mod; len += len2; } return res; } int getMax(int x, int l, int r) { if (l <= a[x].l && a[x].r <= r) return a[x].Max; int mid = (a[x].l + a[x].r) >> 1; int res = 0; if (l <= mid) res = max(res, getMax(ls(x), l, r)); if (r > mid) res = max(res, getMax(rs(x), l, r)); return res; } void change(int p, LL val) { int x = mp[p]; for (int P = 1; P <= 10; ++P) a[x].sum[P] = val; a[x].Max = val; while (x >>= 1) pushup(x); } int main() { scanf("%d%d", &n, &q); scanf("%s", str + 1); for (int i = 1; i <= n; ++i) str2int[i] = str[i] - '0'; build(1, 1, n); while (q--) { int opt, x, y; scanf("%d%d%d", &opt, &x, &y); if (opt == 1) change(x, y); else { int len = 0; int P = getMax(1, x, y) + 1; LL res = query(1, x, y, P, len); printf("%lld\n", res); } } return 0; }

C题 蓝彗星 (差分/线段树)

我们即将看到 n 颗彗星,分成蓝色和红色两种。

i 颗彗星会在时间 ai 到,持续 t 秒,也就是说我们在时间段 [ai,ai+t1] 内能看到这个彗星。

现在,我们想要求出,有多少时间,我们能够在天上看到蓝色彗星,且没有红色彗星?

1n,t,ai105

我们可以将时间轴抽象为一个序列,对于第 i 颗彗星,如果是蓝的,那就给 [ai,ai+t1] 都加上 1,反之都减去 109。统计答案时候,如果某个时间对应的值大于 0,说明此时显然仅有蓝色彗星。(也可以维护两个序列)

能实现多次区间加的数据结构很多,这里直接差分即可,因为不需要查询。

#include <bits/stdc++.h> using namespace std; #define LL long long const int N = 200010; int n, t, a[N]; char str[N]; LL d[N]; int main() { //read scanf("%d%d", &n, &t); scanf("%s", str + 1); for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); //solve for (int i = 1; i <= n; ++i) { int L = a[i], R = a[i] + t - 1; LL add = str[i] == 'B' ? 1 : -1e9; d[L] += add, d[R + 1] -= add; } int ans = 0; for (int i = 1; i < N; ++i) { d[i] += d[i - 1]; if (d[i] > 0) ans++; } printf("%d", ans); return 0; }

D题 雪色光晕 (计算几何)

一个比较小烦的计算几何,没有思维难度,就是写起来比较难受。

#include<bits/stdc++.h> using namespace std; #define LL long long inline double dis(double x0, double y0, double x1, double y1) { return sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)); } double calcDouble(double x0, double y0, double x1, double y1, double a, double b) { double k1 = (y1 - y0) / (x1 - x0), t1 = y0 - k1 * x0; double k2 = -1 / k1, t2 = a / k1 + b; double s = (t2 - t1) / (k1 - k2), t = k1 * s + t1; if (min(x0, x1) <= s && s <= max(x0, x1)) return dis(s, t, a, b); else return min(dis(x0, y0, a, b), dis(x1, y1, a, b)); } double calc(LL x0, LL y0, LL x1, LL y1, LL a, LL b) { if (y0 == y1) { if (x0 > x1) swap(x0, x1); if (a <= x0) return dis(x0, y0, a, b); else if (a >= x1) return dis(x1, y1, a, b); else return abs(y0 - b); } if (x0 == x1) { if (y0 > y1) swap(y0, y1); if (b <= y0) return dis(x0, y0, a, b); else if (b >= y1) return dis(x1, y1, a, b); else return abs(x0 - a); } return calcDouble(x0, y0, x1, y1, a, b); } int main() { int n; cin >> n; LL s, t, a, b; cin >> s >> t >> a >> b; double ans = 1e18; for (int i = 1; i <= n; ++i) { LL x, y; cin >> x >> y; ans = min(ans, calc(s, t, s + x, t + y, a, b)); s += x, t += y; } printf("%.10f", ans); return 0; }

E题 真假签到题 (数学/记忆化搜索)

给定一段代码:

long long f(long long x){ if(x==1)return 1; return f(x/2)+f(x/2+x%2); }

给定正整数 n,求出 f(n) 的值。(1n1018

我们将其转化为公式形式,有:

f(x)={1x=1f(n2)+f(n2)x>1

方法一:记忆化搜索

去年杭电多校有一个类似的签到题,也用到了相似的技巧:虽然 x 的值域范围很大,但是不难注意到,数的种类很少(logx 层,每层至多两个数)。对于这种值域大,种类少的题,可以开一个 map 来处理。

#include<bits/stdc++.h> using namespace std; #define LL long long map<LL, LL> dp; LL f(LL x) { if (x == 1) return 1; if (dp[x] != 0) return dp[x]; return dp[x] = f(x / 2) + f(x / 2 + x % 2); } int main() { LL x; cin >> x; cout << f(x); return 0; }

方法二:数学证明

打表或者观察,发现 f(x)=x 满足题意。

我们开始进行数学证明:

  1. x=1 时显然满足题意

  2. 假设 x=k 时满足 f(k)=k

  3. x=k+1 时:

    1. 若 k 是偶数,那么有 f(k+1)=f(k2)+f(k2+1)=k+1
    2. 若 k 是奇数,那么有 f(k+1)=2f(k+12)=k+1

    所以 x=k+1 时成立

综上得:f(x)=x

#include<bits/stdc++.h> using namespace std; int main() { long long x; cin >> x; cout << x; return 0; }

F题 小红的记谱法 (模拟)

一个比较普通的模拟题,没啥好说的。

#include<bits/stdc++.h> using namespace std; const int N = 1010; char str[N]; int n = 0; void print(char ch) { printf("%c", ch); char c = n > 0 ? '*' : '.'; for (int i = 0; i < abs(n); ++i) putchar(c); } map<char, char> vis; int main() { vis['C'] = '1', vis['D'] = '2', vis['E'] = '3', vis['F'] = '4', vis['G'] = '5', vis['A'] = '6', vis['B'] = '7'; // scanf("%s", str + 1); int len = strlen(str + 1); for (int i = 1; i <= len; ++i) { if (str[i] == '<') n--; else if (str[i] == '>') n++; else print(vis[str[i]]); } return 0; }

G题 子序列权值乘积 (算贡献,扩展欧拉降幂)

假定一个数组的价值,为该数组内的最小值乘最大值。

那么给定一个长度为 n 的正整数序列 {an},问该数组的所有非空的子序列的价值之积。

1n2105,1ai109

我们将序列从小到大排个序,然后对于每个值单独算贡献:

例如第 i 个值为 x,那么我们选定这个值作为最小值,那么在它前面的都不选,后面的都可以选,那么 x 作为最小值就出现了 2ni 次;同理,其作为最大值出现了 2i1 次。

我们综合一下,发现答案为

ans=(i=1nai2ni)(i=1nai2i1)

质数过大,加上不保证互质性质,所以则套一个扩展欧拉降幂。

注:若 x 是质数,则 ϕ(x)=x1,所以本题不需要单独写欧拉函数了。

#include<bits/stdc++.h> using namespace std; #define LL long long const int N = 200010; const LL mod = 1e9 + 7, P = 1e9 + 6; int n; LL a[N]; LL power(LL a, LL b, LL M) { LL res = 1; while (b) { if (b & 1) res = res * a % M; b >>= 1; a = a * a % M; } return res; } LL solve(LL x, LL y) { LL p = power(2, y, P); if (y < 30) return power(x, p, mod); else return power(x, p + P, mod); } int main() { cin >> n; for (int i = 1; i <= n; ++i) cin >> a[i]; sort(a + 1, a + n + 1); LL ans = 1; for (int i = 1; i <= n; ++i) ans = ans * solve(a[i], n - i) % mod; for (int i = 1; i <= n; ++i) ans = ans * solve(a[i], i - 1) % mod; cout << ans << endl; return 0; }

H题 真真真真真签到题 (计算几何)

A 和 B 在一个正方体内部,A 选择前往一个位置,随后 B 选择前往另一个位置。A 希望离 B 尽量近,而 B 希望离 A 尽量远。现在给定了选定位置之后两人距离 x,求正方体体积。

1x100

样例好心的提醒了我们:最优策略就是 A 在中心,B在正方体一角。

那么,设正方体边长为 2a,那么中心到角的距离为 x=3a。计算并输出即可。

#include<bits/stdc++.h> using namespace std; int main() { double x; cin >> x; double a = x / sqrt(3); printf("%.10lf", 8 * a * a * a); return 0; }

I题 爆炸的符卡洋洋洒洒 (01背包变形)

给定 n 张卡牌,第 i 张卡牌消耗 ai 法力,造成 bi 伤害。

现在,我们想要选取一些卡牌,消耗的法力的总和为 k 的倍数,并求出最多能造成多少伤害。(凑不出来卡牌组合则输出 -1)

1n,k103,1ai,bi109

第三次碰到这种基于整除的 01 背包题了,前两次分别是 [USACO09MAR]Cow Frisbee Team S 和 20年校赛那个黄焖鸡。

直接记 dpi,j 的两个维度,i 表示当前到了第 i 个物品,j 表示现在选择的卡牌,消耗对 k 取模后的值,那么有

dpi,j=max(dpi1,j,dpi1,(jai)modk+bi)

复杂度 O(nk),空间上可以滚动数组优化。

#include<bits/stdc++.h> using namespace std; #define LL long long const int N = 1010; int n, k; LL a[N], b[N], dp[N][N]; int main() { //read cin >> n >> k; for (int i = 1; i <= n; ++i) cin >> a[i] >> b[i]; //solve for (int i = 1; i <= n; ++i) a[i] %= k; memset(dp, -1, sizeof(dp)); dp[0][0] = 0; for (int i = 1; i <= n; ++i) { for (int j = 0; j < k; ++j) { dp[i][j] = dp[i - 1][j]; if (dp[i - 1][(j - a[i] + k) % k] != -1) dp[i][j] = max(dp[i][j], dp[i - 1][(j - a[i] + k) % k] + b[i]); } } cout << (dp[n][0] ? dp[n][0] : -1); return 0; }

J题 区间合数的最小公倍数 (质因数分解,素数判断)

给定一个区间 [l,r],问区间内所有合数的最小公倍数是多少(没有合数则输出 -1)?
1lr3104

这个区间范围太小,以至于我们甚至不需要素数筛,直接朴素判断即可找出所有素数。

对于每个合数,直接质因数分解,然后遵循算术基本定理的定义来求 lcm 即可。

#include<bits/stdc++.h> using namespace std; const int N = 30010; #define LL long long const LL mod = 1e9 + 7; // bool check(int x) { for (int i = 2; i * i <= x; ++i) if (x % i == 0) return true; return false; } int cnt[N]; int p[N], c[N]; void divide(int x) { //divide int m = 0; for (int i = 2; i <= sqrt(x); ++i) if (x % i == 0) { p[++m] = i, c[m] = 0; while (x % i == 0) x /= i, c[m]++; } if (x > 1) p[++m] = x, c[m] = 1; //update for (int i = 1; i <= m; ++i) cnt[p[i]] = max(cnt[p[i]], c[i]); } LL power(LL a, LL b) { LL res = 1; while (b) { if (b & 1) res = res * a % mod; b >>= 1; a = a * a % mod; } return res; } int main() { int l, r; cin >> l >> r; bool flag = false; for (int i = l; i <= r; ++i) if (check(i)) { flag = true; divide(i); } if (!flag) { puts("-1"); return 0; } LL ans = 1; for (int i = 1; i < N; ++i) ans = ans * power(i, cnt[i]) % mod; cout << ans; return 0; }

K题 小红的真真假假签到题题 (位运算)

给定一个正整数 x,显然让我们求另一个正整数 y,要求:

  1. yx 的倍数,且 xy
  2. 在二进制下,xy 的一个子串,且 x,y 中 1 的数量不一样
  3. y1019

1x109

这题构造方法挺多,其中一个比较显然的方法为 y=(231+1)x,也就是先左移,移出足够空位之后加上一个 x 即可。(注意,开 ULL 而不是 LL)

#include<bits/stdc++.h> using namespace std; #define ULL unsigned long long int main() { ULL x; cin >> x; cout << (x << 31) + x; return 0; }

L题 在这冷漠的世界里光光哭哭 ()


__EOF__

本文作者cyhforlight
本文链接https://www.cnblogs.com/cyhforlight/p/15899241.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyhforlight  阅读(108)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
历史上的今天:
2021-02-16 CF教育场 104 解题补题报告
点击右上角即可分享
微信分享提示