2022牛客冬令营 第四场 题解
A题 R (二分/双指针)
给定一个长度为 \(n\) 的纯大写字母字符串,我们想要知道,这个字符串有多少个连续子串,内部有至少 \(k\) 个 R,且不包含 P。
\(1\leq n\leq 2*10^5,1\leq k\leq 20\)
官方题解有两个解法,我们这里用第二个(因为我也是用第二个方法过的)。
我们枚举左端点 \(L\),发现右端点 \(R\) 必须满足:
- 区间 \([L,R]\) 内 R 的数量要大于等于 k
- 区间 \([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\) 的序列 \(\{a_n\}\),保证任意时刻每个序列上面的值都在 \([0,10)\) 之间。现在,我们有 \(q\) 次操作,分两类:
- 1 x y,使得 \(a_x=y\)
- 2 x y,求出序列区间 \([x,y]\) 上面这些数按照字符串形式生成的数(例如 \([1,2,5,4]\) 子区间生成的数就是 1254),问使用几进制来表示它可以使得其值最小,并输出这个最小值(对 \(10^9+7\) 取模)。
\(1\leq n,q \leq 10^5\)
区间 \([l,r]\) 上面最大值为 \(P\),那么按照 \(P+1\) 进制生成的值就是最小的。
显然,我们不可能每次暴力算一遍这个值是多少,加上频繁修改,显然就是使用线段树了。对于每个线段树的节点,维护三个核心值:区间长度,区间最大值(方便查询用几进制),区间数在各进制下所表示的值。
对于查询,我们直接找到这若干个区间,按照正常字符串拼接时候计算对应值的方式来看看他们怎么加即可(例如 123 和 56 在 8 进制下拼接,显然有 \(val(12356)=val(123)*8^2+val(56)\),其中 2 是 56 的长度)。
对于修改,我们找到对应的叶子节点,改好之后一路向上 pushup 即可。
综上,复杂度为 \(O(10n\log n)\)。
#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\) 颗彗星会在时间 \(a_i\) 到,持续 \(t\) 秒,也就是说我们在时间段 \([a_i,a_i+t-1]\) 内能看到这个彗星。
现在,我们想要求出,有多少时间,我们能够在天上看到蓝色彗星,且没有红色彗星?
\(1\leq n,t,a_i\leq 10^5\)
我们可以将时间轴抽象为一个序列,对于第 \(i\) 颗彗星,如果是蓝的,那就给 \([a_i,a_i+t-1]\) 都加上 1,反之都减去 \(10^9\)。统计答案时候,如果某个时间对应的值大于 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)\) 的值。(\(1\leq n\leq 10^{18}\))
我们将其转化为公式形式,有:
方法一:记忆化搜索
去年杭电多校有一个类似的签到题,也用到了相似的技巧:虽然 \(x\) 的值域范围很大,但是不难注意到,数的种类很少(\(\log x\) 层,每层至多两个数)。对于这种值域大,种类少的题,可以开一个 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\) 满足题意。
我们开始进行数学证明:
-
\(x=1\) 时显然满足题意
-
假设 \(x=k\) 时满足 \(f(k)=k\)
-
\(x=k+1\) 时:
- 若 k 是偶数,那么有 \(f(k+1)=f(\frac{k}{2})+f(\frac{k}{2}+1)=k+1\)
- 若 k 是奇数,那么有 \(f(k+1)=2f(\frac{k+1}{2})=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\) 的正整数序列 \(\{a_n\}\),问该数组的所有非空的子序列的价值之积。
\(1\leq n\leq 2*10^5,1\leq a_i\leq 10^9\)
我们将序列从小到大排个序,然后对于每个值单独算贡献:
例如第 i 个值为 \(x\),那么我们选定这个值作为最小值,那么在它前面的都不选,后面的都可以选,那么 \(x\) 作为最小值就出现了 \(2^{n-i}\) 次;同理,其作为最大值出现了 \(2^{i-1}\) 次。
我们综合一下,发现答案为
质数过大,加上不保证互质性质,所以则套一个扩展欧拉降幂。
注:若 \(x\) 是质数,则 \(\phi(x)=x-1\),所以本题不需要单独写欧拉函数了。
#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\),求正方体体积。
\(1\leq x\leq 100\)
样例好心的提醒了我们:最优策略就是 A 在中心,B在正方体一角。
那么,设正方体边长为 \(2a\),那么中心到角的距离为 \(x=\sqrt{3}a\)。计算并输出即可。
#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\) 张卡牌消耗 \(a_i\) 法力,造成 \(b_i\) 伤害。
现在,我们想要选取一些卡牌,消耗的法力的总和为 \(k\) 的倍数,并求出最多能造成多少伤害。(凑不出来卡牌组合则输出 -1)
\(1\leq n,k\leq 10^3,1\leq a_i,b_i\leq 10^9\)
第三次碰到这种基于整除的 01 背包题了,前两次分别是 [USACO09MAR]Cow Frisbee Team S 和 20年校赛那个黄焖鸡。
直接记 \(dp_{i,j}\) 的两个维度,i 表示当前到了第 i 个物品,j 表示现在选择的卡牌,消耗对 \(k\) 取模后的值,那么有
复杂度 \(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)?
\(1\leq l\leq r \leq 3*10^4\),
这个区间范围太小,以至于我们甚至不需要素数筛,直接朴素判断即可找出所有素数。
对于每个合数,直接质因数分解,然后遵循算术基本定理的定义来求 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\),要求:
- \(y\) 是 \(x\) 的倍数,且 \(x\not=y\)
- 在二进制下,\(x\) 是 \(y\) 的一个子串,且 \(x,y\) 中 1 的数量不一样
- \(y\leq 10^{19}\)
\(1\leq x\leq 10^9\)
这题构造方法挺多,其中一个比较显然的方法为 \(y=(2^{31}+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;
}