1. 程式人生 > >【演算法】2SUM/3SUM/4SUM問題

【演算法】2SUM/3SUM/4SUM問題

之前就總結過一些Leetcode上各種sum問題,今天再拿出來完整得總結一番。
nSUM問題是指,在一個數組中,找出n個數相加和等於給定的數,這個叫做nSUM問題。
常見的有2SUM,3SUM,4SUM問題,還有各種SUM問題的變種.
Leetcode上SUM問題包括:
1. 2SUM
15. 3Sum
16. 3Sum Closest
18. 4Sum
454. 4Sum II

2SUM問題

最常見的是2SUM問題(1. 2SUM),就是陣列中找出兩個數相加和為給定的數。
這道題有兩種思路:
1. 一種思路從首尾搜尋兩個數的和,並逐步逼近。
2. 另外一種思路是固定一個數A,看SUM-A是否在這個陣列之中。

對於第一種思路如下:

此方法是先將陣列從小到大排序
設定兩個指標,一個指向陣列開頭,一個指向陣列結尾,從兩邊往中間走。直到掃到滿足題意的為止或者兩個指標相遇為止。
此時這種搜尋方法就是類似於楊氏矩陣的搜尋方法,就是從 楊氏矩陣的左下角開始搜尋,直到找到為止。
如果此時題目條件變為如果沒有找出最接近的2SUM和問題,解決方法跟上面是一樣的
用這種方法2SUM問題的時間複雜度是O(nlogn)的,主要在於排序的時間。

第二種思路方法如下:

對陣列中的每個數建立一個map/hash_map 然後再掃一遍這個陣列,判斷target-nums[i]是否存在,如果存在則說明有,不存在繼續找。當然這樣做的話,需要處理一個細節:判重的問題。
程式碼如下【注意因為題目中說一定有解所以才下面這樣寫,如果不一定有解,則還要再加一些判斷】

vector<int> twoSum(vector<int>& nums, int target) {
    unordered_map<int,vector<int>> mark;
    for(int i=0;i<nums.size();i++)
        mark[nums[i]].push_back(i);
    for(int i = 0;i<nums.size();i++){
        if(target-nums[i] == nums[i]){
            if(mark[nums[i]].size() > 1
){ vector<int> tmp{i,mark[nums[i]][1]}; return tmp; } }else{ if(mark.find(target-nums[i]) != mark.end()){ vector<int> tmp{i,mark[target-nums[i]][0]}; return tmp; } } } }

這種方法的時間複雜度為O(n)

比較一下這兩個方法:

第一種方法的思路還是比較好的,魯棒性好,而且寫起來比較容易,但是因為預處理——排序的時間複雜度佔了大頭,所以其總時間複雜度為O(nlogn)
第二種方法,時間複雜度低,但是需要處理重複情況,略麻煩

3SUM問題

然後對於3 Sum問題,解決方法就是最外層遍歷一遍,等於選出一個數,
之後的陣列中轉化為找和為target-nums[i]的2SUM問題。
因為此時排序複雜度在3SUM問題中已經不佔據主要複雜度了,所以直接採用思路1的方法就好。

void two_sum(vector<int>& nums,int i,int target,vector<vector<int>> &result){
    int j = nums.size()-1;
    int b = i-1;
    while(i<j){
        if(nums[i]+nums[j] == target){
            result.push_back(vector<int>{nums[b],nums[i],nums[j]});
            //處理重複的情況
            i++;
            j--;
            while(i<j && nums[i] == nums[i-1]) i++;
            while(i<j && nums[j+1] == nums[j]) j--;
        }else{
            if(nums[i]+nums[j] < target)
                i++;
            else
                j--;
        }
    }
    return;
}
vector<vector<int>> threeSum(vector<int>& nums) {
    set<vector<int>> result;
    vector<vector<int>> result2;
    sort(nums.begin(),nums.end());
    for(int i=0;i<nums.size();i++)
        if(i>0&&nums[i-1]== nums[i])
            continue;
        else
            two_sum(nums,i+1,-nums[i],result2);

    return result2;
}

對於3SUM問題,上面這個解法的時間複雜度為O(n2)
其實對於3SUM問題,另外一種求解思路就是,先預處理一遍陣列中兩兩相加的結果,然後再遍歷每一個數nums[i],判斷target-nums[i]是否在預處理的那個和中,不過這種方法的複雜度也是O(n2)主要是預處理的複雜度。

4SUM問題

對於4Sum問題又衍生出了兩種思路:
1. 先遍歷第一個數,然後固定第一個數之後,轉化為剩下元素的3SUM問題
2. 先遍歷兩個數,求和,然後轉化為剩下元素的2SUM問題

第一種思路

其演算法複雜度是穩定的O(n3),最外層遍歷一遍O(n),然後轉為3SUM問題之後又是O(n2)。這種方法相當於4SUM呼叫3SUM,然後3SUM再呼叫2SUM,這樣函式呼叫有點多,不方便

具體寫出來的形式,可以寫成最外層兩個迴圈,即固定為兩個數之後,再化為2SUM。
程式碼如下【這個程式碼我是抄的Discuss裡的,我自己的是層層呼叫巢狀形式的寫法,執行時間有點長】

vector<vector<int>> fourSum(vector<int>& nums, int target) {
    vector<vector<int>> total;
    int n = nums.size();
    if(n<4)  return total;
    sort(nums.begin(),nums.end());
    for(int i=0;i<n-3;i++)
    {
        if(i>0&&nums[i]==nums[i-1]) continue;
        if(nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target) break;
        if(nums[i]+nums[n-3]+nums[n-2]+nums[n-1]<target) continue;
        for(int j=i+1;j<n-2;j++)
        {
            if(j>i+1&&nums[j]==nums[j-1]) continue;
            if(nums[i]+nums[j]+nums[j+1]+nums[j+2]>target) break;
            if(nums[i]+nums[j]+nums[n-2]+nums[n-1]<target) continue;
            int left=j+1,right=n-1;
            while(left<right){
                int sum=nums[left]+nums[right]+nums[i]+nums[j];
                if(sum<target) left++;
                else if(sum>target) right--;
                else{
                    total.push_back(vector<int>{nums[i],nums[j],nums[left],nums[right]});
                    do{left++;}while(nums[left]==nums[left-1]&&left<right);
                    do{right--;}while(nums[right]==nums[right+1]&&left<right);
                }
            }
        }
    }
    return total;
}

第二種思路

因為本質上我們是最外層兩個迴圈之後,找是否有target-now的值,我們可以事先做好預處理,即空間換時間,先迴圈兩次,找出兩個數所有可能的和,存到map裡(這裡可以unordered_map)。這兩等於是兩個O(n2)的時間複雜度相加和,所以最後時間複雜度為O(n2)
但是此時需要有一個判重的問題,所以需要map中存如下數
mp[num[i]+num[j]].push_back(make_pair(i,j));
然後再判重。

typedef pair<int,int> pii ;
vector<vector<int>> fourSum(vector<int>& nums, int target) {
    unordered_map<int,vector<pii>> mark;
    set<vector<int>> res;
    vector<vector<int>> res2;
    if(nums.size()<4)
        return res2;
 //這個地方也可以不用排序的,排序是因為減少一些計算量,方便下面的迴圈判定提前跳出條件
    sort(nums.begin(),nums.end());
    for(int i=0;i<nums.size();i++)
        for(int j=i+1;j<nums.size();j++)
            mark[nums[i]+nums[j]].push_back(make_pair(i,j));

//注意注意這個地方有一個巨大的坑,中間的判斷條件: i<nums.size()-3,會陷入到死迴圈中
//因為nums.size()是一個unsigned的型別,其與int相運算,得到的還是unsigned!!!!!
//所以如果nums.size()<3的話就會出現死迴圈,切記切記
    for(int i=0;i<nums.size()-3;i++){
    //先判定,提前跳出的情況
        if(nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target)
            break;
        for(int j=i+1;j<nums.size()-2;j++){
            if(mark.find(target-(nums[i]+nums[j])) != mark.end()){
                for(auto &&x:mark[target-(nums[i]+nums[j])]){
                    if(x.first>j){
                        vector<int> tmp{nums[i],nums[j],nums[x.first],nums[x.second]};
                        res.insert(tmp);
                    }
                }
            }
        }
    }

    for(auto &&x:res){
        res2.push_back(x);
    }
    return res2;
}

因為需要判重,所以最糟糕情況下其時間複雜度為O(n4)
那麼如果沒有判重問題,是否就可以解決了呢?
那就是454. 4Sum II問題
這道題是在四個陣列中,各選出一個數,使其和為某一定值。
則可以按照上述方法,講前兩個陣列所有可能的和做一個map,然後遍歷後兩個陣列所有可能的和,所以這個是O(n2)的演算法。

nSUM問題的推廣和複雜的分析

按照上面的演算法複雜度思路,4~6SUM問題,都可以用O(n3)來解決。

比如對於6SUM問題,先用一個O(n3)的方法,將其中所有三個數相加的可能的情況都儲存下來,這一步的時間複雜度是O(n3)。接下來用兩種方法都行:
1. 遍歷三個數,然後看剩下的和是否在儲存的可能性中。這一步的時間複雜度是O(n3)
2. 直接在儲存的可能性中遍歷,遍歷到SUM1之後,看target-SUM1是否也在這個可能性中。這一步的時間複雜度是O(n2)