跳转至

ABC371(A-G)

A - Jiro

题目大意

有三个不相等的数字 \(A\)\(B\)\(C\),给你三个符号 \(S_{AB}\)\(S_{AC}\)\(S_{BC}\),这三个符号要么是 <,要么是 >。如果 \(S_{AB}\)<,表示 \(A < B\),如果 \(S_{AB}\)> ,表示 \(A > B\),其他情况以此类推。问:三个数字中第二大的数字是哪个?

解题思路

如果不想分类讨论的话,可以小于号表示为偏序关系,若 \(x < y\),则图中有一条从 \(x\)\(y\) 的有向边。在这种图中,记录下每个点的入度,就是小于这个点的数字的个数。

参考代码
#include <iostream>
#include <vector>

using namespace std;

int main()
{
    char s;
    vector<int> ind(3);
    for (int i = 0; i < 3; i++)
        for (int j = i + 1; j < 3; j++)
        {
            cin >> s;
            if(s == '<')
                ind[j]++;
            else
                ind[i]++;
        }
    for(int i = 0; i < 3; i++)
        if(ind[i] == 1)
            cout << char('A' + i) << endl;
    return 0;
}

B - Taro

题目大意

某个国家有 \(N\) 个家庭,初始的时候,每个家庭都没有小孩。有 \(M\) 个小孩出生的事件按顺序到达,第 \(i\) 个事件可以用 \(A_i\)\(B_i\) 表示,其中 \(A_i\) 是整数且 \(1 \le A_i \le N\),表示这个小孩出生在第 \(A_i\) 个家庭。\(B_i\) 是一个字符,只可能取 MF, 其中 M 表示男性,F 表示女性。定义 长子 表示某个家庭中最年长的男孩子(注意,可能存在比长子年长的姐姐)。对于这个 \(M\) 个按顺序出生的孩子,如果这个孩子是长子,输出 Yes,否则输出 No

参考代码
#include <iostream>
#include <vector>

using namespace std;

int main()
{
    int n, m;
    cin >> n >> m;
    vector<bool> vis(n+1);
    while(m--)
    {
        int a;
        char f;
        cin >> a >> f;
        if(!vis[a] && f == 'M')
        {
            vis[a] = true;
            cout << "Yes" << endl;
        }
        else
            cout << "No" << endl;
    }
    return 0;
}

C - Make Isomorphic

题目大意

给你两个简单无向图 \(G\)\(H\),这两个图都有 \(N(1 \le N \le 8)\) 个点,但两个图的边数不一定相同。

你可以执行以下操作任意次:

  • 从图 \(H\) 中选择两个点,如果这两个点之间没有连边,你可以花费 \(A_{i, j}\) 添加这条边。如果这两个点之间有连边,你可以花费 \(A_{i, j}\) 移除这条边。

问:为了让图 \(G\) 和图 \(H\) 同构,最少的花费多少?

解题思路

\(N \le 8\),枚举全排列代表同构的方式,然后二重循环求解花费即可。时间复杂度 \(O(N! \times N^2)\)

参考代码
#include <algorithm>
#include <iostream>
#include <limits>
#include <vector>

using namespace std;

int main()
{
    int n, m;
    cin >> n >> m;
    vector<vector<bool>> src(n + 1, vector<bool>(n + 1));
    vector<vector<bool>> dst(n + 1, vector<bool>(n + 1));
    while (m--)
    {
        int u, v;
        cin >> u >> v;
        dst[u][v] = dst[v][u] = true;
    }
    cin >> m;
    while (m--)
    {
        int u, v;
        cin >> u >> v;
        src[u][v] = src[v][u] = true;
    }
    vector<vector<int>> a(n + 1, vector<int>(n + 1));
    for (int i = 1; i <= n; i++)
        for (int j = i + 1; j <= n; j++)
            cin >> a[i][j];
    vector<int> p(n + 1);
    for (int i = 1; i <= n; i++)
        p[i] = i;
    int ans = numeric_limits<int>::max();
    do
    {
        int cost = 0;
        for (int u = 1; u <= n; u++)
            for (int v = u + 1; v <= n; v++)
            {
                if (src[u][v] != dst[p[u]][p[v]])
                    cost += a[u][v];
            }
        ans = min(ans, cost);
    } while (next_permutation(p.begin() + 1, p.end()));
    cout << ans << endl;
    return 0;
}

D - 1D Country

题目大意

在一维数轴上有 \(N(1 \le N \le 2 \times 10 ^ 5)\) 个村庄,第 \(i\) 个村庄的坐标是 \(X_i\),有 \(P_i\) 个村民。

给你 \(Q(1 \le Q \le 2 \times 10 ^ 5)\) 个询问,每个询问包括两个数字 \(L_i\)\(R_i\),你需要回答出在范围 \([L_i, R_i]\) 内有多少村民。

解题思路

在线的做法就是前缀和+二分,每一次查询 \(O(\log N)\) 。离线做法可以离散化所有的坐标,每次查询 \(O(1)\),下面的做法是离线做法。

参考代码
#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;
using LL = long long;

int main()
{
    int n, q;
    cin >> n;
    vector<int> x(n), p(n);
    for (int i = 0; i < n; i++)
        cin >> x[i];
    for (int i = 0; i < n; i++)
        cin >> p[i];
    cin >> q;
    vector<int> l(q), r(q);
    for (int i = 0; i < q; i++)
        cin >> l[i] >> r[i];
    vector<int> xs = x;
    xs.insert(xs.end(), l.begin(), l.end());
    xs.insert(xs.end(), r.begin(), r.end());
    sort(xs.begin(), xs.end());
    xs.erase(unique(xs.begin(), xs.end()), xs.end());

    auto compress = [&xs](vector<int> &v) -> void
    {
        for (auto &x : v)
            x = lower_bound(xs.begin(), xs.end(), x) - xs.begin() + 1;
    };

    compress(x);
    compress(l);
    compress(r);

    int len = xs.size();
    vector<LL> s(len + 1);
    for (int i = 0; i < n; i++)
        s[x[i]] += p[i];
    for (int i = 1; i <= len; i++)
        s[i] += s[i - 1];
    for (int i = 0; i < q; i++)
        cout << s[r[i]] - s[l[i] - 1] << endl;
    return 0;
}

E - I Hate Sigma Problems

题目大意

给你一个长度为 \(N(2 \times 10 ^ 5)\) 的序列 \(A = (A_1, A_2, \cdots, A_N)\),定义 \(f(l, r)\) 表示连续子序列 \((A_l, A_{l+1}, \cdots, A_r)\) 当中不同的数字的数量。求出 \(\sum\limits_{i = 1} ^ N\sum\limits_{j = i}^Nf(i, j)\)

解题思路

对于一个连续子序列,如果有出现两次或更多次的数字,只计入第一个数字所产生的贡献。这样的话,每个数字都会有一个作用范围,设序列 \(A\) 中数字 \(x\) 出现的位置为 \(p_1, p_2, \cdots, p_n\),首先考虑 \(p_1\) 的贡献,如果有序列 \(f(l, r)\) 满足 \(1 \le l \le p_1 \le r \le n\),这都表示 \(p_1\) 位置是第一个数字 \(x\),只计入 \(p_1\) 作贡献,而这样的序列 \(l\) 可能取值有 \(p_1\) 个,\(r\) 的可能取值有 \(n- p_1 + 1\) 个,因此 \(p_1\) 的贡献为 \(p_1 \times(n-p_1+1)\)。考虑 \(p_2\) 的贡献,为了屏蔽掉 \(p_1\),就需要 \(f(l, r)\) 满足 \(p_1+1 \le l \le p_2 \le r \le n\),因此 \(p_2\) 的贡献为 \((p_2 - p_1) \times (n - p_2 + 1)\)。后面同理。

参考代码
#include <iostream>
#include <unordered_map>
#include <vector>

using namespace std;
using LL = long long;

int main()
{
    int n;
    cin >> n;
    unordered_map<int, vector<LL>> num;
    for (int i = 1; i <= n; i++)
    {
        int x;
        cin >> x;
        auto it = num.find(x);
        if (it == num.end())
            num[x] = {0, i};
        else
            it->second.push_back(i);
    }
    LL ans = 0;
    for (auto [_, v] : num)
    {
        int k = v.size();
        for (int i = 1; i < k; i++)
        {
            LL l = v[i] - v[i - 1];
            LL r = n - v[i] + 1;
            ans += l * r;
        }
    }
    cout << ans << endl;
    return 0;
}

F - Takahashi in Narrow Road

题目大意

有一个一维无限长的数轴,数轴上有 \(N(1 \le N \le 2 \times 10 ^ 5)\) 个棋子,初始的时候,第 \(i\) 枚棋子的坐标是 \(X_i\)

在任何时刻,你可以执行以下操作任意次:

  • 每一次操作选择一枚棋子,将这枚棋子向左或者向右移动 \(1\) 单位。注意不能出现两个棋子在同一个坐标的情况,也就是说对于某个坐标为 \(X\) 的棋子,如果 \(X-1\) 或者 \(X+1\) 上已经有一枚棋子,就不能将这枚棋子向左或者向右移动。

按顺序给你 \(Q\) 个任务,每个任务给你一个数字 \(T_i\)\(G_i\),表示你需要将从左往右数的第 \(T_i\) 个棋子移动到 \(G_i\)。对于每个任务,你需要回答至少需要执行几次操作才能完成。

解题思路

假设当前第 \(T_i\) 个棋子需要往右移动到目的地,且第 \(T_i\) 个棋子当前的坐标为 \(X\),如果 \(X + 1,X+2,...\) 一连串的位置都有棋子,根据移动的规则,这些棋子肯定是一起往右移动的,所以为了加快移动的操作,需要批量的将相连的棋子进行移动。于是需要维护每个棋子所在的左右边界,在移动的过程中进行批量的合并和拆解。可以证明,每一次移动至多进行一次拆分(就是 \(T_i\) 处于连续一段的中间时),但是合并却有可能进行多次,所以均摊下来合并的拆解的时间复杂度是 \(O(1)\),至于批量的移动,维护一个 map 表示当前还存在的连续的棋子段,每次移动的时候在 map 当中二分即可,要么二分到达目的地,要么二分到达下一个应该合并的位置。实现的时候略微繁琐。

参考代码
#include <iostream>
#include <limits>
#include <map>

using namespace std;
using LL = long long;

const int NINF = numeric_limits<int>::min() / 2;
const int PINF = numeric_limits<int>::max() / 2;

int main()
{
    int n, q;
    cin >> n;
    map<int, pair<LL, LL>> pos;
    pos[0] = {NINF, NINF};
    pos[n + 1] = {PINF, PINF};
    for (int i = 1; i <= n; i++)
    {
        LL x;
        cin >> x;
        pos[i] = {x, x};
    }
    cin >> q;
    LL ans = 0;
    while (q--)
    {
        int t, g;
        cin >> t >> g;
        auto it = --pos.upper_bound(t);
        auto [id, p] = *it;
        auto [l, r] = p;
        LL now = t - id + l;
        if (now == g)
            continue;
        else if (now < g)
        {
            if(id != t)
                it->second.second = now - 1;
            l = now;
            LL cnt = r - l + 1;
            ++it;
            while (true)
            {
                auto [next_l, next_r] = it->second;
                LL d = min(g - l, next_l - 1 - r);
                ans += d * cnt;
                l += d;
                r += d;
                if (l == g)
                    break;
                cnt += next_r - next_l + 1;
                r = next_r;
                it = pos.erase(it);
            }
            pos[t] = {l, r};
        }
        else
        {
            if (r != now)
                pos[t + 1] = {now + 1, r};
            r = now;
            LL cnt = r - l + 1;
            it = --pos.erase(it);
            int lid = id;
            while (true)
            {
                auto [last_l, last_r] = it->second;
                LL d = min(r - g, l - last_r - 1);
                ans += d * cnt;
                r -= d;
                l -= d;
                if (r == g)
                    break;
                cnt += last_r - last_l + 1;
                l = last_l;
                lid = it->first;
                it = --pos.erase(it);
            }
            pos[lid] = {l, r};
        }
    }
    cout << ans << endl;
    return 0;
}

G - Lexicographically Smallest Permutation

题目大意

给你一个长度为 \(N(1 \le N \le 2 \times 10 ^ 5)\) 的排列 \(P = (P_1, P_2, \cdots, P_N)\) 和长度为 \(N\) 的序列 \(A = (A_1, A_2, \cdots, A_N)\)

你可以执行以下操作任意次:

  • 对于所有的 \(i = 1,2,...,N\)同时 \(A_i\) 替换成 \(A_{p_i}\)

输出能够得到的字典序最小的序列 \(A\)

解题思路

这种替换关系最终会形成若干个环,例如对于第一个环(包含 \(i = 1\) 的环)类似于 \(1 \leftarrow P_1 \leftarrow P_{P_1}\cdots \leftarrow 1\)。在所有的 \(P\) 序列上跑一边,处理出所有的环。

为了让字典序最小,首先需要让第一个环的字典序最小,也就是把最小的元素放在 \(i = 1\) 的位置,假设需要 \(a_1\) 次操作才能做到。下面考虑第二个环,从第二个环开始就不能盲目选择最小的元素放在前面,因为第一个环优先级更高,而第二个环的操作次数有可能影响第一个环。

进一步可以发现,如果设第一个环的长度为 \(L_1\),那么再额外执行 \(L_1\) 次操作不会改变第一个环的结果,也就是后续的环需要保证结果形如 \(a_1 + k_1L_1\)。因此,对于第二个环来说,假设第二个环长度为 \(L_2\),将某个元素移动到首位需要操作 \(a_2\) 次,就必须满足 \(a_1 + k_1L_1 = a_2 + k_2L_2\),仅当这个方程有解,才能在不影响第一个环的情况下将这个元素移动到首位。这就是扩展中国剩余定理,不会的可以看 中国剩余定理 - OI WikiP1495 【模板】中国剩余定理(CRT)/ 曹冲养猪 - 洛谷 P4777 【模板】扩展中国剩余定理(EXCRT) - 洛谷

由于这里环的长度不一定互质,所以需要用扩展中国剩余定理,但是如果根据 oiwiki 上的做法,我们需要实时的将方程进行合并,合并的时候就需要算出 \(L' = \text{lcm}(L_1, L_2)\),这样会爆 long long(这个数字非常大,即使 128 bit 也存不下)。然而官方题解居然是用 python 高精度水过去的。这里给出一个不用高精度的做法,其实非常简单,不进行方程合并就好了,我们存下之前合法的方程,每当碰到一个新的环 \(i\),就遍历其中所有的 \(a_i = 0, 1, ..., L_i-1\),将 \(x \equiv a_i \bmod L_i\) 依次和之前的合法方程计算是否兼容(根据裴蜀定理确定是否有解),仅当与所有之前的方程有解,这个位置 \(a_i\) 才认为合法。在所有合法的 \(a_i\) 中找到数字最小的那一个移动到首位,然后加入到方程中,这样就做完了。

下面分析时间复杂度,首先我们会发现,如果第一个环的 \(a_1\) 已经确定,且第二个环的长度满足 \(L_2 = L_1\) 的话,那么第二个环一定只有 \(a_2 = a_1\) 符合条件,毕竟长度一样,所以前面操作多少次就影响了第二个环多少次。于是在记录合法的方程的时候,实际只需要记录环长度不同的方程,这样的话,方程的数量最多是多少呢?为了让方程数量尽可能多,一定是优先出现长度比较小的环,也就是从小到大,这样,假设有 \(m\) 个环,我们有 \(1 + 2 + 3 + \cdots+m = \frac{m(m+1)}{2}\le n\),所以 \(m\)\(\sqrt n\) 的级别,这样每个位置至多与 \(\sqrt n\) 个方程计算是否合法,总的时间复杂度就是 \(O(n \sqrt n)\)

另外附上一个 \(O(n\log n)\)题解,不过说实话我没有看懂。。

参考代码
#include <iostream>
#include <numeric>
#include <unordered_map>
#include <vector>

using namespace std;
using LL = long long;

int main()
{
    int n;
    cin >> n;
    vector<int> a(n), p(n);
    for (int i = 0; i < n; i++)
    {
        cin >> p[i];
        p[i]--;
    }
    for (int i = 0; i < n; i++)
        cin >> a[i];
    unordered_map<int, int> equations;
    vector<int> ans(n);
    vector<bool> vis(n);

    auto check = [&equations](unordered_map<int, int> &g, int a) -> bool
    {
        for (auto [li, ai] : equations)
            if (abs(a - ai) % g[li] != 0)
                return false;
        return true;
    };

    for (int i = 0; i < n; i++)
    {
        if (vis[i])
            continue;
        vector<int> ring_idx;
        int j = i;
        while (!vis[j])
        {
            vis[j] = true;
            ring_idx.push_back(j);
            j = p[j];
        }
        int len = ring_idx.size();
        unordered_map<int, int> g;
        for (auto [l, _] : equations)
            g[l] = gcd(l, len);
        int min_num = n + 1, min_j = -1;
        for (int j = 0; j < len; j++)
        {
            int idx = ring_idx[j];
            if (a[idx] < min_num && check(g, j))
            {
                min_num = a[idx];
                min_j = j;
            }
        }
        equations[len] =  min_j;
        for (int j = 0, k = min_j; j < len; j++, k = (k + 1) % len)
        {
            int pos = ring_idx[j];
            ans[pos] = a[ring_idx[k]];
        }
    }
    for (int i = 0; i < n; i++)
        cout << ans[i] << ' ';
    return 0;
}


最后更新: 2024-12-07
创建日期: 2024-12-07