Loading

USACO 2025 JAN Silver 赛时记录

前言

今天开始重新进入学习状态, 不要想着颓

话说我 \(10\) 点开始打, 打完下午 \(2\) 点, 那我的中午饭这一块谁来给我补啊

策略分配, 好好做题, 心态要好

然后就是平时把之前的题补一下, 然后每日 \(C / D\) , 每日的复习

看题

不给中文, 差评
发现发给 \(\rm{kimi}\) 可以翻译, 好评

\(\rm{T1}\)

神秘, 不过应该找个规律还是好做的

\(\rm{T2}\)

还是有点困难

\(\rm{T3}\)

好吧神秘


顺序开题

\(\rm{T1}\)

思路

发现是一个求和而非最优性问题, 有些神奇

转化问题

给定数组 \(a, b\) , 求 \(\displaystyle\sum_{l = 1}^{n} \sum_{r = l}^{n} \sum_{i = 1}^{n} [a'_i = b_i]\)

至少要做到 \(\mathcal{O} (n \log n)\)

枚举 \(l, r\) 显然不够, 考虑优化

可不可以换一下贡献主题, 计算每个点做了多少次贡献
问题转化为对于两个位置, 有多少种 \([L, R]\) 可以将他们重合到一起

不难发现这个的解决方法

  • 最小的 \([L, R]\) 一定是 \([L, R]\) 就在这两个位置
  • \(L \gets L - 1, R \gets R + 1\) 显然是可行的

分析这个的复杂度, 发现只能通过随机那一档

厕所思考, 发现其实不难

考虑其本质就是计算 \(a_i = b_j\) 的所有情况

显然不用每次都枚举一遍, 先考虑 \(i > j\)
我们正向枚举 \(i\) , 记录每个种类出现的位置, 然后 \(i\) 之前的数对于 \(i\) 的贡献可以前缀和 + 二分轻松计算
逆向枚举 \(i\) , 同理计算即可

实现

框架

建立一个 \(\rm{vector} + pre\) 的结构体, 每次处理计算贡献 + 更新即可

注意清空

代码

#include <bits/stdc++.h>
#define int long long
const int MAXN = 5e5 + 20;

int n;
int a[MAXN], b[MAXN];
struct node {
    std::vector<int> pos;
    std::vector<int> pre;
    int ind = 0; // 前缀和, 下标与 pos 对齐

    void clear() { pos.clear(), pre.clear(), pre.push_back(0); ind = 0; }
} spe[MAXN];

int ans = 0;

signed main()
{
    scanf("%lld", &n);
    for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
    for (int i = 1; i <= n; i++) scanf("%lld", &b[i]);

    /*正向计算*/
    for (int i = 1; i <= n; i++) spe[i].clear();
    for (int i = 1; i <= n; i++) {
        /*处理贡献*/
        if (!spe[a[i]].pos.empty()) {
            auto it = std::upper_bound(spe[a[i]].pos.begin(), spe[a[i]].pos.end(), n - i + 1);
            ans += spe[a[i]].pre[it - spe[a[i]].pos.begin()];
            ans += (spe[a[i]].pos.end() - it) * (n - i + 1);
        }
        /*维护更新*/
        spe[b[i]].pos.push_back(i), spe[b[i]].pre.push_back(spe[b[i]].pre[spe[b[i]].ind] + i), spe[b[i]].ind++;
    }

    /*反向计算*/
    for (int i = 1; i <= n; i++) spe[i].clear();
    for (int i = n; i >= 1; i--) {
        /*处理贡献*/
        if (!spe[a[i]].pos.empty()) {
            auto it = std::upper_bound(spe[a[i]].pos.begin(), spe[a[i]].pos.end(), i);
            ans += spe[a[i]].pre[it - spe[a[i]].pos.begin()];
            ans += (spe[a[i]].pos.end() - it) * i;
        }
        /*维护更新*/
        spe[b[i]].pos.push_back(n - i + 1), spe[b[i]].pre.push_back(spe[b[i]].pre[spe[b[i]].ind] + n - i + 1), spe[b[i]].ind++;
    }

    for (int i = 1; i <= n; i++) {
        if (a[i] == b[i]) ans += std::min(i, (n - i + 1)) + (i * (i - 1)) / 2 + ((n - i) * (n - i + 1)) / 2;
    }

    printf("%lld", ans);

    return 0;
}

\(\rm{T2}\)

时间并不充裕, 分配好抓紧

现在正是寒冷而无聊的日子

思路

首先转化题意

给定数组 \(a\) , 一次操作可以将 \(a_i \gets a_i + 1 \textrm{ or } a_i \gets a_i - 1\) , 求最少的操作次数使得 \(\exists x \textrm{ s.t. } \forall i \in [1, n] , a_i \textrm{ mod } M = x \textrm{ mod } M\)

考虑枚举 \(x \textrm{ mod } M\) 的值, 然后对于每一个 \(a_i\) , 进行一个处理操作次数, 复杂度 \(\mathcal{O} (nM)\)
这个无敌爆炸劣, 考虑优化

首先, 我们可以计算出 \(a_i \textrm{ mod } M\) , 将新数组记为 \(val_i = a_i \textrm{ mod } M\) , 问题转化为找到 \(x \in [0, M)\) , 使得 \(\sum_{i = 1}^{n} \min(|val_i - x|, w)\) 最小, 其中

\[\begin{align*} w &= \begin{cases} \textrm{case }val_i \geq x, M - val_i + x \\ \textrm{otherwise}, val_i + M - x \end{cases} \\ &= M - |val_i - x| \end{align*}\]

化简上面的柿子得到
需要令 \(\sum_{i = 1}^{n} \min(|val_i - x|, M - |val_i - x|)\) 最小

这个怎么做?

只考虑 \(|val_i - x|\) 的话, 显然是丢到中位数上, 但是还需要考虑 \(M - |val_i - x|\)

直接做感觉不好做了, 考虑优化对于一个固定 \(x\) 的计算, 不难发现可以维护一个指针表示两种决策的分界点, 均摊下来应该是 \(\mathcal{O} (n)\) 的, 不管了先吃饭

手摸样例尝试一下

实现

框架

只考虑计算答案部分

首先计算出初始答案和初始分界点, 计算的时候只维护 \(\Delta\) 即可

代码

#include <bits/stdc++.h>
#define int long long
const int MAXN = 2e5 + 20;

int n, M;
int a[MAXN];

int ans = 0x3f3f3f3f;

void solve() {
    /*计算初始答案 & 分割线*/
    int lp = 1, rp = -1, x = a[1], res = 0;
    for (int i = 1; i <= n; i++) {
        /*计算分割线*/ if (std::abs(a[i] - x) <= M - std::abs(a[i] - x) && std::abs(a[i + 1] - x) > M - std::abs(a[i + 1] - x)) rp = i + 1;
        res += std::min(std::abs(a[i] - x), M - std::abs(a[i] - x));
    }
    if (!(~rp)) rp = n + 1; // 特判防爆炸

    /*开始处理*/
    ans = res;
    for (int i = 2; i <= n; i++) {
        /*维护 x 的改变*/
        res += (a[i] - x) * (std::max(0ll, i - lp)); res -= (a[i] - x) * (std::max(0ll, rp - i));
        res -= (a[i] - x) * (std::max(0ll, lp - 1)); res += (a[i] - x) * (std::max(0ll, n - rp + 1));
        x = a[i];

        /*维护指针*/
        while (std::abs(a[lp] - x) > M - std::abs(a[lp] - x) && lp <= n) res -= std::abs(a[lp] - x), res += M - std::abs(a[lp] - x), lp++;
        while (rp < lp) rp++;
        while (std::abs(a[rp] - x) <= M - std::abs(a[rp] - x) && rp <= n) res -= M - std::abs(a[rp] - x), res += std::abs(a[rp] - x), rp++;

        ans = std::min(ans, res);
    }
    printf("%lld\n", ans);
}

signed main()
{
    int T; scanf("%lld", &T);
    while (T--) {
        scanf("%lld %lld", &n, &M);
        for (int i = 1; i <= n; i++) scanf("%lld", &a[i]), a[i] %= M;
        std::sort(a + 1, a + n + 1);
        solve();
    }

    return 0;
}

正确性如何还不可知, 今天的 \(\rm{USACO}\) 打到这里

想了一下决定打个拍子验证一下
搞搞打了个补丁过掉了 [未知] 组数据, 丢了

posted @ 2025-01-25 15:11  Yorg  阅读(11)  评论(0)    收藏  举报