Кількість різних чисел#12448

Кількість різних чисел (Розбір)

Автор задачі:

Андрій Холод (Connect)

Підготував:

Андрій Холод (Connect)

Розбір:

Андрій Холод (Connect)

Ідея

Маємо прихований масив довжини . Кожен запит — це перестановка індексів, а у відповідь ми дізнаємось для кожного елемента, скільки його сусідів (у вибраному порядку) мають таке саме значення. Потрібно знайти кількість різних чисел у масиві за не більше ніж \frac{n}{2} + 1 запитів.

Ключове спостереження 1: відновлення рівностей сусідів

Нехай ми зробили запит з перестановкою і отримали масив . Позначимо \text{eq}[i] = 1, якщо p_i = p_{i+1}, і інакше. Тоді:

b_1 = \text{eq}[1], \quad b_i = \text{eq}[i-1] + \text{eq}[i] \;\text{ для } 1 < i < n, \quad b_n = \text{eq}[n-1].

З цього можна послідовно відновити всі :

\text{eq}[1] = b_1, \quad \text{eq}[i] = b_i - \text{eq}[i-1].

Отже, з одного запиту ми дізнаємось для кожної пари сусідніх елементів у нашій перестановці, чи мають вони однакове значення в масиві .

Ключове спостереження 2: покриття всіх пар

Щоб повністю визначити, які індекси мають однакове значення, потрібно, щоб кожна пара індексів хоча б раз опинилась на сусідніх позиціях у якомусь запиті.

Це класична задача побудови розкладу турніру (round-robin): гравців грають один з одним, і за один тур кожен грає рівно один матч. Для парного потрібно рівно n - 1 турів, але нам достатньо \frac{n}{2} турів, де кожен тур дає перестановку, в якій пари сусідів покривають усі потрібні пари.

Конструкція така: якщо непарне, доповнюємо до m = n + 1 (парного). Далі для кожного туру k = 0, 1, \ldots, \frac{m}{2} - 1 будуємо перестановку за правилом: починаємо з елемента , потім чергуємо елементи k - 1, k + 1, k - 2, k + 2, \ldots, закінчуючи елементом k + \frac{m}{2} (усі індекси беруться за модулем , а якщо індекс  — пропускаємо його).

Це дає рівно \frac{n}{2} (з округленням вгору) запитів, що вкладається в ліміт \frac{n}{2} + 1.

Об'єднання результатів через DSU

Підтримуємо структуру "система неперетинних множин" (DSU). Після кожного запиту, якщо \text{eq}[i] = 1, об'єднуємо індекси та q[i+1] (де  — наша перестановка). Після всіх запитів кількість компонент у DSU і є відповіддю — кількістю різних чисел у масиві.

Чому це коректно?

Два індекси та потраплять в одну компоненту DSU тоді і тільки тоді, коли a_i = a_j. Якщо a_i = a_j, то в тому запиті, де вони сусіди, ми отримаємо \text{eq} = 1 і об'єднаємо їх. Якщо a_i \neq a_j, вони ніколи не будуть об'єднані, бо в кожному запиті, де вони сусіди, \text{eq} = 0.

Транзитивність забезпечує DSU: якщо a_i = a_j і a_j = a_k, то всі три потраплять в одну компоненту, навіть якщо пара безпосередньо не перевірялась як сусіди.

Асимптотика

Кількість запитів: O\!\left(\frac{n}{2}\right).

Побудова перестановок: O(n^2).

DSU з евристиками: O(n \cdot \alpha(n)) на запит.

Загалом: O(n^2), що при n \le 2 \cdot 10^3 працює швидко.

#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> p, sz;
    DSU(int n = 0) {
        p.resize(n);
        sz.assign(n, 1);
        iota(p.begin(), p.end(), 0);
    }
    int find(int v) {
        return p[v] == v ? v : p[v] = find(p[v]);
    }
    void unite(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return;
        if (sz[a] < sz[b]) swap(a, b);
        p[b] = a;
        sz[a] += sz[b];
    }
};

vector<vector<int>> build_queries(int n) {
    int m = n + n % 2;
    vector<vector<int>> queries;
    for (int k = 0; k < m / 2; k++) {
        vector<int> q;
        auto add = [&](int x) {
            x = (x % m + m) % m;
            if (x < n) q.push_back(x);
        };
        add(k);
        for (int t = 1; t <= m / 2 - 1; t++) {
            add(k - t);
            add(k + t);
        }
        add(k + m / 2);
        queries.push_back(q);
    }
    return queries;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n;
    cin >> n;

    DSU dsu(n);
    auto queries = build_queries(n);

    for (auto &q : queries) {
        cout << "? ";
        for (int i = 0; i < n; i++) {
            if (i) cout << ' ';
            cout << q[i] + 1;
        }
        cout << endl;

        vector<int> b(n);
        for (int i = 0; i < n; i++) cin >> b[i];

        if (n == 1) continue;

        vector<int> eq(n - 1);
        eq[0] = b[0];
        for (int i = 1; i < n - 1; i++)
            eq[i] = b[i] - eq[i - 1];

        for (int i = 0; i < n - 1; i++)
            if (eq[i] == 1)
                dsu.unite(q[i], q[i + 1]);
    }

    int ans = 0;
    for (int i = 0; i < n; i++)
        if (dsu.find(i) == i) ans++;

    cout << "! " << ans << endl;
    return 0;
}