比较喜欢wanghui的算法,我格式化了一下,方便阅读,里面的新函数是限制乘数的比如求所有A+B=10的结果。
int NumberOfSetBits(int i)
{
i = i - ((i >> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
};
int main () {
int array [] = {1,2,3,4,5,6,7,8,9};
int length=9;
int requiredSum=10;
int multiplierCount=2;
int sum;
bool isLimiteMultiplierNum=false;
for (unsigned int i=1; i<(1<<length); ++i,sum=0) {//穷举乘数
if(isLimiteMultiplierNum&&NumberOfSetBits(i)!=multiplierCount)//限制乘数数量
continue;
for (unsigned int k=0; k<32; ++k)
{
if ((i&(1<<k)))//i的第k个bit位是否为1
{sum += array[k];}
}
if (sum==requiredSum){
for (unsigned int k=0; k<32; ++k )
{
if ((i&(1<<k)))
{
cout << array[k] << '';
}
}
cout << endl;
}
}
return 0;
}
采用二分法来进行统计
二分法? 能具体点么?
二分法首先要做一次排序,他这个不一定是有序的,排序的算法就不用说了,其实排序之后不需要使用二分法,借鉴归并排序的思想,前后两个指针,加和大了,后指针前移,小了,前指针后移,不过这样是和2个数加起来等于给定的数。
int array [] = {1,2,3,4,5,6,7,8,9};
int length=9;
for (unsigned int i=1; i<(1<<length); ++i,sum=0) {
for (unsigned int k=0; k<32; ++k)
if ((i&(1<<k)))//i的第k个bit位是否为1
sum += array[k];
if (sum==requiredSum){
for (unsigned int k=0; k<32; ++k )
if ((i&(1<<k)))
cout << array[k] << '';
cout << endl;
}
}
输出:
1 2 3 4
2 3 5
1 4 5
1 3 6
4 6
1 2 7
3 7
2 8
1 9
哥 谢谢你的代码 , 不过 你这个算是定制么?
@rq1986:
当然不是定制的了,array换成你的数组,length换成你的数组长度。
@Wang Hui: 真心没看懂。。。能解释下算法么,我只看出 sum += array[k]; 会数组越界啊。
@水牛刀刀:
1:这里是采用一个整数才表示集合的子集。
任何一个集合含有N个元素,那么子集数目就是2的N次方。这里假定集合的数目小于32,那么就可以用一个整数来表示每一个集合。比如3,其二进制是:0000 0000 0000 0011,表示3代表的集合里含有位置为0和1的数组元素(位置0和1处的二进制数是1)。那么对于含有N个元素的集合,用[1-2的N次方]里的每一个数字就可以表示每一个集合,这里需要稍微理解一下。
2: 算法有一定的局限性,要求数组的长度小于32,如果数组长度太大,时间复杂度会非常高,而且没有办法降低。当然,如果一定要计算大于32个元素的,也可以。
3:外层for循环: for (unsigned int i=1; i<(1<<length); ++i,sum=0)
这里循环遍历[1-2的N次方]中的每一个数字,也就相当于遍历每一个集合。
4:内存循环:for (unsigned int k=0; k<32; ++k)
这里遍历集合内部的元素,:
if ((i&(1<<k)))//i的第k个bit位是否为1
sum += array[k];
i&(1<<k):是取得i的第k个bit位的数值,如果是1,说明集合中含有这个位置的元素,则sum += array[k];加和。
这里不会越界,因为如果你的数组程度是length,而length小于32,则2的length次方在第length+1位置上的二进制数字一定是0,不会进行array[k]操作。
下面就简单了,如果和等于要求的数值,则将当前的集合输出,当前的集合就是i表示的,而集合i中的元素就是i的二进制位为1的位置对应的的数组元素。
当你的数组长度不是很长的时候效率还是比较高的,如果数组长度大于32,比如说超过32而小于64,则将外层循环的i换位64位的,将内存循环的32换为64。
上面代码还有可以优化的地方,你可以想一下,不过如果数组的长度太大,时间复杂度会很高,而且我不认为有什么方法可以降低时间复杂度。
@Wang Hui: 理解了,关键是第一步,用一个整数来表示一个集合的组合情况,确实是一种很好的算法。我的解法是用递归,感觉性能要比这种方法低,学习了。
@Wang Hui: 写的真不错,从效率上来说确实非常快,我格式化了下贴在下面。
@今昭: 谢谢^^
@水牛刀刀: 我测试了一下,数组长度15时,效率还是可以接受的,再长点,就麻烦了。其实这个算法可以优化的,可以剔除一部分不符合要求的集合,效率应该可以提升一些。
@Wang Hui:
非常感谢你的解答, 我用的是C#, for (unsigned int i=1; i<(1<<length); ++i,sum=0) 这句话应该怎么翻译呢?
@Wang Hui:
上面的问题我解决了
另外一个问题 “这里假定集合的数目小于32” 这个32 是怎么来的?
按照我给的示例 所有的集合应该是 2的9次方 对么?
@rq1986:
集合的数目小于32,这句话我写的有问题,应该是集合里的元素数目小于32. 因为我们在表示集合的使用使用的是一个int类型,有32位,如果你使用64位的int,那么就可以表示64个元素的集合了。
按照你给的:A = {1,2,3,4,5,6,7,8,9,10},数组A有10个元素,子集数目是2的10次方。
@rq1986: C#也是一样的,C#中对应的类型就是uint,至于移位操作都是一样的。
@Wang Hui:
谢谢你的回复 和 耐心。
我在尝试 做64个元素的时候, i << length 这个位置 要溢出 高位舍弃后, 数据就不对了, 这里应该怎么做呢?
完整代码:
static HashSet<string> results = new HashSet<string>();
static Stack<int> currentResult = new Stack<int>();
static void TryResult(int[] numbers, int target)
{
if (target == 0)
{
results.Add(string.Join(",", currentResult.OrderBy(n => n)));
}
else
{
var candidates = numbers.Where(n => n <= target);
foreach (var candidate in candidates)
{
currentResult.Push(candidate);
TryResult(numbers.Where(n => n != candidate).ToArray(), target - candidate);
currentResult.Pop();
}
}
}
static void Main(string[] args)
{
var result = 10;
var numbers = Enumerable.Range(1, 10).ToArray();
TryResult(numbers, result);
foreach (var r in results)
{
Console.WriteLine(r);
}
Console.Read();
}
当数据量很大的时候可能会很耗时,最好做下尾递归优化。