Задача подмножества сумм - PullRequest
5 голосов
/ 25 апреля 2010

У меня проблема с подсчетом, которая является продолжением этого вопроса. Я на самом деле не математик, поэтому мне очень трудно разобраться в этом 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 (соответствует 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 или Buy) и т. Д., А затем я удаляю их, как 156, 340 (когда это TransferOut, Sell). Мое решение работает для этого без проблем. У меня проблема в том, что для некоторых старых данных сотрудники вводили сумму как 1500 вместо 500 + 400 + 100 + 500. Как бы я изменил его так, чтобы при наличии Sell/TransferOut или Buy/Transfer In и отсутствии совпадения внутри ArrayList он пытался добавить несколько элементов из этого ArrayList и найти элементы, которые объединяются в совокупность.

Внутри своего кода я пытался решить эту проблему простым суммированием всего, когда нет совпадения (index == 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);
                            }
                        }
                    }

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

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

Ответы [ 2 ]

5 голосов
/ 04 мая 2010

Вот мой алгоритм. Он работает в O(2^(n/2)) и решает SubsetSum(1000, list-of-1000-ones) за 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 миллисекунд (но это лучший случай алгоритма, с реальными данными он будет медленнее).

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

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

5 голосов
/ 26 апреля 2010

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

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

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

Пусть S[i] = true if we can make sum i and false otherwise.

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] = the last number that was used to make sum i. Вы обновите это соответствующим образом в условии if выше.

Временная сложность этого процесса 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 с суммой, отличной от суммы, с которой Find последний раз вызывался. Во всяком случае, это идея.

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

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

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

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

Когда вам нужно найти, есть ли в вашем наборе числа, которые суммируют с числом 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.

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

Пожалуйста, пишите, если у вас есть какие-либо вопросы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...