Статья Автор: Деникина Н.В., Деникин А.В.

Сахир и нубийский рынок

St - ID79502 · Нубийский рынокNubian Market · CF 1084E
SilverTests.rupriority_queue · бинарный поиск · жадный выбор
нубийский рынок

Бинпоиск по ответу + куча для поиска k наименьших


1Суть задачи

Есть n сувениров с базовыми стоимостями a[1], a[2], ..., a[n]. Если Сахир покупает k сувениров, то стоимость сувенира с номером i становится

costi = a[i] + i · k

То есть цена каждого сувенира зависит от того, сколько всего сувениров Сахир решил купить. Нужно найти максимальное k, при котором можно уложиться в бюджет S, и для этого k — минимальную возможную сумму.

Ограничения: n ≤ 10⁵, S ≤ 10⁹, a[i] ≤ 10⁵. Полный перебор подмножеств невозможен, нужно что-то умнее.

2Ключевое наблюдение: монотонность

Пусть f(k) — минимальная возможная сумма покупки ровно k сувениров. Утверждение: если f(k) > S, то и f(k+1) > S. Значит свойство «k сувениров помещается в бюджет» монотонно по k — можно делать бинарный поиск по ответу.

Почему монотонно. Переход от k к k+1 ухудшает ситуацию дважды: во-первых, каждое слагаемое растёт (a[i] + i(k+1) > a[i] + ik), во-вторых, сувениров приходится брать больше, а не меньше. Если уже при k дешевле минимума нельзя — то при k+1 тем более.

Остаётся научиться для фиксированного k быстро считать f(k). Это сводится к задаче: найти сумму k наименьших среди чисел a[i] + i·k для i = 1..n.


3k наименьших элементов через кучу

Классический приём: если нужны k наименьших элементов из потока, используем max-heap размера k. Держим в куче текущих кандидатов, а на вершине — самого «плохого» (наибольшего) из принятых. Когда приходит новый элемент:

  • если в куче меньше k элементов — просто добавляем;
  • иначе сравниваем с вершиной: если новый меньше вершины — вытесняем вершину, кладём нового;
  • если новый больше или равен вершине — игнорируем, он точно не войдёт в k наименьших.

Параллельно поддерживаем sum — текущую сумму элементов в куче. После прохода по всем n элементам в куче лежат ровно k наименьших, а sum и есть f(k).

Альтернатива — просто отсортировать все n значений и взять сумму первых k. Работает за O(n log n) вместо O(n log k). Об этом в последней секции.

4Алгоритм целиком
Внешний цикл — бинпоиск по k
  1. lo = 0, hi = n, ответ bestK = 0, bestT = 0.
  2. На каждой итерации берём mid = (lo + hi) / 2, считаем cost = f(mid).
  3. Если cost ≤ Smid подходит, пробуем больше: bestK = mid, bestT = cost, lo = mid + 1.
  4. Иначе hi = mid - 1.
Внутренняя функция f(k) — куча
  1. Если k == 0 — сумма 0, сразу возвращаем.
  2. Идём по i = 1..n, считаем cost = a[i] + i·k.
  3. Пока в куче меньше k — пушим.
  4. Иначе если cost < top — вытесняем top, пушим cost, обновляем sum.
  5. В конце возвращаем sum.
Тонкость с типами. a[i] до 10⁵, i·k до 10⁵ · 10⁵ = 10¹⁰, а сумма k таких слагаемых — до 10¹⁵. Это точно не влезает в int, все вычисления должны идти в long long.

5Пошаговая симуляция

Выбери массив a и зафиксированное k (внешний бинпоиск здесь опущен для наглядности). Слева — список сувениров со значениями a[i] + i·k, справа — текущее состояние max-heap. Каждый шаг обрабатывает одного кандидата.

Интерактив — куча размера k находит k наименьших значений
Сувениры (costi = a[i] + i·k)
 
Нажми «шаг», чтобы начать симуляцию.
Max-heap размера k (сверху — максимум)
 
sum — текущая сумма в куче 0
 
0 / n

Обрати внимание на момент, когда куча заполнилась и приходит новый кандидат с меньшей стоимостью — вершина (самый дорогой из принятых) вытесняется, сумма уменьшается. Те сувениры, которые игнорируются, становятся блёклыми.


6Реализация
#include <bits/stdc++.h>
using namespace std;

int n;
long long S;
vector<long long> a;

// Минимальная сумма покупки ровно k сувениров
long long minCost(int k) {
    if (k == 0) return 0;
    // max-heap: храним k наименьших значений a[i] + i*k
    priority_queue<long long> pq;
    long long sum = 0;
    for (int i = 1; i <= n; i++) {
        long long cost = a[i] + (long long)i * k;
        if ((int)pq.size() < k) {
            pq.push(cost);
            sum += cost;
        } else if (cost < pq.top()) {
            sum -= pq.top(); pq.pop();
            pq.push(cost);
            sum += cost;
        }
    }
    return sum;
}

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

    cin >> n >> S;
    a.assign(n + 1, 0);
    for (int i = 1; i <= n; i++) cin >> a[i];

    // Бинпоиск: максимальное k, при котором minCost(k) <= S
    int lo = 0, hi = n, bestK = 0;
    long long bestT = 0;
    while (lo <= hi) {
        int mid = (lo + hi) / 2;
        long long cost = minCost(mid);
        if (cost <= S) {
            bestK = mid;
            bestT = cost;
            lo = mid + 1;
        } else {
            hi = mid - 1;
        }
    }

    cout << bestK << ' ' << bestT << endl;
    return 0;
}

В C++ priority_queue<long long> по умолчанию — max-heap, что нам и нужно. Не забывай про (long long)i * k перед умножением, иначе будет переполнение int.

import heapq
import sys

def min_cost(a, n, k):
    if k == 0:
        return 0
    # heapq — min-heap, а нам нужен max-heap,
    # поэтому кладём отрицательные значения
    heap = []
    s = 0
    for i in range(1, n + 1):
        cost = a[i] + i * k
        if len(heap) < k:
            heapq.heappush(heap, -cost)
            s += cost
        elif cost < -heap[0]:         # текущий максимум в куче = -heap[0]
            s -= -heapq.heapreplace(heap, -cost)
            s += cost
    return s

def main():
    data = sys.stdin.read().split()
    n, S = int(data[0]), int(data[1])
    a = [0] + [int(x) for x in data[2:2 + n]]

    lo, hi = 0, n
    best_k, best_t = 0, 0
    while lo <= hi:
        mid = (lo + hi) // 2
        cost = min_cost(a, n, mid)
        if cost <= S:
            best_k, best_t = mid, cost
            lo = mid + 1
        else:
            hi = mid - 1

    print(best_k, best_t)

main()

heapq.heapreplace делает pop+push за одно проседание кучи — чуть эффективнее, чем два отдельных вызова.


7Сложность
Этап Что делает Стоимость
minCost(k) n кандидатов, куча размера k O(n log k)
Бинарный поиск log n вызовов minCost O(n log² n)
Память куча + массив O(n)

При n = 10⁵ это примерно 10⁵ · 17 · 17 ≈ 3·10⁷ операций — укладывается в 1-2 секунды с запасом.


8А что насчёт решения через сортировку?

Раньше мы решали эту задачу другим способом: фиксируем k, считаем все n значений a[i] + i·k, сортируем массив и берём сумму первых k. Сравним два подхода честно.

Асимптотика
Подход Одна проверка f(k) Итого с бинпоиском
Сортировка O(n log n) O(n log² n)
Куча размера k O(n log k) O(n log² n)

Формально одинаково. Разница — в константах и памяти.

Где куча концептуально лучше
  • Память. Сортировка копирует весь массив data = nums на каждую проверку. Куча хранит только k элементов.
  • Малое k. Когда бинпоиск пробует маленькое k (например, 10), сортировка всё равно возится со всем массивом, а куча делает n дешёвых сравнений с вершиной.
  • Поток данных. Если данные приходят по одному — куча работает на лету, сортировка требует всё в памяти.
Где сортировка лучше на практике
  • Код проще. Буквально три строчки: сгенерировать, отсортировать, просуммировать первые k. Меньше шансов ошибиться.
  • Кэш-локальность. std::sort на vector<long long> отлично ложится на кэш — последовательный доступ, предсказуемые ветвления. priority_queue на куче-дереве к кэшу менее дружелюбна. На реальных данных std::sort часто работает быстрее, несмотря на худшую асимптотику.
Вывод. Для этой задачи при n = 10⁵ оба решения проходят с большим запасом. Выбор — стилистический. Куча «правильнее» по смыслу (берём ровно то, что нужно), сортировка проще и часто быстрее по константе. Если хочется честного выигрыша в скорости — переходи на nth_element: он находит k наименьших за O(n), итого O(n log n) на всё решение.
Типичный баг в решении через сортировку

В том варианте бинпоиск стартовал с left = 1. Если даже один сувенир не по карману (пример 3 из условия: n=1, S=7, a=[7]), переменная best остаётся неинициализированной — на выходе мусор. Лечится либо left = 0, либо явной инициализацией best = {0, 0} перед циклом. В решении с кучей мы начинаем с lo = 0 и явно инициализируем bestK = 0, bestT = 0 — случай «ничего не купим» обрабатывается корректно.


9Что запомнить
Схема задачи. «Найти максимальное k при монотонном свойстве» — бинпоиск по ответу. «Найти k наименьших чисел» — max-heap размера k: пушим пока меньше k, иначе сравниваем с вершиной и вытесняем, если новый меньше. Сложность O(n log² n). Типы считаем в long long — произведение i·k до 10¹⁰.
© SilverTests.ru · Разбор · Нубийский рынок (Codeforces 1084E)
Печать