Codeforces Round #825 (Div. 2) A-D

比赛链接

A

题解

知识点:贪心。

考虑两种方法:

  1. 所有不同的位置使用操作1变成相同
  2. 使用操作1将两串01数量相同,然后使用1次操作2

取其中最小的即可。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int a[107], b[107];
bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) cin >> a[i];
    for (int i = 1;i <= n;i++) cin >> b[i];

    int cnt1 = 0;
    int cnt2 = 0;
    for (int i = 1;i <= n;i++) {
        cnt1 += a[i] != b[i];
        cnt2 += a[i] - b[i];
    }
    cout << min(cnt1, abs(cnt2) + 1) << '\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;
}

B

题解

知识点:数论,贪心。

充要条件:存在 \(b\) ,当且仅当 \(gcd(a[i-1],a[i+1]) | a[i]\)

显然,如果存在 \(b\) 那么 \(b[i]\) 一定会包括 \(a[i-1]\)\(a[i]\) 的因子,而 \(a[i] = gcd(b[i],b[i+1])\) 一定能被 \(gcd(a[i-1],a[i])\) 整除。

\(gcd(a[i-1],a[i+1]) | a[i]\) ,那么构造 \(b[i] = lcm(a[i],a[i-1])\) ,则:

\[\begin{aligned} gcd(b[i],b[i+1]) &= gcd(lcm(a[i-1],a[i]),lcm(a[i],a[i+1])) \\ &= a[i] \cdot gcd\bigg(\frac{a[i-1]}{gcd(a[i-1],a[i])},\frac{a[i+1]}{gcd(a[i],a[i+1])}\bigg) \end{aligned} \]

注意到,因为 \(gcd(a[i-1],a[i+1])|a[i]\) ,因此 \(gcd(a[i-1],a[i])\) 一定包含 \(gcd(a[i-1],a[i+1])\) ,同理 \(gcd(a[i],a[i+1])\) 也是,因此 \(gcd\bigg(\dfrac{a[i-1]}{gcd(a[i-1],a[i])},\dfrac{a[i+1]}{gcd(a[i],a[i+1])}\bigg) = 1\) ,所以 \(gcd(b[i],b[i+1]) = a[i]\) ,存在 \(b\)

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int a[100007];
bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) cin >> a[i];
    for (int i = 2;i <= n - 1;i++) {
        if (a[i] % __gcd(a[i - 1], a[i + 1])) return false;
    }
    cout << "YES" << '\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 << "NO" << '\n';
    }
    return 0;
}

C1

题解

知识点:双指针,枚举。

对于区间内的一个位置 \(i\) ,当左端点 \(l\) 满足 \(i-a[i]+1\leq l\) 时,\(i\) 才能可能存在于 \(l\) 开始的区间。

同时,注意到区间端点具有单调性,即 \([l,r]\) 是一个合法区间,那么 \([i,r],i\in[l,r]\) 都是合法区间,因此可以尺取法枚举左端点 \(l\) ,找到第一个不可行右端点 \(r\) ,就直接得到从 \(l\) 为左端点的区间个数 \(r-l\)

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int a[200007];
bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) cin >> a[i];
    int l = 1, r = 1;
    ll ans = 0;
    while (l <= n) {
        while (r <= n) {
            if (r + 1 - a[r] > l) break;
            r++;
        }
        ans += r - l;
        l++;
    }
    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;
}

C2

题解

知识点:前缀和,双指针,枚举。

此题能用线段树二分做,但实际上二分查找右端点没有必要,可以尺取法中同时预处理出来。

先考虑修改 \(a[p]\)\(x\) 会改变什么。如果 \(x > a[p]\) ,那么一些原本右端点被 \(p\) 阻挡的区间现在可能跨越 \(p\) 再往后走一段;如果 \(x<a[p]\) ,那么原本一些跨越了 \(p\) 的区间右端点现在可能会被 \(p\) 阻挡(\(\geq p\) 的不受影响接下来不讨论)。

明确了修改 \(a[p]\) 会导致的变化,那就开始具体讨论:

  1. \(x < a[p]\) ,我们首先需要知道跨越了 \(p\) 的具体区间是什么才能进行下一步操作。因为区间端点是单调的,我们只需要知道第一个右端点可以跨越 \(p\) 的左端点即可,那么到 \(p-1\) 为止的左端点都是可以跨越 \(p\) 的。

    我们在尺取法中扩展右端点时,用 \(Lr[r]\) 记录以 \(r\) 为右端点的左端点,由于每个 \(r\) 只会记录一次,那么 \(Lr[r]\) 一定代表第一次以 \(r\) 为右端点可行的左端点 \(l\) ,所以 \(Lr[p]\) 即区间右端点能跨越 \(p\) 的第一个左端点。

    接下来要考虑 \(a[p]\) 修改成 \(x\) 之后影响了左端点 \([Lr[p],p-1]\) 的哪一部分。显然, \(x\) 在位置 \(p\) 作为右端点时,左端点的可能范围是 \([p-x+1,p-1]\) 。那么只有在 \(p-x+1 > Lr[p]\) 时,才会使 \([Lr[p],p-x]\) 这些原本能跨越 \(p\) 的左端点的右端点被 \(p\) 阻挡,否则不会影响答案。

    因此,我们设前缀和 \(pre[i]\) 代表 \([1,i]\) 的点作为左端点产生的合法区间个数。我们先减去 \([Lr[p],p-x]\) 这部分左端点原本提供的区间个数,再加上新的个数,即 \(\sum_{i = Lr[p]}^{p-x} p-i = \dfrac{(p-Lr[p]+x)(p-x-Lr[p]+1)}{2}\) ,每个左端点 \(i\)\(p\) 阻挡提供 \(p-i\) 个区间。所以最终答案为 \(pre[n] - (pre[p-x]-pre[Lr[p]-1]) + \dfrac{(p-Lr[p]+x)(p-x-Lr[p]+1)}{2}\)

  2. \(x>a[p]\) ,同样我们先确定右端点被 \(p\) 阻挡的左端点区间。我们只需要在每个左端点 \(l\) 扩展不了时,记录一下此时的 \(r\) ,即 \(Ll[r] = l\) 来表示被 \(r\) 阻挡的左端点,同时对于每个 \(r\) 只记录一次,因此 \(Ll[r]\) 就代表第一个被 \(r\) 阻挡的左端点。

    在上文提到 \(Lr[r]\) 代表第一个可以跨越 \(r\) 的左端点,那 \(Lr[r]-1\) 就是最后一个被 \(r\) 阻挡的左端点。于是, \([Ll[p],Lr[p]-1]\) 就是被 \(p\) 阻挡的左端点区间。

    注意到,可能出现不存在左端点会被 \(p\) 阻挡,这时候 \(Ll[p]\) 应为初始化的值 \(0\) ,答案不变。否则,被 \(p\) 阻挡的左端点区间必然存在。

    同时,如果存在这样的区间,那么 \(p-a[p]+1\) 必然等于 \(Lr[p]\) 。因此如果 \(x<a[p]\) ,那么 \(p-x+1\) 一定小于 \(Lr[p]\) ,一定会改变答案。 于是,需要修改答案的左端点区间就是 \([\max(Ll[p],p-x+1),Lr[p]-1]\)

    最后我们还需要知道,这些左端点跨越了 \(p\) 之后的右端点最多到哪。在一开始的尺取法时,我们用 \(rr\) 来记录,当 \(l\)\(r\) 阻挡时,跨越 \(r\) 后又会被阻挡的位置,那么 \(rr-l\)\(l\) 跨越 \(r\) 后的合法区间的新个数。

    我们同样用前缀和记录一下,\(skpre[i]\) 代表 \([1,i]\) 的区间的左端点,可以跨越一次阻挡自己的位置后的合法区间个数。我们先减去 \([\max(Ll[p],p-x+1),Lr[p]-1]\) 的原本的答案 \(pre[Lr[p]-1] - pre[\max(Ll[p],p-x+1)-1]\) ,再加上新的个数 \(skpre[Lr[p]-1] - skpre[\max(Ll[p],p-x+1)-1]\) 。 所以最终答案为 \(pre[n] - (pre[Lr[p]-1] - pre[\max(Ll[p],p-x+1)-1]) + (skpre[Lr[p]-1] - skpre[\max(Ll[p],p-x+1)-1])\)

时间复杂度 \(O(n+q)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

int a[200007];
int Ll[200007], Lr[200007];
ll pre[200007], skpre[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];
    int l = 1, r = 1, rr = 1;//l为目前起点,r为以l为左端点的第一个不行的点,rr为跨越障碍点后第一个不行的点
    while (l <= n) {
        while (r <= n && r - a[r] + 1 <= l) Lr[r++] = l;//Lr[r]为r左边第一个可行l,即左边最后一个以r为障碍的点+1
        rr = min(max(rr, r + 1), n + 1);
        while (rr <= n && rr - a[rr] + 1 <= l) rr++;

        if (!Ll[r]) Ll[r] = l;//Ll[r]为r左边第一个不可行l
        pre[l] = pre[l - 1] + r - l;
        skpre[l] = skpre[l - 1] + rr - l;
        l++;
    }

    int q;
    cin >> q;
    while (q--) {
        ll ans = pre[n];
        int p, x;
        cin >> p >> x;
        if (x < a[p] && p - x + 1 > Lr[p]) {
            ans -= pre[p - x] - pre[Lr[p] - 1];
            ans += 1LL * (x + p - Lr[p]) * (p - x - Lr[p] + 1) / 2;
        }
        else if (x > a[p] && Ll[p]) {
            ans -= pre[Lr[p] - 1] - pre[max(p - x + 1, Ll[p]) - 1];
            ans += skpre[Lr[p] - 1] - skpre[max(p - x + 1, Ll[p]) - 1];
        }
        cout << ans << '\n';
    }
    return 0;
}

D

题解

知识点:构造。

显然,\(1\)\(0\) 的个数为奇数时一定不可能。

猜想,\(1\)\(0\) 个数都为偶数时,一定能够造出。考虑两两构造成 \(s[i] = s[i+1]\) 的形式,如此只要取 \(i = 2k-1\) 即可。

\(s[i] = s[i+1]\) 时符合构造不需要修改,当 \(s[i] \neq s[i+1]\) 时考虑如下修改。

不符合我们构造形式的 \(01\) 组合一定是成对出现,比如 01 出现,因为 \(0\)\(1\) 的个数都为偶数,那么一定会再出现一次 \(s[i] \neq s[i+1]\) 的组合来使个数为偶数。

我们考虑对第一个组合取出一个 \(0\) ,那么剩下一个 \(1\) ,需要从下一个组合取出一个 \(1\) 给这一组即可,那么下一组就会剩下一个 \(0\) ,再从下下一组取出一个 \(0\) ,以此类推。因为成对出现,所以取到最后一定取的是 \(1\) ,剩下一个 \(0\) ,那么把第一组取的 \(0\) 填上即可。

我们注意到上面取的过程,实际上就是右边一组往左边一组传递一个数字,第一组给最后一组传递一个数字,也就是题目给出的一次操作,于是就构造成功了。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;

bool solve() {
    int n;
    string s;
    cin >> n >> s;
    if (count(s.begin(), s.end(), '1') & 1) return false;
    s = '?' + s;
    bool cur = 0;//cur代表上一组不同的缺的数字
    vector<int> b;
    for (int i = 1;i <= 2 * n;i += 2) {
        if (s[i] != s[i + 1]) {
            if (s[i] - '0' == cur) b.push_back(i), cur ^= 1;//取出上一组需要的数字,然后更换cur,往复循环
            else b.push_back(i + 1), cur ^= 1;
        }
    }
    cout << b.size() << ' ';
    for (auto i : b) cout << i << ' ';
    cout << '\n';
    for (int i = 1;i <= 2 * n;i += 2) cout << i << ' ';
    cout << '\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;
}
posted @ 2022-10-16 15:57  空白菌  阅读(52)  评论(0编辑  收藏  举报