算法题中求解绝对值最值的技巧

引言

现在算法题中,有时会遇到求解绝对值最值的问题,比如给定一个数组,求解 a b s a i a j abs|a_i - a_j| 的最大值。诸如此类问题,暴力解法是用 O ( n 2 ) O(n^2) 时间复杂度遍历 i , j i,j 。而一种常用优化是将绝对值拆开,比如 a b s a i a j = m a x ( a i a j , a j a i ) abs|a_i - a_j| = max(a_i - a_j, a_j - a_i) ,将问题拆成两个子问题,而每个子问题通过维护当前最小值,即 O ( n ) O(n) 的时间可解出。最后通过 O ( 1 ) O(1) 取二者最值即可。

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

Leetcode 1131

题意:
给定两个数组 a r r 1 arr1 a r r 2 arr2 ,让找两个下标 i i j j ,使得 a r r 1 [ i ] a r r 1 [ j ] + a r r 2 [ i ] a r r 2 [ j ] + i j |arr1[i] - arr1[j]| + |arr2[i] - arr2[j]| + |i - j| 最大。

思路:
这里直接拆开三个绝对值,得到以下8个子问题:
本图来源于该题目的解答
然后我们把有关于下标 i i 和下标 j j 的项提出来放在一起,发现其实子问题只有以下四个:1&8;2&7;3&6;4&5(比如1和8,它们只不过把两个下标 i i j j 顺序调换了下)。
而这四个子问题的差别就是它们中的符号为加减法的排列组合: + + ++ + + - + - + - - 。这样通过四个 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

题意:
给定一个数组 n u m s nums ,它的 v a l u e value 值被定义为所有 n u m s [ i ] n u m s [ i + 1 ] |nums[i]-nums[i+1]| 的和(其中 0 < = i < = n 0 <= i <= n )。我们有一个操作,能使它某一段连续的子数组翻转一次,问翻转前/后这个数组的 v a l u e value 值最大能为多少?

思路:

  • 如果我们选择不翻转子数组,那么它的 v a l u e value 值可以通过 O ( n ) O(n) 时间遍历得到。这个值我们记录为 s u m 1 sum_1
  • 如果我们翻转子数组 [ L , R ] [L, R] ,这时新数组的 v a l u e value 为:
    s u m 1 + a [ R ] a [ L 1 ] + a [ L ] a [ R + 1 ] a [ L ] a [ L 1 ] 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]| ,这里 s u m 1 sum_1 是不变的就可以不管它,然后又出现了绝对值最值问题。
    此时我们只展开前两个绝对值,因为展开绝对值的目的是把与 L L 相关的放一起,与 R R 相关的放一起,而后两个绝对值里只包含 L / R L/R 了。
    展开后,得到这二者:
    1. ( a [ R ] a [ R + 1 ] a [ R ] a [ R + 1 ] ) ( a [ L 1 ] a [ L ] + a [ L ] a [ L 1 ] ) (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 [ L 1 ] + a [ L ] + a [ L ] a [ L 1 ] ) (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

题意:
给定一个 R C R*C 的01地图,标记为1的位置为快递站点,标记为0的位置为收快递的区域。假如我们站在某个0位置,那么送快递的时间为距离我最近的快递站到我当前位置的曼哈顿距离。为了减小送快递时间,我们可以在某个0的位置新建一个快递站点,问建立完后大家收快递所需要的最长时间最快是多少?

思路:
这个题暴力解法是,对于每个潜在位置,都试图建立一个快递站,然后BFS求一遍最长时间,时间复杂度是 O ( ( R C ) 2 ) O((RC)^2)

由于新建快递站点的位置很难确定,我们可以先用二分法把最优化问题转化为判定性问题,即,给定一个最长时间 T T ,我们能否找到新增一个新快递站,使得所有快递的运输时间都小于等于 T T

扫描二维码关注公众号,回复: 9320094 查看本文章

那么我们二分枚举 T T 的值,然后对于某个 T T ,遍历一边整个地图找到所有运输时间大于 T T 的位置,记录下来(假设有 m m 个, m < R C m < RC )。现在问题存不存在一个新快递快递站点,使得这些位置的运输时间小于T。

这里曼哈顿距离是绝对值形式表示的,于是两点之间的距离可以被表示为: d i s t ( ( x 1 , y 1 ) , ( x 2 , y 2 ) ) = m a x ( a b s ( x 1 + y 1 ( x 2 + y 2 ) ) , a b s ( x 1 y 1 ( x 2 y 2 ) ) ) dist((x1, y1), (x2, y2)) = max(abs(x1 + y1 - (x2 + y2)), abs(x1 - y1 - (x2 - y2))) 。这里假设 ( x 1 , y 1 ) (x1, y1) 为快递点的位置, ( x 2 , y 2 ) (x2, y2) 为运输时间大于 T T 的位置,我们遍可以利用 O ( m ) O(m) 的方式求出 ( x 2 + y 2 ) (x2 + y2) ( x 2 y 2 ) (x2 - y2) 的最值,然后再遍历一边地图检验是否存在这一快递点 ( x 1 , y 1 ) (x1, y1) 即可。

这种解法利用了二分和绝对值优化,它总时间复杂度为 O ( R C l o g ( R + C ) ) O(RC *log(R+C)) 。本题还有一个 O ( R C ) 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;
}
发布了40 篇原创文章 · 获赞 44 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/Site1997/article/details/104177941