1. 程式人生 > >每日一練——從長度為n的數組裡選出m個數使和為固定值sum

每日一練——從長度為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。程式碼如下:
void CalSum(vector<int> &nums, int result)  
{
    int len = nums.size();
    int bit = 1 << len;
    for (int i = 1; i < bit; i++)//從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)
        {
            for (vector<int>::iterator iter = tmp.begin(); iter != tmp.end(); iter++)
            {
                cout << *iter << " ";
            }
            cout << endl;
        }
    }  
} 
網上有個評論說這個方法其實可以進行剪枝優化,原評論如下: 我們先對數字排個序{-10, 6, 9, 10, 17, 18, 20, 35, 45, 99}, 當二進位制數為1001110000,已經算出35了那麼1001110001-1001111111其實都是不用算的(肯定大於35),同樣0001110000已經大於35了,可也需要不少次無用的迴圈校驗,才能進位到0010000000,如果能把中間這些無用的迴圈略過,效率還能有很大提高! 根據這個評論提示,也就是如果1001110000已經為35了那麼下一個就是看1010000000,如果1001010000是35那麼下一個看1001100000。那麼後面那個數字是怎麼算出來的呢,我們可以發現這些數字的共同點就是最左邊的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。程式碼如下:
int 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;
            }
        }
    }  
} 


參考: