Featured image of post Educational Codeforces Round #188

Educational Codeforces Round #188

这场是不是有点太简单~依旧上大分。

C

题目大意:Alice,Bob和Carol到泉水边取水。Alice每隔 $a$ 天去一次,Bob每隔 $b$ 天去一次,Carol每隔 $c$ 天去一次。

每天,去的人会平分 $6$ 升水,问他们在 $m$ 天内各收集了多少水?

数据范围: $1 \leq a,b,c \leq 10^6, 1 \leq m \leq 10^{17}$

思路:非常典的容斥。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void solve() {
    ll a, b, c;
    ll m;
    cin >> a >> b >> c >> m;
    auto check = [&](ll x, ll y) {
        ll g = __gcd(x, y);
        if (x / g > (m + 1) / y) return m + 1;
        return (x / g) * y;
    };
    ll tem = check(a, b);
    ll tot = check(tem, c);
    ll re = m / tot;
    ll tem2 = check(a, c);
    ll tem3 = check(b, c);
    ll re1 = m / tem - re;
    ll re2 = m / tem2 - re;
    ll re3 = m / tem3 - re;
    ll res1 = m / a - (re1 + re2) - re;
    ll res2 = m / b - (re1 + re3) - re;
    ll res3 = m / c - (re2 + re3) - re;
    ll resa = 1LL * 6 * res1 + 1LL * 3 * (re1 + re2) + 1LL * 2 * re;
    ll resb = 1LL * 6 * res2 + 1LL * 3 * (re1 + re3) + 1LL * 2 * re;
    ll resc = 1LL * 6 * res3 + 1LL * 3 * (re2 + re3) + 1LL * 2 * re;
    cout << resa << ' ' << resb << ' ' << resc << endl;
    return;
}

D

题目大意:给定一个 $n$ 个点和 $m$ 条边的无向图,顶点的编号为 $1$ 到 $n$ ,这张图没有自环和重边。

现在需要为每条边选择一个方向,确定边的方向后,我们称一条形如 $v_1 \rightarrow v_2 \leftarrow v_3 \rightarrow v_4 \ldots$ 的路径为交替路径。

如果原始图中以顶点 $v$ 为起点的所有路径(不一定是简单路径)在生成的有向图中都是交替出现的,那么称顶点 $v$ 为美丽顶点,现在请求出美丽顶点的最大数目。

数据范围: $1 \leq n \leq 2 \cdot 10^5, 0 \leq m \leq 2 \cdot 10^5$

思路:首先非常显然地,看到类似交替路径的字眼,就会想到二分图,然后由于这里可以不是简单路径,因此不难验证这边不存在奇环,也确实是二分图。

然后考虑染色(毕竟这个分段能出的只有染色),对于每个联通分量,首先染色判断它是不是二分图,如果是,则贡献为两种染色数量的最大值,如果不是则贡献为0。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void solve() {
    int n, m, x, y;
    cin >> n >> m;
    vvi ma(n);
    rep(i, 0, m - 1) {
        cin >> x >> y;
        ma[x - 1].push_back(y - 1);
        ma[y - 1].push_back(x - 1);
    }
    vector<int> vis(n);
    int ans = 0, ans2 = 0;
    bool flag = true;
    auto dfs = [&](this auto&& dfs, int x, int c) -> void {
        vis[x] = c;
        if (c == 1)
            ans++;
        else
            ans2++;
        for (int& p : ma[x]) {
            if (!vis[p])
                dfs(p, 3 - c);
            else if (vis[p] == c) {
                flag = false;
            }
        }
        return;
    };
    int res = 0;
    rep(i, 0, n - 1) {
        if (vis[i]) continue;
        ans = 0, ans2 = 0;
        flag = true;
        dfs(i, 1);
        if (flag) res += max(ans, ans2);
    }
    cout << res << endl;
    return;
}

E

题目大意:对于正整数 $x$ ,字符串 $S(x)$ 是这么形成的:

首先把当前 $x$ 连接到字符串末尾。

随后,如果 $x \leq 9$ ,则退出,否则 $x=digit(x)$ ,这里 $digit(x)$ 是 $x$ 在十进制下数位和。

给定一个由数字组成的字符串 $S$ ,请重新排列该字符串中的字符,使其组成某个数字 $x$ 的 $S(x)$ ,题目保证有解。

数据范围: $1 \leq |S| \leq 10^5,1 \leq sum(|S|) \leq 10^5$

思路:一看下去其实有点无从下手的题,其实大多数时候都是在找性质。

首先观察到, $x_1+x_2+\ldots +x_k+x_k=digit(S)$ ,其中每个 $x_i$ 是第 $i$ 个数的数位和。

那么,这里可以考虑到 $digit(S) \leq 9 \cdot 10^5$ ,因此可以暴力枚举 $x_1$ ,有了 $x_1$ 之后,其实就得到了第二个数,然后就可以算出后面所有的数了,只需考虑所有的和是否等于 $digit(S)$ ,以及给出的所有数字是否刚好用完即可。

不过有点史,还是需要好好写一下。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
void solve() {
    string s;
    cin >> s;
    if (sz(s) == 1) {
        cout << s << endl;
        return;
    }
    vi cnt(10);
    rep(i, 0, sz(s) - 1) { cnt[s[i] - '0']++; }
    int ans = 0;
    rep(i, 0, 9) ans += cnt[i] * i;
    int maxx = 0;
    rep(i, 1, ans) {
        int sum = i;
        int tem = i;
        vi temp;
        temp.push_back(i);
        while (tem > 0) {
            if (0 <= tem && tem <= 9) {
                sum += tem;
                temp.push_back(i);
                break;
            }
            int tem2 = 0;
            int tem3 = tem;
            while (tem3 > 0) {
                tem2 += tem3 % 10;
                tem3 /= 10;
            }
            sum += tem2;
            temp.push_back(tem2);
            tem = tem2;
        }
        if (1 <= i && i <= 9) {
            if (sum != ans || !cnt[i]) continue;
        } else {
            if (sum != ans) continue;
        }
        int sum2 = i;
        vi cnt2(10);
        while (sum2 >= 10) {
            int tem2 = sum2;
            int tem3 = 0;
            while (tem2 > 0) {
                tem3 += tem2 % 10;
                cnt2[tem2 % 10]++;
                tem2 /= 10;
            }
            sum2 = tem3;
        }
        cnt2[sum2]++;
        int sum3 = 0;
        bool flag = true;
        frep(i, 9, 0) {
            if (cnt2[i] > cnt[i]) {
                flag = false;
                break;
            }
            sum3 += i * (cnt[i] - cnt2[i]);
        }
        if (sum3 != i || !flag) continue;
        maxx = i;
        break;
    }
    string res = "";
    int maxx2 = maxx;
    vi cnt2(10);
    while (maxx2 >= 10) {
        int tem2 = maxx2;
        int tem3 = 0;
        while (tem2 > 0) {
            tem3 += tem2 % 10;
            cnt2[tem2 % 10]++;
            tem2 /= 10;
        }
        maxx2 = tem3;
    }
    cnt2[maxx2]++;
    frep(i, 9, 0) { rep(j, 0, cnt[i] - cnt2[i] - 1) res.push_back('0' + i); }
    vector<string> tem2;
    tem2.push_back(res);
    while (maxx >= 10) {
        tem2.push_back(to_string(maxx));
        int tem = 0;
        while (maxx > 0) {
            tem += maxx % 10;
            maxx /= 10;
        }
        maxx = tem;
    }
    tem2.push_back(to_string(maxx));
    int tot = 0;
    for (auto& p : tem2) {
        rep(i, 0, sz(p) - 1) { tot += (p[i] == '0'); }
    }
    rep(i, 0, cnt[0] - tot - 1) res.push_back('0');
    rep(i, 1, sz(tem2) - 1) res += tem2[i];
    cout << res << endl;
    return;
}

F

题目大意: 我们称分数 $\frac{x}{y}$ 的一次增长为以下两种情况之一:

要么把分子 $x$ 加一,要么当分母 $y > 1$ 时,将其减一。

定义 $MSF(b,k)$ 为给定一个整数数组 $b_1,b_2,\ldots,b_m$ 和一个整数 $k$ 时,构造数组 $\frac{1}{b_1},\ldots,\frac{1}{b_m}$ ,一共执行 $k$ 次增长操作(每次增长的对象可以不同)后,得到的分数和的最大值。

现在给定两个整数数组 $a_1,\ldots,a_n$ 和 $k_1,\ldots,k_m$ ,请对每个 $k_i$ 计算 $\sum\limits_{l=1}^{n}\sum\limits_{r=l}^{n}MSF(a[l \ldots r],k_i) \pmod{998244353}$ 。

数据范围: $1 \leq n,m \leq 5 \cdot 10^5,1 \leq a_i \leq 10^8,0 \leq k_1 \leq k_2 \leq k_m \leq 10^8$ 。

思路:非常迷的是这些 $k_i$ 为啥要排好序,明明可以用在线的说,神秘出题人。

首先,看到这种双求和,在我的分段第一反应一定是单调栈贡献法,此题在Round 1082的d2C2亦有记载。

总的原始和的求法就不讲了,你都做到这里了,没必要吧。

好的,现在我们来看一下,怎么来考虑贡献。

首先,对于 $\frac{a}{b},b > 1$ 时,单次加分子的贡献为 $\frac{1}{b}$ ,减分母的贡献为 $\frac{a}{b(b-1)}$ ,因此当单次加分子的贡献大于等于单次减分母的贡献时,有 $\frac{1}{b} \leq \frac{a}{b(b-1)}$ ,也就是 $b-1 \leq a$ ,而 $b=1$ 时最优策略也是加分子,因此单步的最大贡献必然是加分子。

考虑整个区间,由于显然无论加分子还是减分母,都在分母最小的元素上做文章是最优的,因此这里单调栈的轮廓就浮现出来了,考虑当前元素作为最小的范围。

随后,当 $k$ 很大的时候,又出现了一种策略,那就是首先把 $a_i$ 减到 $1$ ,然后再每次加分子,而当 $k$ 很小的时候直接加分子就可以。

不过,为什么没有别的策略?例如给分子加一些,分母减一些,这个用数学可以证明,此处不提了。

接着,考虑它们之间的分界点,一直加分子的贡献是 $\frac{k}{b}$ ,而先减到 $1$ 再加分子的贡献为 $k-(b-1)+1-\frac{1}{b}=k-b+2-\frac{1}{b}$ ,计算可以得出,一直加分子在 $k \geq b-1$ 的时候是最优的,也就是 $k > b$ 。

于是,由于这里具有单调性,完全可以对 $a$ 进行排序,然后每次查找对应的位置,再做一下前缀和跟后缀和,这里前缀和需要做的是 $\sum\limits_{j=1}^{i} c_j$ 跟 $\sum\limits_{j=1}^{i} -b+2-\frac{1}{b}$ ,后缀和需要做的是 $\sum\limits_{i=1}^{j}\frac{c_i}{b}$ ,这里的 $c_i$ 指的都是当前元素作为最小值的范围。

最后,注意一下C++需要按步取模,第一次交忘了一个取模就wa19了,有点难受,不过这题作为edu的F,最多就是个d2D的水平,不知道出题人在干什么。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
constexpr int MOD = 998244353;
ll qpow(ll x, int n) {
    ll res = 1;
    for (; n; n >>= 1) {
        if (n % 2) res = res * x % MOD;
        x = x * x % MOD;
    }
    return res;
}
void solve() {
    ll n, m, k;
    cin >> n >> m;
    vector<pii> a(n);
    rep(i, 0, n - 1) {
        cin >> a[i].second;
        a[i].first = 1;
    }
    vector<pll> b(n);
    ll tot = 0;
    vi r(n, n);
    vi l(n, -1);
    stack<int> s;
    for (int i = n - 1; i >= 0; i--) {
        while (!s.empty() && a[s.top()].second > a[i].second) s.pop();
        if (!s.empty()) r[i] = s.top();
        s.push(i);
    }  // 求右边第一个小于的下标
    while (!s.empty()) s.pop();
    for (int i = 0; i <= n - 1; i++) {
        while (!s.empty() && a[s.top()].second >= a[i].second) s.pop();
        if (!s.empty()) l[i] = s.top();
        s.push(i);
    }  // 求左边第一个小于的下标
    rep(i, 0, n - 1) {
        b[i].first = a[i].second;
        b[i].second = 1LL * (r[i] - i) * (i - l[i]);
    }
    rep(i, 0, n - 1) {
        tot += 1LL * (i + 1) * (n - i) % MOD * qpow(a[i].second, MOD - 2) % MOD;
        tot %= MOD;
    }
    ranges::sort(b);
    vl pre1(n);
    vl suf1(n);
    vl pre2(n);
    pre1[0] = b[0].second % MOD;
    rep(i, 1, n - 1) { pre1[i] = (pre1[i - 1] + b[i].second % MOD) % MOD; }
    pre2[0] = b[0].second % MOD * (((2 - b[0].first % MOD - qpow(b[0].first, MOD - 2)) % MOD + MOD) % MOD) % MOD;
    rep(i, 1, n - 1) {
        ll tem = b[i].second % MOD * (((2 - b[i].first % MOD - qpow(b[i].first, MOD - 2)) % MOD + MOD) % MOD) % MOD;
        pre2[i] = (pre2[i - 1] + tem) % MOD;
    }
    suf1[n - 1] = b[n - 1].second % MOD * qpow(b[n - 1].first, MOD - 2) % MOD;
    frep(i, n - 2, 0) {
        ll tem = b[i].second % MOD * qpow(b[i].first, MOD - 2) % MOD;
        suf1[i] = (suf1[i + 1] + tem) % MOD;
    }
    rep(i, 0, m - 1) {
        cin >> k;
        auto x = ranges::upper_bound(b, make_pair(k, LLONG_MAX));
        int tem = x - b.begin();
        ll tem2, tem3;
        if (tem == 0)
            tem2 = 0;
        else
            tem2 = (k * pre1[tem - 1] % MOD + pre2[tem - 1]) % MOD;
        if (tem == n)
            tem3 = 0;
        else
            tem3 = k * suf1[tem] % MOD;
        cout << (tot + tem2 + tem3) % MOD << endl;
    }
    return;
}