LeetCode 16 3Sum Closest 可用三分法
題意: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的函式是先減後增的,是一個經典的可以使用三分法計算最優解的模型。
很久沒有寫三分了,可能別人有更好的寫法,這裡介紹一種常數可能高一點但一定不會錯的寫法:
- 首先取l,r,得到端點間距離d=r-l;
- 如果d<3,退出迴圈,將l到r的每個點對應的值直接算出來從而對答案進行維護;
- 否則機算三等分的長度len=d/3,取p1=l+len,p2=r-len;
- 因為len=d/3<=(double)d/3,再加上之前d>=3時的約束,可以保證p1<p2;
- 比較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+1,r=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;
}
};
以及時間效率的對比: