2023牛客暑期多校训练营5 ABCDEGHI

比赛链接

A

题解

知识点:莫队,树状数组。

区间询问显然可以离线莫队,考虑端点移动对答案的影响。

不妨先考虑右端点右移一个位置,对答案的改变。假设右端点右移后在 r ,我们先要知道 [l,r1] 中和 ar 相等的位置。对于每个这样的位置 l ,我们将 [l,r1] 中小于 ar 的数字个数加起来,就是右移的贡献。

我们可以先树状数组预处理出,对于每个点在它之前比它小的数字个数,记为 lesi 。那么我们便可以简化最后一步的计算个数,最终答案就是 l:l[l,r1],al=arlesrlesl

我们发现其中 lesr 是定值,若我们知道了 [l,r1] 中所有与 ar 相等的位置个数 cntar ,那么总的贡献又可以变为 cntarlesrl:l[l,r1],al=arlesl ,因此我们可以同步维护 cnti 表示 [l,r] 中值为 i 的数字个数。

同时,我们也可以同步维护 sumi 表示 [l,r] 中所有值为 i 的位置 llesl 的和,来直接得到最后一项求和。

类似的有其他三个操作:左端点左移、右端点左移、左端点右移。

注意,莫队分块的最佳大小是 nm

时间复杂度 O(nm)

空间复杂度 O(n)

代码

#include <bits/stdc++.h>using namespace std;using ll = long long; template <class T>class Fenwick {    int n;    vector<T> node; public:    Fenwick(int _n = 0) { init(_n); }     void init(int _n) {        n = _n;        node.assign(n + 1, T());    }     void update(int x, T val) { for (int i = x;i <= n;i += i & -i) node[i] += val; }     T query(int x) {        T ans = T();        for (int i = x;i >= 1;i -= i & -i) ans += node[i];        return ans;    }    T query(int l, int r) {        T ans = T();        ans += query(r);        ans -= query(l - 1);        return ans;    }     int lower_bound(T val) {        int pos = 0;        for (int i = 1 << __lg(n); i; i >>= 1) {            if (pos + i <= n && node[pos + i] < val) {                pos += i;                val -= node[pos];            }        }        return pos + 1;    }    int upper_bound(T val) {        int pos = 0;        for (int i = 1 << __lg(n); i; i >>= 1) {            if (pos + i <= n && node[pos + i] <= val) {                pos += i;                val -= node[pos];            }        }        return pos + 1;    }};/// 树状数组,修改查询O(logn),单点修改、前缀查询//* 修改操作需满足结合律,即满足线段树的lazy标记要求//* 任意区间查询只支持可以减法的运算//* 倍增查找只支持基本类型 int a[500007];int les[500007]; struct Query {    int l, r, id;}Q[500007]; int cnt[500007];ll sum_les[500007];ll cur; void add(int x, bool isLeft) {    ll res = 1LL * cnt[a[x]] * les[x] - sum_les[a[x]];    cnt[a[x]]++;    sum_les[a[x]] += les[x];    cur += (isLeft ? -1 : 1) * res;} void del(int x, bool isLeft) {    cnt[a[x]]--;    sum_les[a[x]] -= les[x];    ll res = 1LL * cnt[a[x]] * les[x] - sum_les[a[x]];    cur -= (isLeft ? -1 : 1) * res;} ll ans[500007]; int main() {    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);     int n, m;    cin >> n >> m;     Fenwick<int> fw(n);    for (int i = 1;i <= n;i++) {        cin >> a[i];        les[i] = fw.query(a[i] - 1);        fw.update(a[i], 1);    }     for (int i = 1;i <= m;i++) {        int l, r;        cin >> l >> r;        Q[i] = { l,r,i };    }     int B = n / sqrt(m);    sort(Q + 1, Q + m + 1, [&](Query a, Query b) {        if (a.l / B == b.l / B) return a.l / B & 1 ? a.r > b.r:a.r < b.r;        else return a.l / B < b.l / B;    });     int L = 1, R = 0;    for (int i = 1;i <= m;i++) {        auto [l, r, id] = Q[i];        while (L > l) add(--L, 1);        while (R < r) add(++R, 0);        while (L < l) del(L++, 1);        while (R > r) del(R--, 0);        ans[id] = cur;    }     for (int i = 1;i <= m;i++) cout << ans[i] << '\n';    return 0;}

B

题解

知识点:贪心,枚举,优先队列。

k0 ,当存在一个位置大于等于 k 时,形成自环即可,逆序数为 0 ;否则所有位置一定小于 0 ,一定无解。接下来考虑 k>0 的情况。

我们先考虑全是正数的特殊情况,显然我们优先选择合法的连续区间 [l,r] 的元素循环左移一次构造大环,其他位置构成自环,这样的逆序数是 rl ,可以证明是理论最优的,从中去掉某些元素会导致花费增大。这样,我们枚举所有区间构造是 O(n2) 的,用尺取法是 O(n) 的。

现在问题包含了负数,尺取法就失效了,因为区间的和不具备单调性,但我们依旧可以枚举所有区间。不过,此时对于一个区间,有些位置我们是不能选的,否则就不合法了,因此选择的位置不再连续。我们先考虑不连续的位置产生的逆序数的最小值。

若在 [l,r] 这个连续区间中选择了 x 个数,我们可以将选中的位置对应的数循环左移一次,那么对于环上的元素相互产生了 x1 个逆序对,对于每个不在环上的元素都与环元素产生了 2 个逆序对,即没被选择的点都额外产生了一个逆序对,这样产生的逆序数为 rl+(rl+1x) ,可以证明是理论最优的。

结合上面的结论,我们知道对于一个固定区间 [l,r] ,选的数越多越好,这样贪心选择的方式就顺其自然的出现了:正数都选,随后负数选最大的直到不能再选。

这里,区间端点原则上是一定要选的,不然求出的答案会比真实答案要大。但对于这种情况的真实答案,其等价于一个更小区间的真实答案,因此即使舍弃导致求出错误答案,也不会使得最终答案错误,所以为了实现方便就允许存在这样的操作。

直接枚举每个区间取数的复杂度是 O(n3) 的。我们发现,对于每一个右端点,左端点从右到左的时候,区间长度一直在增加,那么当没被选择的数严格减小时答案才会更优,因此之前选过的负数是一定会被选上,否则没被选择的数一定比之前更大,答案不会更优。因此,用优先队列维护没被选择的负数即可,选过的就不需要考虑了。

时间复杂度 O(n2logn)

空间复杂度 O(n)

代码

#include <bits/stdc++.h>using namespace std;using ll = long long; int a[1000007];int main() {    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);    int n, k;    cin >> n >> k;    for (int i = 1;i <= n;i++) cin >> a[i];    if (k <= 0) {        bool ok = 0;        for (int i = 1;i <= n;i++) ok |= a[i] >= k;        if (ok) cout << 0 << '\n';        else cout << -1 << '\n';        return 0;    }     int ans = 1e9;    for (int i = 1;i <= n;i++) {        ll sum = 0;        priority_queue<int> pq;        for (int j = i;j >= 1;j--) {            if (a[j] >= 0) sum += a[j];            else pq.push(a[j]);            while (pq.size() && pq.top() + sum >= k) {                sum += pq.top();                pq.pop();            }            if (sum >= k) {                int sz = pq.size();                ans = min(ans, i - j + sz);            }        }    }    if (ans < 1e9) cout << ans << '\n';    else cout << -1 << '\n';    return 0;}

C

题解

知识点:图匹配,竞赛图。

(x,y+n) 的关系转化为 (x,y) ,那么原图会变为一个竞赛图,而原图的匹配关系则变为竞赛图的若干条不相交的路径。

首先竞赛图一定有一条哈密顿路径,因此答案至少为 n1

当且仅当每个点都在一个哈密顿回路时(每个点都在大于等于 3 强连通分量内),答案才为 n 。可以根据兰道定理:竞赛图强连通,当且仅当从小到大的度数序列的前缀和 s[1,i] 严格大于 (i2) 。我们可以枚举度数序列,找到所有强连通分量。

时间复杂度 O(n2)

空间复杂度 O(n)

代码

#include <bits/stdc++.h>using namespace std;using ll = long long; int deg[3007];int main() {    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);    int n;    cin >> n;    for (int i = 1;i <= n;i++)        for (int j = 1;j <= n;j++) {            int x;            cin >> x;            deg[i] += x;        }     // (x,y+n)看为(x,y)的边,可转化为竞赛图    // 竞赛图必有哈密顿路径答案至少为n - 1,当且仅当所有点可以在大于等于3的环中时答案为n    /// 兰道定理:一个序列是竞赛图的分数序列当且仅当度数序列前缀和s[1,i]大于等于C(i,2),i=n时等于    /// 兰道定理:竞赛图强连通当且仅当度数序列前缀和s[1,i]严格大于C(i,2)    sort(deg + 1, deg + n + 1);    int s = 0;    for (int i = 1, j = 0;i <= n;i++) {        s += deg[i];        if (s == i * (i - 1) / 2) { // 此时不强连通            if (i - j <= 2) { // [j,i)的点是强连通的,若小于等于2则不能成环,                cout << n - 1 << '\n';                return 0;            }            j = i;        }    }    cout << n << '\n';    return 0;}

D

题解

知识点:因数集合。

由条件 bc ,我们枚举所有 c 的因数,可以求出 a ,最后验证一下即可。

时间复杂度 O(c)

空间复杂度 O(c)

代码

#include <bits/stdc++.h>using namespace std;using ll = long long; void get_factor(int n, vector<int> &factor) {    for (int i = 1;i * i <= n;i++) {        if (!(n % i)) {            factor.push_back(i);            if (i != n / i) factor.push_back(n / i);        }    }} bool solve() {    int k, c, n;    cin >> k >> c >> n;     vector<int> factor;    get_factor(c, factor);     int ans = 0;    for (auto b : factor) {        if ((c - b) % k) continue;        int a = (c - b) / k;        if (a <= 0 || gcd(a, b) < n) continue;        ans++;    }    cout << ans << '\n';    return true;} int main() {    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);    int t = 1;    cin >> t;    while (t--) {        if (!solve()) cout << -1 << '\n';    }    return 0;}

E

题解

知识点:递归,分治。

不难发现区间的嵌套关系是一个树状结构,天然满足递归的性质,我们把每个不被约束的数字单独看作一个约束区间方便实现。

对于一个约束区间 [l,r] ,分类讨论:

  1. 若没有子约束,若要求奇数,我们随便交换两个即可,否则不用交换。

  2. 若有子约束,统计子约束的影响,若和自己同奇偶则无需改动,否则随意选两个相邻约束区间,取左边的最大值和右边的最小值交换即可。

时间复杂度 O(m(n+m))

空间复杂度 O(m)

代码

#include <bits/stdc++.h>using namespace std;using ll = long long; int n, m;array<int, 3> seg[1007];int p[1007], cnt; void dfs(int l, int r, int w = -1) {    if (l == r) {        p[l] = ++cnt;        return;    }    int pos = l, wsum = 0;    vector<pair<int, int>> sub_seg;    for (int i = 1;i <= m;i++) {        auto [l1, r1, w1] = seg[i];        if (pos <= l1 && r1 <= r && (w == -1 || r1 - l1 + 1 < r - l + 1)) {            while (pos < l1) {                dfs(pos, pos, -1);                sub_seg.push_back({ pos,pos });                pos++;            }            sub_seg.push_back({ l1,r1 });            dfs(l1, r1, w1);            pos = r1 + 1;            wsum ^= w1;        }    }    while (pos <= r) {        dfs(pos, pos, -1);        sub_seg.push_back({ pos,pos });        pos++;    }    if (w != -1 && w != wsum) {        auto [l1, r1] = sub_seg[0];        auto [l2, r2] = sub_seg[1];        int x = find(p + l1, p + r1 + 1, r1) - p;        int y = find(p + l2, p + r2 + 1, l2) - p;        swap(p[x], p[y]);    }} int main() {    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);    cin >> n >> m;    for (int i = 1;i <= m;i++) {        int l, r, w;        cin >> l >> r >> w;        seg[i] = { l,r,w };        if (l == r && w) {            cout << -1 << '\n';            return 0;        }    }    sort(seg + 1, seg + m + 1, [&](auto a, auto b) { return a[0] == b[0] ? a[1] > b[1]:a[0] < b[0];});     dfs(1, n);     for (int i = 1;i <= n;i++) cout << p[i] << " \n"[i == n];    return 0;}

G

题解

知识点:双指针。

尺取法枚举区间即可。

时间复杂度 O(n)

空间复杂度 O(n)

代码

#include <bits/stdc++.h>using namespace std;using ll = long long; int a[100007];int cnt[5];int main() {    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);    int n, k;    cin >> n >> k;    for (int i = 1;i <= n;i++) cin >> a[i];     int ans = 1e9;    int l = 1, r = 1;    while (l <= n) {        while (r <= n) {            if (cnt[1] && cnt[2] && cnt[3] && cnt[4] >= k) break;            cnt[a[r]]++;            r++;        }        if (cnt[1] && cnt[2] && cnt[3] && cnt[4] >= k) ans = min(ans, r - l);        cnt[a[l]]--;        l++;    }    cout << ans << '\n';    return 0;}

H

题解

知识点:贪心,背包dp。

注意到背包大小是递增的,因此我们考虑后 min(n,m) 个背包即可。

一个显然的方法是,考虑了前 i 次,共处理前 j 个奶酪的最大价值。但这样,转移时我们要枚举最后一次处理了多少奶酪,并进行背包dp,复杂度是 O(n3max{szi}) 的。

我们可以考虑预处理区间 [i,j] 且背包大小是 k 时的最大价值,复杂度是 O(n2max{szi}) 的,而对于之前的转移复杂度就变为 O(n3) 了。

时间复杂度 O(n2max{szi}+n3)

空间复杂度 O(m+n2max{szi})

代码

#include <bits/stdc++.h>using namespace std;using ll = long long; int a[207], b[207];int sz[100007];ll f[207][207][207];ll g[207][207];int main() {    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);    int n, m;    cin >> n >> m;    for (int i = 1;i <= n;i++) cin >> a[i] >> b[i];    for (int i = 1;i <= m;i++) cin >> sz[i];     for (int i = 1;i <= n;i++) {        for (int j = i;j <= n;j++) {            for (int k = 0;k <= sz[m];k++) {                f[i][j][k] = f[i][j - 1][k];                if (k >= a[j]) f[i][j][k] = max(f[i][j][k], f[i][j - 1][k - a[j]] + b[j]);            }        }    }    reverse(sz + 1, sz + m + 1);    m = min(m, n);    reverse(sz + 1, sz + m + 1);    for (int i = 1;i <= m;i++) {        for (int j = 0;j <= n;j++) {            for (int k = 0;k <= j;k++) {                g[i][j] = max(g[i][j], g[i - 1][j - k] + f[j - k + 1][j][sz[i]]);            }        }    }    cout << g[m][n] << '\n';    return 0;}

I

题解

知识点:位运算,线性dp。

先预处理前缀异或和方便处理,并考虑按位计算贡献。

我们先考虑一个简单的问题,求出所有子区间的异或和的和,与蓝桥杯省赛的一道题完全一致。朴素求所有子区间,最暴力的就是 O(n3) 选完再求,稍微优化一点是 O(n2) 枚举右端点左端点可以递推,但实际左端点的递推过程也可以 O(1) 解决,因此最佳复杂度就是 O(n) 的,具体解法如下。

枚举固定的右端点 r ,若一个位置 l1 使得 [1,l1] 的异或和第 k 位和 [1,r] 的异或和第 k 位不同,那么 [l,r] 的异或和第 k 位为 1 ,就会在以 r 为右端点的这个状态产生 2k 的贡献。

因此,我们考虑维护 [1,r1] 所有前缀(可以为空)异或和第 k 位的 0,1 个数 sumk,0/1 ,便可轻松获得前缀与 [1,r] 的异或和第 k 位不同的个数。例如[1,r] 的异或和第 k 位为 1 ,那么贡献就是 2ksumk,0 。最后,把 [1,r] 的异或和的每位情况加到 sum 即可。此时,我们可以得到以 r 为右端点的所有子区间的异或和。

对于这个简单问题,我们直接加到答案里即可。但为了原来问题的处理,我们考虑求 fi 表示右端点小于等于 i 的所有子区间的异或和之和。这个只需要求出以 r 为右端点的所有子区间的异或和后保存在 fr ,最后枚举完所有右端点后做前缀和即可。

回到原来的问题,发现其实就是分段求异或和再乘起来,求所有情况的和。问题与 k 子段问题非常相似,考虑线性dp。设 fi,j 表示分了 i 段,且所有区间右端点小于等于 j 时的答案,状态顺序交换也是可以写的,不过会麻烦一点。 i=1 时,f1,j 即上述简单问题最终答案,其实之后的递推思路是完全类同的。

当分了 i 段时,枚举第 i 段固定的右端点 j ,若一个位置 l1 使得 [1,l1] 的异或和第 k 位和 [1,j] 的异或和第 k 位不同,那么 [l,j] 的异或和第 k 位为 1 ,就会在分了 i 段最后一段以 j 为右端点的这个状态(不是 fi,j)产生 2kfi1,l1 的贡献。

因此,我们考虑维护 [1,j1] 所有前缀(可以为空)分了 i1 段且异或和的第 k 位为 0,1 时的总贡献 sumk,0/1 ,便可轻松获得前缀与 [1,j] 的异或和第 k 位不同时,前缀分了 i1 段的总贡献。例如[1,r] 的异或和第 k 位为 1 ,那么贡献就是 2ksumk,0 。最后,根据 [1,j] 的异或和每位情况,将 fi1,j 加到 sum 即可。此时,我们可以得到分了 i 段,且最后一段以 j 为右端点的答案。我们对其做前缀和即可得到 fi,j

这类问题最关键的是,枚举右端点时,将左端点的 O(n) 递推,根据异或的性质优化为对每位贡献的 O(1) 求和,即拆位求和的优化。

时间复杂度 O(n)

空间复杂度 O(n)

代码

#include <bits/stdc++.h>using namespace std;using ll = long long; const int P = 998244353;int a[200007];int main() {    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);    int n;    cin >> n;    for (int i = 1;i <= n;i++) cin >> a[i], a[i] ^= a[i - 1];     vector<int> f(n + 1, 1);    for (int i = 1;i <= 3;i++) {        vector<int> g(n + 1);        array<array<int, 2>, 30> sum{};        for (int j = 0;j <= n;j++) {            for (int k = 0;k < 30;k++) {                (g[j] += (1LL << k) * sum[k][~(a[j] >> k) & 1] % P) %= P;                (sum[k][(a[j] >> k) & 1] += f[j]) %= P;            }        }        for (int j = 1;j <= n;j++) (g[j] += g[j - 1]) %= P;        swap(f, g);    }    cout << f[n] << '\n';    return 0;}

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY1fzlU1' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片