1. 程式人生 > 其它 >LeetCode 16 3Sum Closest 可用三分法

LeetCode 16 3Sum Closest 可用三分法

技術標籤:leetcodemedium標籤leetcode

題意:3Sum題變種,給出n個數nums[]和一個target,求n中的3個數的組合使得其和與target相差值最小。

思路:傳統的3Sum做法(LeetCode15),列舉三個數中的兩個,然後求證剩餘的數字是否在給出的集合中。後面一步可以時間換空間,用二分來做,這樣複雜度乘上一個logn;也可以空間換時間,用陣列直接儲存或者hash掛鏈。

對於這一題,可以繼續用傳統的解法,先列舉三個數中的兩個,但注意到目標答案的變化並不是單調的,即取出兩個數nums[i]和nums[j]後,最優解的計算過程為ans=f(v)=min(target-nums[i]-nums[j]-v),這個關於v的函式是先減後增的,是一個經典的可以使用三分法計算最優解的模型。

很久沒有寫三分了,可能別人有更好的寫法,這裡介紹一種常數可能高一點但一定不會錯的寫法:

  1. 首先取l,r,得到端點間距離d=r-l;
  2. 如果d<3,退出迴圈,將l到r的每個點對應的值直接算出來從而對答案進行維護;
  3. 否則機算三等分的長度len=d/3,取p1=l+len,p2=r-len;
  4. 因為len=d/3<=(double)d/3,再加上之前d>=3時的約束,可以保證p1<p2;
  5. 比較f(p1)與f(p2)的大小,取較高者進行三分;

關於第5步取較高點的原因,下圖即為例子:

若是取低點,即左側藍點,則左端點向右側移動,很明顯最優解被排除到區間外。

以下為程式碼部分:

#include <algorithm>
#include <cmath>

class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        int ans = nums[0] + nums[1] + nums[2];
        for (int i = 0; i < n; ++i){
            for (int j = i + 1; j < n; ++j){
                int now = target - nums[i] - nums[j];
                int l = j + 1, r = n - 1;
                int p1, p2, v1, v2;
                while(r - l > 3){
                    int len = (r-l) / 3;
                    int p1 = l + len;
                    int p2 = r - len;
                    v1 = abs(now-nums[p1]);
                    v2 = abs(now-nums[p2]);
                    if (v1 > v2) l = p1;
                    else r = p2;
                }
                while(l <= r){
                    if (abs(target-ans) > abs(now-nums[l]))
                        ans = nums[i] + nums[j] + nums[l];
                    ++l;
                }
            }
        }
        return ans;
    }
};

當然O(n²logn)並不是最優解,事實上這一題有O(n²)的做法。只需要列舉三個數字中最小的一個nums[i],然後取[l,r],其中l=i+1r=n-1,然後每次使用ans=nums[i]+nums[l]+nums[r]對答案進行維護,當ans小於target時,++l,否則r--;

這次得到了新的二元函式f(l,r),試證明正確性:

用反證法,假設真正的答案在[l,r]收斂過程之外,記為x,y,那麼可以確定在[l,r]收斂過程中,必定存在[x+1,r](或[l,y-1]對應另一種情況),此時有r>y;

那麼nums[x]+nums[r]+nums[i]<target,又y<r,則nums[y]<nums[r],

=>nums[x]+nums[y]+nums[i]<nums[x]+nums[r]+nums[i];

因此[x,y]並不是真正的最優解,與假設矛盾,故得證。

(一把年紀了居然還會做證明題)

程式碼部分如下:

#include <algorithm>
#include <cmath>

class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        int n = nums.size();
        int ans = nums[0] + nums[1] + nums[2];
        int delta = abs(target-ans);
        sort(nums.begin(), nums.end());
        for(int i = 0; i < n; ++i){
            int l = i + 1;
            int r = n - 1;
            while(l < r){
                int now = nums[i] + nums[l] + nums[r];
                if(now == target)
                    return now;
                if(abs(now-target) < delta){
                    delta = abs(now-target);
                    ans = now;
                }
                if(now - target < 0)
                    ++l;
                else
                    r--;
            }
        }
        return ans;
    }
};

以及時間效率的對比: