我遇到计数问题,这是this问题的延续。我不是一个真正的数学家,所以我很难弄清楚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的corrseponds)
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;
}
}
基本上我根据事务类型,事务ID和数字添加和删除Array中的内容。我正在向ArrayList添加数字,如156,340(当它是TransferIn或Buy)等等,然后我删除它们就像156,340那样(当它是TransferOut,卖出时)。我的解决方案适用于此而没有问题。我遇到的问题是,对于一些旧数据,员工输入的金额是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);
}
}
}
但它仅在满足某些条件时才有效,而其余条件则无效。
编辑:由于你们中的一些人对我的波兰变量名称感到非常惊讶(并且蒙蔽了眼睛),因此为了简单和可见,我将所有这些名称翻译成英语。希望这能帮助我得到一些帮助: - )
答案 0 :(得分:5)
你应该如何做到这一点取决于一些重要的事情:你将拥有多少数字以及它们有多大?另外,据我所知,您的数据可以更改(添加/删除数字等),对吧?您需要多久进行一次这些查询?
我将提出两种解决方案。我建议你使用第二种,因为我怀疑它对你需要的更好,而且更容易理解。
解决方案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,在快速计算机上甚至可能是10万,并假设您不经常连续快速地执行此操作。无论如何,如果您需要,可以优化线性搜索。)。找到号码后,将其从列表中删除。相应地更新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
的数字。
我认为这个算法应该对你需要的东西有益。它可以很好地处理数据集的更改,并且可以很好地处理大量数据。它也不取决于数字有多大,如果您有大数字,这非常有用。
如果您有任何问题,请发布。
答案 1 :(得分:5)
这是我的算法。它在O(2^(n/2))
中运行,并在20毫秒内解决SubsetSum(1000, list-of-1000-ones)
。请参阅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;
}
最后一个在大约5毫秒内解决了100000个问题(但这是算法的最佳情况,现实世界数据会慢一些。)
对于您的使用,此算法可能足够快(我没有看到任何明显的改进)。如果您输入10,000个产品,其随机价格在0到20之间,并且您的目标是总和为500,那么在我的笔记本电脑上将在0.04秒内解决。
编辑:我刚刚在维基百科上看到,最着名的算法是O(2^(n/2)*n)
。这个是O(2^(n/2))
。我获得图灵奖吗?