【问题标题】:Find all k-size subsets with sum s of an n-size bag of duplicate unsorted positive integers找到一个 n 大小的重复未排序正整数包的总和为 s 的所有 k 大小子集
【发布时间】:2015-07-12 10:39:03
【问题描述】:

请注意,这是 C# .NET 2.0 项目所必需的(Linq 不允许)。

我知道这里已经提出了非常相似的问题,并且我已经生成了一些工作代码(见下文),但仍然希望得到关于如何在给定 k 和 s 条件下使算法更快的建议。

这是我到目前为止所学到的: 动态规划是找到一个(不是全部)子集的最有效方法。如果我错了,请纠正我。有没有办法重复调用 DP 代码来生成更新的子集,直到袋子(重复设置)用尽?

如果没有,那么有没有一种方法可以加速我下面的回溯递归算法,它确实产生了我需要的但在 O(2^n) 中运行,我认为,通过考虑 s 和 k?

这是我的固定数字包,在 n=114 和数字范围从 3 到 286 时永远不会改变:

    int[] numbers = new int[]
    {
        7, 286, 200, 176, 120, 165, 206, 75, 129, 109,
        123, 111, 43, 52, 99, 128, 111, 110, 98, 135,
        112, 78, 118, 64, 77, 227, 93, 88, 69, 60,
        34, 30, 73, 54, 45, 83, 182, 88, 75, 85,
        54, 53, 89, 59, 37, 35, 38, 29, 18, 45,
        60, 49, 62, 55, 78, 96, 29, 22, 24, 13,
        14, 11, 11, 18, 12, 12, 30, 52, 52, 44,
        28, 28, 20, 56, 40, 31, 50, 40, 46, 42,
        29, 19, 36, 25, 22, 17, 19, 26, 30, 20,
        15, 21, 11, 8, 8, 19, 5, 8, 8, 11,
        11, 8, 3, 9, 5, 4, 7, 3, 6, 3,
        5, 4, 5, 6
    };

要求

  • 最大空间限制为 2-3GB,但时间应该是 O(n^something) 而不是 (某事^n)。

  • 袋子不得分拣,重复不得删除。

  • 结果应该是匹配中数字的索引 子集,而不是数字本身(因为我们有重复)。

动态编程尝试

这是改编自 stackoverflow.com 上类似问题的答案的 C# 动态编程版本:

using System;
using System.Collections.Generic;

namespace Utilities
{
    public static class Combinations
    {
        private static Dictionary<int, bool> m_memo = new Dictionary<int, bool>();
        private static Dictionary<int, KeyValuePair<int, int>> m_previous = new Dictionary<int, KeyValuePair<int, int>>();
        static Combinations()
        {
            m_memo.Clear();
            m_previous.Clear();
            m_memo[0] = true;
            m_previous[0] = new KeyValuePair<int, int>(-1, 0);

        }

        public static bool FindSubset(IList<int> set, int sum)
        {
            //m_memo.Clear();
            //m_previous.Clear();
            //m_memo[0] = true;
            //m_previous[0] = new KeyValuePair<int, int>(-1, 0);

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

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

            return m_memo.ContainsKey(sum) && m_memo[sum];
        }
        public static IEnumerable<int> GetLastIndex(int sum)
        {
            while (m_previous[sum].Key != -1)
            {
                yield return m_previous[sum].Key;
                sum -= m_previous[sum].Value;
            }
        }

        public static void SubsetSumMain(string[] args)
        {
            int[] numbers = new int[]
        {
            7, 286, 200, 176, 120, 165, 206, 75, 129, 109,
            123, 111, 43, 52, 99, 128, 111, 110, 98, 135,
            112, 78, 118, 64, 77, 227, 93, 88, 69, 60,
            34, 30, 73, 54, 45, 83, 182, 88, 75, 85,
            54, 53, 89, 59, 37, 35, 38, 29, 18, 45,
            60, 49, 62, 55, 78, 96, 29, 22, 24, 13,
            14, 11, 11, 18, 12, 12, 30, 52, 52, 44,
            28, 28, 20, 56, 40, 31, 50, 40, 46, 42,
            29, 19, 36, 25, 22, 17, 19, 26, 30, 20,
            15, 21, 11, 8, 8, 19, 5, 8, 8, 11,
            11, 8, 3, 9, 5, 4, 7, 3, 6, 3,
            5, 4, 5, 6
        };

            int sum = 400;
            //int size = 4; // don't know to use in dynamic programming

            // call dynamic programming
            if (Numbers.FindSubset(numbers, sum))
            {
                foreach (int index in Numbers.GetLastIndex(sum))
                {
                    Console.Write((index + 1) + "." + numbers[index] + "\t");
                }
                Console.WriteLine();
            }
            Console.WriteLine();

            Console.ReadKey();
        }
    }
}

递归编程尝试

这是 C# 递归编程版本,改编自 stackoverflow.com 上类似问题的答案:

using System;
using System.Collections.Generic;

namespace Utilities
{
    public static class Combinations
    {
        private static int s_count = 0;
        public static int CountSubsets(int[] numbers, int index, int current, int sum, int size, List<int> result)
        {
            if ((numbers.Length <= index) || (current > sum)) return 0;
            if (result == null) result = new List<int>();

            List<int> temp = new List<int>(result);
            if (current + numbers[index] == sum)
            {
                temp.Add(index);
                if ((size == 0) || (temp.Count == size))
                {
                    s_count++;
                }
            }
            else if (current + numbers[index] < sum)
            {
                temp.Add(index);
                CountSubsets(numbers, index + 1, current + numbers[index], sum, size, temp);
            }

            CountSubsets(numbers, index + 1, current, sum, size, result);
            return s_count;
        }

        private static List<List<int>> m_subsets = new List<List<int>>();
        public static List<List<int>> FindSubsets(int[] numbers, int index, int current, int sum, int size, List<int> result)
        {
            if ((numbers.Length <= index) || (current > sum)) return m_subsets;
            if (result == null) result = new List<int>();

            List<int> temp = new List<int>(result);
            if (current + numbers[index] == sum)
            {
                temp.Add(index);
                if ((size == 0) || (temp.Count == size))
                {
                    m_subsets.Add(temp);
                }
            }
            else if (current + numbers[index] < sum)
            {
                temp.Add(index);
                FindSubsets(numbers, index + 1, current + numbers[index], sum, size, temp);
            }

            FindSubsets(numbers, index + 1, current, sum, size, result);

            return m_subsets;
        }

        public static void SubsetSumMain(string[] args)
        {
            int[] numbers = new int[]
        {
            7, 286, 200, 176, 120, 165, 206, 75, 129, 109,
            123, 111, 43, 52, 99, 128, 111, 110, 98, 135,
            112, 78, 118, 64, 77, 227, 93, 88, 69, 60,
            34, 30, 73, 54, 45, 83, 182, 88, 75, 85,
            54, 53, 89, 59, 37, 35, 38, 29, 18, 45,
            60, 49, 62, 55, 78, 96, 29, 22, 24, 13,
            14, 11, 11, 18, 12, 12, 30, 52, 52, 44,
            28, 28, 20, 56, 40, 31, 50, 40, 46, 42,
            29, 19, 36, 25, 22, 17, 19, 26, 30, 20,
            15, 21, 11, 8, 8, 19, 5, 8, 8, 11,
            11, 8, 3, 9, 5, 4, 7, 3, 6, 3,
            5, 4, 5, 6
        };

            int sum = 17;
            int size = 2;

            // call backtracking recursive programming
            Console.WriteLine("CountSubsets");
            int count = Numbers.CountSubsets(numbers, 0, 0, sum, size, null);
            Console.WriteLine("Count = " + count);
            Console.WriteLine();

            // call backtracking recursive programming
            Console.WriteLine("FindSubsets");
            List<List<int>> subsets = Numbers.FindSubsets(numbers, 0, 0, sum, size, null);
            for (int i = 0; i < subsets.Count; i++)
            {
                if (subsets[i] != null)
                {
                    Console.Write((i + 1).ToString() + ":\t");
                    for (int j = 0; j < subsets[i].Count; j++)
                    {
                        int index = subsets[i][j];
                        Console.Write((index + 1) + "." + numbers[index] + " ");
                    }
                    Console.WriteLine();
                }
            }
            Console.WriteLine("Count = " + subsets.Count);

            Console.ReadKey();
        }
    }
}

请告诉我如何将动态编程版本限制为大小为 k 的子集,以及我是否可以重复调用它,以便在每次调用时返回不同的子集,直到没有更多匹配的子集。

我也不确定在哪里初始化 DP 算法的备忘录。我是在访问任何方法时自动运行的静态构造函数中完成的。这是正确的初始化位置还是需要移到 FindSunset() 方法内部[注释掉]?

至于递归版本,是回溯吗?以及我们如何加快速度。它工作正常,并考虑了 k 和 s,但完全没有效率。

让我们将此线程作为所有 C# SubsetSum 相关问题的母亲!

【问题讨论】:

  • 请不要只是向我们倾倒一堵代码墙并要求我们为您审查它。为此,我们有Code Review
  • 当且仅当代码按预期工作时,问题才可能成为代码审查的主题。这句话:“请让我知道如何将动态编程版本限制为大小为 k 的子集” 听起来像是尚未编写所需的行为。
  • 有两个版本:
  • 有两个版本:回溯在限制 k 大小的情况下正常工作,但效率低下。然而,动态编程版本没有考虑所需的 k 子集大小,但更糟糕的是它只返回它找到的第一个匹配子集。我希望有一个有效的解决方案。如果我按原样找到代码墙,我会节省很多时间。相反,我必须从多个问题中收集它才能使其发挥作用。感谢您对 Code Review 服务的关注,但我的问题仍然存在。 DP 是否能够反复获取下一个子集,直到全部找到?坦Q。

标签: c# algorithm .net-2.0 dynamic-programming subset-sum


【解决方案1】:

有多种解决方案,但没有人展示如何使用动态编程来找到答案。

关键是使用动态规划来构建一个数据结构,以后可以从中找到所有解决方案。

除了请求的函数之外,我还收集了关于有多少解的信息,并写了FindSolution(node, position) 以返回位置position 的解,以node 开头,而不计算其余部分。如果你想要所有这些,使用该功能将是低效的。但是,例如,使用此函数,可以计算将 10000 表示为 20 个素数之和的第 10 亿种方法。使用给定的其他方法,这将是不可行的。

using System;
using System.Collections.Generic;

public class SubsetSum
{
    public class SolutionNode<T>
    {
        // The index we found the value at
        public int Index {get; set;}
        // The value we add for this solution
        public T Value {get; set;}
        // How many solutions we have found.
        public int Count {get; set;}
        // The rest of this solution.
        public SolutionNode<T> Tail {get; set;}
        // The next solution.
        public SolutionNode<T> Next {get; set;}
    }

    // This uses dynamic programming to create a summary of all solutions.
    public static SolutionNode<int> FindSolution(int[] numbers, int target, int subsetSize)
    {
        // By how many are in our list, by what they sum to, what SolutionNode<int> has our answer?
        List<Dictionary<int, SolutionNode<int>>> solutionOf = new List<Dictionary<int, SolutionNode<int>>>();

        // Initialize empty solutions.
        for (int i = 0; i <= subsetSize; i++)
        {
            solutionOf.Add(new Dictionary<int, SolutionNode<int>>());
        }

        // We discover from the last number in the list forward.
        // So discovering from the last index forward makes them ordered.
        for (int i = numbers.Length - 1; -1 < i; i--)
        {
            int number = numbers[i];
            // Descending here so we don't touch solutionOf[j-1] until after we have solutionOf[j] updated.
            for (int j = subsetSize; 0 < j; j--)
            {
                // All previously found sums with j entries
                Dictionary<int, SolutionNode<int>> after = solutionOf[j];
                // All previously found sums with j-1 entries
                Dictionary<int, SolutionNode<int>> before = solutionOf[j-1];
                foreach (KeyValuePair<int, SolutionNode<int>> pair in before)
                {
                    SolutionNode<int> newSolution = new SolutionNode<int>();
                    int newSum = pair.Key + number;
                    newSolution.Index = i;
                    newSolution.Value = number;
                    newSolution.Count = pair.Value.Count;
                    newSolution.Tail = pair.Value;
                    if (after.ContainsKey(newSum))
                    {
                        newSolution.Next = after[newSum];
                        newSolution.Count = pair.Value.Count + after[newSum].Count;
                    }
                    after[newSum] = newSolution;
                }

                // And special case empty set.
                if (1 == j)
                {
                    SolutionNode<int> newSolution = new SolutionNode<int>();
                    newSolution.Index = i;
                    newSolution.Value = number;
                    newSolution.Count = 1;
                    if (after.ContainsKey(number))
                    {
                        newSolution.Next = after[number];
                        newSolution.Count = after[number].Count;
                    }
                    after[number] = newSolution;
                }
            }
        }

        // Return what we found.
        try
        {
            return solutionOf[subsetSize][target];
        }
        catch
        {
            throw new Exception("No solutions found");
        }
    }

    // The function we were asked for.
    public static IEnumerable<List<int>> ListSolutions (SolutionNode<int> node)
    {
        List<int> solution = new List<int>();
        List<SolutionNode<int>> solutionPath = new List<SolutionNode<int>>();

        // Initialize with starting information.
        solution.Add(0); // This will be removed when we get node
        solutionPath.Add(node); // This will be our start.

        while (0 < solutionPath.Count)
        {
            // Erase the tail of our previous solution
            solution.RemoveAt(solution.Count - 1);
            // Pick up our next.
            SolutionNode<int> current = solutionPath[solutionPath.Count - 1];
            solutionPath.RemoveAt(solutionPath.Count - 1);
            while (current != null)
            {
                solution.Add(current.Index);
                solutionPath.Add(current.Next);
                if (current.Tail == null)
                {
                    yield return solution;
                }
                current = current.Tail;
            }
        }
    }

    // And for fun, a function that dynamic programming makes easy - return any one of them!
    public static List<int> FindSolution(SolutionNode<int> node, int position)
    {
        // Switch to counting from the end.
        position = node.Count - position - 1;
        List<int> solution = new List<int>();
        while (node != null)
        {
            while (node.Next != null && position < node.Next.Count)
            {
                node = node.Next;
            }
            solution.Add(node.Index);
            node = node.Tail;
        }
        return solution;
    }

    public static void Main(string[] args)
    {
        SolutionNode<int> solution = FindSolution(
            new[]{
                7, 286, 200, 176, 120, 165, 206, 75, 129, 109,
            123, 111, 43, 52, 99, 128, 111, 110, 98, 135,
            112, 78, 118, 64, 77, 227, 93, 88, 69, 60,
            34, 30, 73, 54, 45, 83, 182, 88, 75, 85,
            54, 53, 89, 59, 37, 35, 38, 29, 18, 45,
            60, 49, 62, 55, 78, 96, 29, 22, 24, 13,
            14, 11, 11, 18, 12, 12, 30, 52, 52, 44,
            28, 28, 20, 56, 40, 31, 50, 40, 46, 42,
            29, 19, 36, 25, 22, 17, 19, 26, 30, 20,
            15, 21, 11, 8, 8, 19, 5, 8, 8, 11,
            11, 8, 3, 9, 5, 4, 7, 3, 6, 3,
            5, 4, 5, 6}
            , 400, 4);
        IEnumerable<List<int>> listing = ListSolutions(solution);
        foreach (List<int> sum in listing)
        {
            Console.WriteLine ("solution {0}", string.Join(", ", sum.ToArray()));
        }
    }
}

顺便说一句,这是我第一次尝试编写 C#。它……非常冗长。

【讨论】:

    【解决方案2】:

    可以通过类似于背包问题的解决方案来解决

    dp[i][j][k]=使用前“i”个元素且总和等于 j 的 k 大小子集的数量

    dp[i][j][k]=dp[i-1][j][k] + dp[i-1][j-a[i]][k-1]

    是dp的更新(使用第i个元素还是不使用)

    for(int i=0;i<=n;i++) dp[i][0][0]=1;
    for(int i=1;i<=n;i++){
        for(int j=0;j<=w;j++){
            for(int k=1;k<=i;k++){
                dp[i][j][k]=dp[i-1][j][k] ;
                if(j>=a[i-1]){
                    dp[i][j][k]+=dp[i-1][j-a[i-1]][k-1];
                }
            }
        }
    }
    

    【讨论】:

    • 请在您的答案中格式化代码以使其清晰。请参阅editing help 了解如何执行此操作。
    【解决方案3】:

    我之前的答案的工作原理是切断要检查的组合数量。但是,一旦您对数组进行排序,这可以得到显着改善。原理类似,但由于解决方案完全不同,所以我决定将其放在单独的答案中。

    我小心翼翼地只使用 .Net Framework 2.0 功能。稍后可能会添加视觉解释,但 cmets 应该足够了。

    class Puzzle
    {
        private readonly int[] _tailSums;
        public readonly SubsetElement[] Elements;
        public readonly int N;
    
        public Puzzle(int[] numbers)
        {
            // Set N and make Elements array
            // (to remember the original index of each element)
            this.N = numbers.Length;
            this.Elements = new SubsetElement[this.N];
    
            for (var i = 0; i < this.N; i++)
            {
                this.Elements[i] = new SubsetElement(numbers[i], i);
            }
    
            // Sort Elements descendingly by their Number value
            Array.Sort(this.Elements, (a, b) => b.Number.CompareTo(a.Number));
    
            // Save tail-sums to allow immediate access by index
            // Allow immedate calculation by index = N, to sum 0
            this._tailSums = new int[this.N + 1];
            var sum = 0;
    
            for (var i = this.N - 1; i >= 0; i--)
            {
                this._tailSums[i] = sum += this.Elements[i].Number;
            }
        }
    
        public void Solve(int s, Action<SubsetElement[]> callback)
        {
            for (var k = 1; k <= this.N; k++)
                this.Solve(k, s, callback);
        }
    
        public void Solve(int k, int s, Action<SubsetElement[]> callback)
        {
            this.ScanSubsets(0, k, s, new List<SubsetElement>(), callback);
        }
    
        private void ScanSubsets(int startIndex, int k, int s,
                                 List<SubsetElement> subset, Action<SubsetElement[]> cb)
        {
            // No more numbers to add, and current subset is guranteed to be valid
            if (k == 0)
            {
                // Callback with current subset
                cb(subset.ToArray());
                return;
            }
    
            // Sum the smallest k elements
            var minSubsetStartIndex = this.N - k;
            var minSum = this._tailSums[minSubssetStartIndex];
    
            // Smallest possible sum is greater than wanted sum,
            // so a valid subset cannot be found
            if (minSum > s)
            {
                return;
            }
    
            // Find largest number that satisfies the condition
            // that a valid subset can be found
            minSum -= this.Elements[minSubsetStartIndex].Number;
    
            // But remember the last index that satisfies the condition
            var minSubsetEndIndex = minSubsetStartIndex;
    
            while (minSubsetStartIndex > startIndex &&
                   minSum + this.Elements[minSubsetStartIndex - 1].Number <= s)
            {
                minSubsetStartIndex--;
            }
    
            // Find the first number in the sorted sequence that is
            // the largest number we just found (in case of duplicates)
            while (minSubsetStartIndex > startIndex &&
                   Elements[minSubsetStartIndex] == Elements[minSubsetStartIndex - 1])
            {
                minSubsetStartIndex--;
            }
    
            // [minSubsetStartIndex .. maxSubsetEndIndex] is the
            // full range we must check in recursion
    
            for (var subsetStartIndex = minSubsetStartIndex;
                 subsetStartIndex <= minSubsetEndIndex;
                 subsetStartIndex++)
            {
                // Find the largest possible sum, which is the sum of the
                // k first elements, starting at current subsetStartIndex
                var maxSum = this._tailSums[subsetStartIndex] -
                             this._tailSums[subsetStartIndex + k];
    
                // The largest possible sum is less than the wanted sum,
                // so a valid subset cannot be found
                if (maxSum < s)
                {
                    return;
                }
    
                // Add current number to the subset
                var x = this.Elements[subsetStartIndex];
                subset.Add(x);
    
                // Recurse through the sub-problem to the right
                this.ScanSubsets(subsetStartIndex + 1, k - 1, s - x.Number, subset, cb);
    
                // Remove current number and continue loop
                subset.RemoveAt(subset.Count - 1);
            }
        }
    
        public sealed class SubsetElement
        {
            public readonly int Number;
            public readonly int Index;
    
            public SubsetElement(int number, int index)
            {
                this.Number = number;
                this.Index = index;
            }
    
            public override string ToString()
            {
                return string.Format("{0}({1})", this.Number, this.Index);
            }
        }
    }
    

    使用和性能测试:

    private static void Main()
    {
        var sw = Stopwatch.StartNew();
        var puzzle = new Puzzle(new[]
            {
                7, 286, 200, 176, 120, 165, 206, 75, 129, 109,
                123, 111, 43, 52, 99, 128, 111, 110, 98, 135,
                112, 78, 118, 64, 77, 227, 93, 88, 69, 60,
                34, 30, 73, 54, 45, 83, 182, 88, 75, 85,
                54, 53, 89, 59, 37, 35, 38, 29, 18, 45,
                60, 49, 62, 55, 78, 96, 29, 22, 24, 13,
                14, 11, 11, 18, 12, 12, 30, 52, 52, 44,
                28, 28, 20, 56, 40, 31, 50, 40, 46, 42,
                29, 19, 36, 25, 22, 17, 19, 26, 30, 20,
                15, 21, 11, 8, 8, 19, 5, 8, 8, 11,
                11, 8, 3, 9, 5, 4, 7, 3, 6, 3,
                5, 4, 5, 6
            });
    
        puzzle.Solve(2, 17, PuzzleOnSubsetFound);
    
        sw.Stop();
        Console.WriteLine("Subsets found: " + _subsetsCount);
        Console.WriteLine(sw.Elapsed);
    }
    
    private static int _subsetsCount;
    
    private static void PuzzleOnSubsetFound(Puzzle.SubsetElement[] subset)
    {
        _subsetsCount++;
        return; // Skip prints when speed-testing
    
        foreach (var el in subset)
        {
            Console.Write(el.ToString());
            Console.Write("  ");
        }
    
        Console.WriteLine();
    }
    

    输出:

    每一行是一个找到的子集,()中的数字是子集中使用的数字的原始索引

    14(60) 3(107)
    14(60) 3(109)
    14(60) 3(102)
    13(59) 4(105)
    13(59) 4(111)
    12(64) 5(96)
    12(64) 5(104)
    12(64) 5(112)
    12(64) 5(110)
    12(65) 5(96)
    12(65) 5(104)
    12(65) 5(112)
    12(65) 5(110)
    11(100) 6(108)
    11(100) 6(113)
    11(61) 6(108)
    11(61) 6(113)
    11(92) 6(108)
    11(92) 6(113)
    11(62) 6(108)
    11(62) 6(113)
    11(99) 6(108)
    11(99) 6(113)
    9(103) 8(93)
    9(103) 8(94)
    9(103) 8(97)
    9(103) 8(98)
    9(103) 8(101)
    找到的子集:28
    00:00:00.0017020(不打印时测量)


    k 越高,可以进行的截断越多。这是您将看到主要的性能差异的时候。当s 变高时,您当前的代码(递归版本)的执行速度也会明显变慢。

    k=5s=60
    您当前的代码:在 2.8602923 秒内找到 45070 个子集
    我的代码:在 0.0116727 秒内找到 45070 个子集
    那就是 99.6% 速度提升

    【讨论】:

    • 优秀的答案。太感谢了。请您处理 k=0 表示任何子集大小的情况,您能否确认在最坏的情况下,这仍然在 O(2^n) 中运行?
    • 如果你愿意,你可以为所有的 k 运行一个循环。它比 O(2^n) 运行得更好,我相信它是 O(n^k) 什么的。
    • 我认为它在 O(2^k) 而不是 O(2^n) 中运行,这是一个巨大的改进。
    • 可能是 O(k^k) 而不是 O(n^n)?这些东西总是让我感到困惑。我只是基准测试。
    • 不,我很确定它是 O(n^k),就像我之前想的那样。在每个级别的递归中,我都在循环遍历集合的一部分。在任何特定递归分支的所有级别中,我扫描(或跳过,但截止并没有真正改变上限)的元素数量都添加到 O(n)。并且有 k 级递归,所以 O(n^k) 听起来通常是正确的。我认为任何尝试的实现都不是 O(2^n)。
    【解决方案4】:

    尝试使用下面的代码。抱歉,我没有时间优化代码。您可以比较您拥有的所有方法,并得出更快的结论,不要忘记分享结果。

    C#(你可以尝试摆脱 List 可能会给你带来性能提升:

    public static IEnumerable<List<int>> findSubsetsWithLengthKAndSumS2(Tuple<int, int> ks, List<int> set, List<int> subSet, List<int> subSetIndex)
        {
            if (ks.Item1 == 0 && ks.Item2 == 0)
            {
                var res = new List<List<int>>();
                res.Add(subSetIndex);
                return res;
            }
            else if (ks.Item1 > 0 && ks.Item2 > 0)
            {
                var res = set.Select((x, i) =>
                {
                    var newSubset = subSet.Select(y => y).ToList();
                    newSubset.Add(x);
    
                    var newSubsetIndex = subSetIndex.Select(y => y).ToList();
                    newSubsetIndex.Add(i);
    
                    var newSet = set.Skip(i).ToList();
                    return findSubsetsWithLengthKAndSumS2(Tuple.Create(ks.Item1 - 1, ks.Item2 - x), newSet, newSubset, newSubsetIndex).ToList();
                }
                ).SelectMany(x => x).ToList();
                return res;
            }
            else
                return new List<List<int>>();
        }
    ...
                var res = findSubsetsWithLengthKAndSumS2(Tuple.Create(2, 293), numbers.ToList(), new List<int>(), new List<int>());
    

    F#(我添加它只是为了好玩=),它也没有优化,我相信代码中最慢的地方是'skip'):

    let skip (list:List<int>) index = 
        list |> List.mapi (fun i x -> if i > index then Some(x) else None) |> List.filter (fun x -> x.IsSome) |> List.map (fun  x -> x.Value)
    
    let rec findSubsetsWithLengthKAndSumS (ks:int*int) (set:list<int>) (subSet:list<int>) =
        [
            match ks with
            |0,0 ->   yield subSet 
            | x,y when x > 0 && y > 0 -> yield! set |> List.mapi (fun i x-> findSubsetsWithLengthKAndSumS ((fst ks)-1,(snd ks)-x)   (skip set i ) (x::subSet)) |> Seq.concat
            | _,_-> yield []
        ]
    ...
        let res = Subsets.findSubsetsWithLengthKAndSumS (2,293)  numbers [] |> List.filter (fun x-> x.Length >0) 
    

    我相信这个迭代版本会比其他版本快很多倍。它使用 .net 2.0

        public delegate IEnumerable< IEnumerable< int > > findSubset();
    
        public delegate bool findSubsetsIterativeFilter( int[] sourceSet, int[] indiciesToSum, int expected );
    
        public static bool Summ( int[] sourceSet, int[] indicies, int expected )
        {
            var sum = 0;
            for( int i = 0; i < indicies.Length; i++ )
                sum += sourceSet[ indicies[ i ] ];
            return sum == expected;
        }
    
        public static IEnumerable< IEnumerable< int > > findSubsetsIterative( int k, int[] sourceSet, findSubsetsIterativeFilter specialCondition, int expected )
        {
            var a = new int[ k ];
            for( int i = 0; i < k; i++ )
                a[ i ] = i; 
    
            var p = k - 1;
            while( p >= 0 )
            {
                if( specialCondition( sourceSet, a, expected ) )
                    yield return ( int[] )a.Clone();
                p = ( a[ k - 1 ] == sourceSet.Length - 1 ) ? p - 1 : k - 1;
                if( p >= 0 )
                    for( int i = k - 1; i >= p; i-- )
                        a[ i ] = a[ p ] + i - p + 1;
            }
        }
        ...
           findSubsetsIterative( 2, a, Summ, 293 );
    

    我已经测量了我的代码和 Yorye 的代码,这就是我得到的。我的代码速度提高了 4-10 倍。您是否在实验中使用了我的回答中的“findSubsetsIterative”?

    findSubsetsIterative(4, GenerateSOurceData(1), Summ, 400) 已过: 00:00:00.0012113 puzzle.Solve(4, 400, PuzzleOnSubsetFound) 已过: 00:00:00.0046170

    findSubsetsIterative(5, GenerateSOurceData(1), Summ, 60) 已过: 00:00:00.0012960 puzzle.Solve(5, 60, PuzzleOnSubsetFound) 已过: 00:00:00.0108568

    在这里,我将传入数组增加了 5 倍(只是将数组复制了 5 次到新的大数组中): findSubsetsIterative(5, GenerateSOurceData(5), Summ, 60) 经过:00:00:00.0013067 Puzzle.Solve(5, 60, PuzzleOnSubsetFound) 经过:00:00:21.3520840

    【讨论】:

    • 恐怕我不允许使用 Linq。必须坚持使用 .NET 2.0,最好仅使用 [] 数组,但 List 也是可以接受的。谢谢你的代码,我希望其他人也能发现它有用。
    • 好的,我添加了可以在 .net 2.0 上运行的迭代版本,这种方法比其他方法快数百倍。这是因为它具有低级数据结构实现并且具有低时间复杂性。适合你吗?
    • 非常感谢您的努力。我会对其进行测试并报告。
    • 好的,我已经对 int k = 4; 的两种解决方案(您的和 Yorye 的)进行了基准测试;整数 s = 400; IEnumerable> 结果 = findSubsetsIterative(k, s, array, Sum);整数计数 = 0; foreach(结果中的 IEnumerable 值){ count++; } sw.Stop(); Console.WriteLine("找到的子集:" + count);找到的子集:5456 00:00:00.5442583 Yorye 的解决方案:找到的子集:5456 00:00:00.0108487 所以 5442583/108487 ~= 快 50 倍
    • @AliAdams,这对我来说很奇怪,在我的旧笔记本上,我的方法采用了 00:00:00:001。你用的是什么环境?你能确保这两种方法都在相同的环境中进行了测试,并且没有打印信息和其他 i/o 吗?
    【解决方案5】:

    只需搜索所有大小 K 的组合,并检查每个组合是否满足条件。

    适合您情况的k 组合的最快算法是:

    for (var i1 = 0; i1 <= n; i1++)
    {
        for (var i2 = i1 + 1; i2 <= n; i2++)
        {
            for (var i3 = i2 + 1; i3 <= n; i3++)
            {
                ...
    
                for (var ik = iOneBeforeK + 1; ik <= n; ik++)
                {
                    if (arr[i1] + arr[i2] + ... + arr[ik] == sum)
                    {
                        // this is a valid subset
                    }
                }
            }
        }
    }
    

    但您是在谈论数字并将它们相加,这意味着您可以使用更智能的算法进行截断。

    由于所有数字都是正数,因此您知道,如果单个数字足够大,则不能再向其添加任何正数并使其总和为 s。给定s=6k=4,搜索中包含的最高数字是s-k+1=33+1+1+1k 数字,1 是您可能的最低数字,总和为 6。任何大于 3 的数字都不能添加 3 个其他正数,并且总和

    但是等等,你的最小可能值不是1,而是3。那就更好了。所以假设k=10n=60min=3。 “最高数字场景”是x+min(k-1)=n -> x=60-3*9=33。所以要考虑的最高数字是33

    这减少了要考虑的数组中相关数字的数量,因此减少了要查找的组合数量。

    但它变得更好。假设k=10n=60min=3。数组中的第一个数字恰好是20,所以它是相关的,应该检查一下。让我们找到包含这 20 个的相关子集:
    一个新的“谜题”出现了! k=10-1n=60-20min=3。您现在可以从子谜题中切出许多数字,并一次又一次地切入其中。

    通过计算子谜题中k-1 最低数字的平均值并将其用作min,可以进一步改进这一点。

    可以通过预先计算子谜题[0..n] 中的最低平均数kk-1 子谜题[1..n] 中的最低平均数和k-2 子谜题@987654353 中的最低平均数来进一步改进这一点@ 等等,并使用它们而不是在每个子谜题评估中一次又一次地重新计算相同的东西。

    【讨论】:

    • 感谢您的回答,但我无法将包的尺寸从 114 更改,因为这会产生仅适用于缩减包的结果,但对于我需要的原始 114 元素包不正确。并且 k 嵌套循环在编译时是固定的,而 k 是运行时参数,而且必须有比这更有效的方法。还是谢谢你。
    • @Ali 这个答案正在进行中。请检查更新。
    猜你喜欢
    • 2015-08-14
    • 1970-01-01
    • 2012-03-27
    • 2022-01-24
    • 2013-08-06
    • 1970-01-01
    • 1970-01-01
    • 2018-09-06
    • 1970-01-01
    相关资源
    最近更新 更多