2022牛客冬令营 第四场 题解

官方题解

A题 R (二分/双指针)

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

\(1\leq n\leq 2*10^5,1\leq k\leq 20\)

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

我们枚举左端点 \(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\) 的序列 \(\{a_n\}\),保证任意时刻每个序列上面的值都在 \([0,10)\) 之间。现在,我们有 \(q\) 次操作,分两类:

  1. 1 x y,使得 \(a_x=y\)
  2. 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}\)

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

\[f(x)=\begin{cases}1&x=1\\ f(\lfloor\frac{n}{2}\rfloor)+f(\lceil\frac{n}{2}\rceil) &x>1\end{cases} \]

方法一:记忆化搜索

去年杭电多校有一个类似的签到题,也用到了相似的技巧:虽然 \(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\) 满足题意。

我们开始进行数学证明:

  1. \(x=1\) 时显然满足题意

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

  3. \(x=k+1\) 时:

    1. 若 k 是偶数,那么有 \(f(k+1)=f(\frac{k}{2})+f(\frac{k}{2}+1)=k+1\)
    2. 若 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}\) 次。

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

\[ans=(\prod\limits_{i=1}^na_i^{2^{n-i}})(\prod\limits_{i=1}^na_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\) 取模后的值,那么有

\[dp_{i,j}=\max (dp_{i-1,j},dp_{i-1,(j-a_i)\bmod k}+b_i) \]

复杂度 \(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\),要求:

  1. \(y\)\(x\) 的倍数,且 \(x\not=y\)
  2. 在二进制下,\(x\)\(y\) 的一个子串,且 \(x,y\) 中 1 的数量不一样
  3. \(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;
}

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

posted @ 2022-02-16 10:10  cyhforlight  阅读(102)  评论(0编辑  收藏  举报