Codeforces Pinely Round 5(div.1 + div.2) A~D题解

写在开头

有不足的地方各位佬多多指教呀!

持续更新(bushi)

Update-20251104

更改了数学公式渲染方式。

A

题面

给定李华的初始rating \(R\) ,div.2的计分上限 \(X\),李华每次rating的变化最大值 \(D\),以及cf比赛的次数 \(n\),问李华最多可以正式参加多少场cf。

\( 0 \le R \le 10^9 \\ 1 \le X \le 10^9 \\ 1 \le D,n \le 1000 \\ \sum{n} \le 3 \times 10^4 \)

解析

由于div.1没有计分下限,所以让李华的rating尽可能低即可。

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

代码

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

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while(t--){
        int rat, x, d, n;
        cin >> rat >> x >> d >> n;
        string s;
        cin >> s;
        int num = 0;
        for(int i = 0; i < n; i++)
            if(s[i] == '1'){
                num++;
                if(rat >= x)
                    rat -= d;
            }else{
                if(rat < x)
                    num++;
            }
        cout << num << '\n';
    }
    return 0;
}

B

题面

给定一个 \(n \times n\) 的表格,每个格子被染上黑白两色中的一个,你可以选择任意多个白色格子并将其染黑,问是否存在一种染色使得所有的黑格是联通的(对角相连不算联通),并且在任一行或一列存在连续3个黑格。

\( 1 \le n \le 100 \\ \sum{n} \le 2000 \)

解析

\(0\) 表示白格,用 \(1\) 表示黑格(这里与原题符号不一样),那么关键发现是所有的满足条件的方案一定是如下形状的:


只有四个黑格构成一个正方形。

\( 0000\\ 0110\\ 0110\\ 0000\\ \)


一条蛇形

\( 110000000\\ 011000000\\ 001100000\\ 000110000\\ 000011000\\ 000001100\\ 000000110\\ 000000011\\ 000000001\\ \)


还是蛇形,方向变了

\( 000000110\\ 000001100\\ 000011000\\ 000110000\\ 001100000\\ 011000000\\ 110000000\\ 000000000\\ 000000000\\ \)


因此我们只需要判断这些情况即可。

第一种情况可以直接判断是否只有四个黑格并且他们构成正方形,如果初始情况不是4个黑格而是一个子图,那么可以并入情况二和三。

第二种情况可以判断所有的黑格是否全部落在相邻的两个对角线上,如果用 \(a_{ij}\) 表示格子,那么这等价于 \(abs(i - j)\) 的值是否是相邻的两个整数。

情况三和二类似,只是判断的时候将 \(i - j\) 改为 \(i + j\) 即可。

我判断情况二和三的方案是用了两个 \(set\),将每个黑格的 \(i - j\)\(i + j\) 插入集合即可。

时间复杂度 \(O(n^2 \log{n})\)

代码

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

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        set <int> s1, s2;
        vector <pair <int, int>> pos;
        vector <vector <char>> a(n, vector <char> (n));
        for(int i = 0; i < n; i++)
            for(int j = 0; j < n; j++){
                cin >> a[i][j];                
                if(a[i][j] == '#')
                    s1.insert(i + j), 
                    s2.insert(i - j),
                    pos.push_back({i, j});
            }
        bool tag = 0;
        if(s1.size() == 2){
            vector <int> v(s1.begin(), s1.end());
            if(abs(v[0] - v[1]) == 1)
                tag = 1;
        }
        if(s2.size() == 2){
            vector <int> v(s2.begin(), s2.end());
            if(abs(v[0] - v[1]) == 1)
                tag = 1;
        }
        if(s1.size() < 2 || s2.size() < 2)
            tag = 1;
        if(pos.size() == 4){
            sort(pos.begin(), pos.end());
            int x = pos[0].first, y = pos[0].second;
            if(pos[1] == make_pair(x, y + 1) && pos[2] == make_pair(x + 1, y) && pos[3] == make_pair(x + 1, y + 1))
                tag = 1;
        }
        cout << (tag ? "Yes\n" : "No\n");
    }
    return 0;
}

C

题面

你要购买 \(n\) 个物品,每个物品价值 \(a_i\),给定 \(x\) 为忠诚度,每个物品价值不超过 \(x\),定义忠诚度等级为 \(\lfloor \frac{s}{x} \rfloor\),其中 \(s\) 为当前已经购买的物品的价值综合,如果购买某个物品时忠诚度等级升级了,那么当前购买物品的价值就是升级获得的积分,求可获得的最多的积分以及对应的一个方案。

\( 1 \le n \le 10^5 \\ 1 \le x \le 10^9 \\ 1 \le a_i \le x \\ \sum n \le 10^5 \)

解析

最终购买的物品总价值是固定的,因此升级次数 \(k\) 是固定的,最贪心的是这 \(k\) 次升级时购买的物品是价值最多的 \(k\) 个物品。这可以由以下构造完成:

定义 \(sum\) 为当前购买的物品的价值和对 \(x\) 取模后的结果(即 \(sum = s \mod{x}\)),每次选择要买的物品时使用如下策略:如果还未被购买的物品中价值最大的(记其价值为 \(a_{max}\))满足 \(sum + a_{max} \ge x\),那么就购买它,否则购买价值最小的,每次买完后更新 \(sum\)。不难发现如果价值最大的都无法升级时价值小的也无法升级,所以取出的都是价值最大的。

时间复杂度 \(O(n \log{n})\),瓶颈在排序。

代码

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

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while(t--){
        int n, x;
        cin >> n >> x;
        ll sum = 0ll;
        vector <int> a(n + 1, 0);
        for(int i = 1; i <= n; i++){
            cin >> a[i];
            sum += a[i];
        }
        sort(a.begin() + 1, a.end());
        int k = sum / x;
        ll ans = 0ll;
        for(int i = n; i > n - k; i--)
            ans += a[i];
        cout << ans << '\n';
        int l = 1, r = n, num = 0;
        while(l <= r){
            if(num + a[r] >= x){
                num = (num + a[r]) % x;
                cout << a[r] << ' ';
                r--;
            }else{
                num += a[l];
                cout << a[l] << ' ';
                l++;
            }
        }
        cout << '\n';
    }
    return 0;
}

D

题面

给定一个长为n的数组b,问最少移除多少个元素,才能使得不存在 \(i\)\(j\),使得 \(1 \le i \lt j \le n\)\(b_j - b_i = 1\)(不妨把满足这种条件的 \(i\)\(j\) 对成为坏的)。

\( 1 \le n \le 3 \times 10^5 \\ 1 \le b_i \le n \\ \sum{n} \le 3 \times 10^5 \)

解析一

考虑问题的反面,最多可以取多少个元素,我们来进行 \(dp\)

考虑一个 \(pair\) 数组 \(a\)\(a_i = \{ b_i, i \}\),将这个数组排序,第一关键字按增序排列,第二关键字按降序排列,记 \(dp_i\) 表示排列后前 \(i\) 个数最多可以取多少个,那么有状态转移方程如下:

\[dp_i = 1 + \max_{a_j.first + 1 \ne a_i.first \cup a_j.second \gt a_i.second} {dp_j} \]

最后 \(n - \max{dp}\) 就是答案。

那为什么是这样?考虑这样一组样例:
n = 3, a[n] = {2, 1, 3}
如果我们直接用 \(dp_i = 1 + \max_{j \lt i, a_j.first + 1 \ne a_i.first} {dp_j}\) 来转移会有一个问题,在更新 \(3\)\(dp\) 值时因为有 \(1\) 的影响会直接更新为 \(3\),但是 \((2, 3)\) 也是不被允许的,更新错误的原因在于 \(2\)\(3\) 没有紧挨着。所以我们可以按 \(a.first\) 的值的顺序来更新,这个时候对于位于 \(i\) 后面的 \(j\) 但是满足 \(b_j + 1 = b_i\) 这样的数可以用其 \(dp\) 值直接更新 \(dp_i\),于是得到了上面的转移方程。至于为什么第二关键字是倒序,这时因为对于 \(a.first\) 相等的数来说,它的原本的位置越靠前,越容易继承到第一关键字比它小但是第二关键字比它大的数的 \(dp\) 值。

考虑如何维护这个 \(max\),不难发现,处理到 \(i\) 的时候所有已经处理过的数都是第一关键字不大于 \(a_i.first\) 的数,于是分为三类,第一类是 \(a_j.first + 2 \le a_i.first\),这一类用树状数组维护最值(这里相当于将第二类情况延时处理了一轮),第二类是 \(a_j.first + 1 = a_i.first\),这一类比较第二关键字,第三类是 \(a_j.first = a_i.first\),这一类直接继承前一个数的 \(dp + 1\) 值,同时如果在某个 \(j\) 满足第二类时特别处理一下即可。

时间复杂度 \(O(n \log{n})\)

代码

#include <bits/stdc++.h>
using namespace std;
typedef pair <int, int> pii;

template <class T>
struct Fenwick{
    int n;
    vector <T> tr;

    Fenwick(int _n){init(_n);}

    void init(int _n){
        n = _n;
        tr.assign(n + 1, T{});
    }

    int lowbit(int x){return x & -x;}

    void add(int x, T w){
        for(int i = x; i <= n; i += lowbit(i))
            tr[i] = tr[i] + w;
    }

    T query(int x){
        T ans{};
        for(int i = x; i; i -= lowbit(i))
            ans = ans + tr[i];
        return ans;
    }
};

struct i32{
    int x;

    i32 operator+ (const i32 n) const{
        return {max(x, n.x)};
    }
};

void solve(){
    int n;
    cin >> n;
    vector <pii> a(n + 1), d(n + 1, {0, 0});
    vector <int> dp(n + 1, 0);
    for(int i = 1; i <= n; i++){
        int x;
        cin >> x;
        a[i] = {x, i};
    }
    sort(a.begin() + 1, a.end(), [&](pii x, pii y){
        if(x.first == y.first) return x.second > y.second;
        return x.first < y.first;
    });
    for(int i = 1; i <= n; i++){
        if(!d[a[i].first].first) 
            d[a[i].first].first = i;
        d[a[i].first].second = i;
    }
    Fenwick <i32> t(n);
    auto find = [&](int s, int t, int x){
        int ans = -1;
        while(s <= t){
            int mid = s + ((t - s) >> 1);
            if(a[mid].second > x)
                ans = mid, s = mid + 1;
            else 
                t = mid - 1;
        }
        return ans;
    };
    for(int i = 1; i <= n; i++){
        if(d[i].first){
            int m = t.query(n).x + 1;
            for(int j = d[i].first; j <= d[i].second; j++){
                if(d[i - 1].first){
                    int x = find(d[i - 1].first, d[i - 1].second, a[j].second);
                    if(x != -1)
                        dp[a[j].second] = max(m + j - d[i].first, dp[a[x].second] + 1);
                    else
                        dp[a[j].second] = m + j - d[i].first;
                }else
                    dp[a[j].second] = m + j - d[i].first;
                if(j > d[i].first)
                    dp[a[j].second] = max(dp[a[j - 1].second] + 1, dp[a[j].second]);
            }
        }
        if(d[i - 1].first)
            for(int j = d[i - 1].first; j <= d[i - 1].second; j++)
                t.add(a[j].second, {dp[a[j].second]});
    }
    if(d[n].first)
        for(int j = d[n].first; j <= d[n].second; j++)
            t.add(a[j].second, {dp[a[j].second]});
    cout << n - t.query(n).x << '\n';
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    while(t--) solve();
    return 0;
}

解析二

这个做法来自于 \(peltorator\)(cf名),代码链接:https://codeforces.com/contest/2161/submission/346684700

这是本蒟蒻在比赛的评论区发现的大佬的解答orz,感觉很妙,在这里分享给各位佬。

考虑构造一个图,将所有的坏的 \(i\)\(j\) 对之间连一条边,那么这道题变为找这个图的最小点覆盖,注意到这个图是个二分图,因为点 \(i\) 仅和值为 \(b_i + 1\) 的点相连,所以由Kőnig定理,最小点覆盖等于最大边匹配,这个边匹配可以贪心的取:

先找 \(b_i\) 的值最小的 \(i\), 如果有一样小的就取 \(i\) 值最大的,如果它和点 \(j\) 连有边,那么一定有 \(b_j = b_i + 1\),我们在所有满足这个条件的 \(j\) 中选择 \(j\) 值最大的,然后将 \(i\)\(j\) 一同删去,重复这个过程。

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

posted @ 2025-10-31 18:25  x1a0qiya  阅读(96)  评论(2)    收藏  举报