Проблема суммы подмножества

У меня есть проблема с подсчетом, который является продолжением этого вопроса. Я не действительно математический человек, таким образом, мне действительно трудно выяснить это subset sum problem который был предложен в качестве разрешения.

Я имею 4 ArrayList в котором я держу данные: alId, alTransaction, alNumber, alPrice

Введите | транзакция | число | цена
8 | Покупают | 95.00000000 | 305.00000000
8 | Покупают | 126.00000000 | 305.00000000
8 | Покупают | 93.00000000 | 306.00000000
8 | Передача | 221.00000000 | 305.00000000
8 | Передача в | 221.00000000 | 305.00000000
8 | Продают | 93.00000000 | 360.00000000
8 | Продают | 95.00000000 | 360.00000000
8 | Продают | 126.00000000 | 360.00000000
8 | Покупают | 276.00000000 | 380.00000000

В конце я пытаюсь получить то, что уехало в клиента и что оставляют, я поместил в 3 списка массива:
- alNewHowMuch (соответствует alNumber),
- alNewPrice (соответствует alPrice),
- alNewInID (corrseponds к alID)

        ArrayList alNewHowMuch = new ArrayList();
        ArrayList alNewPrice = new ArrayList();
        ArrayList alNewInID = new ArrayList();
        for (int i = 0; i < alTransaction.Count; i++) {
            string transaction = (string) alTransaction[i];
            string id = (string) alID[i];
            decimal price = (decimal) alPrice[i];
            decimal number = (decimal) alNumber[i];
            switch (transaction) {
                case "Transfer out":
                case "Sell":
                    int index = alNewHowMuch.IndexOf(number);
                    if (index != -1) {
                        alNewHowMuch.RemoveAt(index);
                        alNewPrice.RemoveAt(index);
                        alNewInID.RemoveAt(index);
                    } else {
                        ArrayList alTemp = new ArrayList();
                        decimal sum = 0;
                        for (int j = 0; j < alNewHowMuch.Count; j ++) {
                            string tempid = (string) alNewInID[j];
                            decimal tempPrice = (decimal) alNewPrice[j];
                            decimal tempNumbers = (decimal) alNewHowMuch[j];
                            if (id == tempid && tempPrice == price) {
                                alTemp.Add(j);
                                sum = sum + tempNumbers;
                            }
                        }
                        if (sum == number) {
                            for (int j = alTemp.Count - 1; j >= 0; j --) {
                                int tempIndex = (int) alTemp[j];
                                alNewHowMuch.RemoveAt(tempIndex);
                                alNewPrice.RemoveAt(tempIndex);
                                alNewInID.RemoveAt(tempIndex);
                            }
                        }
                    }
                    break;
                case "Transfer In":
                case "Buy":
                    alNewHowMuch.Add(number);
                    alNewPrice.Add(price);
                    alNewInID.Add(id);
                    break;
            }
        }

В основном я добавляю и удаляю вещи из Массива в зависимости от Типа Транзакции, идентификатора Транзакции и Чисел. Я добавляю числа к ArrayList как 156, 340 (когда это - TransferIn, или Купите), и т.д., и затем я удаляю их делающий его как 156, 340 (когда это - TransferOut, Продайте). Мое решение работает на это без проблемы. Проблема, которую я имею, состоит в том, которые для некоторых старых сотрудников данных вводили сумму как 1500 вместо 500+400+100+500. Как я изменил бы его так, чтобы, когда существует Sell/TransferOut или Buy/Transfer In и там не идет ни в какое сравнение в ArrayList, он должен попытаться добавить несколько объектов от этогоArrayList и найдите элементы тем объединением в агрегат.

В моем коде я пытался разрешить, что проблема с простым подведением итогов всего, когда там не идет ни в какое сравнение (индекс == 1)

                    int index = alNewHowMuch.IndexOf(number);
                    if (index != -1) {
                        alNewHowMuch.RemoveAt(index);
                        alNewPrice.RemoveAt(index);
                        alNewInID.RemoveAt(index);
                    } else {
                        ArrayList alTemp = new ArrayList();
                        decimal sum = 0;
                        for (int j = 0; j < alNewHowMuch.Count; j ++) {
                            string tempid = (string) alNewInID[j];
                            decimal tempPrice = (decimal) alNewPrice[j];
                            decimal tempNumbers = (decimal) alNewHowMuch[j];
                            if (id == tempid && tempPrice == price) {
                                alTemp.Add(j);
                                sum = sum + tempNumbers;
                            }
                        }
                        if (sum == number) {
                            for (int j = alTemp.Count - 1; j >= 0; j --) {
                                int tempIndex = (int) alTemp[j];
                                alNewHowMuch.RemoveAt(tempIndex);
                                alNewPrice.RemoveAt(tempIndex);
                                alNewInID.RemoveAt(tempIndex);
                            }
                        }
                    }

Но это только работает, если определенные условия соблюдены, и сбои для остальных.

Править: Так как некоторые из Вас были так удивлены (и ослеплены) моими именами переменной полировки, я перевел всех их в английский язык для простоты и видимости. Надо надеяться, это поможет мне получить некоторую справку :-)

5
задан Community 23 May 2017 в 12:13
поделиться

2 ответа

Как вы должны это делать, зависит от ряда важных вещей: сколько чисел у вас будет и насколько они будут большими? Кроме того, насколько я понимаю, ваши данные могут изменяться (добавлять / удалять числа и т. Д.), Не так ли? Как часто вам нужно делать эти запросы?

Я представлю два решения. Я предлагаю вам использовать второй, поскольку я подозреваю, что он лучше для того, что вам нужно, и его намного легче понять.

Решение 1 - динамическое программирование

Пусть S [i] = true, если мы можем сделать сумму i, и false в противном случае.

S[0] = true // we can always make sum 0: just don't choose any number
S[i] = false for all i != 0
for each number i in your input
    for s = MaxSum downto i
        if ( S[s - i] == true )
            S[s] = true; // if we can make the sum s - i, we can also make the sum s by adding i to the sum s - i.

Чтобы получить фактические числа, составляющие вашу сумму, вы должны оставить другой вектор P [i] = последнее число, которое использовалось для создания суммы i . Вы должны обновить это соответственно в , если условие выше.

Временная сложность этого составляет O (numberOfNumbers * maxSumOfAllNumbers) , что довольно плохо, особенно с учетом того, что вам придется повторно запускать этот алгоритм всякий раз, когда ваши данные меняются.Это также медленно даже для одного запуска, если ваши числа могут быть очень большими, а их может быть много. На самом деле «много» вводит в заблуждение. Если у вас есть 100 чисел, и каждое число может достигать 10 000, вы будете выполнять примерно 100 * 10 000 = 1 000 000 операций при каждом изменении данных.

Это хорошее решение, но оно не очень полезно на практике, по крайней мере, в вашем случае, я думаю.

У него есть C # для подхода, который я предлагаю:

   class Program
      {
        static void Main(string[] args)
        {
            List<int> testList = new List<int>();

            for (int i = 0; i < 1000; ++i)
            {
                testList.Add(1);
            }

            Console.WriteLine(SubsetSum.Find(testList, 1000));

            foreach (int index in SubsetSum.GetLastResult(1000))
            {
                Console.WriteLine(index);
            }
        }
    }

    static class SubsetSum
    {
        private static Dictionary<int, bool> memo;
        private static Dictionary<int, KeyValuePair<int, int>> prev;

        static SubsetSum()
        {
            memo = new Dictionary<int, bool>();
            prev = new Dictionary<int, KeyValuePair<int, int>>();
        }

        public static bool Find(List<int> inputArray, int sum)
        {
            memo.Clear();
            prev.Clear();

            memo[0] = true;
            prev[0] = new KeyValuePair<int,int>(-1, 0);

            for (int i = 0; i < inputArray.Count; ++i)
            {
                int num = inputArray[i];
                for (int s = sum; s >= num; --s)
                {
                    if (memo.ContainsKey(s - num) && memo[s - num] == true)
                    {
                        memo[s] = true;

                        if (!prev.ContainsKey(s))
                        {
                            prev[s] = new KeyValuePair<int,int>(i, num);
                        }
                    }
                }
            }

            return memo.ContainsKey(sum) && memo[sum];
        }

        public static IEnumerable<int> GetLastResult(int sum)
        {
            while (prev[sum].Key != -1)
            {
                yield return prev[sum].Key;
                sum -= prev[sum].Value;
            }
        }
    }

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

Решение 2 - рандомизированный алгоритм

Теперь это проще. Сохраните два списка: usedNums и unusedNums . Также сохраните переменную usedSum , которая в любой момент времени содержит сумму всех чисел в списке usedNums .

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

Всякий раз, когда вам нужно удалить число из вашего набора, выясните, в каком из двух списков оно находится. Вы можете сделать это с помощью линейного поиска, если у вас мало (на этот раз много означает более 10 000, может быть, даже 100 000 на быстром компьютере и при условии, что вы не выполняете эту операцию часто и в быстрой последовательности. В любом случае, линейный поиск можно оптимизировать, если вам это нужно.).Как только вы найдете номер, удалите его из списка. Обновление соответственно использовало сумму .

Всякий раз, когда вам нужно найти числа в вашем наборе, которые суммируются с числом S , используйте этот алгоритм:

while S != usedSum
    if S > usedSum // our current usedSum is too small
        move a random number from unusedNums to usedNums and update usedSum
    else // our current usedSum is too big
        move a random number from usedNums to unusedNums and update usedSum

В конце алгоритма список usedNums ] даст вам числа, сумма которых равна S .

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

Если у вас есть вопросы, напишите нам.

5
ответ дан 13 December 2019 в 22:04
поделиться

Вот мой алгоритм. Он выполняется в O (2 ^ (n / 2)) и решает SubsetSum (1000, список-1000-единиц) за 20 миллисекунд. Смотрите комментарии в конце поста IVlad.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace SubsetSum
{
    class Program
    {
        static void Main(string[] args)
        {
            var ns = new List<int>();
            for (int i = 0; i < 1000; i++) ns.Add(1);
            var s1 = Stopwatch.StartNew();
            bool result = SubsetSum(ns, 1000);
            s1.Stop();
            Console.WriteLine(result);
            Console.WriteLine(s1.Elapsed);
            Console.Read();
        }

        static bool SubsetSum(ist<int> nums, int targetL)
        {
            var left = new List<int> { 0 };
            var right = new List<int> { 0 };
            foreach (var n in nums)
            {
                if (left.Count < right.Count) left = Insert(n, left);
                else right = Insert(n, right);
            }
            int lefti = 0, righti = right.Count - 1;
            while (lefti < left.Count && righti >= 0)
            {
                int s = left[lefti] + right[righti];
                if (s < target) lefti++;
                else if (s > target) righti--;
                else return true;
            }
            return false;
        }

        static List<int> Insert(int num, List<int> nums)
        {
            var result = new List<int>();
            int lefti = 0, left = nums[0]+num;
            for (var righti = 0; righti < nums.Count; righti++)
            {

                int right = nums[righti];
                while (left < right)
                {
                    result.Add(left);
                    left = nums[++lefti] + num;
                }
                if (right != left) result.Add(right);
            }
            while (lefti < nums.Count) result.Add(nums[lefti++] + num);
            return result;
        }
    }
}

А вот улучшенная версия, которая сокращает наборы:

static bool SubsetSum(List<int> nums, int target)
{
    var remainingSum = nums.Sum();
    var left = new List<int> { 0 };
    var right = new List<int> { 0 };
    foreach (var n in nums)
    {
        if (left.Count == 0 || right.Count == 0) return false;
        remainingSum -= n;
        if (left.Count < right.Count) left = Insert(n, left, target - remainingSum - right.Last(), target);
        else right = Insert(n, right, target - remainingSum - left.Last(), target);
    }
    int lefti = 0, righti = right.Count - 1;
    while (lefti < left.Count && righti >= 0)
    {
        int s = left[lefti] + right[righti];
        if (s < target) lefti++;
        else if (s > target) righti--;
        else return true;
    }
    return false;
}

static List<int> Insert(int num, List<int> nums, int min, int max)
{
    var result = new List<int>();
    int lefti = 0, left = nums[0]+num;
    for (var righti = 0; righti < nums.Count; righti++)
    {

        int right = nums[righti];
        while (left < right)
        {
            if (min <= left && left <= max) result.Add(left);
            left = nums[++lefti] + num;
        }
        if (right != left && min <= right && right <= max) result.Add(right);
    }
    while (lefti < nums.Count)
    {
        left = nums[lefti++] + num;
        if (min <= left && left <= max) result.Add(left);
    } 
    return result;
}

Последняя решила проблему со 100000 наборами примерно за 5 миллисекунд (но это лучший вариант алгоритма, с данными реального мира он будет медленнее) .

Для вашего использования этот алгоритм, вероятно, достаточно быстр (и я не вижу очевидных улучшений). Если вы введете 10 000 товаров со случайной ценой от 0 до 20 и ваша цель - суммировать 500, на моем ноутбуке это будет решено за 0,04 секунды.

Редактировать: Я только что прочитал в Википедии, что наиболее известным алгоритмом является O (2 ^ (n / 2) * n) . Это O (2 ^ (n / 2)) . Получу ли я премию Тьюринга?

5
ответ дан 13 December 2019 в 22:04
поделиться
Другие вопросы по тегам:

Похожие вопросы: