2022 同济大学校赛 题解

A题 盒饭盲盒(签到)

食堂有 \(n\) 种菜,其中 \(a\) 种是素菜,\(n-a\) 种是荤菜。

现在我们去食堂打三份饭,每份饭都会是这 \(n\) 种菜中的一种(不过如果三份菜都是素的话就会重新打),问三份菜都是荤菜的概率有多大?

\(T\leq 1000,1\leq a<n\leq 10^6\)

古典概型,\(p=\dfrac{(n-a)^3}{n^3-a^3}\)。(这个不是很严谨,但是确实是正确的,忘了这个在概率论里面是啥东西了)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL gcd(LL a, LL b) { return !b ? a : gcd(b, a % b); }
LL f(LL x) { return x * x * x; }
void solve() {
    LL n, a;
    cin >> n >> a;
    LL A = f(n - a), B = f(n) - f(a);
    printf("%lld/%lld\n", A / gcd(A, B), B / gcd(A, B));
}
int main()
{
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}

C题 攻城 (数学)

给定 \(n\) 个堡垒,第 \(i\) 个堡垒的血量为 \(a_i\)

现在我们可以不停的进行攻击,每次普通攻击可以选定一个堡垒,对其造成一点伤害。特别的,当攻击次数为 7 的倍数时,这次普通攻击会转化为特殊攻击,他将对所有堡垒(而非某个选定的堡垒)造成一点伤害。

现在,我们想要在某次攻击中一下子摧毁全部堡垒(在这次攻击前,所有堡垒必须仍然存活),问是否可行?

\(n\leq 10^6,1\leq h_i\leq 10^9\)

我们记 \(x\)\(\{a_n\}\) 中的最小值,\(s=\sum\limits_{i=1}^na_i\)

\(n=1\) 时,怼着这唯一一个打就行了。

\(n>1\) 时,意味着我们必须在第 \(7k\) 次消灭所有堡垒,意味着我们要进行 \(6k\) 次普通攻击和 \(k\) 次特殊攻击,那么:

  1. \(s\)\(n+6\) 的倍数(保证伤害刚刚好)
  2. \(k\leq x\)(保证不会有堡垒在最后一次攻击前 G 了)

复杂度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
bool solve() {
    LL n, h, sum = 0, Min = 1e9 + 10;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> h;
        sum += h, Min = min(Min, h);
    }
    return n == 1 || (sum % (n + 6) == 0 && sum / (n + 6) <= Min);
}
int main()
{
    int T;
    cin >> T;
    while (T--) puts(solve() ? "YES" : "NO");
    return 0;
}

D题 两串糖果(线性+区间DP)

给定两串糖果 \(\{a_n\},\{b_n\}\),我们记满意度为 \(\sum\limits_{i=1}^na_ib_i\)

我们可以选择糖果串 A 上面的若干个区间并进行反转,但是这些区间之间无法重叠,问我们可能使得满意度最大为多少?

\(1\leq n\leq 5*10^3,1\leq a_i,b_i\le 100\)

我们记 \(dp_{i,0}\) 为以 \(i\) 为结尾,且 \(i\) 并非为某区间右端点下的最大满意度(\(dp_{i,1}\) 就是选定 \(i\) 做某个区间右端点),\(f_i=\max(dp_{i,0},dp_{i,1})\)\(g(l,r)\) 为对应区间的反转值(即 \(g(l,r)=\sum\limits_{i=l}^ra_ib_{l+r-i}\)),不难想出 DP 方程:

\[\begin{cases} dp_{i,0}=f_{i-1}+a_ib_i\\ dp_{i,1}=\max\limits_{0\leq j<i}\{f_j+g(j+1,i)\} \end{cases} \]

不过这个 DP 表达式是 \(O(n^2)\) 的,所以必须得把 \(g\) 给压到 \(O(1)\)(也就意味着 \(O(n^2)\) 的空间复杂度和预处理时间)。

  1. 方法1:枚举中间点

    跟判断回文串一样,枚举中间点(注意中间点是一个值还是两个值之间空隙),往两边拓展

  2. 方法2:区间DP

    \[g(i,j)= \begin{cases} g(i+1,j-1)+a_ib_j+a_jb_i&i<j \\ a_ib_i & i=j \\ 0&i>j \end{cases} \]

#include<bits/stdc++.h>
using namespace std;
const int N = 5010;
int n, a[N], b[N];
int st[N][N];
int g(int l, int r) {
    if (l > r || st[l][r]) return st[l][r];
    if (l == r) return st[l][r] = a[l] * b[l];
    return st[l][r] = g(l + 1, r - 1) + a[l] * b[r] + a[r] * b[l];
}
int dp[N][2], f[N];
int main()
{
    //read
    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    for (int i = 1; i <= n; ++i) cin >> b[i];
    //DP
    for (int i = 1; i <= n; ++i) {
        dp[i][0] = f[i - 1] + a[i] * b[i];
        for (int j = 0; j < i; ++j)
            dp[i][1] = max(dp[i][1], f[j] + g(j + 1, i));
        f[i] = max(dp[i][0], dp[i][1]);
    }
    cout << f[n] << endl;
    return 0;
}

E题 只想要保底(二分+状压+枚举优化)

给定一个二维矩阵 \(A_{n,m}\),现在我们会随机选择两行 \(i,j\)(可以选取两个相同的行),并构造新数列 \(B_k=\max(A_{i,k},A_{j,k})\)。问,我们应该怎么选,可以使得数列 B 的最小值最大?(要求输出方案,多组方案时选择字典序最小的那种(让 i 尽可能小,然后是 \(j\)))

\(n\leq 5*10^4,m\leq 8,0\leq A_{i,j}\leq 10^9\)

一眼黄焖鸡

最小值最大,那么是典型的二分答案,我们可以直接二分这个最大值 \(x\),然后去写 check。

我们可以让原矩阵中小于 \(x\) 的记为 0,大于等于的记为 1,然后找出两行能互补出全 1 的即可。显然,因为 \(m\leq 8\),所以每一行都可以压成一个 \([0,2^m)\) 之间的整数,然后判断是否存在两个数的或的值为 \(2^m-1\) 即可。

直接 \(O(n^2)\) 寻找显然不行,但是我们发现数的值域极小,所以我们直接在值域上面枚举即可,复杂度降为了 \(O(2^{2m})\)

对于打印方案,那就是每次标记的时候,\(vis\) 改为存储出现过这个数的最小行即可,然后枚举时候用一个 pair 来不断更新。

总复杂度:\(O(\log 10^9*(nm+2^{2m}))\)

#include<bits/stdc++.h>
using namespace std;
const int N = 50010;
int n, m, M, a[N][8];
int vis[256];
void build(int val) {
    memset(vis, 0, sizeof(vis));
    for (int i = 1; i <= n; ++i) {
        int x = 0;
        for (int k = 0; k < m; ++k)
            if (a[i][k] >= val) x |= 1 << k;
        vis[x] = vis[x] ? min(vis[x], i) : i;
    }
}
auto solve(int x) {
    build(x);
    auto ans = make_pair(n + 1, n + 1);
    for (int i = 0; i < M; ++i)
        for (int j = 0; j < M; ++j)
            if (vis[i] && vis[j] && (i | j) == M - 1)
                ans = min(ans, make_pair(vis[i], vis[j]));
    return ans;
}
int main()
{
    //read
    cin >> n >> m;
    M = 1 << m;
    for (int i = 1; i <= n; ++i)
        for (int j = 0; j < m; ++j)
            scanf("%d", &a[i][j]);
    //check
    int l = -1, r = 1e9 + 10;
    while (l < r) {
        int mid = (l + r + 1) >> 1;
        if (solve(mid).first <= n) l = mid;
        else r = mid - 1;
    }
    //output
    auto ans = solve(l);
    cout << ans.first << " " << ans.second << endl;
    return 0;
}

G题 归零(数据结构)

给定一个长度为 \(n\) 的数列 \(\{a_n\}\)\(m\) 组询问 \((l,r,k)\),每次询问的目标是将子区间 \([l,r]\) 全部变为 0。

我们可以执行两种操作:

  1. \(a_i=(a_i+1)\% k\)
  2. \(a_i=(a_i-1)\%k\)

对于每次询问,输出至少需要多少次操作才能达到目标?

\(1\leq n,m\leq 2*10^5,k\leq 10^9,\forall i \forall k\{0\leq A_i<k\}\)

显然,对于某个数 \(x\),当 \(x\leq \lfloor \frac{k}{2}\rfloor\) 时候适合执行操作1,反之执行操作2。也就是说,记 \(f(x)\)\(x\) 所需要的操作次数,有

\[f(x)= \begin{cases} x&x\leq \lfloor \frac{k}{2}\rfloor \\ k-x & x >\lfloor \frac{k}{2}\rfloor \end{cases} \]

在线做法:归并树/主席树

我们记一段区间内小于等于 \(\lfloor \frac{k}{2}\rfloor\) 的数的和为 \(s_1\),大于的为 \(s_2\),大于的数的个数为 \(n_2\),那么答案为 \(s_1+n_2k-s_2\)

\(s_1\) 可以表示为区间所有元素的和(这个可以前缀和写)减去 \(s_2\),所以我们需要一个数据结构,来查询:

  1. 一段区间内大于 \(x\) 的数的个数
  2. 一段区间内大于 \(x\) 的数的和

主席树可以解决这个问题,但我不是很会。考虑到本题不带修,所以我们用另一个方法:归并树。

归并树类似线段树,不过每个节点存储的都是对应区间内所有元素排好序的结果(所以很占空间,空间复杂度也是 \(O(n\log n)\))。建立流程类似线段树和归并排序的合体(整体框架是线段树,pushup是归并排序)。

void build(int d, int l, int r) {
    if (l == r) { Merge[d][l] = a[l]; return; }
    int mid = (l + r) >> 1;
    build(d + 1, l, mid);
    build(d + 1, mid + 1, r);
    //mergesort
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r)
        Merge[d][k++] = Merge[d + 1][Merge[d + 1][i] < Merge[d + 1][j] ? i++ : j++];
    while (i <= mid) Merge[d][k++] = Merge[d + 1][i++];
    while (j <= r  ) Merge[d][k++] = Merge[d + 1][j++];
}

那么,构造完毕后,这两个操作都便于解决了:操作1的话只要找到那几个对应区间,每个区间都 upper_bound 一下,然后根据下表来统计数量即可;对于操作2,则也维护一个前缀和即可,单次操作的复杂度为 \(O(\log^2 n)\)

总复杂度为 \(O(n\log n+m\log^2 n)\),有点小卡,但是能过。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 200010;
int n, m;
LL a[N], s[N];
//MergeTree
LL Merge[21][N], ms[21][N];
void build(int d, int l, int r) {
    if (l == r) { Merge[d][l] = a[l]; return; }
    int mid = (l + r) >> 1;
    build(d + 1, l, mid);
    build(d + 1, mid + 1, r);
    //mergesort
    int i = l, j = mid + 1, k = l;
    while (i <= mid && j <= r)
        Merge[d][k++] = Merge[d + 1][Merge[d + 1][i] < Merge[d + 1][j] ? i++ : j++];
    while (i <= mid) Merge[d][k++] = Merge[d + 1][i++];
    while (j <= r  ) Merge[d][k++] = Merge[d + 1][j++];
}
LL Query1(int l, int r, LL V, int L = 1, int R = n, int d = 0) {
    if (l <= L && R <= r) {
        int P = upper_bound(Merge[d] + L, Merge[d] + R + 1, V) - Merge[d];
        return R - P + 1;
    }
    int mid = (L + R) >> 1;
    LL ans = 0;
    if (l <= mid) ans += Query1(l, r, V, L, mid, d + 1);
    if (r >  mid) ans += Query1(l, r, V, mid + 1, R, d + 1);
    return ans;
}
LL Query2(int l, int r, LL V, int L = 1, int R = n, int d = 0) {
    if (l <= L && R <= r) {
        int P = upper_bound(Merge[d] + L, Merge[d] + R + 1, V) - Merge[d];
        return ms[d][R] - ms[d][P - 1];
    }
    int mid = (L + R) >> 1;
    LL ans = 0;
    if (l <= mid) ans += Query2(l, r, V, L, mid, d + 1);
    if (r >  mid) ans += Query2(l, r, V, mid + 1, R, d + 1);
    return ans;
}
//
int main()
{
    //read
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%lld", &a[i]);
    //init
    for (int i = 1; i <= n; ++i)
        s[i] = s[i - 1] + a[i];
    build(0, 1, n);
    for (int d = 20; d >= 0; d--)
        for (int i = 1; i <= n; ++i)
            ms[d][i] = ms[d][i - 1] + Merge[d][i];
    //query
    while (m--) {
        int l, r, k;
        scanf("%d%d%d", &l, &r, &k);
        printf("%lld\n", Query1(l, r, k / 2) * k + (s[r] - s[l - 1]) - 2 * Query2(l, r, k / 2));
    }
    return 0;
}

离线做法

直接将询问离线,按照 \(k\) 的大小来排序,然后从小到大依次处理。

我们开两个树状数组(大小为 \(n\)),一个存数量一个存值,每当读到一个询问的时候,我们就将所有小于等于 \(\lfloor \frac{k}{2}\rfloor\) 的数全部插入树状数组里面,对对应区间直接查询即可得到所有小于等于 \(\lfloor \frac{k}{2}\rfloor\) 的数的个数以及数值之和,随后套公式即可得到答案。

该方案复杂度为 \(O(n\log n+m\log m)\) 规模,复杂度更优,而且常数还小。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 200010;
int n, m;
LL presum[N];
struct BIT {
    LL a[N];
    inline LL lowbit(LL x) { return x & -x; }
    void add(int i, LL x) {
        for (; i <= n; i += lowbit(i)) a[i] += x;
    }
    LL ask(LL i) {
        LL res = 0;
        for (; i; i -= lowbit(i)) res += a[i];
        return res;
    }
    LL query(int l, int r) { return ask(r) - ask(l - 1); }
} t1, t2;
struct Node { int num, id; } nodes[N];
struct Query { int l, r, k, id; } query[N];
LL ans[N];
int main()
{
    //read
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &nodes[i].num);
        nodes[i].id = i;
    }
    for (int i = 1; i <= m; i++) {
        Query &q = query[i];
        scanf("%d%d%d", &q.l, &q.r, &q.k);
        q.id = i;
    }
    //init
    for (int i = 1; i <= n; ++i)
        presum[i] = presum[i - 1] + nodes[i].num;
    sort(nodes + 1, nodes + n + 1, [](Node a, Node b) { return a.num < b.num; });
    sort(query + 1, query + m + 1, [](Query a, Query b) { return a.k < b.k; });
    //solve
    for (int i = 1, j = 1; i <= m; i++) {
        Query &q = query[i];
        int l = q.l, r = q.r, k = q.k;
        while (k / 2 >= nodes[j].num && j <= n) {
            Node &nd = nodes[j++];
            t1.add(nd.id, 1), t2.add(nd.id, nd.num);
        }
        LL n2 = r - l + 1 - t1.query(l, r), s1 = t2.query(l, r);
        LL s2 = presum[r] - presum[l - 1] - t2.query(l, r);
        ans[q.id] = (presum[q.r] - presum[q.l - 1] - s2) + n2 * k - s2;
    }
    //output
    for (int i = 1; i <= m; ++i)
        printf("%lld\n", ans[i]);
    return 0;
}

K题 乐观的R家族(签到)

\(n\) 个人参加考试,一共有 \(m\) 个选择题(选择题的答案为 ABCDE 中的某一个),答对了第 \(i\) 题可以获得 \(a_i\) 分。

现在我们知道了每个人的作答(一共 \(n\) 个长度为 \(m\) 的字符串),问他们的总分之和的可能的最大值。

\(1\leq n,m,a_i\leq 10^3\)

对于每一题,选择作答最多的那个选项为正确答案即可,这样可以使得总分最大化。

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m, a[N];
char s[N][N];
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) scanf("%s", s[i] + 1);
    for (int i = 1; i <= m; ++i) scanf("%d", &a[i]);
    int res = 0;
    for (int i = 1; i <= m; ++i) {
        int v[5] = {0, 0, 0, 0, 0};
        for (int j = 1; j <= n; ++j) v[s[j][i] - 'A']++;
        sort(v, v + 5);
        res += v[4] * a[i];
    }
    cout << res << endl;
    return 0;
}
posted @ 2022-05-25 12:29  cyhforlight  阅读(21)  评论(0编辑  收藏  举报