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

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Рассмотрите один столбик (допустим, номер i). Какой высоты в нем может быть вода? Обозначим за x. Она не должна выливаться ни слева ни справа. Значит, с обеих сторон должен быть столбик с высотой хотя бы x. Раз есть хотя бы один, то максимум точно должен быть хотя бы x. Отсюда x <= max(h[0..i-1]) и x <= max(h[i+1..n-1]) Значит высота воды в i-ом столбике: min(max(h[0..i-1]), max(h[i+1..n-1])). Уже можно просто вот это подсчитать за 2 прохода и получить решение. Ну, только не забыть что там надо max(x, h[i])-h[i] к ответу прибавить, ведь если текущий столбик слишком высокий, то вся вода с него стечет.

    Но можно думать дальше. Давайте обрабатывать не все столбики подряд, а посмотрим с краев. С крайних столбиков все, понятно, вытекает наружу. Пусть крайний левый меньше. Рассмотрим второй столбик слева. Мы уже знаем max(h[0..i-1]) в нем - это один левый столбик. И оно точно меньше max(h[i+1..n-1]), потому что справа уже есть крайний столбик, который выше самого левого. Мы незнаем точное значение max(h[i+1..n-1]), но мы знаем что max(h[i+1..n-1]) >= h[n-1] >= h[0] = max(h[0..i-1]). Вот мы знаем высоту воды в столбике 1, мы же берем минимум из двух значений. Даже если мы не знаем максимум справа, мы знаем, что он точно больше максимума слева и в ответе не участвует.

    Вот мы и обработали второй столбик. Теперь перейдем к третьему. При этом можно первые 2 столбика объеденить в один высоты max(h[0], h[1]). Это и есть сдвиг указателя, только при этом мы смотрим не на сам столбик, а аккумулируем максимумы с краев.

    Но, если бы мы смотрели на второй справа столбик, рядом с большим их двух крайних, то мы не могли бы сразу сказать, а какая там высота воды. Мы в нем знаем max(h[i+1..n-1]) - высота последнего столбика. Но какое в нем max(h[0..i-1]) мы не знаем и не можем сказать, больше ли оно или нет, а значит, не можем посчитать x.
    Ответ написан
    1 комментарий
  • Всегда ли DP можно представить в виде DAG?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Тут вопрос определений. Что считать "представить в виде DAG".

    В ДП можно составить граф зависимостей: какие состояния участвуют в вычислении каждого. Это будет DAG. Иначе у вас надо вычислять значения через самих себя и у вас уже не рекуррентные соотношения, а система уравнений.
    В некоторых задачах эту систему уравнений можно решать иерархически, по частям, отдельно в каждой компоненте сильной связности. Это немного напоминает ДП, но им не является. Суть ДП в том, что у вас рекуррентные формулы. Это некий более общий алгоритм.

    Если интерпретировать вопрос как: можно ли сформулировать задачу в виде "дан граф, подсчитать вот такую функцию в каждой вершине", то тут можно натянтуть сову на глобус, придраться к деталям и сказать, что нельзя. Именно это и говорится в SO.

    Потому что иногда рекуррентные формулы не симметричные и вам надо, например, одно значение прибавить, другое вычесть. Это не всегда просто определить исключительно в терминах графа. Нельзя сказать что-то вроде "сложить значение во всех вершинах, куда ведут ребра". Но если добавить в этот граф пометки на ребрах или параметры состояний в вершинах, то можно задать нужную функцию (вроде: взять значение для вершины, в которую ведет ребро с пометкой 1 и вычесть значение из вершины, куда ведет ребро с пометкой 2).

    Но эти помеченные графы все еще будут DAG.
    Ответ написан
    3 комментария
  • What is the running time of insertion sort?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Квадратичное. При вставке каждого элемента в среднем отсортированная часть будет на треть состоять из 0, на треть и 1 и на треть и 2. Если текущий элемент 2, то вставка за O(1), если текущий элемент 1, то вы потратите i/3 операций. Если элемент 0, то 2/3i. В среднем i/3 операций на один элемент. Если просуммировать это от 1 до n, то получится n(n-1)/6. Значит общее количество операций O(n^2).
    Ответ написан
    2 комментария
  • Какой смысл prime nulls в hungarian algorithm?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Почитайте вот тут описание алгоритма: e-maxx.ru/algo/assignment_hungary

    По вашей ссылке звездочками отмечены нули, соответствующие ребрам в текущем паросочетании (потенциалы уже вычтены из всех значений в матрице, поэтому нули соответствуют жестким ребрам). "primed", т.е. отмеченные символом ' нули соответствуют ребрам, которые вы перебираете в удлинняющей цепи. Помеченные столбцы и строки - соответствуют обойденным вершинам.

    Смысл в том, что ' нули вытесняют какие-то * нули и в итоге у вас получается на 1 ноль больше в паросочетании.
    Ответ написан
    Комментировать
  • Покрытие графа циклами?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Вы расписали то, что в доказательстве скрывается за "clearly, any 2-factor of G translates into a perfect matching of G' and vice versa". Можно было бы чуть поформальнее, но основная идея правильная. Или можно еще проще: разбиение на циклы равносильно перестановке, которая для каждой вершины задает следующую в цикле. Перестановка равнозначна максимальному паросочетанию.

    Действительно, полное парсочетание становится циклами. Да, если граф не ориентированный, или содержит "тривальные" циклы (длины 2), то они могут войти в покрытие. Если найдется не полное паросочетание, то в графе будут какие-то непересечающиеся циклы и возможно пути. Он может быть даже не весь покрыт при этом.
    Ответ написан
    3 комментария
  • Как добавляются потенциалы тогда в Hungarian algorithm?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    вы ссылку-то свою вообще читали?
    ( $u[i]=v[i]=0$  for all  $i$ ),

    В начале. Вы начинаете с нулевых потенциалов, потом увеличиваете их как описанно в алгоритме, пока можете. Попутно обновляя множество H.

    Кстати, даже при нулевых потенциалах H может быть не пустым, если в матрице есть нули.
    Ответ написан
  • Может ли такое быть, что менее продвинутый алгоритм сортировки выполняется быстрее?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Разные алгоритмы имеют разные лучшие и худшие случаи. Например, на почти отсортированном массиве сортировка шелла отработает весьма быстро - сильно быстрее сортировки случайного массива. Сортировка слиянием же выполнит примерно одинаковое количество операций для любого массива. Так что можно подобрать даже весьма большой тест, на котором сортировка Шелла обгонит сортировку слиянием.
    Ответ написан
    Комментировать
  • Можно ли добиться постоянного O(nlogn) для квиксорта в любом случае?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Да, можно. Для этого надо в качестве pivot'а выбрать медиану. если это сделать за O(n) в худшем случае, то общая сложность QuickSort'а будет O(n log n).

    Для выбора медианы за O(n) есть, например, вот такой алгоритм. В каких-то источниках его еще называли алгоритмом кнута-пратта-мориса-ривеста-тарьяна. Кажется, но я их найти не могу, так что я какие-то фамилии напутал, но помню, что там было 5 великих информатиков.

    На практике же это не применяют, потому что этот алгоритм хоть и имеет линейную сложность в худшем, константа там такая, что там на квадрат хватит и еще на логарифм сверху останется. Просто используя какие-нибудь случайные числа можно добиться гораздо лочшей производительности.
    Ответ написан
    Комментировать
  • Как из критерия унимодальности следует унимодальность функции?

    wataru
    @wataru Куратор тега Математика
    Разработчик на С++, экс-олимпиадник.
    В унимодальной функции разности соседних элементов сначала неотрицательные а потом неположительные. И наоборот, если разности такие, то, очевидно, сначала функция возрастает (возможно, ступенчато) а потом убывает.

    Вы доказали, что частичные разницы не возрастают. Тут могут быть 3 варианта:
    1) они все неотрицательные. Функция унимодальна (монтонность с максимумом в конце - частный случай унимодальности).
    2) они все неположительные. Монотонно убывающая функция.
    3) есть и положительные и отрицательные. Но раз разности не могут возрастать, после первого 0 могут идти только нули а после отрицательного только отрицательные. А заначит разности выглядят ровно так, как у унимодальной функции.
    Ответ написан
    Комментировать
  • Алгоритм для поиска мин. разреза?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    У вас ошибка в понимании остаточной сети. Раз фиолетовые ребра насыщены, то на самом деле в остаточной сети есть обратные им ребра. И вершина v отлично найдется обходом из s.

    Без этих обратных ребер алгоритм поиска потока не работает, ведь у него не будет возможности "отменять" потоки.
    Например в графе:
    ________    
      a-b-c
     /  |   \
    s   |     t
    \   v   /
      d-e-f


    Если сначала найдется поток по пути s-a-b-e-f-t, то дальше единственный способ его дополнить - это взять путь s-d-e-b-c-t, и надо будет "отменить" поток по перекрестному ребру b-e.
    Ответ написан
    1 комментарий
  • Можно ли обойтись без бин. поиска?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Именно max flow, а не mincostmaxflow? Потому что есть очевидное решение с mincostmaxflow.

    Граф почти такой же. Раздвоенное ребро. В середину каждого ребра вход из истока пропускной способностью 1 и стоимостью 0. Вдоль ребра в обе стороны такие же ребра - пропускной способности 1 и стоимости 0. Из вершины делаем degree(v) ребер в сток. Каждое пропускной способностью 1 и прогрессивно увеличивающимися стоимостями. Первые ребра стоимостью 1. Вторые - n+1, сделующие (n+1)^2 и т.д.

    Стоимость полного потока будет тем меньше, чем меньше максимальная степень, ведь даже одно ребро стоимостью (n+1)^k для вершины степени k+1 хуже чем если бы все вершины имели степень k и ответ был бы n*(n+1)^(k-1).

    Если нужен именно просто поток, а не минимальной стоимости, то возможно можно изменить алгоритм потока, чтобы искать его итеративно для всех возможных значений x в вашем графе. Ведь алгоритм итерационно ищет дополняющий путь в остаточной сети. И ответ для x можно использовать в качестве стартового потока для задачи с x+1. Т.е не надо логарифм раз запускать поток для разных графов, а после выполнения увеличить пропускные сопособности нужных ребер на 1 и запустить поток дальше. Это будет как если бы вы запустили поток один раз с максимальной пропускной способностью сразу.
    Ответ написан
    2 комментария
  • Расширенный алгоритм Евклида с усеченными остатками?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Идея этого усечения в том, что если вы ищите gcd(a, b), то можно также искать gcd(a,a-b). Вот эта замена b' = a-b у вас в коде и происходит.

    Вы там поддерживаете инвариант x_i*a+y_i*b = r_i

    Сответсвтенно, вам надо пересчитать x_cur и у_cur вместе с изменением r_cur.

    У вас есть x_prev*a+y_prev*b = r_prev и x_cur*a+y_cur*b = r_cur. Вам надо составить линейную комбинацию r_prev - r_cur. Ну вычтите одно уравнение из другого и все.

    Соответственно вам надо сделать вот это при применении вашего усечения:
    x_cur = x_prev - x_cur
    y_cur = y_prev - y_cur
    Ответ написан
  • Как свести задачу к минимальному разрезу?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Вам нужен 2-дольный граф. Левая доля соответствует строкам, правая - столбцам. Из истока ребора в левую долю (ценой ri), из правой доли - в сток (ценой ci). Ребра между долями ценой aij.

    Любой разрез тут соответствует покрытию всех вершин. Вы должны разрезать ребро между ij или si или jt что чоответствует покрытию клетки, строки иои столбца.
    Ответ написан
    1 комментарий
  • Где ошибка в доказательстве?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Доказательство не верно из-за индуктивного перехода. Вы рассматриваете только уже отсортированные наборы задач. Не ясно, что добавляя задачи в другом порядке вы получаете варианты не лучше. Вы так же не показали, что вот эти 2 варианта дают оптимальное время для k+1 задачи.

    Правильное доказательство весьма простое и без индукции. Общее время будет
    Max(sum{i-начальнику}(bi), max{i - подчиненным}(ai))
    .
    Отсюда видно, что если вы какую-то задачу дали подчиненному, то все задачи с меньшим A нет смысла давать начальнику. Ведь второе слагаемое-максимум от этого не изменится, но первое увеличится. Можно "бесплатно" выполнить эти задачи подчиненными. Поэтому в оптимальном ответе вы даете какое-то множетсво минимальных A подчиненым, а остальные начальнику. Иначе можно какие-то задачи перекинуть с начальника на подчиненного и уменьшить первое слагаемое не меняя второе, уменьшив таким образом ответ, т.е. в этом случае решение точно не оптимальное.
    Чтобы все варианты возможных оптимумов перебрать надо отсортировать задачи по возрастанию A. Это код и делает. поддерживается сумма B у всех оставшихся задач, а от текущей берется A - который и будет максимумом для всех задач с меньшим A.
    Ответ написан
    Комментировать
  • Как решать задачу?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Надо сделать несколько наблюдений: во-первых, нам без разницы, в каком порядке шары на каждом уровне - важны лишь количества там шаров всех 4 цветов. Во-вторых, если на каком-то уровне остались только белые шары - то мы этот уровень больше никогда трогать не будем. В-третьих, что бы мы не делали на одном уровне - это никак не влияет на другие уровни. Поэтому можно их все рассматривать независимо. Надо решить задачу для каждого уровня отдельно и просуммировать количество дней (и единицы, если на уровне можно что-то оставить).

    Рассмотрим теперь один уровень, который описан 4 числами a,b,c,d и нам надо оставить как можно больше шаров белого цвета (их d). За один ход мы можем приравнять к 0 одно из 4 чисел и вычесть по 1 из отсавшихся ненулвевых. Ясно, что нет смысла занулять d. Т.о. за 3 хода мы можем получть 0,0,0,max(0,d-3). Но, например, если у нас было 2 2 2 3, то занулив a и b мы уменьшениями на 1 зануляем и c. Т.е. для маленьких чисел имеет смысл подумать в каком порядке их занулять. Но мне лень даже думать как именно - ведь их всего 3 числа - можно тупо перебрать все 6 перестанвок и выбрать ту, в которой за наименьшее количество ходов мы их все занулим.
    Ответ написан
    6 комментариев
  • Почему в Python последовательность append-ов суммируется в O(n), а не в O(n logn)?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    а общее число перераспределений для n добавлений уменьшается как O(logn). Почему тогда в таком случае для n последовательности append-ов общая сложность равна O(n), а не O(n logn)?


    Потому что эти распределения разного размера. Первое - совсем маленькое. Второе чуть больше и т.д. Чтобы суммарно было O(n Log n), каждое из них должно быть пропорционально n.

    1+4+10+⋯≤O(n)


    Вот это и есть ответ. Первые слагаемые маленькие. Получается геометрическая прогрессия, которая дает линейную сумму от n.
    Ответ написан
    Комментировать
  • Как корректно распределить сумму внутри элементов массива?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Во-первых, давайте считать в копейках. Умножте все цены на 100 перед началом алгоритма, и поделите на 100 при выводе. Потому что никогда нельзя деньги считать или хранить в вещественных числах. При сериализации и внешних интерфейсах, конечно, придется запятые ставить, но лучше при выводе выводить x/100, "," и x%100. И молиться, что у читателя хватит точности это прочитать, если он, конечно, не заморачивается с подобным трюком.

    Далее алгоритм (на псевдокоде):
    total_price - изначальная цена. goods[i].price - цена товара, goods[i].amount - количество товара, discount - сколько скидки.
    discount_left = discount
    for i in 1..n {
      cur_discount = discount * goods[i].price / total_price # целочисленное деление нацело с округлением вниз.
      goods[i].price -= cur_discount
      discount_left -= cur_discount*goods[i].amount
    }
    for i in 1..n {
      if discount_left == 0: break
      if goods[i].amount <= discount_left {
        goods[i].price -= 1;
        discount_left -= goods[i].amount
      }  else {
       // разбиваем категорию на 2 штуки, в одной discount_left товаров, в другой остальное.
       new_good = Good{.price = goods[i].price-1, .amount = discount_left}
       goods[i].amount -= discount_left
       discount_left = 0
       goods.append(new_good)
       break
      }
    }


    Как это работает: если бы мы могли идеально делить копейки с любой точностью, то каждый товар получил бы скидку price*discount/total_price. Одинаковый процент скидки везде. но у нас-то целые копейки, поэтому если мы эти скидки округлим вниз, то мы сколько-то копеек недоскинем. Но для каждой штуки товара мы потеряли меньше одной копейки, а значит суммарно - меньше общего количества товаров. Значит можно из каких-то товаров вычесть 1 и все сойдется. Вот и вычитаем из первых discount_left товаров. Если категория целиком покрыта - просто уменьшаем у нее цену. Если нет, то разбиваем на 2 куска и у одного цену уменьшаем.

    Тут могут быть проблемы, если скидка очень большая и есть товары очень маленькой стоимости. Скажем, товар стоимостью 5 копеек и кидка в 85% уже между 0 и 1. Округленная вниз она будет 4, но вот если вы из этого товара потом еще и 1 вычтите, то может так получится, что вы снизите цену до 0. Допустимо ли это? Чтобы этого избежать надо товары отсортировать по убыванию цены. Но все равно может не помочь, тогда надо тогда такие товары ценой в 1 копейку исключить из рассмотрения и запустить цикл вычитания 1 опять, если после цикла discount_left не 0.

    Второй момент, если вам очень хочется избегать разбиения категорий на части, то можно попробовать реализовать динамическое программирование для решения задачи размена монет, и таким образом найти какие категории надо взять целиком, чтобы набрать discount_left. Но вряд ли вам это так уж надо, ибо предвижу весьма частый случай, когда разбивать все-равно придется. Да и разбиение тут максимум одно происходит.
    Ответ написан
    Комментировать
  • Как доказать что мы действительно не пропустим такую пару i,j которая дает правильный ответ в методе двух указателей?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    В такой реализации это плохо видно, но можно переписать основной цикл так:
    j = Len(B)-1
    for i in range(len(A)):
      while j >= 0 and A[i] + B[j] > c:
        --j;
      if A[i] + B[j] == c:
        found = True
        break


    В этом случае после цикла while поддерживается инвариант, что a[i]+b[j] <= c и это максимальное такое j.
    Ведь, если этот инвариант поддерживался для пердыдущей итерации, то у нас было a[i-1] +b[j] <=c и a[i-1]+b[j+1]>c. Отсюда получается, a[i]+b[j+1] >= a[i-1]+b[j+1] > c. Т.е. если мы уменьшим j в цикле while инвариант останется действовать - это будет самое большое j, т.ч. a[i]+b[j] <= c.

    А этот инвариант гарантирует, что когда в цикле по i переберется значение из ответа, j гарантированно будет указываать на j из ответа. Потому что, если существуют i', j', т.ч. a[i']+b[j'] = c, то можно увеличить j' пока b там не меняется, т.ч. b[j']>b[j'], отсюда получается a[i']+b[j'] <=c и a[i']+b[j+1] > c - а это и есть наш инвариант. Т.е. на итерации i=i' найдется именно j=j'.
    Ответ написан
    3 комментария
  • Как найти ближайщую точку из списка географических координат?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Если у вас задача найти ближайшую к одной один раз, то весь "алгоритм" - цикл for.
    Перебираете все точки из файла, считаете расстояние по формуле.

    Как сказал Alexandroppolus можно считать не расстояние, а квадрат. Если координаты близкие, то можно упрастить формулу еще больше, но это будет небольшой выигрыш в скорости.

    Если же вам надо много раз для многих точек находить ближайшую из заданных, то имеет смысл построить какой-то индекс по опорным точкам. Какое-нибудь R-tree или квадро-дерево.
    Ответ написан
    Комментировать
  • Есть ли у этой задачи название?

    wataru
    @wataru Куратор тега Алгоритмы
    Разработчик на С++, экс-олимпиадник.
    Если вам можно брать вершины только из одной доли, то это задача set cover (покрытие множества). Каждая вершина в R задает множество в L и вам надо взять минимальное их количество, чтобы все L было покрыто. Это NP-сложная задача и у нее есть только медленные решения, вроде перебора. Еще можно свести ее к задаче целочисленного линейного программирования и решать каким-нибудь солвером.
    Ответ написан
    2 комментария