牛客练习赛129 C-E 题解

1 篇文章 0 订阅
1 篇文章 0 订阅

比赛链接

C. 和天下

题目大意

给定 n n n 个结点的图以及整数 k k k,第 i i i 个结点的权值为 a i ( 1 ⩽ i ⩽ n ) a_i (1 \leqslant i \leqslant n) ai(1in)

如果对于两个结点 u ,   v u, \ v u, v,满足 a u ⊙ a v ⩾ k a_u \odot a_v \geqslant k auavk,那么 u ,   v u, \ v u, v 之间就有一条无向边,否则没有边。

其中 ⊙ \odot 表示按位!!!!

求整张图中最大联通块的大小。

数据范围如下。

  • 1 ⩽ n ⩽ 2 × 1 0 5 , 1 \leqslant n \leqslant 2 \times 10^5, 1n2×105,
  • 0 ⩽ a i ,   k ⩽ 1 0 18 0 \leqslant a_i, \ k \leqslant 10^{18} 0ai, k1018.

Solution

k = 0 k = 0 k=0,则任意两个数与起来都是大等于 k k k 的,那么答案为 n n n

k > 0 k > 0 k>0,我们考虑按位与的性质,即如果两个数 x ,   y x, \ y x, y 的某一位都是 1 1 1,那么 x ⊙ y x \odot y xy 的这一位也是 1 1 1

考虑 k k k 最高位的 1 1 1 在第 h i hi hi 位。

  • 如果存在 a u ,   a v a_u, \ a_v au, av,使得 a u ⊙ a v a_u \odot a_v auav h i + 1 hi + 1 hi+1 或者更高位上有 1 1 1,那么这些满足条件的 ( u ,   v ) (u, \ v) (u, v) 就可以随便连边,因为高位满足,低位就无需考虑。

  • 因此我们可以先按照第 b ∈ [ h i + 1 ,   60 ) b \in [hi + 1, \ 60) b[hi+1, 60) 位上是否有 1 1 1 来进行连边操作,也就是在第 b b b 位上,所有满足这一位是 1 1 1 a i a_i ai,对应的下标 i i i 都属于同一个联通块。

接下来考虑第 b ∈ [ 0 ,   h i ] b \in [0, \ hi] b[0, hi] 位。

  • k k k 在第 b b b 位上为 1 1 1,那就要求 a i a_i ai 在这一位上也为 1 1 1,也就是说,对于那些在这一位上不为 1 1 1 a i ′ a_{i'} ai,对应的下标 i ′ i' i 在之后的更低位都是禁止选择的(即在此之后 i ′ i' i 不可能再和别的结点连边了)。

  • k k k 在第 b b b 位上为 0 0 0,那 a i a_i ai 就没要求。而如果 a i a_i ai 在这一位上为 1 1 1,且 i i i 在此之前没有(因为上一段的原因)被 b a n ban ban 掉,那么 i i i 就是可以选择和其他结点连边的,将 i i i 加入当前第 b b b 位的可选集合。最后得到的可选集合内的下标都在同一个联通块内。

从第 h i hi hi 位操作到第 0 0 0 位后,我们能够得到最后仍然合法(即没被 b a n ban ban)的下标,这些下标可以在同一个联通块。

时间复杂度 O ( n log ⁡ V ) O(n \log V) O(nlogV)

  • V V V 为值域最大值。

C++ Code

#include <bits/stdc++.h>

using i64 = int64_t;
using u64 = uint64_t;
using f64 = double_t;
using i128 = __int128_t;

struct DSU {
    std::vector<int> f, sz;
    DSU() {}
    DSU(int n) {
        init(n);
    }
    void init(int n) {
        f.resize(n);
        std::iota(f.begin(), f.end(), 0);
        sz.assign(n, 1);
    }
    int find(int x) {
        while (x != f[x]) {
            x = f[x] = f[f[x]];
        }
        return x;
    }
    int size(int x) {
        return sz[find(x)];
    }
    bool merge(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) {
            return false;
        }
        sz[x] += sz[y];
        f[y] = x;
        return true;
    }
    bool same(int x, int y) {
        return find(x) == find(y);
    }
};

void solve() {
    int n;
    i64 k;
    std::cin >> n >> k;

    std::vector<i64> a(n);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }

    if (k == 0) {
        std::cout << n << "\n";
        return;
    }

    int hi = std::__lg(k);

    DSU dsu(n);
    auto work = [&](int &root, int i) {
        if (root != -1) {
            dsu.merge(root, i);
        } else {
            root = i;
        }
    };

    for (int j = 59; j > hi; j--) {
        int root = -1;
        for (int i = 0; i < n; i++) {
            if (a[i] >> j & 1) {
                work(root, i);
            }
        }
    }

    std::vector<bool> valid(n, true);
    for (int j = hi; j >= 0; j--) {
        int root = -1;
        for (int i = 0; i < n; i++) {
            if (~k >> j & 1 and a[i] >> j & 1 and valid[i]) {
                work(root, i);
            } else if (k >> j & 1 and ~a[i] >> j & 1) {
                valid[i] = false;
            }
        }
    }

    int root = -1;
    for (int i = 0; i < n; i++) {
        if (valid[i]) {
            work(root, i);
        }
    }

    int ans = 0;
    for (int i = 0; i < n; i++) {
        ans = std::max(ans, dsu.size(i));
    }

    std::cout << ans << "\n";
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
    std::cout << std::fixed << std::setprecision(12);
    
    int T = 1;
    std::cin >> T;
    
    while (T--) {
        solve();
    }
    
    return 0;
}

D. 搬家

题目大意

给定 n n n 个物品, m m m 个箱子,均排成一行。

每个箱子体积为 k k k,第 i i i 个物品体积为 a i ( 1 ⩽ i ⩽ n ) a_i (1 \leqslant i \leqslant n) ai(1in)

规定要从第 1 1 1 个箱子按顺序装到第 m m m 个箱子,一开始箱子什么都没装。

初始时指定第一个箱子,并选择一个起始位置 i i i,从第 i i i 个物品开始按顺序遍历它和它右侧的所有物品,对于每一个被遍历到的物品,我们的操作分为三步:

  • 如果当前物品可以装入现在指定的收纳箱,则将其装入,
  • 否则,只有其右侧存在收纳箱,才指定其右侧的第一个收纳箱,接着重复第一步,
  • 若物品被遍历完,或者右侧不存在收纳箱,就停止整个过程。

求最多可以装入的物品个数。

数据范围如下。

  • 1 ⩽ n ,   m ⩽ 1 0 5 , 1 \leqslant n, \ m \leqslant 10^5, 1n, m105,
  • 1 ⩽ k ⩽ 1 0 9 , 1 \leqslant k \leqslant 10^9, 1k109,
  • ∀ i ∈ [ 1 ,   n ] ,   0 < a i ⩽ k . \forall i \in [1, \ n], \ 0 < a_i \leqslant k. i[1, n], 0<aik.

Solution

由于从固定的 i i i 开始,往右跳到第一次停止过程,落点 r i r_i ri 是固定的,因此这种固定落点跳跃问题我们可以想到倍增优化。

我们称向右跳一步表示上述的从 i i i r i r_i ri

考虑 r [ i ] [ j ] r[i][j] r[i][j] 表示从 i i i 向右跳 2 j 2^j 2j 步能到达的下标, c n t [ i ] [ j ] cnt[i][j] cnt[i][j] 表示从 i i i 向右跳 2 j 2^j 2j 步能在所有箱子里装下的物品总数。

r [ i ] [ 0 ] r[i][0] r[i][0] c n t [ i ] [ 0 ] cnt[i][0] cnt[i][0] 的计算可以用双指针,剩下 j > 0 j > 0 j>0 的部分可以用倍增求得。

接着我们枚举起点 i i i,然后模拟向右走的过程,具体如下(我的下标从 0 0 0 开始)。

设当前已经从 i i i 到了 j j j,还想向右走 2 b 2^b 2b 步,使用过(且无法再使用的)箱子总数为 t o t tot tot,所装下的物品总数为 r e s res res

  • r [ j ] [ b ] ⩽ n r[j][b] \leqslant n r[j][b]n t o t + 2 b ⩽ m tot + 2^b \leqslant m tot+2bm,说明这 2 b 2^b 2b 步是可以走的,直接执行以下三步
    • r e s = r e s + c n t [ j ] [ b ] , res = res + cnt[j][b], res=res+cnt[j][b],
    • t o t = t o t + 2 b , tot = tot + 2^b, tot=tot+2b,
    • j = r [ j ] [ b ] . j = r[j][b]. j=r[j][b].
  • b = b − 1 b = b - 1 b=b1,若满足以下三个条件的其中一个,就停止过程
    • j = n , j = n, j=n,
    • b < 0 , b < 0, b<0,
    • t o t = m . tot = m. tot=m.

答案就是从每个 i i i 出发的答案的最大值。

时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

C++ Code

#include <bits/stdc++.h>

using i64 = int64_t;
using u64 = uint64_t;
using f64 = double_t;
using i128 = __int128_t;

void solve() {
    int n, m, k;
    std::cin >> n >> m >> k;

    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }

    std::vector<std::array<int, 17>> r(n + 1);
    std::vector<std::array<int, 17>> cnt(n + 1);
    for (int i = 0, j = 0, sum = 0; i < n; i++) {
        while (j < n and sum + a[j] <= k)  {
            sum += a[j++];
        }
        r[i][0] = j;
        cnt[i][0] = j - i;
        sum -= a[i];
    }

    r[n][0] = n;
    for (int j = 1; j < 17; j++) {
        for (int i = 0; i <= n; i++) {
            r[i][j] = r[r[i][j - 1]][j - 1];
            cnt[i][j] = cnt[i][j - 1] + cnt[r[i][j - 1]][j - 1];
        }
    }

    i64 ans = 0;
    for (int i = 0; i < n; i++) {
        i64 res = 0;
        for (int j = i, b = 16, tot = 0; j < n and b >= 0 and tot < m; b--) {
            if (r[j][b] <= n and tot + (1 << b) <= m) {
                res += cnt[j][b];
                tot += 1 << b;
                j = r[j][b];
            }
        }
        ans = std::max(ans, res);
    }

    std::cout << ans << "\n";
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
    std::cout << std::fixed << std::setprecision(12);
    
    int T = 1;
    std::cin >> T;
    
    while (T--) {
        solve();
    }
    
    return 0;
}

E. Alice and Bod(疑似打错)

题目大意

给定一个长度为 n n n 字符串 s s s,仅由小写字母组成。

现在给定 m m m,表示要做 m m m 次操作,每次操作有两种类型:

  • 1 l r:询问 s [ l . . . r ] s[l...r] s[l...r] 是否是回文串,是输出 YES,否则输出 NO
  • 2 l r x:将 s [ l . . . r ] s[l...r] s[l...r] 中每个字符都改为其第 x x x 个后继,比如 a a a 的第 2 2 2 个后继是 c c c z z z 的第 1 1 1 个后继是 a a a a a a 的第 26 26 26 个后继是 a a a

数据范围如下。

  • 1 ⩽ n ,   m ⩽ 1 0 5 , 1 \leqslant n, \ m \leqslant 10^5, 1n, m105,
  • 0 ⩽ x ⩽ 1 0 9 . 0 \leqslant x \leqslant 10^9. 0x109.

Solution

做法是线段树 + 哈希。

首先,要判断一个字符串是否是回文串,哈希是很明显的。

但是这里有修改操作,因此不能直接上普通的字符串哈希。

这里的哈希方式是,对 26 26 26 个字母每个都建立一个 01 01 01 串,例子如下:

  • s = a a b c a s = aabca s=aabca,那么 a a a 对应的 01 01 01 串为 11001 11001 11001 b b b 对应的为 00100 00100 00100 c c c 对应的为 00010 00010 00010,其余 23 23 23 个字母都是全 0 0 0

但是比较不能直接比这 26 26 26 个串,否则就是 O ( n ) O(n) O(n) 的比较了;所以我们需要对新生成的 01 01 01 串进行哈希,这里的底数和模数可以去搜一下,我选择的是 B = 29 ,   P = 1 ′ 00 0 ′ 00 0 ′ 007 B = 29, \ P = 1'000'000'007 B=29, P=1000000007

上面的对 01 01 01 串哈希的意思是,把这些 01 01 01 串看成(在 P P P 模数系下)的 B B B 进制数,例如 101 101 101 就表示 ( 1 ⋅ 2 9 2 + 0 ⋅ 2 9 1 + 1 ⋅ 2 9 0 )   %   P (1 \cdot 29^2 + 0\cdot 29^1 + 1 \cdot 29^0) \ \% \ P (1292+0291+1290) % P,这就是它的哈希值。

这样,我们判断一段子串是否是回文串,就只要正反两次哈希,然后比较这 26 26 26 个正反哈希值是否一致。

关键点是修改操作。

每次我们需要把字母调整为其第 x x x 个后继,其实就是让这个后继占了原字母的位置。再换句话说,其实就是把当前字母的状态,赋值给它的第 x x x 个后继。

因此修改字符这个操作,就变成了一个区间赋值操作,可以用带懒标记的线段树实现。

所以我们用线段树维护每个点的 26 26 26 个哈希值即可。

时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

C++ Code

#include <bits/stdc++.h>

using i64 = int64_t;
using u64 = uint64_t;
using f64 = double_t;
using i128 = __int128_t;

template<class Info, class Tag>
struct LSGT {
    int n;
    std::vector<Info> info;
    std::vector<Tag> tag;
    LSGT() {}
    LSGT(int _n, Info _v = Info()) {
        init(std::vector(_n, _v));
    }
    template<class T>
    LSGT(const std::vector<T> &a) {
        init(a);
    }
    template<class T>
    void init(const std::vector<T> &a) {
        n = a.size();
        info.assign(4 << std::__lg(n), Info());
        tag.assign(4 << std::__lg(n), Tag());
        auto build = [&](auto self, int p, int l, int r) -> void {
            if (r - l == 1) {
                info[p] = a[l];
                return;
            }
            int m = l + r >> 1;
            self(self, p * 2, l, m);
            self(self, p * 2 + 1, m, r);
            pull(p);
        };
        build(build, 1, 0, n);
    }
    void pull(int p) {
        info[p] = info[p * 2] + info[p * 2 + 1];
    }
    void modify(int p, int l, int r, int x, const Info &v) {
        if (r - l == 1) {
            info[p] = v;
            return;
        }
        int m = l + r >> 1;
        if (x < m) {
            modify(p * 2, l, m, x, v);
        } else {
            modify(p * 2 + 1, m, r, x, v);
        }
        pull(p);
    }
    void modify(int x, const Info &v) {
        modify(1, 0, n, x, v);
    }
    void push(int p) {
        apply(p * 2, tag[p]);
        apply(p * 2 + 1, tag[p]);
        tag[p] = Tag();
    }
    void apply(int p, const Tag &t) {
        info[p].apply(t);
        tag[p].apply(t);
    }
    Info query(int p, int l, int r, int x, int y) {
        if (l >= y or r <= x) {
            return Info();
        }
        if (l >= x and r <= y) {
            return info[p];
        }
        push(p);
        int m = l + r >> 1;
        return query(p * 2, l, m, x, y) + query(p * 2 + 1, m, r, x, y);
    }
    Info query(int l, int r) {
        return query(1, 0, n, l, r);
    }
    void Apply(int p, int l, int r, int x, int y, const Tag &t) {
        if (l >= y or r <= x) {
            return;
        }
        if (l >= x and r <= y) {
            apply(p, t);
            return;
        }
        push(p);
        int m = l + r >> 1;
        Apply(p * 2, l, m, x, y, t);
        Apply(p * 2 + 1, m, r, x, y, t);
        pull(p);
    }
    void Apply(int l, int r, const Tag &t) {
        Apply(1, 0, n, l, r, t);
    }
};

constexpr int A = 26;
constexpr int B = 29;
constexpr int P = int(1E9) + 7;

std::vector<i64> p;

struct Tag {
    int x = 0;
    void apply(const Tag &t) {
        x = (x + t.x) % A;
    }
};
struct Info {
    int l = 0;
    int r = 0;
    std::array<i64, A> h{};
    void apply(const Tag &t) {
        if (t.x == 0) {
            return;
        }
        auto h1 = h;
        for (int x = 0; x < A; x++) {
            h[(x + t.x) % A] = h1[x];
        }
    }
};
Info operator+(const Info &a, const Info &b) {
    std::array<i64, A> h{};
    for (int i = 0; i < A; i++) {
        h[i] = (a.h[i] * p[b.r - b.l] + b.h[i]) % P;
    }
    return {a.l, std::max(a.r, b.r), h};
}
bool operator==(const Info &a, const Info &b) {
    return a.h == b.h;
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
    std::cout << std::fixed << std::setprecision(12);
    
    int n, m;
    std::cin >> n >> m;

    std::string s;
    std::cin >> s;

    p.assign(n + 1, 1);
    for (int i = 0; i < n; i++) {
        p[i + 1] = p[i] * B % P;
    }

    LSGT<Info, Tag> sgt(n);
    LSGT<Info, Tag> rsgt(n);

    std::array<i64, A> c{};
    for (int i = 0; i < n; i++) {
        c[s[i] - 'a'] = 1;
        sgt.modify(i, {i, i + 1, c});
        c[s[i] - 'a'] = 0;
    }
    for (int i = 0; i < n; i++) {
        c[s[n - i - 1] - 'a'] = 1;
        rsgt.modify(i, {i, i + 1, c});
        c[s[n - i - 1] - 'a'] = 0;
    }

    while (m--) {
        int o, l, r;
        std::cin >> o >> l >> r;
        l--;

        if (o == 1) {
            std::cout << (sgt.query(l, r) == rsgt.query(n - r, n - l) ? "YES\n": "NO\n");
        } else {
            int x;
            std::cin >> x;
            sgt.Apply(l, r, {x});
            rsgt.Apply(n - r, n - l, {x});
        }
    }
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值