Форум Всеукраїнської інтернет-олімпіади NetOI


На форумі обговорюються лише питання, пов'язані з олімпіадою

Ви не зайшли.

#1 2020-02-13 12:46:45

Dim_ov
Новий користувач
Зареєстрований: 2009-11-29
Повідомлень: 312
Вебсайт

Рішення і розбори

Повідомлення про завершення туру ще не було, але рішення вже перевірені, так що думаю, розв'язками можна ділитися.
Задачі цього року трохи простіші, ніж зазвичай (і суб'єктивно і судячи з таблиці результатів, де у 44 учасників 250+ балів. Зазвичай таких не більше 15-20).

Не всі розбори дуже докладні, але думаю, ідеї в більшості мають бути зрозумілими.
Удачі учасникам 4 туру smile


Train2020

Помітимо, що якщо вагони N та M зчеплені за рахунок спільного дільника K, то між N та M можна вставити довільну кількість чисел, що діляться на K. Тобто якщо вагон можна з'єднати хоча-б з двома іншими вагонами - його точно можна буде кудись прилаштувати (мабуть можна і математично/строго це довести, але я не доводив). Усі такі вагони можна взяти в поїзд.
Якщо у вагону є лише один потенційний сусід - такий вагон повинен буде стояти в голові, або в хвості поїзда. Тобто таких вагонів можна взяти не більше двох.
Ну і якщо для вагону немає сусідів - включити його в поїзд не вийде.

Підрахуємо ці кількості.
У вагону з номером K немає жодного сусіда тоді і тільки тоді, коли K - просте число і N < 2 * K.
У вагону з номером K є лише один сусід тоді і тільки тоді, коли K - просте число і 2 * K ≤ N < 3 * K.
В усіх інших випадках у вагонів є щонайменше 2 сусіди.

Таким чином, задача розв'язується за один прохід решета Ератосфена:

З самого початку ініціалізуємо відповідь значенням N, а кількість вагонів з одинм сусідом - нулем.
Запускаємо решето Ератосфена.
Коли знайшли чергове просте число p:
якщо N < 2 * p - зменшуємо відповідь на 1 (знайшли вагон без сусідів)
якщо 2 * p ≤ N < 3 * p - збільшуємо на 1 кількість вагонів з одинм сусідом.
Якщо після закінчення роботи решета кількість вагонів з одним сусідом більша за 2 - зменшуємо відповідь на ("кількість вагонів з одинм сусідом" - 2).

Асимптотика.
Звичайна асимптотика решета Ератосфена.

Час: O(N * log log N)
Пам'ять: O(N)

Код рішення на С++.

Код:

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

int solve(int n) {
    constexpr int MAX_N = 100000;

    if (n == 2 || n == 3) return 2;

    int res = n;
    int n_single_neighbour = 0;

    bool prime[MAX_N + 1];
    fill_n(prime, n + 1, true);

    for (int p = 2; p <= n; ++p) {
        if (prime[p]) {
            for (int i = p * 2; i <= n; i += p) prime[i] = false;

            res -= p * 2 > n;
            n_single_neighbour += p * 2 <= n && p * 3 > n;
        }
    }

    return res - max(0, n_single_neighbour - 2);
}

int main() {
    int n;
    cin >> n;

    cout << solve(n) << endl;
}

Game2020

Гра зводиться до класичної гри Нім згідно теореми Шпрага-Гранді. Розмірами купок тут будуть суми розмірів непустих кошиків, що йдуть підряд. Наприклад, тесту

Код:

10
1 0 1 2 3 0 7 5 0 9

відповідає гра в Нім з розмірами купок

Код:

1 6 12 9

Доведемо цю відповідність (якщо цікавить лише рішення, а не доведення його коректності, то розділ можна пропустити)

Код:

Якщо група кошиків складається з одного кошика - відповідність очевидна,
адже над кошиком можна виконувати ті ж самі операції, що й над Нім-купкою
і жодних інших. Якщо ж група складається з кількох кошиків - крім
звичайних операцій над купкою (зменшення) гравцю стає доступною ще й
операція "зменшення з розділенням". Наприклад:

1 2 3 -> 1 0 3
або
1 2 3 4 5 -> 1 0 2 0 3

Нехай початкова сума групи кошиків була N, гравець взяв всього X цукерок
і отримав K нових груп розмірами M[i​]. g(N) - значення Шпрага-Гранді для
групи розміром N.

У такому випадку згідно з теоремою, значенням Шпрага-Гранді для N буде
g(N) = mex (states), де states =
{0, ..., N - 1} адже гравець все ще може взяти від 1 до N цукерок, не розділяючи групу
∪
{g(M[1​] ⊕ ... ⊕ M[K​]) для всих можливих X та всих можливих розділень}

Для доведення відповідності нам необхідно і достатньо показати, що серед
значень g(M[1​] ⊕ ... ⊕ M[K​]) немає значення N.

Показати це нескладно, адже XOR-сума не може бути більшою за алгебраїчну
суму. Тобто усі значення M[1​] ⊕ ... ⊕ M[K​] будуть менші або рівні за (N - X).
А оскільки X > 0, то M[1​] ⊕ ... ⊕ M[K​] < N.

Таким чином, рішення доволі просте - пробігаємося по вхідному масиву, рахуємо суми розмірів груп непустих кошиків і xor-имо ці суми.

Зауваження по реалізації. Важливо правильно обрати формат даних, адже хоча окремі числа і менші за 10^5, але їх може бути 10^6 штук, і ми їх сумуємо. Тобто загальне значення може бути до 10^11 і в 32-бітний інт воно не влізе.

Асимптотика.
Час: O(N)
Пам'ять: O(1)

Код рішення на С++.

Код:

#include <iostream>

using namespace std;

long long sprague_grundy(int n, int *a) {
    long long res = 0;
    long long sum = 0;

    for (int i = 0; i < n; ++i) {
        sum += a[i];
        if (a[i] == 0) {
            res ^= sum;
            sum = 0;
        }
    }
    res ^= sum;

    return res;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    int t;
    cin >> t;

    string s;

    while (t--) {
        static int a[1000000];
        int n;
        cin >> n;

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

        s += sprague_grundy(n, a) ? 'N' : 'Y';
    }
    cout << s << endl;
}

Wall2020
На каналі 3Blue1Brown є серія відео, присвячених цій задачі. Підозрюю, що автор задачі надихався саме ними smile
Наполегливо рекомендую переглянути ці та інші відео з каналу. Там багато цікавого для тих, хто любить математику.

Власне серія відео:
https://www.youtube.com/watch?v=HEfHFsfGXjs
https://www.youtube.com/watch?v=jsYwFizhncE
https://www.youtube.com/watch?v=brU5yLm9DZM

Більш строгий документ з описом експерименту, купою математики і строгим виведенням того, як обчислити число пі за допомогою двох ідеально пружних кубів на ідеально рівній поверхні:
https://www.maths.tcd.ie/~lebed/Galperi … h%20pi.pdf

Можливо пізніше додам основні моменти з відео в пост, але мені поки що лінь, а відео хороше smile

Асимптотика.
Час: O(1)
Пам'ять: O(1)

Код рішення на С++.

Код:

#include <iostream>
#include <cstdio>
#include <cmath>

using namespace std;

const long double pi = 3.141592653589793238462643383279502884L;

int main() {
    long double m1, m2;

    cin >> m1 >> m2;

    auto theta = atan(sqrt(m1 / m2));
    int result = static_cast<int>(pi / theta);

    if (pi - result * theta < 1e-6L) {
        --result;
    }

    cout << result << endl;
}

Trunks
Відповіддю є послідовність A059881 з OEIS, яка в свою чергу є сумою послідовностей A059882 та A059883.

Задача розв'язується за допомогою ДП.
Нехай
T[n​][k​] - кількість способів набрати n​ монет, взявши з першої скрині рівно k​ монет, з другої - більше за k​, з третьої - менше, ніж з другої і т.д.
S[n​][k​] - кількість способів набрати n​ монет, взявши з першої скрині рівно k​ монет, з другої - менше за k​, з третьої - більше, ніж з другої і т.д.

тоді
T[n​][k​] дорівнюватиме сумі S[n-k​][j​] для усих j​ від k​+1 до n-k включно
S[n​][k​] дорівнюватиме сумі T[n-k​][j​] для усих j​ від 1 до k-1 включно
І база ДП: T[n​][n​] = S[n​][n​] = 1

Якщо реалізувати таку динаміку влоб, то отримаємо кубічну асимптотику і в ТЛ не вліземо. Але можна дуже легко перетворити підрахунок суми з лінійного на константний, якщо зберігати в наших таблицях не самі значення T[n​][k​] та S[n​][k​], а префіксні суми. Тобто суми значень від T[n​][0​] до T[n​][k​] та від S[n​][0​] до S[n​][k​] відповідно.

І оскільки із застосуванням префіксних сум нам доведеться іноді віднімати значення - треба не забути в кінці зробити значення відповіді додатнім ((res % MOD + MOD) % MOD).

Асимптотика.
Час: O(N^2)
Пам'ять: O(N^2)

Код рішення на С++.

Код:

#include <iostream>

using namespace std;

int solve(int n, int k) {
    constexpr size_t MAX_N = 5001;
    constexpr int MOD = 1000000007;

    if (n == k) return 1;

    static int dp[MAX_N][MAX_N][2] = {};

    for (int total = 1; total <= n; ++total) {
        for (int first = 1; first < total; ++first) {
            int inc_addend = (total - first > first) * (dp[total - first][total - first][0] - dp[total - first][first][0]) % MOD;

            dp[total][first][0] = (dp[total][first - 1][0] + dp[total - first][first - 1][1]) % MOD;
            dp[total][first][1] = (dp[total][first - 1][1] + inc_addend) % MOD;
        }

        dp[total][total][0] = (dp[total][total - 1][0] + 1) % MOD;
        dp[total][total][1] = (dp[total][total - 1][1] + 1) % MOD;

        for (int i = total + 1; i <= n; ++i) {
            dp[total][i][0] = dp[total][total][0];
            dp[total][i][1] = dp[total][total][1];
        }
    }

    int resDec = (dp[n][k][0] - dp[n][k-1][0]) % MOD;
    int resInc = (dp[n][k][1] - dp[n][k-1][1]) % MOD;

    int res = (resInc + resDec) % MOD;

    return (res + MOD) % MOD;
}

int main() {
    int n, k;
    cin >> n >> k;

    cout << solve(n, k) << endl;
}

Kingdoms2020

Ключ до розв'язку - послідовність A051034 з OEIS, або більш загально - гіпотеза Ґольдбаха.
Відмітимо кілька речей:
1. Будь-яке число можна представити у вигляді суми трьох або менше простих чисел.
2. У вигляді суми двох простих чисел можна представити усі парні числа, більші за 2, а також непарні числа N такі, що N-2 - просте.

Таким чином, для розв'язку розділимо вхідний діапазон N на 3 частини:
1. N ≥ 3
2. N = 1
3. N = 2

Для першої частини відповіддю буде (c - d + 1) * (b - max(a, 3) + 1), адже як ми з'ясували, для N ≥ 3 існують усі (N, K)-царства.
Для другої частини відповіддю буде кількість простих чисел у діапазоні [c, d].
Для третьої - кількість чисел i у діапазоні [c, d] таких, що
1. число i просте АБО
2. число i парне АБО
3. число i - 2 просте

Враховуючи дуже невеликі обмеження, особливих вимог до реалізації немає і вона може бути якою завгодно. Все одно працюватиме швидко. Хіба що на простоту варто перевіряти за корінь, а не лінійно. Ну і пам'ятати про граничні випадки (для K = 1 не існує жодного (N, K)-царства).

Асимптотика.
Час: O((d - c) * sqrt(d))
Пам'ять: O(1)

Код рішення на С++.

Код:

#include <iostream>
#include <algorithm>

using namespace std;

bool is_prime(int n) {
    if (n <= 3) return true;
    if (n % 2 == 0 || n % 3 == 0) return false;

    for (int i = 5; i * i <= n; i += 6) {
        if (n % i == 0 || n % (i + 2) == 0) return false;
    }

    return true;
}

int solve(int minN, int maxN, int minK, int maxK) {
    minK = max(minK, 2);
    if (maxK < minK) return 0;

    int result = (maxK - minK + 1) * max(0, maxN + 1 - max(minN, 3));

    if (minN >= 3) return result;
    maxN = min(maxN, 2);

    for (int i = minK; i <= maxK; ++i) {
        if (is_prime(i)) {
            result += maxN - minN + 1;
        } else if (maxN >= 2 && (i % 2 == 0 || is_prime(i - 2))) {
            result++;
        }
    }

    return result;
}

int main() {
    int a, b, c, d;

    cin >> a >> b >> c >> d;

    cout << solve(a, b, c, d) << endl;
}

Поза форумом

 

#2 2024-03-25 07:04:22

Sarik
Олімпієць
Зареєстрований: 2024-03-25
Повідомлень: 3

Re: Рішення і розбори

Засів за читанням, дякую за розбори, допомагає  зрозуміти краще задачі.

Поза форумом

 

Нижній колонтитул

Powered by Likt
© Copyright 2002–2009 Likt