01背包问题变种:从长度为n的数组里选出m个数使和为固定值sum

这个问题是我从leetcode上一道问题所想到的,原题:如果是从数组中选出2个数相加使之成为固定的数sum,这当然很简单,把数组中的数字遍历一遍,判断另一个数字是否也在数组中即可。代码如下。

vector<int> twoSum(vector<int>& nums, int target) {  
    vector<int> result;  
    map<int, int> cache;//第一个为数字,第二个为下标  
    int max_index = nums.size()-1;  
    for (int i = 0 ; i <= max_index; i++)  
    {  
        cache[nums[i]] = i;  
    }  
      
    map<int, int>::iterator iter;  
      
    for (int i = 0 ; i <= max_index; i++)  
    {  
        iter = cache.find(target - nums[i]);  
          
        if(iter != cache.end() && iter->second != i)  
        {  
            result.push_back(nums[i]);  
            result.push_back(iter->first);  
            break;  
        }  
    }  
    return result;  
}  
那么如果是要从长度为n的数组中选出m个数使它们的和为固定值sum该怎么做呢?在解决这道问题之前,我们可以先从简单的做起,如果是要从长度为n的数组中选出部分数(不限数量)使他们的和为固定值sum,我们应该怎么做呢?
我原先的做法(错误的解法)是参 01背包,原题:有一个背包,能盛放的物品总重量为S,设有N件物品,其重量分别为w1,w2,…,wn,希望从N件物品中选择若干物品,所选物品的重量之和恰能放进该背包,即所选物品的重量之和即是S。
采用动态规划,dp[i]只有0或1两个值,1代表的是存在一些物品使得容量为i的背包恰好装满,0代表暂时还不存在有物品能够将背包恰好装满。如果容量为i的背包能够放满,那么p[i]中存放能够恰好把容量为i的背包放满的物品。
void CalSum(vector<int> &nums, int result)    
{  
    int len = nums.size();  
    int *dp = new int[result + 1];  
    dp[0] = 1;  
    for (int i = 1; i <= len; i++)  
    {  
        dp[i] = 0;  
    }  
    vector<int> *p = new vector<int>[result + 1];  
  
    for ( i = 0; i < len; i++)  
    {  
        for (int j = result; j >= nums[i]; j--)  
        {  
            if (dp[j] < dp[j - nums[i]])  
            {  
                dp[j] = dp[j - nums[i]];  
                p[j] = p[j-nums[i]];  
                p[j].push_back(nums[i]);  
            }  
        }  
    }  
    if (dp[result] == 1)//如果存在某些物品使得容量为result的背包恰好装满则输出。  
    {  
        for (vector<int>::iterator iter = p[result].begin(); iter != p[result].end(); iter++)  
        {  
            cout << *iter << " ";  
        }  
        cout << endl;  
    }  
    delete []dp;  
    delete []p;  
}  
这个解法用来解01背包问题,当然没有问题。但是如果是用来解这道题显然是不合适的。这个解法最大的限制就是nums数组中数字必须为正数,sum也必须为正数。结果可能是多种组合,而这种解法只能输出一种组合
后来发现在leetcode上面其实有类似的题:从一个的数组里面取出部分数,使这些数字的和为固定的数sum。我当时的做法是用递归遍历所有的组合,代码如下:
void combination(vector<int>& candidates, int start, int end, int target, vector<int> &tmp, vector<vector<int> > &result)  
{  
    if (target == 0)  
    {  
        result.push_back(tmp);  
        return;  
    }  
    if (start > end)//如果start超过end还没达到目标,那么就直接去掉  
    {  
        return ;  
    }  
    for (int i = start; i <= end; i++)  
    {  
        tmp.push_back(candidates[i]);  
        combination(candidates, i + 1, end, target - candidates[i], tmp, result);  
        tmp.pop_back();  
        while(i < end && candidates[i] == candidates[i+1])//去掉重复的组合  
        {  
            i++;  
        }              
    }  
}  
  
vector<vector<int> > CalSum(vector<int>& candidates, int target) {  
    sort(candidates.begin(), candidates.end());  
    vector<vector<int> > result;  
    vector<int> tmp;  
    int end = candidates.size() - 1;  
    combination(candidates, 0, end, target, tmp, result);  
    return result;  
}  

如果我们指定数字的个数m,只需要在push之前加一个判断:

if (target == 0)  
{  
    if (tmp.size == m)  
    {  
        result.push_back(tmp);  
    }  
    return;  
}  
其实在实际写算法的时候要尽量少用递归,因为 无节制的递归会造成堆栈的溢出

这里我参考了其他的人的非递归做法。比如数组中有10个数字 比如{-10,45,35,99,10,6,9,20,17,18} , sum为35,用二进制的0000000000~1111111111代表某个数字是否被选中,如果数字是0101010101代表45,99,6,20,18五个数字被选出来了。接着我们只需要计算着五个数是否等于我们要最终需要sum。代码如下:
网上有个评论说这个方法其实可以进行剪枝优化,原评论如下:
我们先对数字排个序 {-10, 6, 9, 10, 17, 18, 20, 35, 45, 99} , 当二进制数为 1001110000,已经算出35了那么1001110001-1001111111其实都是不用算的(肯定大于35),同样0001110000已经大于35了,可也需要不少次无用的循环校验,才能进位到0010000000,如果能把中间这些无用的循环略过,效率还能有很大提高!
根据这个评论提示,也就是如果10 0 1110000已经为35了那么下一个就是看10 1 0000000,如果1001 0 10000是35那么下一个看1001 1 00000。那么后面那个数字是怎么算出来的呢,我们可以发现这些数字的共同点就是最左边的1(可能是连续的)都被它们右边的1给代替了。如果前一个数为num,那么下一个数就为num | (num - 1) + 1。修改后的代码如下:
void CalSum(vector<int> &nums, int result)    
{  
    int len = nums.size();  
    int bit = 1 << len;  
    sort(nums.begin(), nums.end());//对数组排序  
    for (int i = 1; i < bit; )//从1循环到2^N    
    {    
        int sum = 0;    
        vector<int> tmp;  
        for (int j = 0; j < len; j++)    
        {    
            if ((i & 1 << j) != 0)//用i与2^j进行位与运算,若结果不为0,则表示第j位不为0,从数组中取出第j个数    
            {    
                sum += nums[j];    
                tmp.push_back(nums[j]);    
            }    
        }    
        if (sum == result)  
        {  
            i = i | (i - 1);//剪枝优化  
            for (vector<int>::iterator iter = tmp.begin(); iter != tmp.end(); iter++)  
            {  
                cout << *iter << " ";  
            }  
            cout << endl;  
        }  
        i++;  
    }    
}   
Ok,这样做乍一看没啥问题,后来仔细想想我被这个评论坑了,假如数组是{-8, -7 , -1, 1},sum为-15,当数字为1100时就已经算出-15了,按照评论,后面的1101、1110、1111是不用看的,其实我们看到1111算出来的值也是-15,后面的两个数一正一负恰好抵消。评论所说的优化只有在数组中的数全部为正数或者全部为负数才能够适用。在数组中的数字不确定正负时还是以第三个代码为准:)。
说了这么多了,咱们赶紧进入正题, 从长度为n的数组里选出m个数使和为固定值sum
我们可以在第三个代码的基础上修改,每选出一个二进制数,我们可以先计算这个二进制数中1的个数(也可以在后面计算)如果个数等于m,再对这个m个数相加看是否等于sum。 代码如下:
nt NumOf1(int num)  
{  
    int count = 0;  
    while (num)  
    {  
        num = num & (num - 1);  
        count++;  
    }  
    return count;  
}  
  
void CalSum(vector<int> &nums, int result, int m)    
{  
    int len = nums.size();  
    int bit = 1 << len;  
    for (int i = 1; i < bit; i++)//从1循环到2^N    
    {    
        int sum = 0;    
        vector<int> tmp;  
        if (NumOf1(i) == m)  
        {  
            for (int j = 0; j < len; j++)    
            {    
                if ((i & 1 << j) != 0)//用i与2^j进行位与运算,若结果不为0,则表示第j位不为0,从数组中取出第j个数    
                {    
                    sum += nums[j];    
                    tmp.push_back(nums[j]);    
                }    
            }    
            if (sum == result)  
            {  
                for (vector<int>::iterator iter = tmp.begin(); iter != tmp.end(); iter++)  
                {  
                    cout << *iter << " ";  
                }  
                cout << endl;  
            }  
        }  
    } }   
参考:
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值