[算法]子数组之和问题

这篇博客探讨了在给定整数数组和目标和的情况下,如何计算满足条件的子数组组合数目。作者通过0-1背包问题的视角,提出了几种解决方法,包括处理负数、数组中重复元素的情况,并提供了不同实现的代码示例。文章提到,对于大型数据,可能需要考虑存储效率更高的数据结构,如使用map代替连续数组。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题描述:给定一个含有n个元素的整形数组a,再给定一个和target,求出数组中满足给定和的所有元素组合的数目。举个例子,设有数组a[5] = { 1, 2, 3, 4, 5},sum = 8,则满足和为8的组合数为3,即{1,2,5}, {1,3,4}, {3, 5}。

http://www.cnblogs.com/graphics/archive/2011/07/14/2105195.html

链接处有个类似的题目,不过要求给出所有的组合情况,楼主采用穷举和回溯两种方法求解。这里只需要求解组合数目,所以可以更简单一点。


对于n个元素的数组,考虑所有的组合情况就是2^n种,即对于每个元素可以选或者不选,有点像0-1背包问题,不过目标不是最大化价值。

我们来看看这个问题可否划分子问题,当我们考虑第i个元素是否选择时,前i-1个元素的组合情况也就是不选i时的组合情况,现在我们考虑i,如果前i-1个数字可以组合出的和有s1,s2,...,st,那么考虑i时,即前i个元素可以组合出的和有i+s1,i+s2,...,i+st。如果我们把每个和的组合情况记录下来,那么再考虑每个元素选或不选的时候就可以直接累加组合出该sum值的情况数。

代码1如下:

// All numbers in the vector 'nums' are positive
int sumCnt1(vector
  
    &nums, int target)
{
    vector
   
     vSumCnt;
    vSumCnt.assign(target+1, 0);
    for(int i = 0; i < nums.size(); ++i)
    {
        vector
    
      nextSum = vSumCnt;
        ++nextSum[nums[i]];
        for(int j = 1; j < target; ++j)
        {
            if(0 == vSumCnt[j] || j + nums[i] > target) continue;
            nextSum[j+nums[i]] += vSumCnt[j];
        }
        swap(vSumCnt, nextSum);
    }
    return vSumCnt[target];
}

    
   
  

感觉从0-1背包的角度来理解这道题上面的代码应该可以了,但是这道题说的不够清楚,所以可能存在两个问题:

1. 如果数组中有负数,怎么办?上面的代码肯定处理不了。

怎么办呢?我们可以把存储空间的下标移动一下,使得临时求和值都不会出现越界的情况。

代码2如下:

// have negative numbers
int sumCnt2(vector
  
    &nums, int target)
{
    int negSum = 0, posSum = 0;
    for(size_t k = 0; k < nums.size(); ++k)
        if(nums[k] < 0) negSum += nums[k];
        else posSum += nums[k];
    int uperbound = posSum - negSum;
    vector
   
     vSumCnt;
    vSumCnt.assign(uperbound + 1, 0);
    for(int i = 0; i < nums.size(); ++i)
    {
        vector
    
      nextSum = vSumCnt;
        ++nextSum[nums[i] - negSum];
        for(int j = 0; j <= uperbound; ++j)
        {
            if(0 == vSumCnt[j] || j + nums[i] > uperbound) continue;
            nextSum[j+nums[i]] += vSumCnt[j];
        }
        swap(vSumCnt, nextSum);
    }
    return vSumCnt[target - negSum];
}

    
   
  

在编写上面的代码过程中,我还犯了一个严重的错误,对于{-1,2,-3,4,-5},target=1的情况,我刚开始采用的辅助存储空间只考虑到[0,1+9],这样就导致了{2,4,-5}这个组合没有计算在内,因为2+4=6,再考虑负数的偏差9之后就会使得该值超过了10,我们不考虑了,其实该值虽然比目标值大,但是可能存在负数使它减小,因此辅助的存储空间需要更大一点,严格点将应该是min{所有正数求和+负数的偏差,target+2*负数的偏差}。

上面的代码虽然在处理较小的数据时非常有效,但是还存在一个问题,如果数组是{10000, 5000, 65535, 7896},而target=15000,我们需要的辅助存储空间用到的非常稀疏,虽然采用连续数组存储可以随机访问,但是当数组size较小,但值较大时,存储空间有大量的浪费。下面是采用map进行存储的修正版本,但是两者哪个更好,得看实际情况。

代码3如下:

// have negative number, use map
int sumCnt3(vector
  
    &nums, int target)
{
    map
   
     mapSumCnt, nextMap;
    map
    
     ::iterator it, nextIt;
    for(int i = 0; i < nums.size(); ++i)
    {
        nextIt = nextMap.find(nums[i]);
        if(nextIt != nextMap.end()) ++(nextIt->second);
        else nextMap[nums[i]] = 1;
        for(it = mapSumCnt.begin(); it != mapSumCnt.end(); ++it)
        {
            nextIt = nextMap.find(it->first + nums[i]);
            if(nextIt != nextMap.end()) nextIt->second += it->second;
            else nextMap[it->first + nums[i]] = it->second;
        }
        mapSumCnt = nextMap;
    }
    return mapSumCnt[target];
}

    
   
  

第二个问题,来举个例子,数组{-1, 2, 2},target=1,究竟应该输出1合适2?

如果把数组中两个2看作不同的数字,应该是2种组合方式,但是如果只看值不看位置呢?如何去除这种重复?

能不能先去掉数组中重复的元素,再用上面的方法?那么看看这个例子{2, 2},target = 4?所以,答案是不能。

还是可以用背包问题的角度来看,现在不是0-1了,有些物品不止一个,假设有k个,你可以考虑不选、选1个、选2个、...、选k个。

代码4如下:

// delete duplication
int sumCnt4(vector
  
    &nums, int target)
{
    map
   
     mapSumCnt, nextMap;
    map
    
     ::iterator it, nextIt;
    sort(nums.begin(), nums.end());
    int i = 0;
    while(i < nums.size())
    {
        int key = nums[i];
        int cnt = 1;
        while(++i < nums.size() && nums[i] == key) 
        {
            ++cnt;
        }
        for(int k = 1; k <= cnt; ++k)
        {
            int add = key * k;
            nextIt = nextMap.find(add);
            if(nextIt != nextMap.end()) ++(nextIt->second);
            else nextMap[add] = 1;
            for(it = mapSumCnt.begin(); it != mapSumCnt.end(); ++it)
            {
                nextIt = nextMap.find(it->first + add);
                if(nextIt != nextMap.end()) nextIt->second += it->second;
                else nextMap[it->first + add] = it->second;
            }
        }
        mapSumCnt = nextMap;
    }
    return mapSumCnt[target];
}

    
   
  


注:以上代码仅在小数据集上进行测试,有任何问题欢迎讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值