算法題中求解絕對值最值的技巧

引言

現在算法題中,有時會遇到求解絕對值最值的問題,比如給定一個數組,求解absaiajabs|a_i - a_j|的最大值。諸如此類問題,暴力解法是用O(n2)O(n^2)時間複雜度遍歷i,ji,j。而一種常用優化是將絕對值拆開,比如absaiaj=max(aiaj,ajai)abs|a_i - a_j| = max(a_i - a_j, a_j - a_i),將問題拆成兩個子問題,而每個子問題通過維護當前最小值,即O(n)O(n)的時間可解出。最後通過O(1)O(1)取二者最值即可。

下面分享三道利用此技巧的算法題,難度按升序排列。

Leetcode 1131

題意:
給定兩個數組arr1arr1arr2arr2,讓找兩個下標iijj,使得arr1[i]arr1[j]+arr2[i]arr2[j]+ij|arr1[i] - arr1[j]| + |arr2[i] - arr2[j]| + |i - j|最大。

思路:
這裏直接拆開三個絕對值,得到以下8個子問題:
本圖來源於該題目的解答
然後我們把有關於下標ii和下標jj的項提出來放在一起,發現其實子問題只有以下四個:1&8;2&7;3&6;4&5(比如1和8,它們只不過把兩個下標iijj順序調換了下)。
而這四個子問題的差別就是它們中的符號爲加減法的排列組合:++++++ -+- +- -。這樣通過四個O(n)O(n)的循環即可求出最優解。

代碼:

const int INF = 0x3f3f3f3f;
class Solution {
public:
    int maxAbsValExpr(vector<int>& x, vector<int>& y) {
        int res = 0, n = x.size();
        for (int sign1=-1; sign1<=1; sign1+=2)
            for (int sign2=-1; sign2<=1; sign2+=2) {
                int mx = -INF, mn = INF;
                for (int i=0; i<n; i++) {
                    int val = x[i] + sign1*y[i] + sign2*i;
                    mx = max(mx, val);
                    mn = min(mn, val);
                }
                res = max(res, mx - mn);
            }
        return res;
    }
};

Leetcode 1330

題意:
給定一個數組numsnums,它的valuevalue值被定義爲所有nums[i]nums[i+1]|nums[i]-nums[i+1]|的和(其中0<=i<=n0 <= i <= n)。我們有一個操作,能使它某一段連續的子數組翻轉一次,問翻轉前/後這個數組的valuevalue值最大能爲多少?

思路:

  • 如果我們選擇不翻轉子數組,那麼它的valuevalue值可以通過O(n)O(n)時間遍歷得到。這個值我們記錄爲sum1sum_1
  • 如果我們翻轉子數組[L,R][L, R],這時新數組的valuevalue爲:
    sum1+a[R]a[L1]+a[L]a[R+1]a[L]a[L1]a[R]a[R+1]sum_1 + |a[R] - a[L-1]| + |a[L] - a[R+1]| - |a[L] - a[L-1]| - |a[R] - a[R+1]|,這裏sum1sum_1是不變的就可以不管它,然後又出現了絕對值最值問題。
    此時我們只展開前兩個絕對值,因爲展開絕對值的目的是把與LL相關的放一起,與RR相關的放一起,而後兩個絕對值裏只包含L/RL/R了。
    展開後,得到這二者:
    1.(a[R]a[R+1]a[R]a[R+1])(a[L1]a[L]+a[L]a[L1])(a[R]-a[R+1] - |a[R] - a[R+1]|) - (a[L-1] - a[L] + |a[L] - a[L-1]|)
    2.(a[R]+a[R+1]a[R]a[R+1])(a[L1]+a[L]+a[L]a[L1])(a[R]+a[R+1] - |a[R] - a[R+1]|) - (a[L-1] + a[L] + |a[L] - a[L-1]|)
    最後,取二者最大值即可。

代碼:

class Solution {
public:
    
    int solve(const vector<int>& A) {
        int n = A.size(), base = 0;
        for (int i=0; i<n-1; i++)
            base += abs(A[i] - A[i+1]);
        int inc = 0;
        for (int sign=-1; sign<=1; sign+=2) {
            int mnVal = A[0] + sign*A[1] + abs(A[0]-A[1]);
            for (int i=1; i<n-1; i++) {
                int biggerVal = A[i] + sign*A[i+1] - abs(A[i]-A[i+1]);
                inc = max(inc, biggerVal - mnVal);
                mnVal = min(mnVal, A[i] + sign*A[i+1] + abs(A[i]-A[i+1]));
            }
        }
        return base + inc;
    }
    
    int maxValueAfterReverse(vector<int>& nums) {
        if (nums.size() <= 1) return 0;
        int res1 = solve(nums);
        reverse(nums.begin(), nums.end());
        int res2 = solve(nums);
        return max(res1, res2);
    }
};

Google Kick Start Round A 2019 - Parcels

題意:
給定一個RCR*C的01地圖,標記爲1的位置爲快遞站點,標記爲0的位置爲收快遞的區域。假如我們站在某個0位置,那麼送快遞的時間爲距離我最近的快遞站到我當前位置的曼哈頓距離。爲了減小送快遞時間,我們可以在某個0的位置新建一個快遞站點,問建立完後大家收快遞所需要的最長時間最快是多少?

思路:
這個題暴力解法是,對於每個潛在位置,都試圖建立一個快遞站,然後BFS求一遍最長時間,時間複雜度是O((RC)2)O((RC)^2)

由於新建快遞站點的位置很難確定,我們可以先用二分法把最優化問題轉化爲判定性問題,即,給定一個最長時間TT,我們能否找到新增一個新快遞站,使得所有快遞的運輸時間都小於等於TT

那麼我們二分枚舉TT的值,然後對於某個TT,遍歷一邊整個地圖找到所有運輸時間大於TT的位置,記錄下來(假設有mm個,m<RCm < RC)。現在問題存不存在一個新快遞快遞站點,使得這些位置的運輸時間小於T。

這裏曼哈頓距離是絕對值形式表示的,於是兩點之間的距離可以被表示爲:dist((x1,y1),(x2,y2))=max(abs(x1+y1(x2+y2)),abs(x1y1(x2y2)))dist((x1, y1), (x2, y2)) = max(abs(x1 + y1 - (x2 + y2)), abs(x1 - y1 - (x2 - y2)))。這裏假設(x1,y1)(x1, y1)爲快遞點的位置,(x2,y2)(x2, y2)爲運輸時間大於TT的位置,我們遍可以利用O(m)O(m)的方式求出(x2+y2)(x2 + y2)(x2y2)(x2 - y2)的最值,然後再遍歷一邊地圖檢驗是否存在這一快遞點(x1,y1)(x1, y1)即可。

這種解法利用了二分和絕對值優化,它總時間複雜度爲O(RClog(R+C))O(RC *log(R+C))。本題還有一個O(RC)O(RC)的方法,在此不做介紹(不會…)。

關鍵代碼:

bool ok(int K) {
    vector<pair<int, int> > focusBlanks;
    int mx1 = -INF, mn1 = INF, mx2 = -INF, mn2 = INF;
    for (int i=0; i<row; i++) {
        for (int j=0; j<col; j++) if (dis[i][j] > K) {
            focusBlanks.push_back(make_pair(i, j));
            mx1 = max(mx1, i + j); mn1 = min(mn1, i + j);
            mx2 = max(mx2, i - j); mn2 = min(mn2, i - j);
        }
    }
    if (focusBlanks.size() == 0) return true;
    for (int i=0; i<blanks.size(); i++) {
        int x2 = blanks[i].first, y2 = blanks[i].second;
        int dis1 = max(abs(mx1 - (x2 + y2)), abs(mn1 - (x2 + y2)));
        int dis2 = max(abs(mx2 - (x2 - y2)), abs(mn2 - (x2 - y2)));
        if (max(dis1, dis2) <= K) return true;
    }
    // printf("%d : %d\n", K, false);
    return false;
}
發佈了41 篇原創文章 · 獲贊 44 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章