CF Round 767 Div2 题解

A题 Download More RAM

总计 \(T\) 组数据(\(1\leq T \leq 100\))。

现在我们有大小为 \(k\) 的内存,外加 \(n\) 个扩容器:第 \(i\) 个扩容器在载入的时候需要消耗 \(a_i\) 的内存(载入完毕之后就不消耗了),然后永久给内存容量增加 \(b_i\)

问我们至多可以将内存扩大到多少?

\(1\leq n\leq 100,1\leq k,a_i,b_i\leq 10^3\)

显然,我们将扩容器按照 \(a\) 的大小排序,优先用小的即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int n, k;
struct Node {
    int a, b;
    bool operator < (const Node &rhs) const {
        return a < rhs.a;
    }
} t[N];
void solve()
{
    //read
    cin >> n >> k;
    for (int i = 1; i <= n; ++i)
        cin >> t[i].a;
    for (int i = 1; i <= n; ++i)
        cin >> t[i].b;
    //solve
    sort(t + 1, t + n + 1);
    for (int i = 1; i <= n; ++i) {
        if (t[i].a <= k) k += t[i].b;
        else break;
    }
    cout << k << endl;
}

int main()
{
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}

B题 GCD Arrays

\(T\) 组数据(\(1\leq T \leq 10^5\))。

给定 \(a,l,r\),则 \(\{a_n\}\) 为一个 \([l,r]\) 上面的连续数列(例如 \(l=3,r=7\),则 \(\{a_n\}=\{3,4,5,6,7\}\))。

我们可以进行一种操作:从数列中选择任意两个数,然后从数列中删除他俩,随后向数列中插入这两个数的乘积。问,我们能否在进行不超过 \(k\) 次操作的情况下,使得 \(\operatorname{gcd}(a)>1\)

\(1\leq l\leq r\leq 10^9,0\leq k \leq r-l\)

序列长度为 1 的时候,看看这个唯一的数是不是 1 就行了。

序列长度大于 1 时,那就是看整个数列缩到最后之后,每个数都有一个大于 1 的质因数。因为初始数列连续,那么不难想到这个质因数为 2(让这个质因数为 3 或者更大的数,难度要比 2 大得多)。

如果序列长度为偶数,那么奇偶一一配对即可,反之则加一减一来调一调(对于长度为 2 或者 3 的数列似乎也适用)。

#include<bits/stdc++.h>
using namespace std;

bool solve()
{
    int l, r, k;
    scanf("%d%d%d", &l, &r, &k);

    if (k == 0) return l == r && l != 1;

    int len = r - l + 1;
    if (len % 2 == 0) return len / 2 <= k;
    else {
        if (l % 2 == 0) return len / 2 <= k;
        else return len / 2 + 1 <= k;
    }
}

int main()
{
    int T;
    cin >> T;
    while (T--) puts(solve() ? "YES" : "NO");
    return 0;
}

C题 Meximum Array

总计 \(T\) 组数据(\(1\leq T \leq 100\))。

对于非负整数序列 \(\{a_n\}\),记 \(\operatorname{mex}(a)\) 为最小的且未在 \(a\) 中出现的非负整数。

现在给定一个数列 \(\{a_n\}\),问我们能否将其划为几段,使得各段的 \(\text{mex}\) 组成的新数列的字典序最大(并输出它)?(遵循字符串的字典序定义)

\(\sum n\leq 2*10^5,0\leq a_i\leq n\)

抛开算法不谈,我们先看看怎么构造这玩意。

我们假设我们已经处理好了前 \(L-1\) 个数,现在准备处理 \([L,n]\) 上面的数。

  1. 显然,我们要处理下,看看这部分的 \(\text{mex}\) 是多少。
  2. 随后,我们压缩右区间 \(R\),即找出最小的 \(R\),使得 \([L,R]\) 上面的 \(\text{mex}\)\([L,n]\) 相同。
  3. \(L=R+1\),继续循环,直到 \(L=n+1\) 为止。

但是很离谱的是,每一轮的复杂度都能够达到最坏 \(O(n)\),而整个程序最坏能进行 \(n\) 轮,即总复杂度 \(O(n^2)\),这显然无法接受,必须降到 \(O(n\log n)\) 或者 \(O(n)\)

对于处理 \(\text{mex}\),我们有这样一种策略:我们首先预处理出 \([1,n]\) 上面的 \(\text{mex}\)(开一个 \(vis\) 数组(计数用的,并非纯粹判断这个数是否填过),填完之后从 0 到 n 扫一遍)。每次移动左端点 \(L\) 时,我们依次清理在 \(vis\) 里面的记录,如果过程中发现 \(vis_x=0\)\(x < \text{Mex}\),那么我们将 \(\text{Mex}\) 更新为 \(x\)

可惜的是,我们只完成了左端点的优化,对于右端点的移动没啥方法,最坏复杂度依然为 \(O(n^2)\)

法一:树状数组维护区间和

我们尝试更改一下套路,让右端点的移动从左向右进行,这样就实现了双指针,框架复杂度可以达到 \(O(n)\),里面还可以掺一些 \(O(\log n)\) 的操作。从正向角度,我们不难想到一种策略:我们再开一个 \(vis\) 数组(这里不计数,纯粹用 01 表示有无),随后使 \(R\)\(L\) 开始,逐渐推进,当 \(\sum\limits_{i=0}^{x-1}vis_i=x\)\(vis_x=0\) 时,则 \([L,R]\) 上面的 \(\text{mex}\) 就是 \(x\)。又要修改又要查询,那真就只有树状数组和线段树了(注意,这两个数据结构正常都是维护 \([1,n]\) 上面的,所以对于题目中那种需要维护 \([0,n]\) 的,需要手动维护下标为 0 的部分或者整体平移区间,还有就是这个 \(vis\) 不是随便加的,如果本来为 1 就别继续加,本来为 0 就别继续减啥的,反正小细节亿堆)。

#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, a[N];
int vis[N];
int Mex;
//
struct TreeArray {
    int arr[N];
    inline int lowbit(int x) { return x & (-x); }
    int ask(int x) {
        x++;
        int res = 0;
        for (; x; x -= lowbit(x)) res += arr[x];
        return res;
    }
    int query(int l, int r) {
        return ask(r) - ask(l - 1);
    }
    int getv(int x) {
        return query(x, x);
    }
    void add(int x, int val) {
        if ((getv(x) == 1 && val == 1) || (getv(x) == 0 && val == -1)) return;
        x++;
        for (; x <= n + 1; x += lowbit(x))
            arr[x] += val;
    }
} ta;
//
int getR(int L, int mex) {
    for (int i = L; i <= n; ++i) {
        ta.add(a[i], 1);
        if (ta.query(0, mex - 1) == mex && ta.getv(mex) == 0)
            return i;
    }
    return n;
}
void clear(int L, int R) {
    for (int i = L; i <= R; ++i) {
        vis[a[i]]--;
        ta.add(a[i], -1);
        if (vis[a[i]] == 0 && a[i] <= Mex)
            Mex = a[i];
    }
}

int ans[N];
void solve()
{
    //read
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //init
    memset(vis, 0, sizeof(int) * (n + 1));
    memset(ta.arr, 0, sizeof(int) * (n + 2));
    for (int i = 1; i <= n; ++i)
        vis[a[i]]++;
    for (int i = 0; i <= n; ++i)
        if (vis[i] == 0) {
            Mex = i;
            break;
        }
    //two pointer
    int L = 1, cnt = 0;
    while (L <= n) {
        int R = getR(L, Mex);
        ans[++cnt] = Mex;
        clear(L, R);
        L = R + 1;
    }
    //output
    printf("%d\n", cnt);
    for (int i = 1; i <= cnt; ++i)
        printf("%d ", ans[i]);
    puts("");
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--) solve();
    return 0;
}

法二:set 维护

为了一个简单的需求来写一个树状数组太逆天了(昨天还给我调了将近半小时,最离谱的是还没调出来),所以我们尝试一下别的更简单的数据结构。我们综合一下,分析一下需求,发现我们需要:

  1. 对于一个 bool 数列求和(值仅有 01,且查询的区间固定)
  2. 能够快速的进行 bool 加
  3. 用完之后可以光速清零

我们不妨在开桶记录的时候,只有小于 \(\text{Mex}\) 的才统计,然后单独开一个 \(cnt\) 变量来判断是否 \(\sum\limits_{i=0}^{x-1}vis_i=x\)(考虑到题目保证有解,那么一些极端情况就不考虑了)。不过普通的基于数组的桶,清零是基于值域的,比较麻烦,所以我们用 set 或者 map 进行代替。

#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, a[N], vis[N], Mex;
//
int getR(int L, int mex) {
    set<int> s;
    int cnt = 0;
    for (int i = L; i <= n; ++i) {
        if (a[i] < Mex) s.insert(a[i]);
        if (s.size() == Mex) return i;
    }
}
void clear(int L, int R) {
    for (int i = L; i <= R; ++i) {
        vis[a[i]]--;
        if (vis[a[i]] == 0 && a[i] <= Mex)
            Mex = a[i];
    }
}

int ans[N];
void solve()
{
    //read
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //init
    memset(vis, 0, sizeof(int) * (n + 1));
    for (int i = 1; i <= n; ++i)
        vis[a[i]]++;
    for (int i = 0; i <= n; ++i)
        if (vis[i] == 0) {
            Mex = i;
            break;
        }
    //two pointer
    int L = 1, cnt = 0;
    while (L <= n) {
        int R = getR(L, Mex);
        ans[++cnt] = Mex;
        clear(L, R);
        L = R + 1;
    }
    //output
    printf("%d\n", cnt);
    for (int i = 1; i <= cnt; ++i) printf("%d ", ans[i]);
    puts("");
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--) solve();
    return 0;
}

D题 Peculiar Movie Preferences

\(T\) 组数据(\(1\leq T \leq 100\))。

给定 \(n\) 个长度至多为 3 的字符串,问能否从中选几个重新排列,以组成一个新的回文串?

\(\sum n \leq 10^5\)

本题有一个不是很显然,不好证明,但是多玩几组样例之后会隐约发现的结论:如果能的话,只需要至多两个字符串即可凑成一个新的回文串。

证明的话,详情可见官方题解,我这里简单说两个感性想到的点:

  1. 如果有长度为 1 的串,那么自己就是回文串,也就不需要拼,所以需要超过 2 个字符串凑成的回文串,子串必然都是长度为 2 或 3
  2. 对于这个长回文串,显然仅保留头尾两个字符串,同样是一个回文串(22,23,32,33四种情况均成立)

如果仅需要一个字符串就可以拼成,那么我们直接对 \(n\) 个字符串一一扫一遍即可。

如果需要两个,分类讨论:

  1. 22 或者 33 型,看看自己的反转串有没有出现过(开个 map 就行)
  2. 23 或者 32 型,把那个长度为 2 的反转过来,然后看看是不是谁的前缀或者后缀

注意,这玩意必须按照原顺序组成回文串,所以 23 和 32 在遍历时候必须按照一定顺序(顺序或者倒序),详情看代码。

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
string str[N];
string Rev(string s) {
    reverse(s.begin(), s.end());
    return s;
}
bool solve()
{
    //read
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> str[i];
    //solve
    //type1
    for (int i = 1; i <= n; ++i)
        if (str[i] == Rev(str[i])) return true;
    //type2
    map<string, int> vis1;
    for (int i = 1; i <= n; ++i) vis1[str[i]] = 1;
    for (int i = 1; i <= n; ++i)
        if (vis1.find(Rev(str[i])) != vis1.end()) return true;
    //type3
    map<string, int> vis2;
    for (int i = n; i >= 1; --i) {
        if (str[i].length() == 3) vis2[str[i].substr(1, 2)] = 1;
        else if (vis2.find(Rev(str[i])) != vis2.end()) return true;
    }
    //type4
    map<string, int> vis3;
    for (int i = 1; i <= n; ++i)
        if (str[i].length() == 3) vis3[str[i].substr(0, 2)] = 1;
        else if (vis3.find(Rev(str[i])) != vis3.end()) return true;

    return false;
}
int main()
{
    ios::sync_with_stdio(false);

    int T;
    cin >> T;
    while (T--)
        cout << (solve() ? "YES" : "NO") << endl;
    return 0;
}
posted @ 2022-01-24 12:47  cyhforlight  阅读(28)  评论(0编辑  收藏  举报