Перетягивание каната: разделить набор из n объектов int на подмножества

0

Я нашел проблему Tug Of War, выполняя некоторые алгоритмы в Интернете:

Заявление:

Учитывая набор из n целых чисел, разделите множество на два подмножества n/2 размеров каждый такой, чтобы разность суммы двух подмножеств была как можно меньше. Если n четно, то размеры двух подмножеств должны быть строго n/2, а если n нечетно, то размер одного подмножества должен быть (n-1)/2, а размер другого подмножества должен быть (n + 1)/2,

Например, пусть заданное множество

{3, 4, 5, -3, 100, 1, 89, 54, 23, 20}

размер набора равен 10. Выход для этого набора должен быть равен

{4, 100, 1, 23, 20} 

а также

{3, 5, -3, 89, 54}

Оба выходных подмножества имеют размер 5, а сумма элементов в обоих подмножествах одинакова (148 и 148).

Рассмотрим другой пример, где n нечетно. Пусть заданное множество

{23, 45, -34, 12, 0, 98, -99, 4, 189, -1, 4}

Выходные подмножества должны быть

{45, -34, 12, 98, -1} 

а также

{23, 0, -99, 4, 189, 4}

Суммы элементов в двух подмножествах составляют соответственно 120 и 121.

Я нашел подход к рюкзаку в Интернете, ища решение здесь:

Я не получаю эту часть решения:

for (int i = 1; i <= N; ++i)
    for (int j = sum; j >= 0; --j)
        if (dp[j])
            dp[j + W[i]] |= dp[j] << 1;

Я понимаю, что мы пытаемся найти количество объектов, на которые приходится сумма i. Но делать dp[i] << 1 - это то, что я не получаю должным образом:

Кроме того, почему мы повторяем переменную j назад, начиная с суммы до 0? Почему бы не 0 суммировать?

Может кто-нибудь, пожалуйста, объясните логическую логику более простым и обобщенным образом?

благодаря

  • 1
    Часто повторение в обратном направлении выполняется потому, что операции над элементами с более низким индексом уничтожают или делают недействительными элементы с более высоким индексом. Поэтому элементы с более высоким индексом должны обрабатываться первыми.
Теги:
algorithm
dynamic-programming
bit-manipulation

2 ответа

2

Я думаю, что комментарий делает это довольно ясным, на самом деле.

 // If (dp[i] << j) & 1 is 1, that means it is possible
 // to select j out of the N people so that the sum of
 // their weight is i.

Вы начинаете с начального условия dp [0] = 1 << 0, что означает "можно выбрать 0 человек, так что сумма их веса равна 0".

Затем для каждой записи в dp, отличной от нуля (для части if (dp[j])), вы обновляете dp для текущего пользователя в списке.

Теперь вы спрашиваете "почему dp[j] << 1 "? Итак, представьте, что первые 3 элемента - 1, 2, 3.

Тогда dp [3] будет 110 в двоичном выражении, то есть "вы можете сделать сумму 3, используя 1 человека (3), ИЛИ двух человек (1 и 2).

Если следующее число равно 10, то, когда мы приходим к dp [3], мы делаем dp[3+10] |= dp[3] << 1, делая dp [13] 1100. Значение "так как мы можем сделать 3 с либо 2 человека, или один человек, с дополнительными 10 в миксе, мы можем сделать 13 с 3 людьми (1, 2, 10) или 2 людьми (3, 10) "

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

Обратите внимание, что этот алгоритм НЕ укажет вам, какие номера вам нужно выбрать, чтобы получить эту сумму; эта информация теряется. Он расскажет вам, какие лучшие две суммы. Хотя было бы нелегко немного изменить алгоритм и использовать некоторую структуру данных, чтобы сохранить эту информацию (так что каждая запись в dp говорит: "Я могу сделать эту сумму из трех чисел, а эти числа...").

О, и о повторении в обратном направлении: это не позволяет нам считать один и тот же номер дважды. Если первая запись была 1, мы бы сказали: "Я могу сделать 0 из 0 чисел, теперь я могу сделать 1 из 1 числа". Затем сразу после этого "я могу сделать 1 из 1 числа, теперь я могу сделать 2 из 2 чисел". И так далее.

EDIT: поскольку вы спросили, вот один из способов сделать это (обратите внимание, что он сломается, если вы введете неположительные числа, я оставлю это вам, чтобы исправить это):

int N;
int W[100 + 5];
std::map<int, std::vector<int>> dp[450 * 100 + 5];

void solve()
{
    int sum = accumulate(W + 1, W + N + 1, 0);

    // If (dp[i][j]) contains a nonempty vector, that means it is possible
    // to select j out of the N people so that the sum of
    // their weight is i, with those people indices being the values of said vector
    dp[W[1]][1].push_back(1);

    for (int i = 2; i <= N; ++i)
    {
        for (int j = sum; j >= 0; --j)
        {
            for (std::map<int, std::vector<int>>::iterator it = dp[j].begin(); it != dp[j].end(); ++it)
            {
                dp[j + W[i]][it->first+1] = it->second;
                dp[j + W[i]][it->first+1].push_back(i);
            }
        }
    }

    std::vector<int> selected;
    int minDiff = 450 * 100;
    int teamOneWeight = 0, teamTwoWeight = 0;
    for (int i = 0; i <= sum; ++i)
    {
        if (!dp[i].empty())
        {
            int diff = abs(i - (sum - i));
            if (diff < minDiff)
            {
                minDiff = diff;
                teamOneWeight = i;
                teamTwoWeight = sum-i;
                selected = dp[i].begin()->second;
            }
        }
    }
    cout << "Team 1, sum of " << teamOneWeight << ": ";
    for (int i = 1; i <= N; ++i)
    {
        if (std::find(selected.begin(), selected.end(), i) != selected.end())
            cout << W[i] << ' ';
    }
    cout << endl << "Team 1, sum of " << teamTwoWeight << ": ";
    for (int i = 1; i <= N; ++i)
    {
        if (std::find(selected.begin(), selected.end(), i) == selected.end())
            cout << W[i] << ' ';
    }
    cout << endl;
}
  • 0
    Как мы можем изменить алгоритм для хранения всех элементов, которые генерируют конкретную сумму?
1

Основной подход

Этот подход можно рассматривать как использование динамического программирования для вычисления содержимого трехмерного массива f.

f определяется как:

f[i][sum][j] = 1 если можно использовать j весов в диапазоне 1..i для получения общего веса суммы.

Предположим теперь, что f[i][sum][j] равно 1 для некоторых значений i, sum, j. Если мы решим включить вес i, то мы выводим, что f[i+1][sum+W[i]][j+1] также должно быть истинным.

f[i+1][sum+W[i]][j+1] |= f[i][sum][j]

Если мы не будем включать вес i, то мы выводим, что f[i+1][sum][j] также должно быть истинным.

Оптимизация 1

Первая оптимизация - сохранить третье измерение в одном 64-битном целом, а не в 64 1 битных целых числах. Это заставляет код работать намного быстрее.

Предположим, что f [i] [sum] = двоичный 1001, это означает, что f [i] [sum] [j] равно 1 для j, равному 0 или 3. Теперь мы заключаем, что нам нужно установить f [i] [sum + w [i]] [j] равным 1 для j, равным 1 или 4, поэтому нам нужно ИЛИ с двоичным 10010 = 1001 << 1, это объясняет операцию << 1.

Оптимизация 2

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню