EduBrick
классная работа

Перебор списка с условием

Программирование на Python: от нуля до олимпиад

0/18решено 0 из 18до зачёта осталось 10

Чему научитесь

  • Проверять условия, для которых нужен весь список сразу
  • Находить первый и последний подходящий элемент и не путать их
  • Работать с соседями, не выходя за края списка
  • Считать суммы окон, поправляя результат вместо пересчёта

Как устроено занятие

Сначала разбор с примерами и короткими проверками понимания — они нужны, чтобы поймать непонятое сразу, а не через три темы.

Дальше 15 задач лестницей: разминка, основа, со звёздочкой. Занятие засчитывается, когда решено 10 — остальные не пропадают и учитываются отдельно.

После занятия — вторая часть, ещё 15 задач на те же приёмы в новых сюжетах.

Сколько это займёт

Примерно час-полтора вместе с задачами. Сроков нет: можно закрыть вкладку и вернуться когда удобно — прогресс сохранится.

теория

Что список умеет, а поток не умел

Считать элементы по условию вы умеете с занятия про накопление: завёл счётчик, прошёл, посчитал. Список ничего к этому не добавляет.

Зато он добавляет три вещи, которых у потока не было:

Пройти данные дважды. Задача «сколько элементов больше среднего» в потоке нерешаема: пока не прочитано всё, среднее неизвестно, а прочитанное уже забыто. Со списком — два прохода, и всё.

a = list(map(int, input().split()))
average = sum(a) / len(a)          # первый проход — внутри sum

count = 0
for x in a:                        # второй проход
    if x > average:
        count += 1

Посмотреть вперёд. «Элемент больше обоих соседей» требует знать следующий, а он в потоке ещё не пришёл.

Обратиться по номеру. «Сумма kk подряд идущих», «что стоит между позициями» — всё это про номера, которых у потока нет вовсе.

Отсюда и задачи занятия: не «посчитать по условию» вообще, а именно те условия, которые без списка не проверить.

теория

Первый и последний по условию

Две почти одинаковые задачи с разными решениями.

Первый подходящий. Нашли — и выходим: дальше искать нечего.

answer = 0
for i in range(len(a)):
    if a[i] > x:
        answer = i + 1
        break            # выход сразу
print(answer)

Последний подходящий. Прерывать нельзя — надо дойти до конца, каждый раз обновляя ответ.

answer = 0
for i in range(len(a)):
    if a[i] > x:
        answer = i + 1   # без break: пусть перезапишется
print(answer)

Разница в одной строке, и перепутать их легко. Признак ошибки: программа стабильно выдаёт номер не с той стороны списка.

Обратите внимание на answer = 0 до цикла. Ноль здесь — не номер, а признак «не нашлось»: номера начинаются с единицы, поэтому спутать нельзя. Условие всегда говорит, что выводить, если подходящих нет, — этот случай надо прочитать и обработать.

И ещё: цикл идёт по номерам, а не по значениям. Как только в ответе нужен номер, for x in a не подходит.

тест

Проверка: первый или последний

Что найдёт этот цикл?

answer = 0
for i in range(len(a)):
    if a[i] > 0:
        answer = i + 1
Войдите, чтобы ответить.
теория

Соседи и границы

Задачи про соседей выглядят одинаково, а ломаются всегда на краях.

count = 0
for i in range(1, len(a) - 1):        # без первого и последнего
    if a[i] > a[i - 1] and a[i] > a[i + 1]:
        count += 1

Границы цикла здесь не случайны: у элемента с номером 0 нет соседа слева, у последнего — справа. Возьмёте range(len(a)) — программа упадёт на a[i + 1].

что сравниваем границы цикла
с предыдущим range(1, len(a))
со следующим range(len(a) - 1)
с обоими соседями range(1, len(a) - 1)

Отдельно проверьте, что будет при одном и двух элементах. При len(a) = 1 третий вариант даёт range(1, 0) — пустой промежуток, цикл не выполнится ни разу, и это верно: локальных максимумов там нет.

Про дроби. Условие «элемент равен среднему соседей» лучше писать без деления: 2 * a[i] == a[i - 1] + a[i + 1]. Деление даёт дробь, а сравнение дробей на равенство — источник неточностей.

теория

Окно из k подряд

Задача: найти наибольшую сумму kk подряд идущих элементов.

Прямолинейное решение — для каждого начала посчитать сумму:

for i in range(len(a) - k + 1):
    total = sum(a[i:i + k])           # каждый раз заново

Работает верно, но делает до nkn \cdot k действий. При n=100000n = 100\,000 и k=50000k = 50\,000 это пять миллиардов — не пройдёт.

А между соседними окнами разница крошечная: одно число ушло слева, одно пришло справа. Значит сумму можно не пересчитывать, а поправлять:

total = sum(a[:k])            # первое окно — честно
best = total

for i in range(k, len(a)):
    total += a[i] - a[i - k]  # пришёл новый, ушёл старый
    if total > best:
        best = total

Получается один проход вместо nkn \cdot k действий. Приём называется скользящим окном и встречается всюду, где надо перебрать все куски одинаковой длины.

Та же идея работает и в задачах про «сумму слева и справа»: вместо пересчёта обеих сумм на каждой позиции держат общую сумму и накапливают левую по ходу.

расчёт

Проверка: сколько окон

В списке 10 элементов, длина окна равна 3.

Сколько всего наборов из трёх подряд идущих элементов? Введите целое число.

Войдите, чтобы ответить.
теория

Три ошибки этого занятия

Выход за границу у соседей. a[i + 1] на последнем шаге. Признак: list index out of range. Лечится границами цикла, а не проверками внутри.

Перепутанные «первый» и «последний». Лишний break или его отсутствие. Признак: ответ верный по значению, но номер не тот.

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

Как проверять себя

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

Практика: пятнадцать задач

Лестница прежняя: пять разминочных, семь основных, три со звёздочкой. Зачёт при десяти решённых.

Разминочные — про условия и номера, основные — про соседей и характеристики всего списка, со звёздочкой — про окна и накопление по ходу.

В трёх задачах прямолинейное решение верно, но не укладывается в лимит. Ограничения в условии на это указывают: если написано 10510^5, а ваше решение проходит по списку для каждого элемента — оно не пройдёт.

лестница задач
развёрнутый ответ

Решение, которое не доживает до конца

Задача: «дан список из nn чисел, nn до 10510^5; для каждого элемента посчитайте, сколько элементов слева от него меньше его самого, и выведите наибольшее из этих количеств».

Ученик написал:

n = int(input())
a = list(map(int, input().split()))
best = 0

for i in range(len(a)):
    count = 0
    for j in range(i):
        if a[j] < a[i]:
            count += 1
    if count > best:
        best = count

print(best)

На маленьких тестах ответ верный, на больших — превышение времени. Оцените количество действий, объясните, почему добавление проверок внутрь цикла не поможет, и предложите, в какую сторону думать.

Войдите, чтобы ответить.