Блог пользователя Kolyanchick

Автор Kolyanchick, история, 22 месяца назад, По-русски

Примечания к задаче

Крош и битовые операции — очень интересная задача, основанная на свойствах операции побитового исключающего ИЛИ, которую можно встретить в Зимнем личном турнире памяти С. А. Григорьева в 2023 году.

Условия к задаче

Данные

Имя входного файла: Стандартный ввод / input.txt

Имя выходного файла: Стандартный вывод / output.txt

Ограничение по времени: 1 секунда

Ограничение по памяти: 256 мегабайт

Условие

Крош недавно ознакомился с битовыми операциями, и ему в голову пришла следующая задача: Пусть дан массив A из 2ⁿ неотрицательных элементов $$$a_{0}$$$,$$$a_{1}$$$,...,$$$a_{2ⁿ−1}$$$. Ему нравится операция побитового исключающего ИЛИ – xor, поэтому он рассматривает все пары индексов (i,j), такие, что i⊕j=k, где k – заданное число, ⊕ – операция побитового исключающего ИЛИ. По всем таким парам он хочет найти наибольшее суммарное значение величины $$$a_{i}$$$+$$$a_{j}$$$.

Формат входных данных

В первой строке вам дано число n (1 ≤ n ≤ 18) и число k (1 ≤ k < 2ⁿ). В следующей строке вам даны 2ⁿ целых неотрицательных чисел – $$$a_{0}$$$,$$$a_{1}$$$,...,$$$a_{2ⁿ−1}$$$ (0 ≤ $$$a_{i}$$$ ≤ 10⁹).

Формат выходных данных

Выведите ответ на задачу.

Система оценки

  • Подзадача 1 (40 баллов) n ≤ 9.
  • Подзадача 2 (60 баллов) без дополнительных ограничений.

Пример

Стандартный ввод:

3 3

1 4 2 7 3 1 5 2

Стандартный вывод:

8

Объяснение задачи

Ввод

Первым делом, мы должны прописать правильный ввод данных.

Реализовать ввод нескольких значений в одну строку можно при помощи встроенной функции map:

n, k = map(int, input().split())

Таким образом, мы присвоили переменным n, k значения, введя их в одну строку. Тоже самое мы делаем с набором чисел a. При этом, мы преобразуем его в список при помощи функции list:

a = list(map(int, input().split()))

Теперь, в переменной a хранится список, состоящий из введённых во второй строке чисел через пробел. Получаем код:

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

Алгоритм решения

Что же делать дальше? Как решить задачу?

40 баллов ✓

Первое, что может прийти в голову — это тупой перебор, реализуется он довольно просто.

Давайте попробуем его написать, для начала создаём вложенный цикл, в котором мы будем перебирать индексы i и j:

n, k = map(int, input().split())
a = list(map(int, input().split()))
for i in range(2 ** n):
    for j in range(2 ** n):
        pass

Мы перебираем индексы именно до 2ⁿ, так как длина списка по условию задачи равна 2ⁿ. Ничего не делающий оператор-заглушка pass оставлен для удобства, он показывает место, где мы ещё не успели дописать код.

Теперь нам нужно сделать поиск максимума, это тоже не так трудно. Перед началом цикла создадим переменную, в которую будем записывать максимум, а в самом цикле сделаем проверку выполнения равенства i⊕j=k и обновление максимума, которое ориентируется на сумму i-того и j-того элементов. Не забудем и про вывод ответа, в результате получив следующий код:

n, k = map(int, input().split())
a = list(map(int, input().split()))
mx = -1
for i in range(2 ** n):
    for j in range(2 ** n):
        if i ^ j == k and a[i] + a[j] > mx:
            mx = a[i] + a[j]
print(mx)

Обратите внимание, что за выполнение операции побитового исключающего ИЛИ в языке Python отвечает оператор ^.

Из-за вложенного цикла данный код будет работать довольно медленно, из-за чего он превысит ограничение по времени выполнения на второй подзадаче. В результате, за этот код мы получим 40 баллов.

100 баллов ✓✓

Как же оптимизировать алгоритм и не напороться на ограничение по времени выполнения? Для этого нам нужно избавиться от вложенного цикла из решения на 40 баллов, так как он сильно замедляет время работы нашей программы.

Сделать это можно, опираясь на интересное свойство операции xor. Это свойство гласит о том, что если выполняется равенство i⊕j=k, то тогда j=i⊕k (в этом можно убедиться и самостоятельно, подобрав разные числа и сравнив значения чисел i, j, k). Таким образом, зная индекс i, мы можем найти подходящий к нему индекс j при помощи числа k (т. к. j=i⊕k). Это означает, что мы можем перебирать только индексы i и уже по ним находить индексы j и проводить поиск максимальной суммы.

Вот так мы и должны заменить вложенный цикл на линейный, что позволит нам обойти ограничение по времени выполнения нашей программы. Теперь осталось лишь написать наш "победный" код. Сделать это очень просто, для начала создадим линейный цикл, перебирающий индексы i:

n, k = map(int, input().split())
a = list(map(int, input().split()))
for i in range(2 ** n):
    pass

Мы перебираем индексы именно до 2ⁿ, так как длина списка по условию задачи равна 2ⁿ. Ничего не делающий оператор-заглушка pass оставлен для удобства, он показывает место, где мы ещё не успели дописать код.

Далее нам нужно сделать поиск максимума, это тоже не так трудно. Перед началом цикла создадим переменную, в которую будем записывать максимум, а в самом цикле будем находить индекс j по формуле j=i⊕k и обновлять максимум, ориентируясь на сумму i-того и j-того элементов. Не забудем и про вывод ответа, в результате получив следующий конечный код:

n, k = map(int, input().split())
a = list(map(int, input().split()))
mx = -1
for i in range(2 ** n):
    j = i ^ k
    if a[i] + a[j] > mx:
        mx = a[i] + a[j]

Обратите внимание, что за выполнение операции побитового исключающего ИЛИ в языке Python отвечает оператор ^.

За счёт использования в коде линейного цикла вместо вложенного, данная программа не превысит ограничения по времени ни на одной из подзадач, в результате чего мы получим 100 баллов за её решение. Вот и всё, задача решена.

Мой комментарий

Вот таким вот получился разбор, очень надеюсь, что вам он понравился. Если так и есть, то обязательно голосуйте плюсиком за этот разбор, мне будет очень приятно :)

Обратите внимание, что задача для этого разбора намного сложнее задачи для предыдущего, так что если вам что-то непонятно, вы хотите предложить задачу на разбор или рассказать о вашем решении, то обязательно пишите в комментариях ваши сообщения, я буду очень рад с вами пообщаться!

Всем удачи! :D

  • Проголосовать: нравится
  • +21
  • Проголосовать: не нравится

»
5 дней назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Здравствуйте, где можно найти факты про XOR, просто сталкиваюсь с рядом задач на эту тему и нигде не могу найти собранный список фактов, которые можно использовать для решения подобных проблем?

  • »
    »
    40 часов назад, # ^ |
    Rev. 4   Проголосовать: нравится +1 Проголосовать: не нравится

    (ого, этот кринж, который я написал ради вклада, ещё кто-то читает)

    Вот все основные свойства XOR, которые я когда-либо использовал для решения задач (все они достаточно очевидны):

    1. Самообратимость: $$$a \oplus a = 0$$$ (операция ксор обратна самой себе, так же, как, например, — обратен +)

    2. Нейтральный элемент: $$$a \oplus 0 = a$$$ (число $$$0$$$ является так называемым нейтральным элементом для операции XOR, так как никак не влияет на результат операции, и поэтому его можно просто убирать, если он встречается в выражениях)

    3. Отрицание: пусть $$$a \oplus b = c$$$, тогда если $$$i$$$-й бит в $$$b$$$ равен $$$1$$$, то $$$i$$$-й бит в $$$c$$$ обратен $$$i$$$-му биту в $$$a$$$

    4. Коммутативность: $$$a \oplus b = b \oplus a$$$ (неважно, в каком направлении применять операцию, слева направо или справа налево)

    5. Ассоциативность: $$$(a \oplus b) \oplus c = a \oplus (b \oplus c)$$$ (неважно, в каком порядке применять операции)

    Из этих свойств следуют много других. Парочку особо важных из них я тоже здесь приведу.

    1. В данной задаче имеем равенство $$$i \oplus j = k$$$. Проксорим левую и правую часть с числом $$$i$$$: $$$i \oplus j \oplus i = k \oplus i$$$. По первому свойству $$$i \oplus i = 0$$$, поэтому два числа $$$i$$$ в левой части можно убрать, и у нас останется $$$j = k \oplus i$$$. Таким образом, утверждение из разбора задачи доказано. Получается, что если у нас есть какое-то равенство, в котором встречаются только операции XOR, то тогда мы можем свободно перетаскивать любые числа в этом равенстве из одной части в другую, не изменяя операцию XOR, которая с ними производится. Это достаточно полезное свойство.

    2. Из первого и второго свойств следует, что если у нас есть выражение $$$a \oplus a \oplus \ldots \oplus a \oplus a$$$, то если количество чисел $$$a$$$ в нём нечётно, то его результат равен $$$a$$$, а если чётно, то его результат равен $$$0$$$. Это полезное свойство, благодаря которому можно избавляться от одинаковых чисел в выражениях с XOR.