Codeforces 题解集合

大概只会放 *2000 以上的题目上去了

[Change Log]

created on 2022.5.6
latest updated on 2022.12.6

目录

Div1

先留着(


Div1 + Div2 & Global Round

Global Round 20

D. Cyclic Rotation ( \(\color{#AAF}{1700}\) )

构造,双指针, multiset,逆向,等价变换

题意

给定长度为 \(n\) 的数组 \(a\),和 \(a\) 的一个排列数组 \(b\),对 \(a\) 进行操作:

  • 如果 \(a_l =a_r\),则 \([a_l,...,a_r] = [a_{l+1},...,a_r,a_l],\; 1\leq l<r\leq n\)

询问能否对 \(a\) 进行任意次操作得到 \(b\)

数据范围
\(1\leq n \leq 2 * 10^5\)
\(1\leq a_i,b_i \leq n\)

思路

  • 对操作进行等价变换,每次对 \(a_l,a_r\) 的操作相当于把 \(a_l\) 删除, 在 \(a_r\) 前拷贝一份 \(a_r\)
  • 尝试逆向,我们看能否从 \(b\) 得到 \(a\)
  • 双指针构造方案:
    • \(i,j\) 指向 \(a,b\) 最后一个元素,维护一个 multiset
    • while (b[j] == b[j - 1]) j--; ,向 multiset 中插入 \(b_j\)
    • 如果 \(a_i = b_j\) , i--,j--
    • 否则,在 multiset 中查找 \(a_i\) ,找到则 i-- 否则无解
  • 因为每次操作,\(a_r\) 的位置其实相对是不变的,然后根据第一点,我们从右往前查 multiset中没有的元素一定不合法。

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/

template<class T>
struct BIT {
    int n;
    vector<T> B;
    BIT(){};
    BIT(int _n) : n(_n), B(_n + 1, 0) {}
    void init(int _n) {
            n = _n;
            B.resize(_n + 1);
    }
    inline int lowbit(int x) { return x & (-x); }
    void add(int x, T v) {
            for (int i = x; i <= n; i += lowbit(i)) B[i] += v;
    }
    T ask(int x) {
            T res = 0;
            for (int i = x; i; i -= lowbit(i)) res += B[i];
            return res;
    }
};

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        vector<int> a(n), b(n);
        for (auto &x : a)
            cin >> x;
        for (auto &x : b)
            cin >> x;
        int j = n - 1, i = n - 1;
        multiset<int> s;
        bool fl = true;
        while (i && j && fl) {
            while (i && j && b[j] == b[j - 1]) {
                s.insert(b[j]);
                j--;
            }
            if (a[i] == b[j]) 
                i--, j--;
            else {
                if (s.find(a[i]) != s.end()) {
                    s.erase(s.find(a[i]));
                    i--;
                }
                else fl = false;
            }
        }
        fl ? cout << "YES\n" : cout << "NO\n";
    }
    return 0;
}

Global Round 21

D. Permutation Graph ( \(\color{#F9F}{1900}\) )

构造,分治,或数据结构贪心

题意

给定长度为 \(n\) 的排列,有 \(n\) 个点,两点 l, r 之间当且仅当下标区间在 \([l,r]\)\(a[l], a[r]\) 为区间的两个最值,可以连边,询问从 \(1-n\) 的最短路。

数据范围
\(1\leq n \leq 2.5*10^5\)

思路

  • 抓住题目性质,先找到区间最大值的位置 \(pos\) ,假设 \(pos\neq1,n\)
  • 横跨 \(pos\) 的区间两点是不可能连边的,所以分治求解 \(dist(1,pos),\; dist(pos, n)\)
  • 对于 \(dist(1,pos)\) ,我们找出 [1,pos] 区间的最小值位置 \(mi\),递归求解 \(dist(1,mi),\; dist(mi, pos)\), 且 \(dist(mi,pos)\) 返回 1。
  • 对于 \(dist(pos,n)\) 求解方式相同,依次递归出口为两端点相同。区间满足条件 l != 1 && r != n 就表明是区间两最值端点应当 return \(1\)
  • 然后需要一点细节,就通过本题,时间复杂度为 \(O(n)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
int n;
const int N = 3e5;
int a[N], premx[N], premin[N], sufmx[N], sufmin[N];

int dist(int l, int r) {
    // cout << l << " " << r << endl;
    if (l == r) return 0;
    if (l == 1) {
        if (a[r] == premin[r]) {
            if (a[l] == premx[r])
                return 1;
            for (int i = r - 1; i; i--)
                if (a[i] == premx[i])
                    return dist(1, i) + dist(i, r);
        }
        else if (a[r] == premx[r]) {
            if (a[l] == premin[r])
                return 1;
            for (int i = r - 1; i; i--)
                if (a[i] == premin[i])
                    return dist(1, i) + dist(i, r);
            exit(0);
            
        }
    }
    else if (r == n) {
        if (a[l] == sufmin[l]) {
            if (a[r] == sufmx[l])
                return 1;
            for (int i = l + 1; i <= n; i++) 
                if (a[i] == sufmx[i])
                    return dist(l, i) + dist(i, n);
        }
        else if (a[l] == sufmx[l]) {
            if (a[r] == sufmin[l])
                return 1;
            for (int i = l + 1; i <= n; i++) {
                if (a[i] == sufmin[i])
                    return dist(l, i) + dist(i, n);
            }
        }
    }
    return 1;
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while (T--) {
        cin >> n;

        for (int i = 1; i <= n; i++){
            cin >> a[i];
            premx[i] = premin[i] = sufmx[i] = sufmin[i] = 0;
        }
        for (int i = 1; i <= n; i++) {
            if (i == 1)
                premx[i] = premin[i] = a[i];
            else {
                premx[i] = max(premx[i - 1], a[i]);
                premin[i] = min(premin[i - 1], a[i]);
            }
        }
        for (int i = n; i; i--) {
            if (i == n)
                sufmx[i] = sufmin[i] = a[i];
            else {
                sufmx[i] = max(sufmx[i + 1], a[i]);
                sufmin[i] = min(sufmin[i + 1], a[i]);
            }
        }
        if (n != 1 && ((premin[n] == a[1] && premx[n] == a[n]) || (premin[n] == a[n] && premx[n] == a[1]))) {
            cout << 1 << endl;
            continue;
        }
        int pos = max_element(a + 1, a + 1 + n) - a;
        if (pos == n || pos == 1) {
            int mi = min_element(a + 1, a + 1 + n) - a;
            if (pos == n) cout << dist(1, mi) + (n != 1) << endl;
            else cout << dist(mi, n) + (n != 1) << endl;
        }
        else 
            cout << dist(1, pos) + dist(pos, n) << endl;
    }
    return 0;
}

Div2

Round #264 div2

D. Bear and Floodlight (\(\color{#FA8}{2200}\))

状压DP,简单计算几何

题意

在二维坐标上,一个人从 \((l,0)\) 走到 \((r,0)\) ,有 \(n\) 盏路灯,每盏灯的照射角度最大为 \(a_i\),坐标为 \((x_i,y_i)\)。询问这个人最多能走多远,到 \(r\) 停止。

数据范围
\(1\leq n\leq 20\)
\(-10^5 \leq l\leq r \leq 10^5\)
\(-1000\leq x_i\leq 1000, 1\leq y_i \leq 1000, 1\leq a_i\leq 90\)

思路

  • 观察数据范围\(n\leq 20\) ,两种想法,选和不选和二进制枚举,自然是二进制枚举。
  • 涉及二进制枚举,一个很自然的想法是状压DP,设计一下状态。
  • 发现对于每盏灯的贡献和当前所在点有关。不妨设状态 \(f[i]\) 表示状态为 \(i\) 时能到达的最远距离。
  • 状态转移:f[i | (1<<j)] = max(f[i|(1<<j)], calc(f[i], j)) ,calc 表示当前在 f[i] 点,第 \(j\) 盏灯能到达的最远距离
  • 计算 calc() 函数
    • 可以用 atan2 求出 \(P(f[i],0)\) 对应的角,用内角和算出 \(B(x_b,0)\) 对应的角,然后用其补角 tan 求出 \(x - x_b\),进而求出 \(x_b\)
    • 也可以用旋转矩阵,先求出向量 \((dx,dy)=(f[i]-x,-y)\) 做逆时针旋转矩阵新的向量 \((dx,dy)=(dx * cos(a) - dy*sin(a),dx*sin(a)+dy*cos(a))\) ,那么 \(x_b = x - y * dx/dy\)
    • 两种情况都需要特判,最远点是无穷大或者其他边界问题。
  • 目标答案为 \(f[(1<<n)-1]\) ,时间复杂度为 \(O(n*2^n)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
const double PI = acos(-1);
const int N = 1 << 20;
int n;
double l, r;
double f[N];

struct P{
    double x, y, a;
};

// double cal(double l, P& p) {
//     auto [x, y, a] = p;
//     double dx = l - x, dy = -y;
//     double nx = dx * cos(a) - dy * sin(a), ny = dx * sin(a) + dy * cos(a);
//     if (fabs(ny) < 1e-12) return r;
//     if (ny > 0) return r;
//     return min(x - y * nx / ny, r);
// }

double cal(double l, P& p) {
    auto [x, y, a] = p;
    double angleA = atan2(y, x - l);
    if (angleA + a >= PI)
        return r;
    double angleB = PI - angleA - a;
    if (angleB == PI / 2) 
        return x;
    return x + y / tan(angleB);
}

int main() {
    cin >> n >> l >> r;
    vector<P> p(n);
    r -= l;
    for (int i = 0; i < n; i++) {
        cin >> p[i].x >> p[i].y >> p[i].a;
        p[i].x -= l;
        p[i].a *= PI / 180;
    }
    for (int i = 0; i < 1 << n; i++) {
        for (int j = 0; j < n; j++) {
            if ((i >> j & 1) == 0) {
                int nxt = i + (1 << j);
                f[nxt] = max(f[nxt], cal(f[i], p[j]));
            }
        }
    }
    double ans = f[(1<<n) - 1];
    cout << fixed << setprecision(9) << min(ans, r) << endl;
    return 0;
}

E. Bear in the Field (\(\color{#FB5}{2300}\))

矩阵快速幂

题意

在大小为 \(n\times n\) 的棋盘上,一个点起始坐标为 \((sx,sy)\) 起始速度是 \((dx,dy)\) ,询问经过 \(t\) 秒后的位置,对于每一秒:

  • 点处于 \((i,j)\),给速度的贡献为 \(k\), 速度将变为 \((dx+k, dy+k)\)
  • 移动到 \((x+dx-1, y + dy-1)\), 这里的 \(dx,dy\) 已经更新
  • 每个点给速度的贡献 \(+1\)

数据范围
\(1\leq n \leq 10^9\)
\(1\leq sx,sy \leq n\)
\(-100\leq dx,dy \leq 100\)
\(0\leq t \leq 10^{18}\)

思路

  • 观察数据范围,\(t\leq 10^{18}\) 很大很大,相当于询问你经过 \(t\) 步之后的位置,显然的矩阵快速幂。
  • 状态矩阵参数:\((dx,dy,x,y,0,1)\) 其中 \(0\) 其实代表当前经过的时间。
  • 转移矩阵,先看如何转移:
    • 因为运算过程中涉及对 \(n\) 取模,先将坐标下标从 \(0\) 开始比较好算。
    • \(dx = dx + (x + y + 2) + t\)
    • \(dy = dy + (x + y + 2) + t\)
    • \(x = x + dx + (x + y + 2) + t\)
    • \(y = y + dy + (x + y + 2) + t\)
    • \(t = t + 1\)
    • \(1 = 1\)
    • 故状态矩阵为

    \[ \begin{bmatrix} 1 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 1 & 0 & 0 \\ 1 & 1 & 2 & 1 & 0 & 0 \\ 1 & 1 & 1 & 2 & 0 & 0 \\ 1 & 1 & 1 & 1 & 1 & 0 \\ 2 & 2 & 2 & 2 & 1 & 1 \\ \end{bmatrix} \]

  • 时间复杂度为 \(O(6^3 * logt)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
ll n, val;
#define _DEBUG

template <typename T, const int N> struct Mat {
       int len;

       Mat() { memset(data, 0, sizeof(data)); len = N; }

       T *operator[](int i) { return data[i]; }

       const T *operator[](int i) const { return data[i]; }

       T add(T a, T b){
               return (a + b + val) % n;
       }

       Mat &operator += (const Mat &o) {
               for (int i = 0; i < len; ++i) 
                       for (int j = 0; j < len; ++j) 
                               data[i][j] = add(data[i][j], o[i][j]);
               return *this;
       }

       Mat operator + (const Mat &o) const {
               return Mat(*this) += o;
       }

       Mat &operator -= (const Mat &o) {
               for (int i = 0; i < len; ++i) 
                       for (int j = 0; j < len; ++j) 
                               data[i][j] = add(data[i][j], -o[i][j]);
               return *this;
       }

       Mat operator-(const Mat &o) const {
               return Mat(*this) -= o;
       }

       Mat operator*(const Mat &o) const {
               static T buffer[N];
               Mat result;
               for (int j = 0; j < len; ++j) {
                       for (int i = 0; i < len; ++i) 
                               buffer[i] = o[i][j];
                       for (int i = 0; i < len; ++i) 
                               for (int k = 0; k < len; ++k) 
                                       result[i][j] = (result[i][j] + (data[i][k] * buffer[k]) + val)% n;
               }
               return result;
       }

       Mat power(ull k) const {
               Mat res;
               for (int i = 0; i < len; ++i) 
                       res[i][i] = T{1};
               Mat a = *this;
               while (k) {
                       if (k & 1ll) 
                               res = res * a;
                       a = a * a;
                       k >>= 1ll;
               }
               return res;
       }
       private:
               T data[N][N];
};

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    ll sx, sy, dx, dy, t;
    cin >> n >> sx >> sy >> dx >> dy >> t;
    sx--, sy--;
    val = 10000 * n;
    Mat<ll, 6> a;
    Mat<ll, 6> b;
    b[0][0] = dx, b[0][1] = dy, b[0][2] = sx, b[0][3] = sy, b[0][4] = 0, b[0][5] = 1;
    a[0][0] = a[0][2] = a[1][1] = a[1][3] = a[2][0] = a[2][1] = a[2][3] = 1;
    a[3][0] = a[3][1] = a[3][2] = a[4][0] = a[4][1] = a[4][2] = a[4][3] = a[4][4] = a[5][4] = a[5][5] = 1;
    a[2][2] = a[3][3] = a[5][0] = a[5][1] = a[5][2] = a[5][3] = 2; 
    #ifdef _DEBUG
    for (int i = 0; i < 6; i++) {
        for (int j = 0; j < 6; j++) {
            cout << a[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
    for (int i = 0; i < 6; i++)
        cout << b[0][i] << " ";
    cout << endl;
    #endif
    a = a.power(t);
    Mat<ll, 6> res;
    b = b * a;
    cout << b[0][2] + 1 << " " << b[0][3] + 1 << endl;
    return 0;
}

Round #564 div2

D.Nauuo and Circle ( \(\color{#F9F}{1900}\) ) (结论、计数)

题意

给了一颗 \(n\) 个节点的树,询问存在多少种合法排列,使得 \(n\) 个点在圆环上排列对应位置上,相连的边不会有相交。

答案模 \(998244353\)

数据范围
\(2\leq n \leq 2 * 10^5\)

思路

  • 核心点:一个结论:一个节点的子树所有点在圆环上连续的一段。
  • 所以一个节点的子节点可以有阶乘级别的排列。
  • 更正式的表达,\(f[u] = ((u != root) + son[u]!) * \Pi_{v\in son[u]}{f[v]}\)
  • 由于整棵树根可以随意设置,但最后效果其实和 \(1\) 放在 \(n\) 个位置上的效果一样, 所以最后答案等于 \(n * f[1]\) , 而实际上把每个点的度数阶乘相乘即可。

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
const int N = 2e5 + 10, mod = 998244353;
ll fact[N], d[N];

// f[u] = ((u != root) + son[u]!) * \PI{f[v]}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n;
    cin >> n;
    for (int i = 1; i < n; i++) {
        int a, b;
        cin >> a >> b;
        d[a] ++, d[b]++;
    }
    fact[0] = 1;
    for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % mod;
    ll ans = n;
    for (int i = 1; i <= n; i++) ans = ans * fact[d[i]] % mod;
    cout << ans << endl;
    return 0;
}

Round #747 div2

C. Make Them Equal ( \(\color{#9F7}{1200}\) )

题意

输入长度为 \(n\) 的字符串 \(s\) 和一个字符 \(c\) ,可以进行一个操作,对于 \(1≤x≤n\) ,可以将不能整除 \(x\)\(i(1≤i≤n)\) ,所在的下标字符替换成 \(c\) ,问最少经过多少次操作可以使 \(s\) 每个字符都是 \(c\)

思路

  • 由整除出发,
    • 对于 \(x=n\) 而言,\(1≤i≤n-1\) 都可以被替换。
    • 对于 \(x=n-1\) 而言,\(i=n\) 可以被替换。
  • 因此最多需要2次操作就可以将 \(s\) 替换成想要的字符串。
  • 对于做 \(0\) 次操作,遍历数组就行。
  • 对于做 \(1\) 次操作,当数组中\(\exists i<n\)\(i\)\(i\) 的倍数下标位置的字符都是 \(c\) 的话,只需要一次操作就可以把 \(s\) 都替换成 \(c\) ,输出 \(i\) 的下标。
  • 对于做 \(2\) 次操作,就是 \(\nexists i<n\)\(i\)\(i\) 的倍数下标位置的字符都是 \(c\) ,输出 \(n-1\)\(n\)

赛中

上述的第一点想到了,但是忽略了做1次操作的做法,也是这道题的核心,由第1点衍生出,\(i\)可以替换不是它倍数的下标位置(其实就是题意)。太蠢了

Solution

#include<iostream>
#include<cstring>
using namespace std;
int main(){
    int T;
    scanf("%d", &T);
    while(T--){
        int n;
        char c;
        string s;
        cin >> n >> c;
        cin >> s;
        bool flag = true;
        for(int i = 0; i < n; i++){
            if(s[i] != c){
                flag = false;
                break;
            }
        }
        if(flag){
            printf("0\n");
            continue;
        }
        bool f = false;
        for(int i = 1; i <= n; i++){
            flag = true;
            for(int j = i; j <= n; j += i){
                if(s[j - 1] != c){
                    flag = false;
                    break;
                }
            }
            if(flag){
                f = true;
                printf("1\n%d\n", i);
                break;
            }
        }
        if(!f){     // 1-n-1不存在能代替所有字符的位置
            printf("2\n%d %d\n", n - 1, n);
        }
    }
    return 0;
}

Round #782 div2

D. Reverse Sort Sum ( \(\color{}{1900}\) )

构造、逆序操作

题意

求一个长度为 \(n\) 的 01 数组 \(A\),对 \(A\) 进行 \(n\) 次操作, 每次对 \([1,n]\) 的区间进行排序结果累加到 \(C\) 数组中,已知 \(C\) 数组求 \(A\) 数组。

数据范围
\(1\leq n \leq 2 * 10^5\)
\(0\leq c_i \leq n\)

思路

  • 观察1:将 \(C\) 数组求和 \(/n\) 可以得到 \(1\) 的个数 \(num\)。由贡献思考
  • 观察2:如果 \(C_n=n/1\) ,那么 \(A_n = 1/0\)
  • 由观察1得到 1 的个数,抓住题目性质,在第 \(n\) 次操作时,是由后缀为 \(num\)\(1\) 的数组累加。\(A_n\) 已经求出,看前 \(n-1\) 位,如果我们把第\(n\) 次的后缀累加给撤销,得到的 \(C[1,n-1]\) 其实是第 \(n-1\) 次操作累加之后的 \(C\) 的前 \(n-1\) 位,这时候可以不用管 \(A_n,C_n\) 。然后同理如果 \(C_{n-1}=n-1/1,\; A_{n-1} = 1/0\) 。就这样往前递推。
  • 但需要注意,如果该位为 \(1\)\(num\) 需要减 1,很好理解,在 \(i\) 之前 \(A_i=1\) 没有在排序范围内自然也不属于后缀。
  • 涉及区间更新操作就懒得 \(O(n)\) 搞来搞去了,直接上差分树状数组,复杂度 \(O(n*logn)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/

template<class T>
struct BIT {
    int n;
    vector<T> B;
    BIT(){};
    BIT(int _n) : n(_n), B(_n + 1, 0) {}
    void init(int _n) {
            n = _n;
            B.resize(_n + 1);
    }
    inline int lowbit(int x) { return x & (-x); }
    void add(int x, T v) {
            for (int i = x; i <= n; i += lowbit(i)) B[i] += v;
    }
    T ask(int x) {
            T res = 0;
            for (int i = x; i; i -= lowbit(i)) res += B[i];
            return res;
    }
};

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        vector<int> c(n + 1), ans(n + 1, 0);
        BIT<int> bt(n);
        ll num = 0;
        for (int i = 1; i <= n; i++) {
            cin >> c[i];
            num += c[i];
            bt.add(i, c[i]), bt.add(i + 1, -c[i]);
        }
        num /= n;
        for (int i = n; i; i--) {
            if (bt.ask(i) >= i) {
                ans[i] = 1;
                bt.add(i - num + 1, -1);
                bt.add(i + 1, 1);
                num--;
            }
        }
        for (int i = 1; i <= n; i++)
            cout << ans[i] << " ";
        cout << endl;
    }
    return 0;
}

Round #788 div2

E. Hemose on the Tree ( \(\color{#FA8}{2200}\) )

构造

题意

给了 \(n\) 个点的树, \(n\)\(2\) 的幂次,边权和点权是 \([1, 2 * n - 1]\) 的排列。询问从任意点为根,从根出发途中经过的的点和路径权值 \(异或\) 最大值的最小值是多少?

数据范围
\(1\leq p\leq 17\)
\(2^p = n\)

思路

  • 首先如果单独选一个大于 \(2^n\) 的点为根,最大值 \(\geq 2^p\) ,所以考虑根权值为 \(2^p\) ,并且途径最大值为 \(2^p\) ,其实就是一个构造
  • 接下来需要 \(n-1\) 对数分别为 \(x, x + 2^p\)
  • 分类讨论构造一下
    • 父节点 \(>= 2^p\),子节点值为 \(x\) ,边为 \(x + 2^p\)
    • 父节点 \(< 2^p\),子节点值为 \(x + 2^p\),边为 \(x\)
  • 这样构造到根的子节点,异或和为0,而由于根大(指大于 \(2^p\)) ,那么子节点小,到了子节点到子节点的边异或值为 \(x\),到了子节点的子节点异或值为 \(2^p\) ,如此循环往复,所有值一定 \(\leq 2^p\)
#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 1 << 18;
int n, p;
vector<PII> edge[N];
int ans_edge[N], ans_node[N], tot;

void dfs(int u, int fa){
	for(auto v: edge[u]){
		if(v.x == fa) continue;
		if(ans_node[u] & n){
			ans_node[v.x] = ++tot;
			ans_edge[v.y] = tot + n;
		}
		else{
			ans_edge[v.y] = ++tot;
			ans_node[v.x] = tot + n; 
		}
		dfs(v.x, u);
	}
}

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		tot = 0;
		cin >> p;
		n = 1 << p;
		for(int i = 1; i < n; i++){
			int a, b;
			cin >> a >> b;
			edge[a].pb({b, i});
			edge[b].pb({a, i});
		}
		ans_node[1] = 1 << p;
		dfs(1, -1);
		cout << 1 << endl;
		for(int i = 1; i <= n; i++)
			cout << ans_node[i] << " ";
		cout << endl;
		for(int i = 1; i <= n - 1; i++)
			cout << ans_edge[i] << " ";
		cout << endl;
		for(int i = 1; i <= n; i++)
			edge[i].clear();
	}
    return 0;
}

Round #789 div2

D. Tokitsukaze and Meeting ( \(\color{#AAF}{1700}\) )

DP + 行列分开计算贡献

题意

\(n*m\) 矩阵左上角里塞 0 和 1, \(m\) 列的元素前往下一行第1列, 依次后移, 询问每一次行和列中有多少个行和列里有 \(1\)

数据范围
\(n*m\leq 10^6\)

思路

  • 行列分开考虑.
  • 对于列, 进入 0 答案不变, 进入 1 如果第一列此时没有元素为1, 列答案++
  • 对于行, 答案显然与 i - m 的行答案有关系, 相当于新加入了一行, 如果这行中有 1, 则行答案++, 具体实现见代码
#include<bits/stdc++.h>
#define pb push_back
#define endl "\n"
using namespace std;

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		int n, m;
		cin >> n >> m;
		string s;
		cin >> s;
		s = " " + s;
		vector<int> dp(n * m + 1, 0), ans(n * m + 1, 0);
		int cnt_col = 0, last = -0x3f3f3f3f;
		for(int i = 1; i <= n * m; i++){
			if(s[i] == '1'){
				bool fl = false;
				for(int j = i - m; j > 0 && !fl; j -= m)
					if(s[j] == '1')
						fl = true;
				if(!fl)
					cnt_col++;
				last = i;
			}
			dp[i] = dp[max(i - m, 0)] + ((i - last < ) ? 1 : 0);
			ans[i] = dp[i] + cnt_col;
		}
		for(int i = 1; i <= n * m; i++)
			cout << ans[i] << " ";
		cout << endl;
	}
    return 0;
}

Round #795 div2

D. Max GEQ Sum (\(\color {#AAF}{1800}\))

题意转换, 单调栈, 前缀和, 最值查询

题意

给了一个长度为 \(n\) 的数组 \(a\), 询问该数组是否满足任意连续子序列 \([i, j]\) 满足:

\[max(a_i, a_{i+1},\cdots , a_j) \geq a_i + a_{i + 1} + \cdots + a_j \]

数据范围
\(1\leq n \leq 2 * 10^5\)
\(-10^9\leq a_i \leq 10^9\)

思路

  • 题意转换: 实际询问从 \(a_i\) 向两边扩展, 在未达到更新最大值的位置 \([l,r]\), 考虑一般情况下, 前缀和 \(pre[r] - pre[l - 1] > a[i]\) 是否成立
  • 以上转换, 可以把问题拆分多个问题空间的不重不漏的子集.
  • 显然需要用单调栈\笛卡尔树 求出左右两边大于 \(a_i\) 的第一个下标分别记为 \([L,R]\)
  • 则第一点的式子转化为是否存在 \(l,r(L<l<i, i< r<R)\) 满足 \(sum[l, i -1] + sum[i + 1, r] + a[i] > a[i]\).
  • \(sum[l, i-1] + sum[i+1, r] > 0\) , 仔细观察发现等式只需要只要其中一个和大于 \(0\) 就能满足了
  • 故问题转化为求是否 \(max{sum[l, i-1]} > 0 \; or\; max{sum[i+1,r]} > 0\)
  • 实际代码形式: querysuf(L+1, i-1) - suf[i] > 0 || querypre(i + 1, R - 1) - pre[i] > 0 , 时间复杂度为 \(O(nlogn)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 2e5 + 10, M = 19;
const ll INF = -2e18;
ll stk[N], L[N], R[N], n, a[N], s[N], suf[N];
ll st1[N][M], st2[N][M];

void init(){
	for(int i = 0; i < M; i++)
		for(int j = 1; j + (1 << i) - 1 <= n; j++)	// 区间长度、右端点-1
			if(!i)
				st1[j][i] = s[j];
			else
				st1[j][i] = max(st1[j][i - 1], st1[j + (1 << (i - 1))][i - 1]);
	for(int i = 0; i < M; i++)
		for(int j = 1; j + (1 << i) - 1 <= n; j++)	// 区间长度、右端点-1
			if(!i)
				st2[j][i] = suf[j];
			else
				st2[j][i] = max(st2[j][i - 1], st2[j + (1 << (i - 1))][i - 1]);
}

ll querypre(int l, int r){
	if(l > r) return INF;
	int len = r - l + 1;
	int k = log(len) / log(2);
	return max(st1[l][k], st1[r - (1 << k) + 1][k]);	// 查询处记得+1
}

ll querysuf(int l, int r){
	if(l > r) return INF;
	int len = r - l + 1;
	int k = log(len) / log(2);
	return max(st2[l][k], st2[r - (1 << k) + 1][k]);	// 查询处记得+1
}

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		cin >> n;
		suf[n + 1] = 0;
		for(int i = 1; i <= n; i++){
			cin >> a[i];
			s[i] = s[i - 1] + a[i];
		}
		for(int i = n; i; i--)
			suf[i] = suf[i + 1] + a[i];
		init();
		int top = 0;
		stk[0] = 0;
		for(int i = 1; i <= n; i++){
			while(top && a[stk[top]] <= a[i]) top--;
			L[i] = stk[top];
			stk[++top] = i;
		}
		top = 0;
		stk[0] = n + 1;
		for(int i = n; i >= 1; i--){
			while(top && a[stk[top]] <= a[i]) top--;
			R[i] = stk[top];
			stk[++top] = i;
		}
		bool fl = true;
		for(int i = 1; i <= n; i++){
			int l = L[i] + 1, r = R[i] - 1;
			if(querysuf(l, i - 1) - suf[i] > 0 || querypre(i + 1, r) - s[i] > 0){
				fl = false;
				break;
			}
		}
		fl ? cout << "YES\n" : cout << "NO\n";
	}
    return 0;
}

Round #801 div2

C. Zero Path ( \(\color{#AAF}{1700}\) )

方格dp、思维

题意

询问在大小为 \((n, m)\) 的方格中,每个格子上值为 \(-1\)\(1\),是否存在一条从 \((1,1)\)\((n, m)\) 的路径,满足路径和为 \(0\)

数据范围
\(1\leq n,m \leq 1000\)

思路

  • 这个题很重视观察性质。
  • 如果 \(n + m\) 为偶数,则最后一定经过 奇数 个格子,此时无解。 那么一定经过偶数个格子。
  • 明显可以通过 dp 预处理出到达每个格子的最大值和最小值。如果 \(dp[n][m]\)\([最小值,最大值]\) 包含 \(0\) 则有解,否则无解,证明:
    • 如果不包含显然无解。证明包含一定有解。
    • 因为经过偶数个格子,最后的和一定是偶数,(奇数-奇数,偶数-偶数)
    • 如果进行一次调整让和变化,一定是 \(2,0,-2\) 。因为本来和就是偶数,所以只要区间包含 0, 一定可以调整到和为 0, 证毕。
  • 时间复杂度 \(O(n*m)\) ,代码上还需要一些细节。一道不错的题。

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
using namespace std;
/*----------------------------------------------------------------------------------------------------*/
const int N = 1010;
int n, m;
int g[N][N], dp[N][N];

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while (T--) {
        cin >> n >> m;
        auto dp = Vector<int>(2, n + 2, m + 2);
        auto g = Vector<int>(n + 1, m + 1);
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                cin >> g[i][j];
        if ((n + m - 1) & 1) {
            cout << "NO\n";
            continue;
        }
        for (int i = 1; i <= m; i++) {
            dp[0][0][i] = 1e9;
            dp[1][0][i] = -1e9;
        }
        for (int i = 1; i <= n; i++) {
            dp[0][i][0] = 1e9;
            dp[1][i][0] = -1e9;
        }
        dp[0][1][1] = dp[1][1][1] = g[1][1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if(i == 1 && j == 1) continue;
                dp[0][i][j] = g[i][j] + min(dp[0][i - 1][j], dp[0][i][j - 1]);
                dp[1][i][j] = g[i][j] + max(dp[1][i - 1][j], dp[1][i][j - 1]);
            }
        }
        bool fl = false;
        // cout << dp[0][n][m] << " " << dp[1][n][m] << endl;
        if (dp[0][n][m] <= 0 && dp[1][n][m] >= 0)
            fl = true;
        fl ? cout << "YES\n" : cout << "NO\n";
    }
    return 0;
}

Round #802 div2

C. Helping the Nature ( \(\color{#AAF}{1700}\) )

差分、构造

题意

给了长度为 \(n\) 数组 \(a\),可以进行三种操作:

  1. 对任意前缀所有元素 \(-1\) .
  2. 对任意后缀所有元素 \(+1\) .
  3. 对全部元素 \(=1\) .

询问最少经过多少次操作可以将所有元素置 \(0\)

数据范围
\(1\leq n \leq 200000\)
\(1\leq a_i \leq 10^9\)

思路

  • 由于是区间操作,不妨用差分来思考
  • 构造差分数组 b[i] = a[i] - a[i - 1], 目标状态 \(b[i],\; i\leq n\) 全为 \(0\)
  • 对于三种操作,等价于对差分数组:
    1. b[1]--, b[i>1]++
    2. b[n + 1]++, b[i<=n]--
    3. b[1]++
  • 所以先将 \(2 \leq i \leq n\) 的所有 b[i] 全部设为 \(0\),最后将 $b[1] $ 置 \(0\),将 \(b[1]\)\(0\) 三种操作都可以,贪心取最划算的即可。
  • 时间复杂度 \(O(n)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define endl "\n"
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;
        vector<ll> a(n + 2, 0), b(n + 2, 0);
        ll ans = 0;
        for (int i = 1; i <= n + 1; i++) {
            if (i <= n)
                cin >> a[i];
            b[i] = a[i] - a[i - 1];
        }
        for (int i = 2; i <= n; i++) {
            ans += abs(b[i]);
            if (b[i] > 0) {
                b[n + 1] += b[i];
                b[i] = 0;
            }
            else {
                b[1] += b[i];
                b[i] = 0;
            }
        }
        cout << ans + max(abs(b[1]), abs(b[n + 1])) << endl;
    }
    return 0;
}

Education Round


Round 42 (Rated for Div. 2)

E. Byteland, Berland and Disputed Cities ( \(\color{#FA8}{2200}\) )

贪心

题意

给了 \(n\,(2\leq n\leq 2*10^5)\) 个点,坐标 \(-10^9 \leq x \leq 10^9\) 。每个点属于 P,B,R 其中一个。

询问在 {P,B} 点集中连接所有点和 {P,R} 点集中连接所有点,两者加起来的花费最小值。

思路

  • 一点点考虑发现是个贪心分类讨论题。
  • 对于没有 P 点存在,答案很显然。
  • 如果两个 P 点中夹杂 B/P。对于这一段连接方式有两种。
    • 相邻两点不管什么颜色直接连接。
    • 先连接这一段的两个端点 P,这样在上一种连接方式下,可以扣除中间跨度最大的相邻点的连线,这样可能会更优。
  • 时间复杂度 \(O(n)\)

Solution

CF962E-评测记录


Round 47 (Rated for Div. 2)

这场vp, 打的很烂, D 题读漏条件, B 题贪错方向

B. Minimum Ternary String ( \(\color{#8DB}{1400}\) )

贪心
题意

给定长度为 \(n(1\leq n\leq 10^5)\) 的串, 只有 \(0,1,2\) 组成, 相邻 \(0\)\(1\) 可以互换, 相邻 \(1\)\(2\) 可以互换, 询问任意互换次数后整个串的最小字典序

思路

  • 主要把握住题目条件, 发现 1 可以与 0 和 2 换, 所以不会有 1 在 2 之后, 第一个 2 之前的 0 在最前面, 之后相对顺序不变

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	string s;
	cin >> s;
	int n = s.size();
	queue<int> q1, q2;
	bool fl = false;
	int pos = 0, cnt1 = 0;
	for(int i = 0; i < n; i++){
		if(s[i] == '2'){
			if(!fl)
				pos = i;
			fl = true;
		}
		else if(s[i] == '1')
			cnt1++;
	}
	if(!fl){
		for(int i = 0; i < n - cnt1; i++)
			cout << 0;
		for(int i = 0; i < cnt1; i++)
			cout << 1;
	}
	else{
		int cnt0 = 0;
		for(int i = 0; i < pos; i++){
			if(s[i] == '0')
				cnt0++;
		}
		for(int i = 0; i < cnt0; i++)
			cout << 0;
		for(int i = 0; i < cnt1; i++)
			cout << 1;
		for(int i = pos; i < n; i++){
			if(s[i] != '1')
				cout << s[i];
		}
	}
    return 0;
}

C. Annoying Present ( \(\color{#AAF}{1700}\) )

贪心---货仓选址模型利用, 注意开 long double

题意

输入 \(n,m\) , 表示长度为 \(n\) 的数组, \(m\) 次操作, 每次操作有 \(x\)\(d\), 每次操作选择一个下标 \(i\), 对数组所有元素加上 \(x + d * |i-j|\), 询问最终操作后数组平均值最大值

数据范围

\(1\leq n, m \leq 10^5\)
\(-10^3\leq x_i,d_i\leq 10^3\)

思路

  • 平均值最大 -> 数组和最大, 负数影响最小, 正数影响最大.
  • \(x\) 对和的贡献固定, 考虑距离的贡献, 由货仓选址模型可知, 距离和最小的选点应在中点, 反之最大的点应在边缘点, 之后便可求解该题
  • 观察数据范围, 要开 long double

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int n, m;
	cin >> n >> m;
	long double sum = 0;
	long double tot1 = 0, tot2 = 0;
	int mid = (n + 1) / 2;
	for(int i = 1; i <= n; i++){
		tot1 += abs(i - mid);
		tot2 += i - 1;
	}
	while(m--){
		long double x, d;
		cin >> x >> d;
		sum += (long double)n * x;
		if(d < 0)
			sum += d * tot1;
		else
			sum += tot2 * d;
	}
	cout << fixed << setprecision(7) << sum / n << endl;
    return 0;
}

D. Relatively Prime Graph ( \(\color{#AAF}{1700}\) )

构造, 边上两点编号互质, 图要求连通

题意

给定一个图, 询问是否能构造出 \(n\) 个点, \(m\) 条边的连通图, 任意边两端点编号互质

数据范围
\(1\leq n,m\leq 100000\)

思路

  • 观察数据范围, 最多只有 \(m, 1000000\) 条边, 对于每一个数与其互质小于它的数字个数为 \(\Phi (x)\), 累加到 \(600\) 左右时, 已经大于了 \(10^5\)
  • 故可以暴力用 gcd() 判断两点互质来连边
  • 记得是连通图, 即 \(m > n - 1\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;

ll gcd(ll a, ll b){
	return b ? gcd(b, a % b) : a;
}

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int n, m;
	scanf("%d%d", &n, &m);
	vector<PII> ans;
	int now = 1, cnt = 0;
	if(m < n - 1){
		printf("Impossible\n");
		return 0;
	}
	while(cnt < m && now <= n){
		if(now == 1){
			for(int i = 2; i <= n && cnt < m; i++){
				ans.pb({now, i});
				cnt++;
			}
			now++;
			continue;
		}

		for(int i = now + 1; i < 2 * now && cnt < m; i++){
			if(gcd(i, now) != 1) continue;
			for(int j = 0; i + j * now <= n && cnt < m; j++){
				int v = i + j * now;
				ans.pb({now, v});
				cnt++;
			}
		}
		now++;
	}
	if(cnt < m)
		printf("Impossible\n");
	else{
		printf("Possible\n");
		for(int i = 0; i < m; i ++)
			printf("%d %d\n", ans[i].x, ans[i].y);
	}
    return 0;
}

E. Intercity Travelling ( \(\color{#F9F}{2000}\) )

组合数, 递推, 期望

题意

你要从 \(0~n\) ,有 \(n\) 个站点 \((1~n)\),每个站点都有 \(1/2\) 的几率有休息点。你连续坐 \(k\) 站时,每两站间的疲劳值为 \(a_1,a_2……a_k\)
如果第 \(k\) 站有休息点,那么你可以在此处休息,然后接下来的站点的疲劳值又从 \(a_1\) 开始。求 \(p(疲劳值的期望)*2^(n-1)\)

数据范围
\(1\leq n,a_i\leq 10^6\)

思路

  • 我们要求的其实是疲劳值之和, 直接算肯定不好搞, 要按贡献来算, 有两种思路, 考虑每两点之间的答案贡献, 考虑每个 \(a_i\) 的贡献, 前者请参考官方题解
  • 计算每个 \(a_i\) 的贡献其实是求每个 \(a_i\) 的出现次数
  • 考虑 \(a_1\) , 由于 \(0\) 点的特殊性, \(0->1\) 一定有一个 \(a_1\), 无论后面怎么走, 有 \(2^{n-1}\) 次, 考虑后面的 \(n\) 个点相邻两点 x 和 y, 一定是 x 休息, 其他点无所谓(但不考虑终点的休息)有 \((n-1)*2^{n-2}\)
  • 同样考虑 \(a_2\), 对于 \(0\) 点特殊考虑, \(0->1->2\), 2 及以后的店怎么搞无所谓, 有 \(2^{n-2}\) 次, 同样和前面一样, x, y, z 三个点, 一定是 x 休息, y 不休息, 其他点无所谓, 有 \((n-2)*2^{n-3}\)
  • 所以对于任意 \(a_i\) 出现次数为 \(2^{n-i} + (n-i)*2^{n-i-1}\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int mod = 998244353, N = 1e6 + 10;
ll mi[N];

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	mi[0] = 1;
	int n;
	cin >> n;
	for(int i = 1; i <= n; i++)
		mi[i] = mi[i - 1] * 2 % mod;
	ll ans = 0;
	for(int i = 1; i <= n; i++){
		ll a;
		cin >> a;
		ans = (ans + a * (mi[n - i] + 1ll * (n - i) * mi[n - i - 1] % mod) % mod) % mod;
	}
	cout << ans << endl;
    return 0;
}

Round 84 (Rated for Div. 2)

D. Infinite Path ( \(\color{#FA8}{2200}\) )

置换环、数论

题意

定义排列乘法 \(c=a\times b\) , \(c[i]=b[a[i]]\) 。自然有排列的幂次 \(p^k = p*p*\cdots *p\)

给定在长度为 \(n\,(1\leq n \leq 2 \times 10^5)\) 的排列 \(p\) 。和每个点的颜色 \(c\) 。询问最小的幂次 \(k\) 能存在一个
\(i\) 使得 \(i,p[i],p[p[i]],p[p[p[i]]],\dots\) 都是同一种颜色。

思路

  • 读完题目很容易联想置换环这个东西,每个点 \(i\)\(p[i]\) 连边,会形成若干个环。
  • 对于每个环的内部,经过环的长度幂次 \(len\) 一定能存在一个 \(i\)
  • 还有更快的方式就是,经过环的因子次 \(L\) 变换以后,会分成 \(L\) 个长度为 \(len / L\) 的小环。如果存在一个小环所有点颜色相同则 \(L\) 合法。
    • 给一个样例 p: 6 5 4 2 1 3 color: 1 1 1 3 4 5 答案为 \(3\)
  • 解法就是找出所有的环,再暴力枚举环长度的所有因子,来检验是否合法,简单分析复杂度是够的。

Solution

评测记录


Round 115 (Rated for Div. 2)

这场是vp的,4题138罚时,performance在 \(1835\)

E. Staircase ( \(\color{#FA8}{2100}\) )

经典放格计数, DP 预处理, 修改只考虑单点对答案的贡献

题意

一张 \(n * m\) 的图,有两种楼梯形,L7 形,即向下向右和向右向下这样无限重复,单点和横着的两个点和竖着的两个点也算,有 \(q\) 个询问,每次将每个点的权值异或 \(1\) ,如果某个点的值为 \(0\) ,那么这个点就不能用,再询问贡献。

数据范围
\(1\leq n \leq 1000\)
\(1\leq m \leq 1000\)
\(1\leq q \leq 10^4\)

思路

  • 先处理所有格子都 free 的所有方案数,用 dp 来做
    • dp[i][j][0] 表示该点作为左下角的方案数、dp[i][j][1] 表示该点作为右下角的方案数
    • 所有状态初始化为 1, 状态转移:dp[i][j][0] += dp[i - 1][j][1], dp[i][j][1] += dp[i][j - 1][0]
    • 以每个格子结尾对答案的贡献就是 dp[i][j][0] + dp[i][j][1] - 1
  • 然后再考虑对于每个询问来处理,每次询问给出 (x,y) 点,这里有一个处理技巧:只考虑这个点对答案的贡献
  • 从该点出发(先假定这个格子 free),往上和往下最多能走的合法格子数,相乘-1就是贡献,当然注意有两种走法

自己想法

不会对每个询问贡献单独算,不太会处理所有格子free的方案数

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 1010;
int dp[N][N][2], sta[N][N];

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int n, m, q;
	cin >> n >> m >> q;
	ll ans = 0;
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){		// 0左下角
			sta[i][j] = 1;
			dp[i][j][0] = dp[i][j][1] = 1;
			dp[i][j][0] += dp[i - 1][j][1];
			dp[i][j][1] += dp[i][j - 1][0];
			ans += dp[i][j][0] + dp[i][j][1] - 1;
		}
	}
	int dx[4] = {0, -1, 1, 0}, dy[4] = {-1, 0, 0, 1};		// 左、上、下、右
	while(q--){
		int x, y;
		cin >> x >> y;
		int sign = 1, term = 0;
		if(sta[x][y]) sign = -1;
		else term = 1, sta[x][y] = 1;
		int up = 0, down = 0, dir = 0;
		ll d = 0;
		int nx = x, ny = y;
		while(sta[nx][ny]){
			up++;
			nx = nx + dx[dir];
			ny = ny + dy[dir];
			dir ^= 1;
		}
		nx = x, ny = y, dir = 2;
		while(sta[nx][ny]){
			down++;
			nx = nx + dx[dir];
			ny = ny + dy[dir];
			dir = 5 - dir;
		}
		d += 1ll * up * down;
		up = down = 0;
		nx = x, ny = y, dir = 1;
		while(sta[nx][ny]){
			up++;
			nx = nx + dx[dir];
			ny = ny + dy[dir];
			dir ^= 1;
		}
		nx = x, ny = y, dir = 3;
		while(sta[nx][ny]){
			down++;
			nx = nx + dx[dir];
			ny = ny + dy[dir];
			dir = 5 - dir;
		}
		d += 1ll * up * down;
		d -= 1;
		sta[x][y] = term;
		ans += d * sign;
		cout << ans << endl;
	}
    return 0;
}

Round 116 (Rated for Div. 2)

D. Red-Blue Matrix ( \(\color{#F87}{2400}\) )

题意

给了一个 \(n*m\) 的矩阵,每个元素有一个值,现在要把矩阵的行染成蓝色或红色(必须两个颜色都有),染完后将矩阵切成左右两个矩阵,切割行为 \(k\)
左边矩阵红色元素都比蓝色元素大,右边矩阵蓝色元素都比红色元素

数据范围
\(2\leq n,m \leq 5\times 10^5\; , n*m \leq 1e6\)
\(a_{ij} \leq 10^6\)

思路

  • 首先无论分割点在哪里,第一列永远要被考虑,如果将行按第一列元素大小排序,那么永远只有前缀行染成蓝色
  • 然后枚举切割点和染色行检验合法性,要预处理顶点出发的子矩阵最值做到O(1)查询

自己想法

不知道第一列元素的重要性,甚至看了题解半天没反应出来怎么预处理子矩阵最值

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
typedef std::pair<int, int> PII;
typedef std::pair<vector<int>, int> PLL;
int n, m;

bool cmp(PLL& a, PLL& b){
    return a.x[1] < b.x[1];
}

int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while(T--){
        cin >> n >> m;
        vector<PLL> a(n + 1, {vector<int>(m + 1, 0), 0});
        for(int i = 1; i <= n; i++){
            a[i].y = i;
            for(int j = 1; j <= m; j++)
                cin >> a[i].x[j];
        }
        sort(a.begin() + 1, a.end(), cmp);
        vector<vector<int>> lmx(n + 2, vector<int>(m + 2, 0));
        vector<vector<int>> rmx(n + 2, vector<int>(m + 2, 0));
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                lmx[i][j] = a[i].x[j];
                lmx[i][j] = max(lmx[i][j], lmx[i - 1][j]);
                lmx[i][j] = max(lmx[i][j], lmx[i][j - 1]);
            }
        }
        for(int i = n; i >= 1; i--){
            for(int j = m; j >= 1; j--){
                rmx[i][j] = a[i].x[j];
                rmx[i][j] = max(rmx[i][j], rmx[i + 1][j]);
                rmx[i][j] = max(rmx[i][j], rmx[i][j + 1]);
            }
        }
        vector<vector<int>> lmin(n + 2, vector<int>(m + 2, 1e9));
        vector<vector<int>> rmin(n + 2, vector<int>(m + 2, 1e9));
        for(int i = 1; i <= n; i++){
            for(int j = m; j >= 1; j--){
                rmin[i][j] = a[i].x[j];
                rmin[i][j] = min(rmin[i][j], rmin[i - 1][j]);
                rmin[i][j] = min(rmin[i][j], rmin[i][j + 1]);
            }
        }
        for(int i = n; i >= 1; i--){
            for(int j = 1; j <= m; j++){
                lmin[i][j] = a[i].x[j];
                lmin[i][j] = min(lmin[i][j], lmin[i + 1][j]);
                lmin[i][j] = min(lmin[i][j], lmin[i][j - 1]);
            }
        }
        PII ans = {0, -1};
        for(int k = 1; k < m; k++){
            for(int blue = 1; blue < n; blue++){
                PII lb = {blue, k}, rb = {blue, k + 1};
                PII lr = {blue + 1, k}, rr = {blue + 1, k + 1};
                if(lmx[lb.x][lb.y] < lmin[lr.x][lr.y] && rmin[rb.x][rb.y] > rmx[rr.x][rr.y]){
                    ans = {blue, k};
                }
            }
        }
        if(!ans.x)
            cout << "NO\n";
        else{
            cout << "YES\n";
            string s(n, 'R');
            for(int i = 1; i <= ans.x; i++)
                s[a[i].y - 1] = 'B';
            cout << s << " " << ans.y << endl;
        }
    }
    return 0;
}

E. Arena ( \(\color{#FA8}{2100}\) )

题意

\(n\) 个人,每人有 \(a_i\) 点生命值 (\(a_i <= k\)) ,每次每人会对其他所有人造成 \(1\) 点伤害。
生命值低于 \(1\) 的会死亡,给出 \(n\)\(k\) ,问有多少种不同初始生命值情况会出现场上无人存活(没有赢家)
输出答案总数 \(\mod 998244353\)

数据范围
\(2\leq n \leq 500\)
\(1\leq x \leq 500\)

思路

  • 观察数据范围,唯一有理做法就是 DP,复杂度大概在 \(O(n^3)\) 左右
  • 首先可以大致确定一个小阶段是进行的轮数,但并不一定用轮数作为一个dp的一个纬度,可能是其他与轮数相关的东西
  • 询问剩余 \(0\) 人,多少种不同初始生命值的方案数,这个集合可以由 剩余 \(0\) 人,遭受了 \(j\) 次攻击的集合组成 (\(0\leq j\leq x\))
  • 状态表示: dp[i][j] 当前轮剩下 \(i\) 个人,已经遭受 \(j\) 次攻击的情况
  • 考虑如何转移,遍历下一轮剩余的人数为 \(k\),则被更新的状态是 dp[k][min(j + i - 1, x)]
  • 状态转移:
    • \(dp[k][min(x, j + i - 1)] = dp[k][min(x, j + i - 1)] + dp[i][j] * C_i^{i - k} * (min(x, j + i - 1) - j) ^ {i-k}\)
    • \(i\) 个人里面选 \(i-k\) 个人死亡,死亡的 \(i-k\) 个人里的生命值组成情况可以是 \([1, min(x, j + i - 1) - j]\) 范围。

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 510, mod = 998244353;
int n, x;
ll dp[N][N];       
// dp[i][j] 当前轮剩下 i 个人,已经遭受 j 次攻击的情况
// 下一轮k个人存活,dp[k][min(x, j + i - 1)] += dp[i][j] * C[i][i - k] * (min(x, j + i - 1) - j) ^ (i-k)
ll c[N][N];        // c[a][b] -> C_a^b

void init(){
    for(int i = 0; i < N; i++)
        for(int j = 0; j <= i; j++)     // b <= a
            if(!j) 
                c[i][j] = 1;
            else
                c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;        // % mod
}

ll qmi(ll a, ll k, int mod){
    ll res = 1;
    while(k){
        if(k & 1)
            res = res * a % mod;
        a = a * a % mod;
        k >>= 1;
    }
    return res;
}

void add(ll& a, ll t){
    a = (a + t) % mod;
}

int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> n >> x;
    init();
    dp[n][0] = 1;
    for(int i = n; i >= 1; i--){
        for(int j = 0; j < x; j++){
            if(!dp[i][j]) continue;
            int nxt = min(x, j + i - 1);
            for(int k = i; k >= 0; k--){
                add(dp[k][nxt], dp[i][j] * c[i][i - k] % mod * qmi(nxt - j, i - k, mod) % mod);
            }
        }
    }
    ll ans = 0;
    for(int i = 0; i <= x; i++)
        add(ans, dp[0][i]);
    cout << ans << endl;
    return 0;
}

Round 117 (Rated for Div. 2)

E. Messages ( \(\color{#F9F}{2000}\) )

题意

现有 \(n\) 名学生,老师希望学生 \(i\) 阅读消息 \(m_i\)
老师可以发布 \(t\) 条消息,但每个学生只会随机选择其中 \(k_i\) 条进行阅读,发布信息数量大于 \(k_i\) 条时,随机选择 \(k_i\) 条信息阅读。
请给出一种发布信息的方案,使得在期望条件下,有尽可能多的学生 \(i\) 阅读了对应的信息 \(m_i\)

数据范围

\(1\leq n\leq 2*10^5\)
\(1\leq k_i \leq 20\)
\(1\leq t \leq 2*10^5\)

思路

  • 从数据范围入手\(k_i\) 最多只有20个,尝试一下暴力,进行分类讨论
  • 单独看每个信件被阅读到的期望贡献,答案就是 \(t\) 个信件期望相加,对于一个学生阅读信件数 \(k_i\) 来说,被期望阅读该信件,则该信件期望增加 \(\frac{min(k_i, t)}{t}\)
  • 在不考虑 \(t\) 的情况下,遍历每个学生,可以获得所有信件的期望,如果 \(t\) 能够确定,那么我们只需要取期望前 \(t\) 大的信件就是最优的。
  • 如果 \(t > 20\),由上可知对于每个信件的期望分母一直变大,由于 \(k_i<20\),所以随着 \(t\) 变大,最后期望变小,所以毫无意义。
  • 现在考虑 \(t \leq 20\),由于随着 \(t\) 变大,尽管看似单个信件的期望在变小,信件数量增加,总期望变化不确定,而由于 \(t\) 最多只有 \(20\),我们就直接暴力枚举 \(t\) 的情况,取最优解即可。
  • 时间复杂度: \(O(20 * 2e5 * log(2e5))\)

Solution

#include<bits/stdc++.h>
#define pb push_back
#define endl "\n"
using namespace std;
const int N = 2e5;
PII pro[N + 10];
bool mp[N + 10];

bool cmp(PII a, PII b){
    return a.x > b.x;
}

int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, cnt = 0;
    cin >> n;
    vector<PII> a(n);
    for(int i = 0; i < n; i++){
        cin >> a[i].x >> a[i].y;
        if(!mp[a[i].x])
            cnt++;
        mp[a[i].x] = true;
    }
    double mx = 0;
    vector<int> ans;
    for(int k = 1; k <= min(20, cnt); k++){
        for(int i = 1; i <= N; i++) 
            pro[i] = {0, i};
        for(int i = 0; i < n; i++)
            pro[a[i].x].x += min(a[i].y, k);
        sort(pro + 1, pro + 1 + N, cmp);
        double res = 0;
        vector<int> temp;
        for(int i = 1; i <= k; i++){
            res += pro[i].x;
            temp.pb(pro[i].y);
        }
        res /= k;
        if(res > mx){
            mx = res;
            ans = temp;
        }
    }
    cout << ans.size() << endl;
    for(auto t: ans)
        cout << t << " ";
    cout << endl;
    return 0;
}

Round 118 (Rated for Div. 2)

D. MEX Sequences ( \(\color{#F9F}{1900}\) )

题意

给定长度 \(n\) 的数组 \(a\),其中 \(0\leq a_i \leq n\) 询问合法子序列的个数,合法子序列的定义,对于这个序列的每一个前缀,最后一个数和前缀的 \(MEX\) 差值的绝对值不大于 \(1\)
答案 \(\mod 998244353\)

数据范围
\(1\leq n \leq 5*10^5\)
\(0\leq a_i \leq n\)

思路

  • 多画几次可以感觉出大概是个 状态机DP
  • 合法序列的状态只有以下几种:
    1. 0 1 1 2 2 3 4 5
    2. 0 2 2 0 0 2 0 2
    3. 由第一种变为第二种 0 1 2 2 4 2 4 2 4
  • DP阶段为从左往右的数组下标
  • 状态表示: f[i][0] 表示 MEX 为 \(i\) 状态为上述第一种的情况, f[i][1] 表示 MEX 为 \(i\) 状态为上述第二种的情况
  • 状态转移:i = a[i_]
    • f[i + 1][0] += f[i + 1][0] + f[i][0], 选或不选 + 递增连接。
    • f[i + 1][1] += f[i + 1][1],在第二种情况选或不选
    • if i > 0, f[i - 1][1] += f[i - 1][1] + f[i - 1][0],第二种情况选或不选 + 将第一种情况变成第二种情况
  • 答案统计 \(\Sigma_{i = 0}^n f[i][0] + f[i][1] \mod 998244353\)

Solution

#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
const int mod = 998244353;

void add(ll &a, ll b){
    a = (a + b) % mod;
}

int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while(T--){
        int n;
        cin >> n;
        vector<vector<ll>> f(n + 2, vector<ll>(2, 0));
        vector<int> a(n, 0);
        for(int i = 0; i < n; i++)
            cin >> a[i];
        f[0][0] = 1;
        for(int i_ = 0; i_ < n; i_++){      
            int i = a[i_];
            add(f[i + 1][0], f[i + 1][0]);
            add(f[i + 1][0], f[i][0]);
            if(i > 0)
                add(f[i - 1][1], f[i - 1][1] + f[i - 1][0]);
            add(f[i + 1][1], f[i + 1][1]);
        }
        ll ans = 0;
        for(int i = 0; i <= n; i++){
            ans = (ans + f[i][0] + f[i][1]) % mod;
        }
        add(ans, mod - 1);
        cout << ans << endl;
    }
    return 0;
}

Round 119 (Rated for Div. 2)

D. Exact Change ( \(\color{#F9F}{2000}\) )

题意

\(3\) 种货币,每个价值 \(1,2,3\) 元,给出 \(n\) 个商品的价值,每个价值为 \(a_i\) ,询问最少的货币数能够准确买下所有商品

思路

  • 此题毒瘤分类讨论,由于最大面值为 \(3\) ,所以 \(3\) 元货币尽可能多,贪心地对最贵商品进行讨论,同时价值相同商品无意义排序去重。
  • 设最大商品价值为 x,对 \(x \mod 3\) 模数讨论。
  • 模数为 \(0\),其他商品如果有模数为 \(1\) 或者 \(2\) ,用一张 3 元 换成 1 + 2,可以凑出所有方案,否则答案为 x / 3
  • 模数为 \(1\),可以选择 x / 3 - 1 张 3 元 + 2 张 2 元或者 x / 3 张 3 元 + 1 张 1 元,这是最少的答案为 x / 3 + 1
    • 对于前者,不能凑出价值 1 元 和 x - 1 元的商品
    • 对于后者,不能凑出模数为 \(2\) 元的商品
    • 所以如果上述两个情况同时成立,ans++
  • 模数为 \(2\),最少的方案是 x / 3 张 3 元 + 1 张 2元 (x / 3 + 1)。如果存在模数为 \(1\) 的商品,将一张 3 元拆成 1 + 2即可,ans++

Solution

#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
const int INF = 1e9;

int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T;
    cin >> T;
    while(T--){
        int n;
        cin >> n;
        vector<int> a(n, 0);
        for(int i = 0; i < n; i++)
            cin >> a[i];
        sort(a.begin(), a.end());
        a.erase(unique(a.begin(), a.end()), a.end());
        n = a.size();
        vector<int> c(3, 0);
        if(n == 1){
            cout << a[0] / 3 + (a[0] % 3 != 0) << endl;
            continue;
        }
        for(int i = 0; i < n - 1; i++)
            c[a[i] % 3] ++;
        int tot = (a[n - 1] + 2) / 3;
        if(a[n - 1] % 3 == 1){
            if(c[2] && (a[0] == 1 || a[n - 2] == a[n - 1] - 1))
                tot ++;
        }
        else if(a[n - 1] % 3 == 2){
            if(c[1])
                tot ++;
        }
        else if(a[n - 1] % 3 == 0){
            if(c[1] || c[2])
                tot++;
        }
        cout << tot << endl;
    }
    return 0;
}

E. Replace the Numbers ( \(\color{#F9F}{1900}\) )

题意

给一个数组初始为空,进行 \(q\) 次操作,一个是向数组末尾插数,一个是将数组中所有的 x 替换乘 y

数据范围
\(1\leq q \leq 5 * 10^5\)
\(1\leq x,y \leq 5 * 10^5\)

思路

  • 模拟一下,发现每次将 \(x\)\(y\) 连边,统计最终值就是看 \(x\) 连向的终点
  • 换种说法,其实就是并查集的操作,而精妙在于如何处理中途插入
  • 正难则反,倒着操作,遇见插入的时候就把 p[x] 放入答案,每次合并等于并查集合并 p[x] = p[y]
  • 最后将答案逆序输出,时间复杂度 \(O(n)\)

Solution

#include<bits/stdc++.h>
#define pb push_back
#define endl "\n"
using namespace std;
const int N = 5e5 + 10;
int p[N];

struct Q{
    int op, x, y;
};

int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int m;
    cin >> m;
    vector<int> ans;
    vector<Q> q(m);
    for(int i = 1; i < N; i++) p[i] = i;
    for(int i = 0; i < m; i++){
        int op, x, y;
        y = 0;
        cin >> op;
        if(op == 1)
            cin >> x;
        else
            cin >> x >> y;
        q[i] = {op, x, y};
    }
    for(int i = m - 1; ~i; i--){
        auto [op, x, y] = q[i];
        if(op == 1)
            ans.pb(p[x]);
        else
            p[x] = p[y];
    }
    reverse(ans.begin(), ans.end());
    for(auto t: ans)
        cout << t << " ";
    cout << endl;
    return 0;
}

Round 120 (Rated for Div. 2)

E. Math Test ( \(\color{#FA8}{2200}\) )

题意

\(n\) 个人做 \(m\) 道题,知道每个人做题的对错情况。每个人有一个预期得分 \(x_i\) 和实际得分 \(r_i\)\(x_i\) 给定,而 \(r_i\) 是该人做对题目的分数之和。定义 “惊喜度” 为 每个人的 \(∣x_i−r_i∣\) 之和。
请你构造出一个每道题的分数方案,使得:

  • 分数是一个 \(1∼m\) 的排列。
  • 在所有的方案中,该方案的 “惊喜度” 最大。

数据范围
\(1\leq n \leq 10\)
\(1\leq m \leq 10^4\)
\(0\leq x_i \leq \frac{m(m+1)}{2}\)

思路

  • 观察数据范围\(n\leq 10\),可以二进制枚举每个人的得分绝对值情况,拆绝对值
  • 答案为 \(\Sigma_{i=1}^n (c[i]*x[i] - c[i] * r[i])\)
  • 前半部分固定,想要答案最大,则后半部分的减数最小,那么对应答对次数小的题目应该获得更高的分数。
  • 枚举不同 c[i] 情况,然后计算每个题目的答对次数(有负数),根据次数排序,从小到大赋值,更新答案即可。
  • 这题时间复杂度比较玄学。按道理就是 \(O(2^n*m + 2^n*m*logm)\),却跑的挺快。

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 5e5 + 10;
int p[N];

struct Q{
    int op, x, y;
};

int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int m;
    cin >> m;
    vector<int> ans;
    vector<Q> q(m);
    for(int i = 1; i < N; i++) p[i] = i;
    for(int i = 0; i < m; i++){
        int op, x, y;
        y = 0;
        cin >> op;
        if(op == 1)
            cin >> x;
        else
            cin >> x >> y;
        q[i] = {op, x, y};
    }
    for(int i = m - 1; ~i; i--){
        auto [op, x, y] = q[i];
        if(op == 1)
            ans.pb(p[x]);
        else
            p[x] = p[y];
    }
    reverse(ans.begin(), ans.end());
    for(auto t: ans)
        cout << t << " ";
    cout << endl;
    return 0;
}

Round 125 (Rated for Div. 2)

E. Star MST ( \(\color{#FA8}{2200}\) )

题意

给了一张无向完全图,点数 \(n\) 和边权值范围 \(k\),求合法图的数量 \(\mod 998244353\),合法图的定义是,与 \(1\) 号点连接的所有边边权和是 \(MST\) 的大小。

数据范围
\(2\leq n \leq 250\)
\(1\leq k \leq 250\)

思路

  • 不难看出,主要核心是 1 号点,并且 1 号点连接的边的边权一定是最小的 \(n-1\) 个,否则不成立和为 \(MST\) ,很容易证明
  • 由数据范围看出,应该是个 DP 题目,然后就开始玄学,可知非中心边的边权在 [中心边边权最大值,k] 范围内。
  • DP阶段:加入图中的点数,方案数来源不同边的权值分配不同
  • 状态表示: 根据最终答案是,\(n\) 个点,边权最大值为 \(k\) 的方案,状态定义为 dp[i][j], 选了 \(i\) 个点,最大值为 \(j\) 方案数
  • 考虑如何转移,每加入一个新点,会产生 i - 1 条边,加入 i-z 个新点,其中新点定义不同对答案有贡献,贡献为 \(C_{i-1}^{z-1}\) , 会产生 (z-1) * (i-z) + (i-z) * (i-z-1) / 2 条新边。
  • 第二维更新,从小到大遍历,jj-1 转移,这样新产生的边不管是中心边还是非中心边,范围都在 \([j, k]\) 之间
  • 状态转移: \(dp[i][j] = dp[i][j] + dp[z][j - 1] * C_{i-1}^{z-1} * (k-j+1)^{(z-1) * (i-z) + (i-z) * (i -z + 1) / 2}\),答案为 dp[n][k]
  • 时间复杂度:\(O(n^2k)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 300, mod = 998244353;
ll dp[N][N];		// dp[i][j], 选了i个点,最大值为 j 方案数
int n, k;
// dp[i][j] = dp[z][j - 1] * c[i - 1][z - 1] * (k - j + 1)^((z - 1) * (i - z) + (i - z) * (i - z - 1) / 2)
// 1 <= z <= i,由dp[z][j - 1] 转移,需要乘上 i-1个点里选z-1个点作为已选择点,新加入的边数 i-z个点和z-1个点连,i-z个点互相连接,每条边的范围在[j,k]

int c[N][N];        

void init(){ 		// 递推预处理组合数
    for(int i = 0; i < N; i++)
        for(int j = 0; j <= i; j++)     // b <= a
            if(!j) 
                c[i][j] = 1;
            else
               c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;        // % mod
}

// 快速幂计算a^k
ll qmi(ll a, ll k, int mod){
	ll res = 1;
	while(k){
		if(k & 1)
			res = res * a % mod;
		a = a * a % mod;
		k >>= 1;
	}
	return res;
}

void add(ll &a, ll b){
	a = (a + b) % mod;
}

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	dp[1][0] = 1;
	cin >> n >> k;
	init();
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= k; j++){
			for(int z = 1; z <= i; z++){
				// 按照推导的公式写就行
				add(dp[i][j], dp[z][j - 1] * c[i - 1][z - 1] % mod * qmi(k - j + 1, (z - 1) * (i - z) + (i - z) * (i - z - 1) / 2, mod) % mod);	
			}
		}
	}
	cout << dp[n][k] << endl;
    return 0;
}

Round 127 (Rated for Div. 2)

E. Preorder ( \(\color{#FA8}{2100}\) )

题意

给一颗节点数为 \(2^n - 1\) 满二叉树,点权为小写字母,根为 \(1\), 可以交换每个节点的两颗子树,问最后对整个树来说会有多少种不同的先序遍历序列(字符串)。

思路

  • 对于叶子节点,只有一种方案
  • 对于非叶子节点,如果两个子树构成不同,该节点数方案为 2 * l * r,否则为 l * r
  • 那么如何判断子树构成是否相同?
  • 将所有子树调整为字典序最大或最小的情况,如 if(s[l] < s[r]) swap(s[l], s[r]), s[u] = " " + s[u] + s[l] + s[r]
  • 然后做一次 DFS 即可求解

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
#define ls u << 1
#define rs u << 1 | 1 
using namespace std;
const int N = (1 << 18) + 10, mod = 998244353;
string s[N];
int n;
int dfs(int u){
	if(u << 1 >= (1 << n)) return 1;
	ll res = 0;
	int l = dfs(ls), r = dfs(rs);
	if(s[ls] < s[rs])
		swap(s[ls], s[rs]);
	if(s[ls] != s[rs])
		res = 1ll * l * r * 2 % mod;
	else
		res = 1ll * l * r % mod;
	s[u] = " " + s[u] + s[ls] + s[rs];
	return res;
}

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n;
	string str;
	cin >> str;
	for(int i = 1; i < 1 << n; i++){
		s[i] = str[i - 1];
	}
	ll res = dfs(1);
	cout << res << endl;
    return 0;
}

Div3/4

Round #748 div3

D2. Half of Same ( \(\color{#F9F}{1900}\) )

题意

D1的加强版,赛中过了,这里就不再单独将D1列出来。D1题意是输入一个数组 \(a(-1e6≤a_i≤1e6),\)n$ 个元素 \((4≤n≤40,n为偶数)\) 求出一个最大的整数 \(k\),对每个元素减去若干个 \(k\) 之后,\(a\)中所有元素相同。

此题(D2)的题意与D1大致相同,但求一个最大的 \(k\),使得数组中至少一半的元素能够相同

思路

  • 两道题答案所包含的元素组成的子数组中,设最小值为 \(v\),不难发现,其他元素与\(v\)的差值(设为\(d\) )一定都对\(\%k\) 同余。(应该这么表示吧?
  • 由上一点可知,\(k\) 一定是所有 \(d\) 的最大公约数
  • D1可以求出最小元素 \(v\),再求其他元素相应的 \(d\),D2需要观察数据范围,采用暴力枚举所有元素为 \(v\),再筛选出其他满足条件的 \(d≥0\),存取所有 \(d\) 的约数,找出大于等于 \(\frac{n}{2}\) 的最大的那个。

自己想法

所有 \(d\%k\) 同余想到了,还是没深入思考到 \(k\) 一定是所有 \(d\) 的最大公约数。

Solution

#include<iostream>
#include<set>
#include<vector>
#include<algorithm>
#include<map>
using namespace std;
int main(){
    int T;
    scanf("%d", &T);
    while(T--){
        int n;
        scanf("%d", &n);
        int a[45];
        for(int i = 1; i <= n; i++)
            scanf("%d", &a[i]);
        int ans = -1;
        for(int i = 1; i <= n; i++){
            int v = a[i];
            vector<int> nums;
            int same = 0;
            for(int j = 1; j <= n; j++){
                int d = a[j] - v;
                if(d == 0) same++;
                if(d > 0) nums.push_back(d);
            }
            if(same >= n / 2){
                ans = INT_MAX;
                continue;
            }
            map<int, int> s;
            for(int j = 0; j < nums.size(); j++){
                int & d = nums[j];
                for(int g = 1; g <= d / g; g++){
                    if(d % g == 0){
                        s[g]++;
                        if(d / g != g)
                        s[d / g]++;
                    }
                }
            }
            for(auto t: s){
                if(t.second >= n / 2 - same){
                    ans = max(ans, t.first);
                }
            }
        }
        if(ans == INT_MAX)
            ans = -1;
        printf("%d\n", ans);
    }
    return 0;
}

Round #780 div3

F2. Promising String ( \(\color{#FA8}{2100}\) )

题意

给了长度为 \(n\) 的字符串,由 '+' 和 '-' 组成,可以将字符串中两个 '+' 换成 1 个 '-',询问合法子串个数,合法子串定义,子串内 '+' 数量和 '-' 数量相同。

数据范围
\(1\leq n\leq 2*10^5\)

思路

  • F1 \(n\) 只有 3000,是一个暴力,F2考虑优化。
  • 我们只关心区间中 '-' 个数与 '+' 个数的差值,自然 '+' 个数比 '-' 小,多模拟一下发现一个简单结论:区间中两数差值 \(\mod 3 =0\) 时,区间合法。
  • 因为是一个区间计数问题,我们可以考虑前缀和思想来做,如果两个前缀模 \(3\) 同余,那么两个前缀间的区间便合法。
  • 对于这样的问题,由于需要统计 3 个模数的情况,并且不好用单一前缀和数组来统计小于 pre[i] 的所有合法前缀和,考虑用树状数组来处理
  • 所以开 3 个树状数组,tr[i] 表示模数为 \(i\) 的前缀和的树状数组,这样统计答案就很方便了。
  • 由于做减法会有负数存在,树状数组只能存正数。所以需要值域整体右移,然后需要一点细节,具体看代码注释。
  • 时间复杂度 \(O(nlogn)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
// 结论:区间内 +号个数 减去 -号个数 % 3 == 0,区间合法,差一定为正数。 
struct BIT {
	int n;
	vector<int> B;
	BIT(){};
	BIT(int _n) : n(_n), B(_n + 1) {}
	void init(int _n){
		n = _n;
		B.resize(_n + 1);
	}
	inline int lowbit(int x) { return x & (-x); }
	void add(int x) {
		for(int i = x; i <= n; i += lowbit(i)) B[i] += 1;
	}
	int ask(int x) {
		int res = 0;
		for(int i = x; i; i -= lowbit(i)) res += B[i];
		return res;
	}
};

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		int n;
		cin >> n;
		string s;
		cin >> s;
		s = " " + s;
		vector<int> pre(n + 1, 0);
		vector<BIT> tr(3, BIT(2 * n + 2));	// 需要加 n + 1, 单单 2 * n 不太够
		for(int i = 1; i <= n; i++)
			pre[i] = pre[i - 1] + (s[i] == '-' ? 1 : -1);
		for(int i = 1; i <= n; i++)
			pre[i] += n + 1;		// 可能为-n,加上n+1才能是正数
		tr[(n + 1) % 3].add(n + 1);	// n+1成了"零"的位置
		ll ans = 0;
		for(int i = 1; i <= n; i++){
			int c = pre[i] % 3;
			ans += tr[c].ask(pre[i]);
			tr[c].add(pre[i]);
		}
		cout << ans << endl;
	}
    return 0;
}

Round #787 div3

F. Vlad and Unfinished Business ( \(\color{#AAF}{1800}\) )

题意

给了点数为 \(n\) 的树,和 \(k\) 个必须到达的点,出发的起点 \(x\) 和 到达终点 \(y\),边权为 \(1\) ,询问从起点经过指定的
\(k\) 个点,最后到达 \(y\) 的最短路径花费, \(k\) 个点的访问顺序可以任意。

数据范围
\(1\leq k \leq n \leq 2 * 10^5\)

思路

  • 贪心地想,对于所有任务点必须到,然后要返回,那么起点到这些点的路径和至少为2倍到达这些点的花费。
  • 问题转化为,经过 \(k\) 个点的"最小生成树",当然这里不需要最小生成树算法,只需要知道我们不需要经过的点数即可,剩余点数为 \(x\),路径长度为 \(x-1\)
  • 那么现在访问了所有点,要更贪心的想,从 \(x->y\) 我们是不是只用走一次就行了,先把 \(y\) 以外的点搜完,最后来搜 \(y\) 的子树即可,那么 \(path:x->y\) 只需要经过一次。
  • 所以最后答案为 \(2*(必经路径上点数-1) - x->y的深度(距离)\) ,用 dfs 求子树标记的方式来解,详细见代码,时间复杂度 \(O(n)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 2e5 + 10;
int n, k, x, y, ans;
vector<int> edge[N];		// 邻接表存图
int dep[N];
bool st[N];

int dfs(int u, int p){
	int tot = 0;
	for(auto v: edge[u]){
		if(v == p) continue;
		dep[v] = dep[u] + 1;	// 更新深度
		tot += dfs(v, u);
	}
	if(!tot && !st[u]) ans--;	// 不是必经点
	return tot + (st[u] == true);		// 传递标记
}

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		cin >> n >> k;
		cin >> x >> y;
		ans = n;
		vector<int> a(k + 1, 0);
		for(int i = 1; i <= k; i++){
			cin >> a[i];
			st[a[i]] = true;		// 必须经过的点
		}
		st[x] = true, st[y] = true;
		for(int i = 1; i < n; i++){
			int u, v;
			cin >> u >> v;
			edge[u].pb(v);
			edge[v].pb(u);
		}
		dfs(x, -1);
		cout << 2 * (ans - 1) - dep[y] << endl;		// 算答案
		for(int i = 1; i <= n; i++){
			edge[i].clear();
			st[i] = false;
			dep[i] = 0;
		}
	}
    return 0;
}

G. Sorting Pancakes ( \(\color{#FB5}{2300}\) )

参考题解: Ximena, 严格鸽

题意
给了一个长度 \(n\) 的数组 \(a\) ,和保证为 \(m\), 每次只能将 \(a_i\) 减 1, \(a_{i-1}\; or\; a_{i+1}\) 加 1,询问最小的操作次数使 \(a\) 非严格递减。

数据范围
\(1\leq n,m \leq 250\)

思路

  • 毫无疑问,一道dp题,根据数据范围,大概可以容忍 \(O(n^3)\) 级别的时空复杂度。
  • 在设计状态前,务必需要一个结论
    • 对于这样的相邻移动元素的操作而言,设操作后的结果为 \(b\) 数组,总操作次数等于 \(\Sigma_{i=1}^n(|S_b[i] - S_a[i]|)\) ,对应前缀和差的绝对值的和
  • 首先dp的阶段是长度 \(i\),由于数组和固定,考虑加入一维当前数组的和 \(j\) ,由于要满足递增或者递减,加入一维记录最后一个数 \(k\) ,由于需要对比大小,这个数其实是一个最值。
  • 那么状态已经设计出来了: dp[i][j][k] 表示数组前 \(i\) 位,和为 \(j\) ,最后一位最值为 \(k\) 的最小操作次数。
  • 考虑如何转移方便,如果按照题意原本描述来看,转移方程是遍历前一个数 \(x,k\leq x\) dp[i][j][k] = min(dp[i][j][k], dp[i - 1][j - k][x] + abs(j - s[i])),观察发现可以使用最小后缀来优化 \(x\) 的遍历,此时预处理最小后缀复杂度为 \(O(n^3)\)
  • 而考虑反着做,即将数组翻转后,考虑将数组构造成非严格递增。那么 \(k\) 代表的是最后一位的最大值。从小到大枚举\(k\) 时,需要的是最小前缀,可以很方便的记录, 如下代码所示
for(int i = 1; i <= n; i++){
    for(int j = 0; j <= m; j++){
        int mi = 0x3f3f3f3f;
        for(int k = 0; k <= m - j; k++) {
            mi = min(mi, dp[i - 1][j][k]);      // 转移 k 时,mi 记录了上一位数值为 [0, k] 的最小值,因此是合法的
            f[i][j + k][k] = min(f[i][j + k][k], mi + abs(s[i] - abs(j + k)));
        }
    }
}
  • 时间复杂度:将 \(n\)\(m\) 同级,\(O(n^3)\) ,翻转数组的方法还没有想到如何优化成 \(O(n^2logn)\)
    • 如果尝试采用记录后缀最小来优化,发现对于下标 \(i\) 上每个数不能超过 m / i ,否则它一定比前面的某个数大(平均值),加入循环条件中可以做到 \(O(n^2logn)\)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 260;
int n, m;
int a[N], s[N];
int dp[N][N][N];		
// 将数组翻转
// dp[i][j][k] 前i个数,和为j,最后一个数最大值为k 满足递增序列最小操作
int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = n; i >= 1; i--)
		cin >> a[i];
	for(int i = 1; i <= n; i++)
		s[i] = s[i - 1] + a[i];
	memset(dp, 0x3f, sizeof dp);
	dp[0][0][0] = 0;        // 前0个数,和为0,数最大值为0,操作次数为0
	for(int i = 1; i <= n; i++){
		for(int j = 0; j <= m; j++){
			int mi = 0x3f3f3f3f;
			for(int k = 0; k <= m - j; k++){
				mi = min(dp[i - 1][j][k], mi);
				dp[i][j + k][k] = min(dp[i][j + k][k], mi + abs((j + k) - s[i]));
			}
		}
	}
	int ans = 0x3f3f3f3f;
	for(int i = 0; i <= m; i++)
		ans = min(ans, dp[n][m][i]);
	cout << ans << endl;
    return 0;
}

Round #797 div3

D. Black and White Stripe ( \(\color{#CCC}{1000}\) )

固定长度最大子段和, 简单只贴代码了

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		int n, k;
		cin >> n >> k;
		vector<int> pre(n + 1, 0);
		string s;
		cin >> s;
		s = " " + s;
		for(int i = 1; i <= n; i++){
			pre[i] = pre[i - 1] + (s[i] == 'B');
		}
		int ans = n;
		for(int i = k; i <= n; i++){
			ans = min(ans, k - (pre[i] - pre[i - k]));
		}
		cout << ans << endl;
	}
    return 0;
}

E. Price Maximization ( \(\color{#8DB}{1500}\) )

双指针, 贪心, 排序, 或者 STL

题意

给了 \(n\) 个货品, \(n\) 是偶数, 每个货品有价值 \(a_i\) , 将货品两两配对, 两两配对和总价值为 \(w_i\), 给定 \(k\), 求 \(\Sigma \lfloor \frac{w_i}{k} \rfloor\) 最大值

数据范围
\(2\leq n \leq 2*10^5\)
\(1\leq k \leq 1000\)
\(0\leq a_i \leq 10^9\)

思路

  • 观察数据范围 , \(k\leq 1000\).
  • 由于答案为重量之和向下取整, 对 \(a_i\) 来说, 无论怎样组合, \(a_i / k\) 的贡献是确定的.
  • 这样我们只用关心 \(a_i \% k\) 的贡献, 贪心 地来想, 对于 \(x=a_i\% k\) , 需要找到满足最小余数 \(\geq k-x\)
  • 可以用两种方式实现
    • 一种是显然的双指针 (呵呵我写挂了, 很离谱, 有时间来补这个解法)
    • 还有一种是 jiangly超人 的 STL 做法(复杂度稍高但聊胜于无)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		int n, k;
		cin >> n >> k;
		ll ans = 0;
		multiset<int> s;
		for(int i = 0; i < n; i++){
			int x;
			cin >> x;
			ans += x / k;
			s.insert(x % k);
		}
		while(!s.empty()){
			auto x = *s.begin();
			s.erase(s.begin());
			auto it = s.lower_bound(k - x);
			if(it != s.end()){
				s.erase(it);
				ans++;
			}
		}
		cout << ans << endl;
	}
    return 0;
}

F. Shifting String ( \(\color{#AAF}{1700}\) )

置换, 环, 字符串最小循环节, LCM

题意

给定一个长度为 \(n\) 的字符串 \(s\), 和一个排列 \(P\) , 每次操作让 \(s_i = s_{P_i}\) , 询问至少经过多少次操作能让字符串与初始状态相同

数据范围
\(1\leq n \leq 200\)
\(1\leq p_i \leq n\)

思路

  • 草稿纸上画几遍, 发现可能与置换中所形成的各个环的长度有关系, 这个关系是各长度的 \(LCM\)
  • 答案其实是与每个环经过多少次变换能变成原来的字符串, 由于是字符串关系, 这个多少次变换, 其实就是每个环上对应字符串的最小循环节.
  • 所以答案就是各个环最小循环节长度的 \(LCM\)
  • 如何求最小循环节? 此题可以暴力枚举循环节长度判断是否合法, 数据规模较大可以使用 KMP 求字符串最小循环节, 得到 \(ne\) 数组判断 if(len % (len - ne[len]) == 0)

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
const int N = 210;
ll gcd(ll a, ll b){
	return b ? gcd(b, a % b) : a;
}

ll LCM(ll a, ll b){
	return a / gcd(a, b) * b;
}

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		int n;
		string s;
		cin >> n >> s;
		s = " " + s;
		vector<bool> st(n + 1, false);
		vector<int> p(n + 1,0);
		for(int i = 1; i <= n; i++)
			cin >> p[i];
		ll ans = 1;
		for(int i = 1; i <= n; i++){
			if(st[i]) continue;
			string tmp = " ";
			int sz = 0;
			for(int j = i; !st[j]; j = p[j]){
				st[j] = true;
				tmp += s[j];
				sz++;
			}
			vector<int> ne(sz + 1, 0);
			ne[1] = 0;
			for(int i = 2, j = 0; i <= sz; i++){
				while(j && tmp[j + 1] != tmp[i]) j = ne[j];
				if(tmp[i] == tmp[j + 1]) j++;
				ne[i] = j;
			}
			ll len = sz - ne[sz];
			if(sz % len == 0)
				ans = LCM(ans, len);
			else
				ans = LCM(ans, sz);
		}
		cout << ans << endl;
	}
    return 0;
}

G. Count the Trains ( \(\color{#F9F}{2000}\) )

STL, 二分, 思维

题意

给了 \(n\) 个点, 每个点有值为 \(a_i\) , 从左往右形成序列, 对于序列上下标 \(i\) 位置, 值为 \(min\{a_j\}, (j\leq i)\)

\(m\) 次操作, 输入 \(k, d\) , 每次操作对 a[k] -= d , 询问每次操作完之后, 序列中有多少个不同的数 ?

思路

  • 容易挖掘题目性质, 对于一个数 \(a_i\) , 如果 \(\exist j < i, a_j\leq a_i\) , 那么 \(a_i\) 不会对答案产生贡献
  • 由于答案是求不同的数个数 , 我们可以联想到 map, set 这一类 STL 容器来解决, 那么问题是如何实现.
  • 参照第一点, 我们尝试用 map<int,int> 容器来实现, 第一关键字为下标, 第二关键字为对应值
  • 每次输入或操作对 map 中插入 i, a[i], 若前面迭代器元素值小于等于 \(a_i\) 则删除现在插入的迭代器, 若没有删除, 检查后面的迭代器是否满足要求(next(it)->second <= it->second)
  • 实现过程中注意代码细节, 防止越界等错误, 时间复杂度为 \(O(n+m)log(n+m))\) , 参考 yyds的jiangly

Solution

#include<bits/stdc++.h>
typedef long long ll;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
typedef unsigned long long ull;
typedef double db;
#define arr(x) (x).begin(),(x).end()
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
using namespace std;
map<int, int> mp;

void add(int i, int x){
	mp[i] = x;
	auto it = mp.find(i);
	if(it != mp.begin() && prev(it)->second <= it->second){
		mp.erase(it);
		return ;
	}
	while(next(it) != mp.end() && next(it)->second >= it->second)
		mp.erase(next(it));
}

int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int T;
	cin >> T;
	while(T--){
		mp.clear();
		int n, m;
		cin >> n >> m;
		vector<int> a(n + 1, 0);
		for(int i = 1; i <= n; i++){
			cin >> a[i];
			add(i, a[i]);
		}
		while(m--){
			int k, d;
			cin >> k >> d;
			a[k] -= d;
			add(k, a[k]);
			cout << mp.size() << " ";
		}
		cout << endl;
	}
    return 0;
}

野场

3000-3500 #932 9e 35 25
2600-2900 #E54 ed 55 46
2400-2500 #F87 ee 75 6e
2300 #FB5 f5 be 5e
2100-2200 #FA8 f7 cd 8a
1900-2000 #F9F f3 90 ff
1600-1800 #AAF ab a9 ff
1400-1500 #8DB 8d d9 ba
1200-1300 #9F7 95 fc 77
800-1200 #CCC cb cb cb

posted @ 2022-05-06 16:03  Roshin  阅读(526)  评论(0编辑  收藏  举报
-->