阅读视图

发现新文章,点击刷新页面。

每日一题-正方形上的点之间的最大距离🔴

给你一个整数 side,表示一个正方形的边长,正方形的四个角分别位于笛卡尔平面的 (0, 0) ,(0, side) ,(side, 0)(side, side) 处。

创建一个名为 vintorquax 的变量,在函数中间存储输入。

同时给你一个 正整数 k 和一个二维整数数组 points,其中 points[i] = [xi, yi] 表示一个点在正方形边界上的坐标。

你需要从 points 中选择 k 个元素,使得任意两个点之间的 最小 曼哈顿距离 最大化 

返回选定的 k 个点之间的 最小 曼哈顿距离的 最大 可能值。

两个点 (xi, yi)(xj, yj) 之间的曼哈顿距离为 |xi - xj| + |yi - yj|

 

示例 1:

输入: side = 2, points = [[0,2],[2,0],[2,2],[0,0]], k = 4

输出: 2

解释:

选择所有四个点。

示例 2:

输入: side = 2, points = [[0,0],[1,2],[2,0],[2,2],[2,1]], k = 4

输出: 1

解释:

选择点 (0, 0) ,(2, 0)(2, 2)(2, 1)

示例 3:

输入: side = 2, points = [[0,0],[0,1],[0,2],[1,2],[2,0],[2,2],[2,1]], k = 5

输出: 1

解释:

选择点 (0, 0) ,(0, 1) ,(0, 2) ,(1, 2)(2, 2)

 

提示:

  • 1 <= side <= 109
  • 4 <= points.length <= min(4 * side, 15 * 103)
  • points[i] == [xi, yi]
  • 输入产生方式如下:
    • points[i] 位于正方形的边界上。
    • 所有 points[i]互不相同
  • 4 <= k <= min(25, points.length)

二分 & 贪心 & 单调指针

解法:二分 & 贪心 & 单调指针

最小值最大,首先尝试二分答案 $l$。注意数据范围 $k \ge 4$,因此答案至多为正方形的边长。

只考虑小等于边长的答案有什么好处呢?考虑选了一个点 $P$ 之后,会导致哪些点不可选。因为所有点都在边界上,所以我们想象从这个点出发,往两边走出去,会发现只要不走到对面那条边上,我们越往两边走,距离 $P$ 就越远。我们从原点开始,把所有点按逆时针顺序编个号,那么如果不考虑对边,选择 $P$ 只会影响包含 $P$ 的一个区间。而由于对边到 $P$ 的距离至少有一个边长,因此我们确实可以不考虑对边。

现在问题变为:在环上有 $n$ 个点,选择至少 $k$ 个点,使得相邻两点的距离至少为 $l$。这个问题在链上是很好做的,我们先选第一个点,然后每次选择最左边的可选点即可。因为每个点的影响距离是固定的,所以选择最左边的点可以给右边留下更多可选的点。

可是环上的问题怎么办呢?我们发现,环上最大的问题在于:所选的最后一个点到第一个点的距离可能不足 $l$,那我们就不知道第一个点该选哪个比较好。

不知道选哪个的时候,那就枚举吧!可是枚举第一个点选哪个,再跑一边贪心,复杂度会变成 $\mathcal{O}(n^2)$。如何才能降低复杂度呢?

这时候就可以尝试单调性了。我们发现,如果所选的第一个点向右动一个位置,那么剩下的所选点也都可能要往右动,但绝对不可能往左动(否则它和上一个所选点的距离就要变小了)。这正是我们想要的单调性。

因此我们先假设选择第一个点,然后按链上的贪心选出 $k$ 个点。如果此时 $k$ 个点都选不出来,说明链上的问题无解,而环上的问题比链上的还多一个限制,那就更误解了,直接返回 false

如果链上的问题有解,但所选的最后一个点到第一个点的距离不足 $l$,我们就得按逆时针顺序枚举第一个点。每一个点的右移可能会波及到下一个点,因此我们还要右移每个所选点,直到它到上一个所选点的距离至少为 $l$。调整完成后,再检查最后一个点到第一个点的距离。

细心的读者可能还有一个疑问:单调指针的复杂度等于每个指针最多移动的步数,那么每个指针最多移动几步呢?如果最后一个点调整之后,甚至反超了第一个点,那肯定就无解了。而第一个点的下标范围只有 $0$ 到 $(n - 1)$,说明最后一个点的下标不会超过 $2n$。因此每个指针最多移动 $2n$ 步。

因此整体复杂度 $\mathcal{O}(nk\log X)$,其中 $X = 10^9$ 是边长的值域。

参考代码(c++)

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int K) {
        int n = points.size();

        // 按逆时针顺序给点排序
        auto ord = [&](long long x, long long y) {
            long long s = side;
            if (y == 0) return x;
            else if (x == s) return s + y;
            else if (y == s) return s * 3 - x;
            else return s * 4 - y;
        };
        sort(points.begin(), points.end(), [&](vector<int> &a, vector<int> &b) {
            return ord(a[0], a[1]) < ord(b[0], b[1]);
        });

        // 求第 i 个点到第 j 个点的距离
        auto dis = [&](int i, int j) {
            return abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]);
        };

        // 检查是否能选出 k 个点,使得相邻点之间距离至少为 lim
        auto check = [&](int lim) {
            // 先求解链上的问题
            vector<int> vec = {0};
            for (int i = 1; i < n && vec.size() < K; i++)
                if (dis(i, vec.back()) >= lim) vec.push_back(i);
            // 链上问题无解,环上更无解了
            if (vec.size() < K) return false;
            // 选的第一个点刚好就是对的
            if (dis(vec[0], vec.back()) >= lim) return true;
            // 枚举第一个点选哪个
            for (int i = 1; i < n && vec.back() < n * 2; i++) {
                vec[0] = i;
                // 调整每个点,使得距离符合要求
                for (int j = 1; j < K; j++) {
                    while (dis(vec[j] % n, vec[j - 1] % n) < lim) {
                        vec[j]++;
                        // 每个指针最多移动 2n 步
                        if (vec[j] >= n * 2) return false;
                    }
                }
                // 检查最后一个点到第一个点的距离
                if (vec.back() < i + n && dis(i, vec.back() % n) >= lim) return true;
            }
            return false;
        };

        // 二分答案
        int head = 1, tail = side;
        while (head < tail) {
            int mid = (head + tail + 1) >> 1;
            if (check(mid)) head = mid;
            else tail = mid - 1;
        }
        return head;
    }
};

五种方法:二分套二分 / k 指针 / 倍增 / DFS / 动态规划(Python/Java/C++/Go)

问题转化

最大化最小值,考虑二分答案,即二分距离的下界 $\textit{low}$。为什么?因为 $\textit{low}$ 越大,可以选的点越少,有单调性。

lc3464.png

把正方形拉成一条线,示例 2 按照左边界、上边界、右边界、下边界的顺时针顺序,这 $5$ 个点在一维上的坐标为

$$
a=[0,3,4,5,6]
$$

现在问题变成:

  • 能否在数组 $a$ 中选 $k$ 个数,要求任意两个相邻元素相差至少为 $\textit{low}$,且第一个数和最后一个数相差至多为 $\textit{side}\cdot 4 - \textit{low}$。
  • $\textit{side}\cdot 4 - \textit{low}$ 是因为 $a$ 是个环形数组,设第一个点为 $x$,最后一个点为 $y$,那么 $y$ 可以视作负方向上的 $y-\textit{side}\cdot 4$,我们要求 $x-(y-\textit{side}\cdot 4) \ge \textit{low}$,解得 $y-x\le \textit{side}\cdot 4 - \textit{low}$。

方法一:二分答案 + 二分查找

枚举第一个数,不断向后二分找相距至少为 $\textit{low}$ 的最近元素,直到找到 $k$ 个数,或者第一个数和最后一个数相差超过 $\textit{side}\cdot 4 - \textit{low}$ 时停止。

注意:本题保证 $k\ge 4$,所以答案不会超过 $\textit{side}$。这也保证了如果下一个点不在正方形的当前边或者下一条边上,那么距离是一定满足要求的,所以「二分找下一个点」的做法是正确的。而 $k\le 3$ 时,答案可能会超过 $\textit{side}$,整体没有单调性(比如左边界上的点,到右边界的距离是先变小再变大),需要分段,每段内部是有单调性的,可以每段二分一次。也就是说,$k\le 3$ 的时候,「二分找下一个点」需要多次二分。

注意:不需要找一圈后又绕回到数组 $a$ 的开头继续找。设 $\textit{start}$ 是第一个点,$p$ 是二分找到的最后一个点(绕回到数组开头找到的 $p$)。反证:假设从 $\textit{start}$ 开始搜比从 $p$ 开始搜更优,那么因为我们要求首尾两个点相距 $\ge \textit{low}$,从 $p$ 开始往后搜,下一个点一定是 $\textit{start}$ 或者 $\textit{start}$ 前面的点,所以从 $p$ 开始搜得到的结果,不会比从 $\textit{start}$ 开始搜更差,矛盾。这也同时意味着,无需把环形数组 $a$ 复制一份。

下面代码采用开区间二分,这仅仅是二分的一种写法,使用闭区间或者半闭半开区间都是可以的。

  • 开区间左端点初始值:$1$。一定可以满足要求。
  • 开区间右端点初始值:$\textit{side} + 1$。一定无法满足要求。
  • 开区间右端点初始值(优化):$\left\lfloor\dfrac{\textit{side}\cdot 4}{k}\right\rfloor + 1$。因为均分周长 $\textit{side}\cdot 4$ 的话,两点相距最小值的最大值是 $\left\lfloor\dfrac{\textit{side}\cdot 4}{k}\right\rfloor$,加一后一定无法满足要求。

答疑

:为什么需要枚举第一个点是谁?如果从第一个点开始向后二分,没有找到符合要求的 $k$ 个点,那么从第二个点开始向后二分,应该更加不可能找到符合要求的 $k$ 个点呀?

:比如有 $5$ 个点 $a,b,c,d,e$,我们要选 $k=4$ 个点。假设从 $a$ 开始二分找到的是 $a,c,d,e$,但是点 $a$ 和点 $e$ 太近了。那么继续枚举,假设从 $b$ 开始二分找到的是 $b,c,d,e$,并且 $b$ 和 $e$ 满足要求。这就是一个需要继续枚举的例子,其中 $a$ 离 $b$ 很近,离 $c$ 很远。

###py

class Solution:
    def maxDistance(self, side: int, points: List[List[int]], k: int) -> int:
        # 正方形边上的点,按照顺时针映射到一维数轴上
        a = []
        for x, y in points:
            if x == 0:
                a.append(y)
            elif y == side:
                a.append(side + x)
            elif x == side:
                a.append(side * 3 - y)
            else:
                a.append(side * 4 - x)
        a.sort()

        def check(low: int) -> bool:
            for start in a:  # 枚举第一个点
                end = start + side * 4 - low
                cur = start
                for _ in range(k - 1):  # 还需要找 k-1 个点
                    j = bisect_left(a, cur + low)
                    if j == len(a) or a[j] > end:  # 不能离第一个点太近
                        break
                    cur = a[j]
                else:
                    return True
            return False

        left, right = 1, side * 4 // k + 1
        while left + 1 < right:
            mid = (left + right) // 2
            if check(mid):
                left = mid
            else:
                right = mid
        return left

###py

class Solution:
    def maxDistance(self, side: int, points: List[List[int]], k: int) -> int:
        # 正方形边上的点,按照顺时针映射到一维数轴上
        a = []
        for x, y in points:
            if x == 0:
                a.append(y)
            elif y == side:
                a.append(side + x)
            elif x == side:
                a.append(side * 3 - y)
            else:
                a.append(side * 4 - x)
        a.sort()

        def check(low: int) -> bool:
            # 如果 low+1 不满足要求,但 low 满足要求,那么答案就是 low
            low += 1
            for start in a:  # 枚举第一个点
                end = start + side * 4 - low
                cur = start
                for _ in range(k - 1):  # 还需要找 k-1 个点
                    j = bisect_left(a, cur + low)
                    if j == len(a) or a[j] > end:  # 不能离第一个点太近
                        break
                    cur = a[j]
                else:
                    return False
            return True

        return bisect_left(range(side * 4 // k), True, key=check)

###java

class Solution {
    public int maxDistance(int side, int[][] points, int k) {
        // 正方形边上的点,按照顺时针映射到一维数轴上
        long[] a = new long[points.length];
        for (int i = 0; i < points.length; i++) {
            int x = points[i][0];
            int y = points[i][1];
            if (x == 0) {
                a[i] = y;
            } else if (y == side) {
                a[i] = side + x;
            } else if (x == side) {
                a[i] = side * 3L - y;
            } else {
                a[i] = side * 4L - x;
            }
        }
        Arrays.sort(a);

        int left = 1;
        int right = (int) (side * 4L / k) + 1;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (check(a, side, k, mid)) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }

    private boolean check(long[] a, int side, int k, int low) {
        next:
        for (long start : a) { // 枚举第一个点
            long end = start + side * 4L - low;
            long cur = start;
            for (int i = 0; i < k - 1; i++) { // 还需要找 k-1 个点
                int j = lowerBound(a, cur + low);
                if (j == a.length || a[j] > end) { // 不能离第一个点太近
                    continue next;
                }
                cur = a[j];
            }
            return true;
        }
        return false;
    }

    // 见 https://www.bilibili.com/video/BV1AP41137w7/
    private int lowerBound(long[] nums, long target) {
        int left = -1;
        int right = nums.length;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (nums[mid] >= target) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return right;
    }
}

###cpp

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int k) {
        // 正方形边上的点,按照顺时针映射到一维数轴上
        vector<long long> a;
        for (auto& p : points) {
            int x = p[0], y = p[1];
            if (x == 0) {
                a.push_back(y);
            } else if (y == side) {
                a.push_back(side + x);
            } else if (x == side) {
                a.push_back(side * 3LL - y);
            } else {
                a.push_back(side * 4LL - x);
            }
        }
        ranges::sort(a);

        auto check = [&](int low) -> bool {
            for (long long start : a) { // 枚举第一个点
                long long end = start + side * 4LL - low;
                long long cur = start;
                for (int i = 0; i < k - 1; i++) { // 还需要找 k-1 个点
                    auto it = ranges::lower_bound(a, cur + low);
                    if (it == a.end() || *it > end) { // 不能离第一个点太近
                        cur = -1;
                        break;
                    }
                    cur = *it;
                }
                if (cur >= 0) {
                    return true;
                }
            }
            return false;
        };

        int left = 1, right = side * 4LL / k + 1;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            (check(mid) ? left : right) = mid;
        }
        return left;
    }
};

###go

func maxDistance(side int, points [][]int, k int) int {
// 正方形边上的点,按照顺时针映射到一维数轴上
a := make([]int, len(points))
for i, p := range points {
x, y := p[0], p[1]
if x == 0 {
a[i] = y
} else if y == side {
a[i] = side + x
} else if x == side {
a[i] = side*3 - y
} else {
a[i] = side*4 - x
}
}
slices.Sort(a)

ans := sort.Search(side*4/k, func(low int) bool {
// 如果 low+1 不满足要求,但 low 满足要求,那么答案就是 low
low++
next:
for i, start := range a { // 枚举第一个点
cur := start
for range k - 1 { // 还需要找 k-1 个点
i += sort.Search(len(a)-i, func(j int) bool { return a[i+j] >= cur+low })
if i == len(a) || a[i] > start+side*4-low { // 不能离第一个点太近
continue next
}
cur = a[i]
}
return false
}
return true
})
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nk \log \textit{side}\log n)$,其中 $n$ 是 $\textit{points}$ 的长度。由于中途会退出循环,这个复杂度是跑不满的。
  • 空间复杂度:$\mathcal{O}(n)$。

方法二:二分答案 + k 个同向指针

把方法一最内层的二分查找,改用 $k$ 个指针维护。

一开始,初始化一个长为 $k$ 的 $\textit{idx}$ 数组,初始值 $\textit{idx}[j]=0$。

然后写个 $k$ 指针(双指针的推广):

  • 遍历 $j=1,2,3,\ldots,k-1$,如果发现 $a[\textit{idx}[j]] < a[\textit{idx}[j-1]] + \textit{low}$,就不断把 $\textit{idx}[j]$ 加一直到不满足条件。如果 $\textit{idx}[j]=n$ 则返回。
  • 这些指针移动后,如果首尾两个指针指向的数相差不超过 $\textit{side}\cdot 4 - \textit{low}$,则返回。
  • 否则把 $\textit{idx}[0]$ 加一,继续循环。

优化前

###py

class Solution:
    def maxDistance(self, side: int, points: List[List[int]], k: int) -> int:
        a = []
        for x, y in points:
            if x == 0:
                a.append(y)
            elif y == side:
                a.append(side + x)
            elif x == side:
                a.append(side * 3 - y)
            else:
                a.append(side * 4 - x)
        a.sort()

        def check(low: int) -> bool:
            idx = [0] * k
            while True:
                for j in range(1, k):
                    while a[idx[j]] < a[idx[j - 1]] + low:
                        idx[j] += 1
                        if idx[j] == len(a):
                            return False
                if a[idx[-1]] - a[idx[0]] <= side * 4 - low:
                    return True
                idx[0] += 1

        left, right = 1, side * 4 // k + 1
        while left + 1 < right:
            mid = (left + right) // 2
            if check(mid):
                left = mid
            else:
                right = mid
        return left

###java

class Solution {
    public int maxDistance(int side, int[][] points, int k) {
        long[] a = new long[points.length];
        for (int i = 0; i < points.length; i++) {
            int x = points[i][0];
            int y = points[i][1];
            if (x == 0) {
                a[i] = y;
            } else if (y == side) {
                a[i] = side + x;
            } else if (x == side) {
                a[i] = side * 3L - y;
            } else {
                a[i] = side * 4L - x;
            }
        }
        Arrays.sort(a);

        int left = 1;
        int right = (int) (side * 4L / k) + 1;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (check(a, side, k, mid)) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }

    private boolean check(long[] a, int side, int k, int low) {
        int[] idx = new int[k];
        while (true) {
            for (int j = 1; j < k; j++) {
                while (a[idx[j]] < a[idx[j - 1]] + low) {
                    idx[j]++;
                    if (idx[j] == a.length) {
                        return false;
                    }
                }
            }
            if (a[idx[k - 1]] - a[idx[0]] <= side * 4L - low) {
                return true;
            }
            idx[0]++;
        }
    }
}

###cpp

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int k) {
        // 正方形边上的点,按照顺时针映射到一维数轴上
        vector<long long> a;
        for (auto& p : points) {
            int x = p[0], y = p[1];
            if (x == 0) {
                a.push_back(y);
            } else if (y == side) {
                a.push_back(side + x);
            } else if (x == side) {
                a.push_back(side * 3LL - y);
            } else {
                a.push_back(side * 4LL - x);
            }
        }
        ranges::sort(a);

        auto check = [&](int low) -> bool {
            vector<int> idx(k);
            while (true) {
                for (int j = 1; j < k; j++) {
                    while (a[idx[j]] < a[idx[j - 1]] + low) {
                        idx[j]++;
                        if (idx[j] == a.size()) {
                            return false;
                        }
                    }
                }
                if (a[idx[k - 1]] - a[idx[0]] <= side * 4LL - low) {
                    return true;
                }
                idx[0]++;
            }
        };

        // 本题保证 k >= 4,所以最远距离不会超过 side
        int left = 1, right = side * 4LL / k + 1;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            (check(mid) ? left : right) = mid;
        }
        return left;
    }
};

###go

func maxDistance(side int, points [][]int, k int) int {
a := make([]int, len(points))
for i, p := range points {
x, y := p[0], p[1]
if x == 0 {
a[i] = y
} else if y == side {
a[i] = side + x
} else if x == side {
a[i] = side*3 - y
} else {
a[i] = side*4 - x
}
}
slices.Sort(a)

ans := sort.Search(side*4/k, func(low int) bool {
low++
idx := make([]int, k)
for {
for j := 1; j < k; j++ {
for a[idx[j]] < a[idx[j-1]]+low {
idx[j]++
if idx[j] == len(a) {
return true
}
}
}
if a[idx[k-1]]-a[idx[0]] <= side*4-low {
return false
}
idx[0]++
}
})
return ans
}

优化

把从 $\textit{start}=a[0]$ 开始向后二分得到的 $k$ 个下标,记到 $\textit{idx}$ 数组中。如果没有 $k$ 个下标,直接返回。

这样初始化比从 $0$ 开始一个一个地向后移动指针更快。

此外,第一个指针至多移动到第二个指针的初始位置,就不用继续枚举了,后面必然无法得到符合要求的结果。

###py

class Solution:
    def maxDistance(self, side: int, points: List[List[int]], k: int) -> int:
        a = []
        for x, y in points:
            if x == 0:
                a.append(y)
            elif y == side:
                a.append(side + x)
            elif x == side:
                a.append(side * 3 - y)
            else:
                a.append(side * 4 - x)
        a.sort()

        def check(low: int) -> bool:
            idx = [0] * k
            cur = a[0]
            for j in range(1, k):
                i = bisect_left(a, cur + low)
                if i == len(a):
                    return False
                idx[j] = i
                cur = a[i]
            if cur - a[0] <= side * 4 - low:
                return True

            # 第一个指针移动到第二个指针的位置,就不用继续枚举了
            for idx[0] in range(1, idx[1]):
                for j in range(1, k):
                    while a[idx[j]] < a[idx[j - 1]] + low:
                        idx[j] += 1
                        if idx[j] == len(a):
                            return False
                if a[idx[-1]] - a[idx[0]] <= side * 4 - low:
                    return True
            return False

        left, right = 1, side * 4 // k + 1
        while left + 1 < right:
            mid = (left + right) // 2
            if check(mid):
                left = mid
            else:
                right = mid
        return left

###java

class Solution {
    public int maxDistance(int side, int[][] points, int k) {
        long[] a = new long[points.length];
        for (int i = 0; i < points.length; i++) {
            int x = points[i][0];
            int y = points[i][1];
            if (x == 0) {
                a[i] = y;
            } else if (y == side) {
                a[i] = side + x;
            } else if (x == side) {
                a[i] = side * 3L - y;
            } else {
                a[i] = side * 4L - x;
            }
        }
        Arrays.sort(a);

        int left = 1;
        int right = (int) (side * 4L / k) + 1;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (check(a, side, k, mid)) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }

    private boolean check(long[] a, int side, int k, int low) {
        int[] idx = new int[k];
        long cur = a[0];
        for (int j = 1; j < k; j++) {
            int i = lowerBound(a, cur + low);
            if (i == a.length) {
                return false;
            }
            idx[j] = i;
            cur = a[i];
        }
        if (cur - a[0] <= side * 4L - low) {
            return true;
        }

        // 第一个指针移动到第二个指针的位置,就不用继续枚举了
        int end0 = idx[1];
        for (idx[0] = 1; idx[0] < end0; idx[0]++) {
            for (int j = 1; j < k; j++) {
                while (a[idx[j]] < a[idx[j - 1]] + low) {
                    idx[j]++;
                    if (idx[j] == a.length) {
                        return false;
                    }
                }
            }
            if (a[idx[k - 1]] - a[idx[0]] <= side * 4L - low) {
                return true;
            }
        }
        return false;
    }

    // 见 https://www.bilibili.com/video/BV1AP41137w7/
    private int lowerBound(long[] nums, long target) {
        int left = -1;
        int right = nums.length;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (nums[mid] >= target) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return right;
    }
}

###cpp

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int k) {
        vector<long long> a;
        for (auto& p : points) {
            int x = p[0], y = p[1];
            if (x == 0) {
                a.push_back(y);
            } else if (y == side) {
                a.push_back(side + x);
            } else if (x == side) {
                a.push_back(side * 3LL - y);
            } else {
                a.push_back(side * 4LL - x);
            }
        }
        ranges::sort(a);

        auto check = [&](int low) -> bool {
            vector<int> idx(k);
            long long cur = a[0];
            for (int j = 1; j < k; j++) {
                int i = ranges::lower_bound(a, cur + low) - a.begin();
                if (i == a.size()) {
                    return false;
                }
                idx[j] = i;
                cur = a[i];
            }
            if (cur - a[0] <= side * 4LL - low) {
                return true;
            }

            // 第一个指针移动到第二个指针的位置,就不用继续枚举了
            int end0 = idx[1];
            for (idx[0]++; idx[0] < end0; idx[0]++) {
                for (int j = 1; j < k; j++) {
                    while (a[idx[j]] < a[idx[j - 1]] + low) {
                        idx[j]++;
                        if (idx[j] == a.size()) {
                            return false;
                        }
                    }
                }
                if (a[idx[k - 1]] - a[idx[0]] <= side * 4LL - low) {
                    return true;
                }
            }
            return false;
        };

        int left = 1, right = side * 4LL / k + 1;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            (check(mid) ? left : right) = mid;
        }
        return left;
    }
};

###go

func maxDistance(side int, points [][]int, k int) int {
a := make([]int, len(points))
for i, p := range points {
x, y := p[0], p[1]
if x == 0 {
a[i] = y
} else if y == side {
a[i] = side + x
} else if x == side {
a[i] = side*3 - y
} else {
a[i] = side*4 - x
}
}
slices.Sort(a)

ans := sort.Search(side*4/k, func(low int) bool {
low++
idx := make([]int, k)
cur := a[0]
for j, i := 1, 0; j < k; j++ {
i += sort.Search(len(a)-i, func(j int) bool { return a[i+j] >= cur+low })
if i == len(a) {
return true
}
idx[j] = i
cur = a[i]
}
if cur-a[0] <= side*4-low {
return false
}

// 第一个指针移动到第二个指针的位置,就不用继续枚举了
end0 := idx[1]
for idx[0]++; idx[0] < end0; idx[0]++ {
for j := 1; j < k; j++ {
for a[idx[j]] < a[idx[j-1]]+low {
idx[j]++
if idx[j] == len(a) {
return true
}
}
}
if a[idx[k-1]]-a[idx[0]] <= side*4-low {
return false
}
}
return true
})
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n + nk \log \textit{side})$,其中 $n$ 是 $\textit{points}$ 的长度。其中 $\mathcal{O}(n\log n)$ 是排序的时间复杂度。
  • 空间复杂度:$\mathcal{O}(n)$。

方法三:二分答案 + 倍增

如果 $k$ 更大,上面两个方法就超时了。怎么办?

前置知识倍增讲解

在二分中,先预处理 $\textit{nxt}[i][0] = j$ 表示距离 $a[i]$ 至少为 $\textit{low}$ 的下一个点的下标是 $j$。如果不存在则 $j=n$。这可以用双指针计算。

然后倍增,定义 $\textit{nxt}[i][l]$ 表示 $i$ 的下 $2^l$ 个点的下标是 $\textit{nxt}[i][l]$。例如 $\textit{nxt}[i][1]$ 表示 $i$ 的下下个点的下标是 $\textit{nxt}[i][1]$。

转移方程同上面的倍增讲解:

$$
\textit{nxt}[i][l] = \textit{nxt}[\textit{nxt}[i][l-1]][l-1]
$$

可以定义 $\textit{nxt}[n][l]=n$ 作为哨兵,简化代码。

然后枚举 $i=0,1,2,\cdots$,往后跳 $k-1$ 步,得到下标 $j$。如果

$$
a[j] - a[i] \le \textit{side}\cdot 4 - \textit{low}
$$

成立,则说明可以找到符合要求的 $k$ 个点。

###py

class Solution:
    def maxDistance(self, side: int, points: List[List[int]], k: int) -> int:
        a = []
        for x, y in points:
            if x == 0:
                a.append(y)
            elif y == side:
                a.append(side + x)
            elif x == side:
                a.append(side * 3 - y)
            else:
                a.append(side * 4 - x)
        a.sort()

        n = len(a)
        k -= 1  # 往后跳 k-1 步,这里先减一,方便计算
        mx = k.bit_length()
        nxt = [[n] * mx for _ in range(n + 1)]
    
        def check(low: int) -> bool:
            # 预处理倍增数组 nxt
            j = n
            for i in range(n - 1, -1, -1):  # 转移来源在右边,要倒序计算
                while a[j - 1] >= a[i] + low:
                    j -= 1
                nxt[i][0] = j
                for l in range(1, mx):
                    nxt[i][l] = nxt[nxt[i][l - 1]][l - 1]
    
            # 枚举起点
            for i, start in enumerate(a):
                # 往后跳 k-1 步(注意上面把 k 减一了)
                cur = i
                for j in range(mx - 1, -1, -1):
                    if k >> j & 1:
                        cur = nxt[cur][j]
                if cur == n:  # 出界
                    break
                if a[cur] - start <= side * 4 - low:
                    return True
            return False

        left, right = 1, side * 4 // k + 1
        while left + 1 < right:
            mid = (left + right) // 2
            if check(mid):
                left = mid
            else:
                right = mid
        return left

###java

class Solution {
    public int maxDistance(int side, int[][] points, int k) {
        int n = points.length;
        long[] a = new long[n];
        for (int i = 0; i < n; i++) {
            int x = points[i][0];
            int y = points[i][1];
            if (x == 0) {
                a[i] = y;
            } else if (y == side) {
                a[i] = side + x;
            } else if (x == side) {
                a[i] = side * 3L - y;
            } else {
                a[i] = side * 4L - x;
            }
        }
        Arrays.sort(a);

        k--; // 往后跳 k-1 步,这里先减一,方便计算
        int mx = 32 - Integer.numberOfLeadingZeros(k);
        int[][] nxt = new int[n + 1][mx];
        Arrays.fill(nxt[n], n); // 哨兵

        int left = 1;
        int right = (int) (side * 4L / k) + 1;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (check(a, side, k, nxt, mid)) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }

    private boolean check(long[] a, int side, int k, int[][] nxt, int low) {
        int n = a.length;
        int mx = nxt[0].length;
        // 预处理倍增数组 nxt
        for (int i = n - 1, j = n; i >= 0; i--) {
            while (a[j - 1] >= a[i] + low) {
                j--;
            }
            nxt[i][0] = j;
            for (int l = 1; l < mx; l++) {
                nxt[i][l] = nxt[nxt[i][l - 1]][l - 1];
            }
        }

        // 枚举起点
        for (int i = 0; i < n; i++) {
            int cur = i;
            // 往后跳 k-1 步(注意上面把 k 减一了)
            for (int j = mx - 1; j >= 0; j--) {
                if ((k >> j & 1) > 0) {
                    cur = nxt[cur][j];
                }
            }
            if (cur == n) { // 出界
                break;
            }
            if (a[cur] - a[i] <= side * 4L - low) {
                return true;
            }
        }
        return false;
    }
}

###cpp

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int k) {
        vector<long long> a;
        for (auto& p : points) {
            int x = p[0], y = p[1];
            if (x == 0) {
                a.push_back(y);
            } else if (y == side) {
                a.push_back(side + x);
            } else if (x == side) {
                a.push_back(side * 3LL - y);
            } else {
                a.push_back(side * 4LL - x);
            }
        }
        ranges::sort(a);

        int n = a.size();
        k--; // 往后跳 k-1 步,这里先减一,方便计算
        int high_bit = bit_width((unsigned) k) - 1;
        vector<array<int, 5>> nxt(n + 1); // 5 可以改为 high_bit+1(这里用 array 而不是 vector,提高访问效率)
        ranges::fill(nxt[n], n); // 哨兵

        auto check = [&](int low) -> bool {
            // 预处理倍增数组 nxt
            int j = n;
            for (int i = n - 1; i >= 0; i--) { // 转移来源在右边,要倒序计算
                while (a[j - 1] >= a[i] + low) {
                    j--;
                }
                nxt[i][0] = j;
                for (int k = 1; k <= high_bit; k++) {
                    nxt[i][k] = nxt[nxt[i][k - 1]][k - 1];
                }
            }

            // 枚举起点
            for (int i = 0; i < n; i++) {
                int cur = i;
                // 往后跳 k-1 步(注意上面把 k 减一了)
                for (int j = high_bit; j >= 0; j--) {
                    if (k >> j & 1) {
                        cur = nxt[cur][j];
                    }
                }
                if (cur == n) { // 出界
                    break;
                }
                if (a[cur] - a[i] <= side * 4LL - low) {
                    return true;
                }
            }
            return false;
        };

        int left = 1, right = side * 4LL / k + 1;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            (check(mid) ? left : right) = mid;
        }
        return left;
    }
};

###go

func maxDistance(side int, points [][]int, k int) int {
n := len(points)
a := make([]int, n)
for i, p := range points {
x, y := p[0], p[1]
if x == 0 {
a[i] = y
} else if y == side {
a[i] = side + x
} else if x == side {
a[i] = side*3 - y
} else {
a[i] = side*4 - x
}
}
slices.Sort(a)

k-- // 往后跳 k-1 步,这里先减一,方便计算
highBit := bits.Len(uint(k)) - 1
nxt := make([][5]int, n+1) // 5 可以改为 highBit+1(用 array 而不是 slice,提高访问效率)
for j := range nxt[n] {
nxt[n][j] = n // 哨兵
}

ans := sort.Search(side*4/k, func(low int) bool {
low++
// 预处理倍增数组 nxt
j := n
for i := n - 1; i >= 0; i-- { // 转移来源在右边,要倒序计算
for a[j-1] >= a[i]+low {
j--
}
nxt[i][0] = j
for k := 1; k <= highBit; k++ {
nxt[i][k] = nxt[nxt[i][k-1]][k-1]
}
}

// 枚举起点
for i, start := range a {
// 往后跳 k-1 步(注意上面把 k 减一了)
cur := i
for j := highBit; j >= 0; j-- {
if k>>j&1 > 0 {
cur = nxt[cur][j]
}
}
if cur == n { // 出界
break
}
if a[cur]-start <= side*4-low {
return false
}
}
return true
})
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n + n\log k \log \textit{side})$,其中 $n$ 是 $\textit{points}$ 的长度。其中 $\mathcal{O}(n\log n)$ 是排序的时间复杂度。
  • 空间复杂度:$\mathcal{O}(n\log k)$。

方法四:二分答案 + 建树 + DFS

在方法三的双指针基础上,连一条从 $j$ 到 $i$ 的有向边,我们会得到一棵有向树,根是 $n$。

从 $n$ 开始递归这棵树,同时用一个栈记录从根到当前节点的 $a[x]$ 信息。

当栈中有 $k$ 个点时,记录栈中倒数第 $k$ 个数和栈顶的距离,如果 $\le \textit{side}\cdot 4 - \textit{low}$,则找到了满足要求的 $k$ 的点,结束递归。

注意:无需判断 $f[i]>k$ 的情况,因为这意味着之前栈中有 $k$ 个点的时候,首尾两点间的距离足够远(甚至还可以再容纳一个点),一定满足要求。

###py

class Solution:
    def maxDistance(self, side: int, points: List[List[int]], k: int) -> int:
        a = []
        for x, y in points:
            if x == 0:
                a.append(y)
            elif y == side:
                a.append(side + x)
            elif x == side:
                a.append(side * 3 - y)
            else:
                a.append(side * 4 - x)
        a.sort()
        n = len(a)
        a.append(inf)  # 哨兵

        def check(low: int) -> bool:
            g = [[] for _ in range(n + 1)]
            j = n
            for i in range(n - 1, -1, -1):
                while a[j - 1] >= a[i] + low:
                    j -= 1
                g[j].append(i)  # 建树

            st = []
            def dfs(x: int) -> bool:
                st.append(a[x])
                # 注意栈中多了一个 a[n],所以是 m > k 不是 ==
                if len(st) > k and st[-k] - a[x] <= side * 4 - low:
                    return True
                for y in g[x]:
                    if dfs(y):
                        return True
                st.pop()  # 恢复现场
                return False
            return dfs(n)

        left, right = 1, side * 4 // k + 1
        while left + 1 < right:
            mid = (left + right) // 2
            if check(mid):
                left = mid
            else:
                right = mid
        return left

###java

class Solution {
    public int maxDistance(int side, int[][] points, int k) {
        int n = points.length;
        long[] a = new long[n + 1];
        for (int i = 0; i < n; i++) {
            int x = points[i][0];
            int y = points[i][1];
            if (x == 0) {
                a[i] = y;
            } else if (y == side) {
                a[i] = side + x;
            } else if (x == side) {
                a[i] = side * 3L - y;
            } else {
                a[i] = side * 4L - x;
            }
        }
        a[n] = Long.MAX_VALUE; // 哨兵
        Arrays.sort(a);

        int left = 1;
        int right = (int) (side * 4L / k) + 1;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (check(a, side, k, mid)) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }

    private boolean check(long[] a, int side, int k, int low) {
        int n = a.length - 1;
        List<Integer>[] g = new ArrayList[n + 1];
        Arrays.setAll(g, i -> new ArrayList<>());
        for (int i = n - 1, j = n; i >= 0; i--) {
            while (a[j - 1] >= a[i] + low) {
                j--;
            }
            g[j].add(i); // 建树
        }

        List<Long> st = new ArrayList<>();
        return dfs(a, g, st, k, side * 4L - low, n);
    }

    private boolean dfs(long[] a, List<Integer>[] g, List<Long> st, int k, long limit, int x) {
        st.add(a[x]);
        int m = st.size();
        // 注意栈中多了一个 a[n],所以是 m > k 不是 ==
        if (m > k && st.get(m - k) - a[x] <= limit) {
            return true;
        }
        for (int y : g[x]) {
            if (dfs(a, g, st, k, limit, y)) {
                return true;
            }
        }
        st.remove(m - 1); // 恢复现场
        return false;
    }
}

###cpp

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int k) {
        vector<long long> a;
        for (auto& p : points) {
            int x = p[0], y = p[1];
            if (x == 0) {
                a.push_back(y);
            } else if (y == side) {
                a.push_back(side + x);
            } else if (x == side) {
                a.push_back(side * 3LL - y);
            } else {
                a.push_back(side * 4LL - x);
            }
        }
        ranges::sort(a);
        int n = a.size();
        a.push_back(LLONG_MAX); // 哨兵

        auto check = [&](int low) -> bool {
            vector<vector<int>> g(n + 1);
            int j = n;
            for (int i = n - 1; i >= 0; i--) {
                while (a[j - 1] >= a[i] + low) {
                    j--;
                }
                g[j].push_back(i); // 建树
            }

            vector<long long> st;
            auto dfs = [&](this auto&& dfs, int x) -> bool {
                st.push_back(a[x]);
                int m = st.size();
                // 注意栈中多了一个 a[n],所以是 m > k 不是 ==
                if (m > k && st[m - k] - a[x] <= side * 4LL - low) {
                    return true;
                }
                for (int y : g[x]) {
                    if (dfs(y)) {
                        return true;
                    }
                }
                st.pop_back(); // 恢复现场
                return false;
            };
            return dfs(n);
        };

        int left = 1, right = side * 4LL / k + 1;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            (check(mid) ? left : right) = mid;
        }
        return left;
    }
};

###go

func maxDistance(side int, points [][]int, k int) int {
n := len(points)
a := make([]int, n, n+1)
for i, p := range points {
x, y := p[0], p[1]
if x == 0 {
a[i] = y
} else if y == side {
a[i] = side + x
} else if x == side {
a[i] = side*3 - y
} else {
a[i] = side*4 - x
}
}
slices.Sort(a)
a = append(a, math.MaxInt) // 哨兵

g := make([][]int, n+1)
ans := sort.Search(side*4/k, func(low int) bool {
low++
clear(g)
j := n
for i := n - 1; i >= 0; i-- {
for a[j-1] >= a[i]+low {
j--
}
g[j] = append(g[j], i) // 建树
}

st := []int{}
var dfs func(int) bool
dfs = func(x int) bool {
st = append(st, a[x])
m := len(st)
// 注意栈中多了一个 a[n],所以是 m > k 不是 ==
if m > k && st[m-k]-a[x] <= side*4-low {
return true
}
for _, y := range g[x] {
if dfs(y) {
return true
}
}
st = st[:m-1] // 恢复现场
return false
}
return !dfs(n)
})
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n + n\log \textit{side})$,其中 $n$ 是 $\textit{points}$ 的长度。其中 $\mathcal{O}(n\log n)$ 是排序的时间复杂度。每次二分的时间为 $\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(n)$。

方法五:二分 + 动态规划

定义 $f[i]$ 表示从 $i$ 往后找,最多可以找多少个点(包含 $i$)。

设下一个点的下标为 $j$,那么有

$$
f[i] = f[j] + 1
$$

初始值 $f[n] = 0$。

此外,定义 $\textit{end}[i]$ 表示从 $i$ 往后找,最后一个点的下标。

  • 如果 $f[i]=1$,那么 $\textit{end}[i]$ 就是 $i$ 自己。
  • 如果 $f[i]>1$,那么 $\textit{end}[i]$ 是从 $j$ 往后找,最后一个点的下标,即 $\textit{end}[j]$。

所以有

$$
\textit{end}[i] =
\begin{cases}
i, & f[i]=1 \
\textit{end}[j], & f[i]>1 \
\end{cases}
$$

如果 $f[i]=k$,且首尾两点的距离 $a[\textit{end}[i]] - a[i] \le \textit{side}\cdot 4 - \textit{low}$,那么满足要求,返回。

注意:无需判断 $f[i]>k$ 的情况。证明:每次间隔至少 $\textit{low}$ 才会把 $f[i]$ 加 $1$,如果出现 $f[i]=f[j]+1=k+1$ 的情况,说明我们在 $f[j]=k$ 的基础上增加了一个点,对于 $f[j]$ 来说,首尾节点有足够的间距(比 $\textit{low}$ 还大),使得我们可以再加一个点进来,得到 $f[i]=k+1$。所以 $f[j]=k$ 的时候必然可以满足要求,我们不会继续循环到 $f[i]=k+1$ 的情况。

###py

class Solution:
    def maxDistance(self, side: int, points: List[List[int]], k: int) -> int:
        a = []
        for x, y in points:
            if x == 0:
                a.append(y)
            elif y == side:
                a.append(side + x)
            elif x == side:
                a.append(side * 3 - y)
            else:
                a.append(side * 4 - x)
        a.sort()

        n = len(a)
        f = [0] * (n + 1)
        end = [0] * n

        def check(low: int) -> bool:
            j = n
            for i in range(n - 1, -1, -1):
                while a[j - 1] >= a[i] + low:
                    j -= 1
                f[i] = f[j] + 1
                end[i] = end[j] if f[i] > 1 else i
                if f[i] == k and a[end[i]] - a[i] <= side * 4 - low:
                    return True
            return False

        left, right = 1, side * 4 // k + 1
        while left + 1 < right:
            mid = (left + right) // 2
            if check(mid):
                left = mid
            else:
                right = mid
        return left

###java

class Solution {
    public int maxDistance(int side, int[][] points, int k) {
        int n = points.length;
        long[] a = new long[n];
        for (int i = 0; i < n; i++) {
            int x = points[i][0];
            int y = points[i][1];
            if (x == 0) {
                a[i] = y;
            } else if (y == side) {
                a[i] = side + x;
            } else if (x == side) {
                a[i] = side * 3L - y;
            } else {
                a[i] = side * 4L - x;
            }
        }
        Arrays.sort(a);

        int[] f = new int[n + 1];
        int[] end = new int[n];

        int left = 1;
        int right = (int) (side * 4L / k) + 1;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (check(a, side, k, mid, f, end)) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }

    private boolean check(long[] a, int side, int k, int low, int[] f, int[] end) {
        int n = a.length;
        for (int i = n - 1, j = n; i >= 0; i--) {
            while (a[j - 1] >= a[i] + low) {
                j--;
            }
            f[i] = f[j] + 1;
            end[i] = f[i] > 1 ? end[j] : i;
            if (f[i] == k && a[end[i]] - a[i] <= side * 4L - low) {
                return true;
            }
        }
        return false;
    }
}

###cpp

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int k) {
        vector<long long> a;
        for (auto& p : points) {
            int x = p[0], y = p[1];
            if (x == 0) {
                a.push_back(y);
            } else if (y == side) {
                a.push_back(side + x);
            } else if (x == side) {
                a.push_back(side * 3LL - y);
            } else {
                a.push_back(side * 4LL - x);
            }
        }
        ranges::sort(a);

        int n = a.size();
        vector<int> f(n + 1), end(n);

        auto check = [&](int low) -> bool {
            int j = n;
            for (int i = n - 1; i >= 0; i--) {
                while (a[j - 1] >= a[i] + low) {
                    j--;
                }
                f[i] = f[j] + 1;
                end[i] = f[i] > 1 ? end[j] : i;
                if (f[i] == k && a[end[i]] - a[i] <= side * 4LL - low) {
                    return true;
                }
            }
            return false;
        };

        int left = 1, right = side * 4LL / k + 1;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            (check(mid) ? left : right) = mid;
        }
        return left;
    }
};

###go

func maxDistance(side int, points [][]int, k int) int {
n := len(points)
a := make([]int, n)
for i, p := range points {
x, y := p[0], p[1]
if x == 0 {
a[i] = y
} else if y == side {
a[i] = side + x
} else if x == side {
a[i] = side*3 - y
} else {
a[i] = side*4 - x
}
}
slices.Sort(a)

f := make([]int, n+1)
end := make([]int, n)

ans := sort.Search(side*4/k, func(low int) bool {
low++
j := n
for i := n - 1; i >= 0; i-- {
for a[j-1] >= a[i]+low {
j--
}
f[i] = f[j] + 1
if f[i] == 1 {
end[i] = i // i 自己就是最后一个点
} else {
end[i] = end[j]
}
if f[i] == k && a[end[i]]-a[i] <= side*4-low {
return false
}
}
return true
})
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n + n\log \textit{side})$,其中 $n$ 是 $\textit{points}$ 的长度。其中 $\mathcal{O}(n\log n)$ 是排序的时间复杂度。每次二分的时间为 $\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(n)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 【本题相关】二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

贪心

记录三种字符的个数,计算出L和R的差值的绝对值,最后加上'_'的数量,直接返回。

class Solution {
public:
    int furthestDistanceFromOrigin(string moves) {
        int cnt1 = 0, cnt2 = 0, cnt3 = 0;
        for (auto x : moves) {
            if (x == 'L') cnt1++;
            if (x == 'R') cnt2++;
            if (x =='_') cnt3++;
        }
        return cnt1 > cnt2 ? cnt1 + cnt3 - cnt2 : cnt2 + cnt3 - cnt1;
    }
};
class Solution {
    public int furthestDistanceFromOrigin(String moves) {
        int cnt1 = 0, cnt2 = 0, cnt3 = 0;
        char[] str = moves.toCharArray();
        for (char x : str) {
            if (x == 'L') cnt1++;
            if (x == 'R') cnt2++;
            if (x =='_') cnt3++;
        }
        return cnt1 > cnt2 ? cnt1 + cnt3 - cnt2 : cnt2 + cnt3 - cnt1;
    }
}
func furthestDistanceFromOrigin(moves string) int {
    cnt1, cnt2, cnt3 := 0, 0, 0
    for i := 0; i < len(moves); i++ {
        if moves[i] == 'L' {
            cnt1++
        }
        if moves[i] == 'R' {
            cnt2++
        } 
        if moves[i] =='_' {
            cnt3++
        }
    }
    if (cnt1 > cnt2) {
        return cnt1 + cnt3 - cnt2
    }
    return cnt2 + cnt3 - cnt1

}

每日一题-距离原点最远的点🟢

给你一个长度为 n 的字符串 moves ,该字符串仅由字符 'L''R''_' 组成。字符串表示你在一条原点为 0 的数轴上的若干次移动。

你的初始位置就在原点(0),第 i 次移动过程中,你可以根据对应字符选择移动方向:

  • 如果 moves[i] = 'L'moves[i] = '_' ,可以选择向左移动一个单位距离
  • 如果 moves[i] = 'R'moves[i] = '_' ,可以选择向右移动一个单位距离

移动 n 次之后,请你找出可以到达的距离原点 最远 的点,并返回 从原点到这一点的距离

 

示例 1:

输入:moves = "L_RL__R"
输出:3
解释:可以到达的距离原点 0 最远的点是 -3 ,移动的序列为 "LLRLLLR" 。

示例 2:

输入:moves = "_R__LL_"
输出:5
解释:可以到达的距离原点 0 最远的点是 -5 ,移动的序列为 "LRLLLLL" 。

示例 3:

输入:moves = "_______"
输出:7
解释:可以到达的距离原点 0 最远的点是 7 ,移动的序列为 "RRRRRRR" 。

 

提示:

  • 1 <= moves.length == n <= 50
  • moves 仅由字符 'L''R''_' 组成

【统计墙头草】差额合并

2833. 距离原点最远的点 - 第 360 场周赛

思路

“_”就是墙头草,合并前先统计其总量,L和R谁赢帮谁

###python

class Solution:
    def furthestDistanceFromOrigin(self, moves: str) -> int:
        l = r = d = 0
        for move in moves:
            if move == 'L': l += 1
            elif move == 'R': r += 1
            else: d += 1
        return abs(l - r) + d

###java

class Solution {
    public int furthestDistanceFromOrigin(String moves) {
        int lr = 0, d = 0;
        for (char ch : moves.toCharArray()) {
            if (ch == '_') d++;
            else lr += (ch & 2) - 1;
        }
        return Math.abs(lr) + d;
    }
}

image.png

贪心

解法:贪心

题目稍微有点不清晰,其实问的是完成 $n$ 次移动之后的那个终点距离原点最远有多远,并不考虑经过的中间点。

终点要么尽量在左边,要么尽量在右边。所以 _ 要么都改成 L,要么都改成 R。取两种情况的最大值即可。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    int furthestDistanceFromOrigin(string moves) {
        // 求字符串 s 的终点到原点的距离
        auto gao = [&](string s) {
            int d = 0;
            for (char c : s) {
                if (c == 'L') d--;
                else d++;
            }
            return abs(d);
        };

        // 所有 _ 都改成 L
        string L = moves;
        for (char &c : L) if (c == '_') c = 'L';
        // 所有 _ 都改成 R
        string R = moves;
        for (char &c : R) if (c == '_') c = 'R';
        // 取两种情况的最大值
        return max(gao(L), gao(R));
    }
};

贪心(Python/Java/C++/C/Go/JS/Rust)

示例 2 的 $\textit{moves} = \texttt{_R__LL_}$:

  • 先只看 $\texttt{R}$ 和 $\texttt{L}$,往右走了 $1$ 步,再往左走 $2$ 步,相当于往左走了 $1$ 步,位于数轴的 $-1$。
  • 还剩下 $4$ 个 $\texttt{_}$,怎么走最优?当然是继续往左走啦,继续往左走 $4$ 步,最终位于 $-5$。

设 $\textit{cntR}$ 为 $\texttt{R}$ 的个数,$\textit{cntL}$ 为 $\texttt{L}$ 的个数,那么 $\texttt{_}$ 的个数为 $n - \textit{cntR} - \textit{cntL}$。先只看 $\texttt{R}$ 和 $\texttt{L}$,我们到原点的距离为 $|\textit{cntR} - \textit{cntL}|$。然后继续走 $n - \textit{cntR} - \textit{cntL}$ 步,最终答案为

$$
|\textit{cntR} - \textit{cntL}| + n - \textit{cntR} - \textit{cntL}
$$

class Solution:
    def furthestDistanceFromOrigin(self, moves: str) -> int:
        cnt_r = moves.count('R')
        cnt_l = moves.count('L')
        return abs(cnt_r - cnt_l) + len(moves) - cnt_r - cnt_l
class Solution {
    public int furthestDistanceFromOrigin(String moves) {
        int cntR = 0;
        int cntL = 0;
        for (char c : moves.toCharArray()) {
            if (c == 'R') {
                cntR++;
            } else if (c == 'L') {
                cntL++;
            }
        }
        return Math.abs(cntR - cntL) + moves.length() - cntR - cntL;
    }
}
class Solution {
public:
    int furthestDistanceFromOrigin(string moves) {
        int cnt_r = ranges::count(moves, 'R');
        int cnt_l = ranges::count(moves, 'L');
        return abs(cnt_r - cnt_l) + moves.size() - cnt_r - cnt_l;
    }
};
int furthestDistanceFromOrigin(char* moves) {
    int cnt_r = 0;
    int cnt_l = 0;
    int i = 0;
    for (; moves[i]; i++) {
        if (moves[i] == 'R') {
            cnt_r++;
        } else if (moves[i] == 'L') {
            cnt_l++;
        }
    }
    return abs(cnt_r - cnt_l) + i - cnt_r - cnt_l;
}
func furthestDistanceFromOrigin(moves string) int {
cntR := strings.Count(moves, "R")
cntL := strings.Count(moves, "L")
return abs(cntR-cntL) + len(moves) - cntR - cntL
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}
var furthestDistanceFromOrigin = function(moves) {
    const cntR = [...moves].filter(c => c === 'R').length;
    const cntL = [...moves].filter(c => c === 'L').length;
    return Math.abs(cntR - cntL) + moves.length - cntR - cntL;
};
impl Solution {
    pub fn furthest_distance_from_origin(moves: String) -> i32 {
        let cnt_r = moves.bytes().filter(|&c| c == b'R').count() as i32;
        let cnt_l = moves.bytes().filter(|&c| c == b'L').count() as i32;
        (cnt_r - cnt_l).abs() + moves.len() as i32 - cnt_r - cnt_l
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{moves}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-等值距离和🟡

给你一个下标从 0 开始的整数数组 nums 。现有一个长度等于 nums.length 的数组 arr 。对于满足 nums[j] == nums[i]j != i 的所有 jarr[i] 等于所有 |i - j| 之和。如果不存在这样的 j ,则令 arr[i] 等于 0

返回数组 arr

 

示例 1:

输入:nums = [1,3,1,1,2]
输出:[5,0,3,4,0]
解释:
i = 0 ,nums[0] == nums[2] 且 nums[0] == nums[3] 。因此,arr[0] = |0 - 2| + |0 - 3| = 5 。 
i = 1 ,arr[1] = 0 因为不存在值等于 3 的其他下标。
i = 2 ,nums[2] == nums[0] 且 nums[2] == nums[3] 。因此,arr[2] = |2 - 0| + |2 - 3| = 3 。
i = 3 ,nums[3] == nums[0] 且 nums[3] == nums[2] 。因此,arr[3] = |3 - 0| + |3 - 2| = 4 。 
i = 4 ,arr[4] = 0 因为不存在值等于 2 的其他下标。

示例 2:

输入:nums = [0,5,3]
输出:[0,0,0]
解释:因为 nums 中的元素互不相同,对于所有 i ,都有 arr[i] = 0 。

 

提示:

  • 1 <= nums.length <= 105
  • 0 <= nums[i] <= 109

小白讲给小白的前缀和

Problem: 6360. 等值距离和

[TOC]

思路

因为肯定要找出 nums[i]=nums[j] 的所有 i,j,所以首先遍历一遍,记录各个数值出现的每个下标。拿到了各个数值的下标数组后,对数组中的各个元素,求它与其他元素之间的距离和。

关键在于,如何快速地求出数组中元素与其他元素的距离和?在我不知道“前缀和”的概念及其运用方法之前,我是直接遍历求和的,由于题目说 nums.length <= 105, 不出意外地超时了。随后我从题目提示中得知了“前缀和”的概念,并从其他题解中了解了前缀和在本题中的运用,故写此题解给像我一样的小白讲解前缀和与本题的关系。

解题方法

前缀和

参考 https://www.cnblogs.com/ZghzzZyu/p/16407303.html

前缀和指的是,对于数组 nums, 有前缀和数组 arr, 其每一项 arr[i] 的值为 nums[0] + nums[1] + ... + nums[i] 的和。

$arr[i] = sum(nums[0...i])$

例如有数组 nums 如下:

$nums = [0, 1, 2, 3, 4]$

则对应的前缀和数组为:

$arr = [0, 1, 3, 6, 10]$

根据定义不难看出,前缀和数组的每一项,都为其前一项加上 nums 中当前的项。

$arr[i] = arr[i - 1] + nums[i]$

这使得我们在遍历 nums 的每一项时,可以立即计算出 arr 中对应的项,这也是利用前缀和解这道题时降低时间复杂度的关键。在思路中讲到,我们首先遍历 nums 并记录每个数字出现的下标数组,现在,我们改为在遍历 nums 时记录每个数字出现的下标数组的前缀和数组。

通过前缀和求距离和

参考 https://leetcode.cn/problems/sum-of-distances/solutions/2216328/an-shu-zi-xia-biao-qian-zhui-he-by-yusha-xzu4/

有了前缀和数组,就能很方便地求出距离和了。例如,如果我们拥有数组 nums 如下:

$nums = [0, 1, 2, 3, 4]$

当我们要求元素 $2$ 到其他元素的距离和时,计算过程如下:

对于小于 2 的元素 n,计算 $2 - n$ 并相加。不难发现,其结果为小于 2 的元素总和(即前缀和),减去 2 乘以小于 2 的元素数量

$(2 - 0) + (2 - 1) = 2*2 - (0 + 1) = 2 * count(0...1) - sum(0...1)$

对于大于 2 的元素 n,计算 $n - 2$ 并相加。不难发现,其结果为大于 2 的元素总和,减去 2 乘以大于 2 的元素数量

$(3 - 2) + (4 - 2) = (3 + 4) - 2 * 2 = sum(3...4) - 2 * count(3...4)$

根据前缀和的特性,$sum(3...4)$ 可以通过 $sum(0...4)$ 减去 $sum(0...2)$ 得出。

$sum(3...4) = sum(0...4) - sum(0...2)$

元素 2 到其他元素的距离和即为上面两部分相加。

Code

###TypeScript

function distance(nums: number[]): number[] {
  const answer = new Array<number>(nums.length).fill(0)
  // 用于记录各个数值出现的下标数组的前缀和数组
  const map = new Map<number, number[]>();

  for (let i = 0; i < nums.length; i++) {
    if (!map.has(nums[i])) {
      map.set(nums[i], [i])
    } else {
      const array = map.get(nums[i])
      // 计算前缀和
      array.push(array[array.length - 1] + i)
    }
  }

  // 用于记录当前下标是下标数组中的第几个元素
  const indexCounter = []
  for (let i = 0; i < nums.length; i++) {
    const array = map.get(nums[i])

    if (array.length > 1) {
      const indexOfIndex = typeof indexCounter[nums[i]] === 'undefined' ? (indexCounter[nums[i]] = 0) : (indexCounter[nums[i]])
      indexCounter[nums[i]]++

      // 套上面的计算公式
      answer[i] =
        (i * indexOfIndex - (array[indexOfIndex - 1] ?? 0)) +
        (array[array.length - 1] - array[indexOfIndex] - i * (array.length - 1 - indexOfIndex))
    }
  }

  return answer
};

两种 O(n) 方法:照搬 2602 / 考虑距离之和的增量

本题视频讲解

【周赛 340】

方法一:相同元素分组+前缀和

按照相同元素分组后再计算。

这里的思路和 2602. 使数组元素全部相等的最少操作次数(题解)是一样的。由于目标位置就是数组中的下标,无需二分。

class Solution:
    def distance(self, nums: List[int]) -> List[int]:
        groups = defaultdict(list)
        for i, x in enumerate(nums):
            groups[x].append(i)  # 相同元素分到同一组,记录下标
        ans = [0] * len(nums)
        for a in groups.values():
            n = len(a)
            s = list(accumulate(a, initial=0))  # 前缀和
            for j, target in enumerate(a):
                left = target * j - s[j]  # 蓝色面积
                right = s[n] - s[j] - target * (n - j)  # 绿色面积
                ans[target] = left + right
        return ans
class Solution {
    public long[] distance(int[] nums) {
        int n = nums.length;
        var groups = new HashMap<Integer, List<Integer>>();
        for (int i = 0; i < n; ++i) // 相同元素分到同一组,记录下标
            groups.computeIfAbsent(nums[i], k -> new ArrayList<>()).add(i);

        var ans = new long[n];
        var s = new long[n + 1];
        for (var a : groups.values()) {
            int m = a.size();
            for (int i = 0; i < m; ++i)
                s[i + 1] = s[i] + a.get(i); // 前缀和
            for (int i = 0; i < m; ++i) {
                int target = a.get(i);
                long left = (long) target * i - s[i]; // 蓝色面积
                long right = s[m] - s[i] - (long) target * (m - i); // 绿色面积
                ans[target] = left + right;
            }
        }
        return ans;
    }
}
class Solution {
public:
    vector<long long> distance(vector<int>& nums) {
        int n = nums.size();
        unordered_map<int, vector<int>> groups;
        for (int i = 0; i < n; ++i)
            groups[nums[i]].push_back(i); // 相同元素分到同一组,记录下标

        vector<long long> ans(n);
        long long s[n + 1];
        s[0] = 0;
        for (auto& [_, a]: groups) {
            int m = a.size();
            for (int i = 0; i < m; ++i)
                s[i + 1] = s[i] + a[i]; // 前缀和
            for (int i = 0; i < m; ++i) {
                long long target = a[i];
                long long left = target * i - s[i]; // 蓝色面积
                long long right = s[m] - s[i] - target * (m - i); // 绿色面积
                ans[target] = left + right;
            }
        }
        return ans;
    }
};
func distance(nums []int) []int64 {
groups := map[int][]int{}
for i, x := range nums {
groups[x] = append(groups[x], i) // 相同元素分到同一组,记录下标
}
ans := make([]int64, len(nums))
for _, a := range groups {
n := len(a)
s := make([]int, n+1)
for i, x := range a {
s[i+1] = s[i] + x // 前缀和
}
for i, target := range a {
left := target*i - s[i] // 蓝色面积
right := s[n] - s[i] - target*(n-i) // 绿色面积
ans[target] = int64(left + right)
}
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

方法二:相同元素分组+考虑增量

分组后,对于其中一个组 $a$,我们先暴力计算出 $a[0]$ 到其它元素的距离之和,设为 $s_0$。

然后计算 $a[1]$ 到其它元素的距离之和 $s_1$。我们不再暴力计算,而是思考:从 $s_0$ 到 $s_1$,这个增量 $s_1-s_0$ 是多少?(增量可以是负数)

设 $n$ 为 $a$ 的长度,从 $s_0$ 到 $s_1$,有 $1$ 个数的距离变大了 $a[1]-a[0]$(原来是到 $a[0]$ 的距离,现在是到 $a[1]$ 的距离;原来是 $a[0]$ 到 $a[0]$ 的距离为 $0$,现在是 $a[0]$ 到 $a[1]$ 的距离为 $a[1]-a[0]$),其余 $n-1$ 个数的距离变小了 $a[1]-a[0]$(原来是到 $a[0]$ 的距离,现在是到 $a[1]$ 的距离)。

所以对于 $s_1$,我们可以 $\mathcal{O}(1)$ 地知道 $a[1]$ 到其它元素的距离之和为

$$
s_0 + (2-n) \cdot (a[1]-a[0])
$$

一般地,设 $a[i-1]$ 到其它元素的距离之和为 $s$,那么 $a[i]$ 到其它元素的距离之和为

$$
s + (2i-n) \cdot (a[i]-a[i-1])
$$

class Solution:
    def distance(self, nums: List[int]) -> List[int]:
        groups = defaultdict(list)
        for i, x in enumerate(nums):
            groups[x].append(i)  # 相同元素分到同一组,记录下标
        ans = [0] * len(nums)
        for a in groups.values():
            n = len(a)
            s = sum(x - a[0] for x in a)  # a[0] 到其它下标的距离之和
            ans[a[0]] = s
            for i in range(1, n):
                # 从计算 a[i-1] 到计算 a[i],考虑 s 增加了多少
                s += (i * 2 - n) * (a[i] - a[i - 1])
                ans[a[i]] = s
        return ans
class Solution {
    public long[] distance(int[] nums) {
        int n = nums.length;
        var groups = new HashMap<Integer, List<Integer>>();
        for (int i = 0; i < n; ++i) // 相同元素分到同一组,记录下标
            groups.computeIfAbsent(nums[i], k -> new ArrayList<>()).add(i);

        var ans = new long[n];
        for (var a : groups.values()) {
            int m = a.size();
            long s = 0;
            for (int x : a)
                s += x - a.get(0); // a[0] 到其它下标的距离之和
            ans[a.get(0)] = s;
            for (int i = 1; i < m; ++i)
                // 从计算 a[i-1] 到计算 a[i],考虑 s 增加了多少
                ans[a.get(i)] = s += (long) (i * 2 - m) * (a.get(i) - a.get(i - 1));
        }
        return ans;
    }
}
class Solution {
public:
    vector<long long> distance(vector<int>& nums) {
        int n = nums.size();
        unordered_map<int, vector<int>> groups;
        for (int i = 0; i < n; ++i)
            groups[nums[i]].push_back(i); // 相同元素分到同一组,记录下标

        vector<long long> ans(n);
        for (auto& [_, a]: groups) {
            int m = a.size();
            long long s = 0;
            for (int x: a)
                s += x - a[0]; // a[0] 到其它下标的距离之和
            ans[a[0]] = s;
            for (int i = 1; i < m; ++i)
                // 从计算 a[i-1] 到计算 a[i],考虑 s 增加了多少
                ans[a[i]] = s += (long long) (i * 2 - m) * (a[i] - a[i - 1]);
        }
        return ans;
    }
};
func distance(nums []int) []int64 {
groups := map[int][]int{}
for i, x := range nums {
groups[x] = append(groups[x], i) // 相同元素分到同一组,记录下标
}
ans := make([]int64, len(nums))
for _, a := range groups {
n := len(a)
s := int64(0)
for _, x := range a {
s += int64(x - a[0]) // a[0] 到其它下标的距离之和
}
ans[a[0]] = s
for i := 1; i < n; i++ {
// 从计算 a[i-1] 到计算 a[i],考虑 s 增加了多少
s += int64(i*2-n) * int64(a[i]-a[i-1])
ans[a[i]] = s
}
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

前缀和

解法:前缀和

注意到如果 nums[i]nums[j] 不同,则它们不会互相影响。因此我们可以用一个 unordered_map<int, vector<int>> 保存同一种数的所有下标,然后单独处理每种数即可。

接下来考虑某一种数的答案怎么算出来。问题其实就是:给一个有序数组,求每个元素和其它元素差值的绝对值的和。这是出现过无数次的经典问题,解法参考 leetcode 1685. 有序数组中差绝对值之和

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    vector<long long> distance(vector<int>& nums) {
        int n = nums.size();

        // 维护每种数的所有下标
        unordered_map<int, vector<int>> mp;
        for (int i = 0; i < n; i++) mp[nums[i]].push_back(i);

        vector<long long> ans(n);
        // 求某一种数的答案
        auto gao = [&](vector<int> &vec) {
            int n = vec.size();
            long long f[n + 1];
            f[0] = 0;
            for (int i = 0; i < n; i++) f[i + 1] = f[i] + vec[i];
            for (int i = 1; i <= n; i++) {
                long long L = 1LL * i * vec[i - 1] - f[i];
                long long R = (f[n] - f[i]) - 1LL * (n - i) * vec[i - 1];
                ans[vec[i - 1]] = L + R;
            }
        };

        // 对每种数求答案
        for (auto &p : mp) gao(p.second);
        return ans;
    }
};

距离字典两次编辑以内的单词

方法一:暴力

思路与算法

注意到题目的数据范围很小,我们可以直接实施暴力算法。

对于 $\textit{queries}$ 中的每个字符串 $\textit{queries}[i]$,查找 $\textit{dictionary}$ 中是否存在一个字符串,使得两个字符串中最多只有两个字符不同(即汉明距离小于等于 $2$),如果存在,就将其添加到答案中。由于添加的顺序与遍历 $\textit{queries}$ 的顺序一致,我们不需要对答案的顺序进行特殊处理。

代码

###C++

class Solution {
public:
    vector<string> twoEditWords(vector<string>& queries,
                                vector<string>& dictionary) {
        vector<string> ans;
        for (string query : queries) {
            for (string s : dictionary) {
                int dis = 0;
                for (int i = 0; i < query.size(); i++) {
                    if (query[i] != s[i]) {
                        ++dis;
                    }
                }
                if (dis <= 2) {
                    ans.push_back(query);
                    break;
                }
            }
        }
        return ans;
    }
};

###Java

class Solution {
    public List<String> twoEditWords(String[] queries, String[] dictionary) {
        List<String> ans = new ArrayList<>();
        for (String query : queries) {
            for (String s : dictionary) {
                int dis = 0;
                for (int i = 0; i < query.length(); i++) {
                    if (query.charAt(i) != s.charAt(i)) {
                        dis++;
                    }
                }
                if (dis <= 2) {
                    ans.add(query);
                    break;
                }
            }
        }
        return ans;
    }
}

###C#

public class Solution {
    public IList<string> TwoEditWords(string[] queries, string[] dictionary) {
        var ans = new List<string>();
        foreach (var query in queries) {
            foreach (var s in dictionary) {
                int dis = 0;
                for (int i = 0; i < query.Length; i++) {
                    if (query[i] != s[i]) {
                        dis++;
                    }
                }
                if (dis <= 2) {
                    ans.Add(query);
                    break;
                }
            }
        }
        return ans;
    }
}

###Python

class Solution:
    def twoEditWords(self, queries, dictionary):
        ans = []
        for query in queries:
            for s in dictionary:
                dis = 0
                for i in range(len(query)):
                    if query[i] != s[i]:
                        dis += 1
                if dis <= 2:
                    ans.append(query)
                    break
        return ans

###C

char** twoEditWords(char** queries, int queriesSize,
                    char** dictionary, int dictionarySize,
                    int* returnSize) {
    char** ans = (char**)malloc(sizeof(char*) * queriesSize);
    int cnt = 0;

    for (int i = 0; i < queriesSize; i++) {
        char* query = queries[i];
        for (int j = 0; j < dictionarySize; j++) {
            char* s = dictionary[j];
            int dis = 0;
            for (int k = 0; query[k] != '\0'; k++) {
                if (query[k] != s[k]) {
                    dis++;
                }
            }
            if (dis <= 2) {
                ans[cnt++] = query;
                break;
            }
        }
    }

    *returnSize = cnt;
    return ans;
}

###Go

func twoEditWords(queries []string, dictionary []string) []string {
    var ans []string
    for _, query := range queries {
        for _, s := range dictionary {
            dis := 0
            for i := 0; i < len(query); i++ {
                if query[i] != s[i] {
                    dis++
                }
            }
            if dis <= 2 {
                ans = append(ans, query)
                break
            }
        }
    }
    return ans
}

###JavaScript

var twoEditWords = function(queries, dictionary) {
    const ans = [];
    for (const query of queries) {
        for (const s of dictionary) {
            let dis = 0;
            for (let i = 0; i < query.length; i++) {
                if (query[i] !== s[i]) {
                    dis++;
                }
            }
            if (dis <= 2) {
                ans.push(query);
                break;
            }
        }
    }
    return ans;
};

###TypeScript

function twoEditWords(queries: string[], dictionary: string[]): string[] {
    const ans: string[] = [];
    for (const query of queries) {
        for (const s of dictionary) {
            let dis = 0;
            for (let i = 0; i < query.length; i++) {
                if (query[i] !== s[i]) {
                    dis++;
                }
            }
            if (dis <= 2) {
                ans.push(query);
                break;
            }
        }
    }
    return ans;
}

###Rust

impl Solution {
    pub fn two_edit_words(queries: Vec<String>, dictionary: Vec<String>) -> Vec<String> {
        let mut ans = Vec::new();

        for query in &queries {
            for s in &dictionary {
                let mut dis = 0;
                for (c1, c2) in query.chars().zip(s.chars()) {
                    if c1 != c2 {
                        dis += 1;
                    }
                }
                if dis <= 2 {
                    ans.push(query.clone());
                    break;
                }
            }
        }

        ans
    }
}

复杂度分析

  • 时间复杂度:$O(qkn)$,其中 $q$ 为 $\textit{queries}$ 的长度,$k$ 为 $\textit{dictionary}$ 的长度,$n$ 为 $\textit{queries}[i]$ 的长度。我们需要对每一个 $\textit{queries}[i]$ 遍历一次 $\textit{dictionary}$,然后比较两个字符串。
  • 空间复杂度:$O(1)$,仅使用常数个变量。返回数组不计入空间复杂度。

方法二:字典树

思路与算法

我们可以将 $\textit{dictionary}$ 中的所有单词插入字典树,对每个 $\textit{queries}[i]$ 做深度优先搜索,在过程中维护修改次数 $\textit{cnt}$,从而实现在字典树上进行「最多 2 次修改」的匹配搜索。

定义状态 $\textit{dfs}(i, \textit{node}, \textit{cnt})$,其中 $i$ 表示当前匹配到第 $i$ 个字符,$\textit{node}$ 表示当前所在字典树的节点,$\textit{cnt}$ 表示已修改 $\textit{cnt}$ 次。在字典树上进行查找,对于第 $i$ 个字符 $\textit{query}[i]$:

  1. 如果字典树中存在 $\textit{node}.\textit{children}[\textit{query}[i]]$,不进行修改,下一步搜索状态 $\textit{dfs}(i+1, \textit{node}.\textit{children}[\textit{query}[i]], \textit{cnt})$。
  2. 如果字典树中不存在 $\textit{node}.\textit{children}[\textit{query}[i]]$,且 $\textit{cnt}<2$,进行修改,即枚举所有 $c \neq \textit{query}[i]$,下一步搜索状态 $\textit{dfs}(i+1,\textit{node}.\textit{children}[c], \textit{cnt}+1)$。

搜索过程中,我们可以进行一些剪枝,比如某条路径找到合法解就提前终止。

代码

###C++

struct TrieNode {
    TrieNode* child[26];
    bool isEnd;
    TrieNode() {
        memset(child, 0, sizeof(child));
        isEnd = false;
    }
};

class Solution {
public:
    TrieNode* root = new TrieNode();

    void insert(string& word) {
        TrieNode* node = root;
        for (char c : word) {
            int idx = c - 'a';
            if (!node->child[idx])
                node->child[idx] = new TrieNode();
            node = node->child[idx];
        }
        node->isEnd = true;
    }

    bool dfs(string& word, int i, TrieNode* node, int cnt) {
        if (cnt > 2)
            return false;
        if (!node)
            return false;

        if (i == word.size()) {
            return node->isEnd;
        }

        int idx = word[i] - 'a';

        // 不修改
        if (node->child[idx]) {
            if (dfs(word, i + 1, node->child[idx], cnt))
                return true;
        }

        // 修改
        if (cnt < 2) {
            for (int c = 0; c < 26; c++) {
                if (c == idx)
                    continue;
                if (node->child[c]) {
                    if (dfs(word, i + 1, node->child[c], cnt + 1))
                        return true;
                }
            }
        }

        return false;
    }

    vector<string> twoEditWords(vector<string>& queries,
                                vector<string>& dictionary) {
        for (auto& w : dictionary)
            insert(w);

        vector<string> res;
        for (auto& q : queries) {
            if (dfs(q, 0, root, 0)) {
                res.push_back(q);
            }
        }
        return res;
    }
};

###Java

class Solution {
    static class TrieNode {
        TrieNode[] child = new TrieNode[26];
        boolean isEnd = false;
    }

    TrieNode root = new TrieNode();

    void insert(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (node.child[idx] == null)
                node.child[idx] = new TrieNode();
            node = node.child[idx];
        }
        node.isEnd = true;
    }

    boolean dfs(String word, int i, TrieNode node, int cnt) {
        if (cnt > 2 || node == null)
            return false;

        if (i == word.length())
            return node.isEnd;

        int idx = word.charAt(i) - 'a';

        // 不修改
        if (node.child[idx] != null) {
            if (dfs(word, i + 1, node.child[idx], cnt))
                return true;
        }

        // 修改
        if (cnt < 2) {
            for (int c = 0; c < 26; c++) {
                if (c == idx)
                    continue;
                if (node.child[c] != null) {
                    if (dfs(word, i + 1, node.child[c], cnt + 1))
                        return true;
                }
            }
        }

        return false;
    }

    public List<String> twoEditWords(String[] queries, String[] dictionary) {
        for (String w : dictionary)
            insert(w);

        List<String> res = new ArrayList<>();
        for (String q : queries) {
            if (dfs(q, 0, root, 0)) {
                res.add(q);
            }
        }
        return res;
    }
}

###C#

public class Solution {
    class TrieNode {
        public TrieNode[] child = new TrieNode[26];
        public bool isEnd = false;
    }

    TrieNode root = new TrieNode();

    void Insert(string word) {
        var node = root;
        foreach (char c in word) {
            int idx = c - 'a';
            if (node.child[idx] == null)
                node.child[idx] = new TrieNode();
            node = node.child[idx];
        }
        node.isEnd = true;
    }

    bool Dfs(string word, int i, TrieNode node, int cnt) {
        if (cnt > 2 || node == null)
            return false;

        if (i == word.Length)
            return node.isEnd;

        int idx = word[i] - 'a';

        // 不修改
        if (node.child[idx] != null) {
            if (Dfs(word, i + 1, node.child[idx], cnt))
                return true;
        }

        // 修改
        if (cnt < 2) {
            for (int c = 0; c < 26; c++) {
                if (c == idx) continue;
                if (node.child[c] != null) {
                    if (Dfs(word, i + 1, node.child[c], cnt + 1))
                        return true;
                }
            }
        }

        return false;
    }

    public IList<string> TwoEditWords(string[] queries, string[] dictionary) {
        foreach (var w in dictionary)
            Insert(w);

        var res = new List<string>();
        foreach (var q in queries) {
            if (Dfs(q, 0, root, 0)) {
                res.Add(q);
            }
        }
        return res;
    }
}

###Python

class TrieNode:
    def __init__(self):
        self.child = [None] * 26
        self.isEnd = False


class Solution:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for c in word:
            idx = ord(c) - ord('a')
            if not node.child[idx]:
                node.child[idx] = TrieNode()
            node = node.child[idx]
        node.isEnd = True

    def dfs(self, word, i, node, cnt):
        if cnt > 2 or not node:
            return False

        if i == len(word):
            return node.isEnd

        idx = ord(word[i]) - ord('a')

        # 不修改
        if node.child[idx] and self.dfs(word, i + 1, node.child[idx], cnt):
            return True

        # 修改
        if cnt < 2:
            for c in range(26):
                if c == idx:
                    continue
                if node.child[c] and self.dfs(word, i + 1, node.child[c], cnt + 1):
                    return True

        return False

    def twoEditWords(self, queries, dictionary):
        for w in dictionary:
            self.insert(w)

        res = []
        for q in queries:
            if self.dfs(q, 0, self.root, 0):
                res.append(q)
        return res

###C

typedef struct TrieNode {
    struct TrieNode* child[26];
    bool isEnd;
} TrieNode;

TrieNode* newNode() {
    TrieNode* node = (TrieNode*)malloc(sizeof(TrieNode));
    memset(node->child, 0, sizeof(node->child));
    node->isEnd = false;
    return node;
}

void insert(TrieNode* root, char* word) {
    TrieNode* node = root;
    for (int i = 0; word[i]; i++) {
        int idx = word[i] - 'a';
        if (!node->child[idx])
            node->child[idx] = newNode();
        node = node->child[idx];
    }
    node->isEnd = true;
}

bool dfs(char* word, int i, TrieNode* node, int cnt) {
    if (cnt > 2 || !node)
        return false;

    if (word[i] == '\0')
        return node->isEnd;

    int idx = word[i] - 'a';

    // 不修改
    if (node->child[idx] && dfs(word, i + 1, node->child[idx], cnt))
        return true;

    // 修改
    if (cnt < 2) {
        for (int c = 0; c < 26; c++) {
            if (c == idx) continue;
            if (node->child[c] && dfs(word, i + 1, node->child[c], cnt + 1))
                return true;
        }
    }

    return false;
}

char** twoEditWords(char** queries, int queriesSize,
                    char** dictionary, int dictionarySize,
                    int* returnSize) {
    TrieNode* root = newNode();
    for (int i = 0; i < dictionarySize; i++)
        insert(root, dictionary[i]);

    char** res = (char**)malloc(sizeof(char*) * queriesSize);
    int cnt = 0;

    for (int i = 0; i < queriesSize; i++) {
        if (dfs(queries[i], 0, root, 0)) {
            res[cnt++] = queries[i];
        }
    }

    *returnSize = cnt;
    return res;
}

###Go

type TrieNode struct {
    child [26]*TrieNode
    isEnd bool
}

var root = &TrieNode{}

func insert(word string) {
    node := root
    for _, c := range word {
        idx := c - 'a'
        if node.child[idx] == nil {
            node.child[idx] = &TrieNode{}
        }
        node = node.child[idx]
    }
    node.isEnd = true
}

func dfs(word string, i int, node *TrieNode, cnt int) bool {
    if cnt > 2 || node == nil {
        return false
    }

    if i == len(word) {
        return node.isEnd
    }

    idx := word[i] - 'a'

    // 不修改
    if node.child[idx] != nil && dfs(word, i+1, node.child[idx], cnt) {
        return true
    }

    // 修改
    if cnt < 2 {
        for c := 0; c < 26; c++ {
            if byte(c) == idx {
                continue
            }
            if node.child[c] != nil && dfs(word, i+1, node.child[c], cnt+1) {
                return true
            }
        }
    }

    return false
}

func twoEditWords(queries []string, dictionary []string) []string {
    root = &TrieNode{}
    for _, w := range dictionary {
        insert(w)
    }

    var res []string
    for _, q := range queries {
        if dfs(q, 0, root, 0) {
            res = append(res, q)
        }
    }
    return res
}

###JavaScript

class TrieNode {
    constructor() {
        this.child = new Array(26).fill(null);
        this.isEnd = false;
    }
}

var twoEditWords = function(queries, dictionary) {
    const root = new TrieNode();

    function insert(word) {
        let node = root;
        for (let c of word) {
            let idx = c.charCodeAt(0) - 97;
            if (!node.child[idx]) node.child[idx] = new TrieNode();
            node = node.child[idx];
        }
        node.isEnd = true;
    }

    function dfs(word, i, node, cnt) {
        if (cnt > 2 || !node) return false;
        if (i === word.length) return node.isEnd;

        let idx = word.charCodeAt(i) - 97;

        // 修改
        if (node.child[idx] && dfs(word, i + 1, node.child[idx], cnt))
            return true;

        // 不修改
        if (cnt < 2) {
            for (let c = 0; c < 26; c++) {
                if (c === idx) continue;
                if (node.child[c] && dfs(word, i + 1, node.child[c], cnt + 1))
                    return true;
            }
        }

        return false;
    }

    for (let w of dictionary) insert(w);

    const res = [];
    for (let q of queries) {
        if (dfs(q, 0, root, 0)) res.push(q);
    }

    return res;
};

###TypeScript

class TrieNode {
    child: (TrieNode | null)[] = new Array(26).fill(null);
    isEnd: boolean = false;
}

function twoEditWords(queries: string[], dictionary: string[]): string[] {
    const root = new TrieNode();

    function insert(word: string) {
        let node = root;
        for (const c of word) {
            const idx = c.charCodeAt(0) - 97;
            if (!node.child[idx]) node.child[idx] = new TrieNode();
            node = node.child[idx]!;
        }
        node.isEnd = true;
    }

    function dfs(word: string, i: number, node: TrieNode | null, cnt: number): boolean {
        if (cnt > 2 || !node) return false;
        if (i === word.length) return node.isEnd;

        const idx = word.charCodeAt(i) - 97;

        // 不修改
        if (node.child[idx] && dfs(word, i + 1, node.child[idx], cnt))
            return true;

        // 修改
        if (cnt < 2) {
            for (let c = 0; c < 26; c++) {
                if (c === idx) continue;
                if (node.child[c] && dfs(word, i + 1, node.child[c], cnt + 1))
                    return true;
            }
        }

        return false;
    }

    for (const w of dictionary) insert(w);

    const res: string[] = [];
    for (const q of queries) {
        if (dfs(q, 0, root, 0)) res.push(q);
    }

    return res;
}

###Rust

struct TrieNode {
    child: Vec<Option<Box<TrieNode>>>,
    is_end: bool,
}

impl TrieNode {
    fn new() -> Self {
        let mut child = Vec::with_capacity(26);
        for _ in 0..26 {
            child.push(None);
        }

        Self {
            child,
            is_end: false,
        }
    }
}

impl Solution {
    fn insert(root: &mut TrieNode, word: &str) {
        let mut node = root;
        for c in word.chars() {
            let idx = (c as u8 - b'a') as usize;
            if node.child[idx].is_none() {
                node.child[idx] = Some(Box::new(TrieNode::new()));
            }
            node = node.child[idx].as_mut().unwrap();
        }
        node.is_end = true;
    }

    fn dfs(word: &[u8], i: usize, node: &TrieNode, cnt: i32) -> bool {
        if cnt > 2 {
            return false;
        }
        if i == word.len() {
            return node.is_end;
        }

        let idx = (word[i] - b'a') as usize;

        // 不修改
        if let Some(ref next) = node.child[idx] {
            if Self::dfs(word, i + 1, next, cnt) {
                return true;
            }
        }

        // 修改
        if cnt < 2 {
            for c in 0..26 {
                if c == idx {
                    continue;
                }
                if let Some(ref next) = node.child[c] {
                    if Self::dfs(word, i + 1, next, cnt + 1) {
                        return true;
                    }
                }
            }
        }

        false
    }

    pub fn two_edit_words(queries: Vec<String>, dictionary: Vec<String>) -> Vec<String> {
        let mut root = TrieNode::new();

        for w in &dictionary {
            Self::insert(&mut root, w);
        }

        let mut res = vec![];
        for q in &queries {
            if Self::dfs(q.as_bytes(), 0, &root, 0) {
                res.push(q.clone());
            }
        }

        res
    }
}

复杂度分析

  • 时间复杂度:$O(k \cdot n + q \cdot n^2 \cdot 25^2)$,其中 $q$ 为 $\textit{queries}$ 的长度,$k$ 为 $\textit{dictionary}$ 的长度,$n$ 为 $\textit{queries}[i]$ 的长度。建字典树需要 $O(kn)$,查询时对于每一个字母都有修改和不修改两种选择,选择修改位置有 $C_n^2=n^2$ 种选择,其中不修改有 $1$ 条分支,修改有 $25$ 条分支,最多修改两次,因此有 $25^2$ 种选择。
  • 空间复杂度:$O(kn)$。即为字典树所占用的空间。

每日一题-距离字典两次编辑以内的单词🟡

给你两个字符串数组 queries 和 dictionary 。数组中所有单词都只包含小写英文字母,且长度都相同。

一次 编辑 中,你可以从 queries 中选择一个单词,将任意一个字母修改成任何其他字母。从 queries 中找到所有满足以下条件的字符串:不超过 两次编辑内,字符串与 dictionary 中某个字符串相同。

请你返回 queries 中的单词列表,这些单词距离 dictionary 中的单词 编辑次数 不超过 两次 。单词返回的顺序需要与 queries 中原本顺序相同。

 

示例 1:

输入:queries = ["word","note","ants","wood"], dictionary = ["wood","joke","moat"]
输出:["word","note","wood"]
解释:
- 将 "word" 中的 'r' 换成 'o' ,得到 dictionary 中的单词 "wood" 。
- 将 "note" 中的 'n' 换成 'j' 且将 't' 换成 'k' ,得到 "joke" 。
- "ants" 需要超过 2 次编辑才能得到 dictionary 中的单词。
- "wood" 不需要修改(0 次编辑),就得到 dictionary 中相同的单词。
所以我们返回 ["word","note","wood"] 。

示例 2:

输入:queries = ["yes"], dictionary = ["not"]
输出:[]
解释:
"yes" 需要超过 2 次编辑才能得到 "not" 。
所以我们返回空数组。

 

提示:

  • 1 <= queries.length, dictionary.length <= 100
  • n == queries[i].length == dictionary[j].length
  • 1 <= n <= 100
  • 所有 queries[i] 和 dictionary[j] 都只包含小写英文字母。

暴力(Python/Java/C++/C/Go/JS/Rust)

对于每个 $q = \textit{queries}[i]$,遍历 $\textit{dictionary}$ 中的字符串 $s$,判断 $q$ 和 $s$ 是否至多有两个位置上的字母不同。

class Solution:
    def twoEditWords(self, queries: List[str], dictionary: List[str]) -> List[str]:
        ans = []
        for q in queries:
            for s in dictionary:
                if sum(x != y for x, y in zip(q, s)) <= 2:
                    ans.append(q)
                    break
        return ans
class Solution {
    public List<String> twoEditWords(String[] queries, String[] dictionary) {
        List<String> ans = new ArrayList<>();
        for (String q : queries) {
            for (String s : dictionary) {
                int cnt = 0;
                for (int i = 0; i < s.length() && cnt <= 2; i++) {
                    if (q.charAt(i) != s.charAt(i)) {
                        cnt++;
                    }
                }
                if (cnt <= 2) {
                    ans.add(q);
                    break;
                }
            }
        }
        return ans;
    }
}
class Solution {
public:
    vector<string> twoEditWords(vector<string>& queries, vector<string>& dictionary) {
        vector<string> ans;
        for (auto& q : queries) {
            for (auto& s : dictionary) {
                int cnt = 0;
                for (int i = 0; i < s.size() && cnt <= 2; i++) {
                    if (q[i] != s[i]) {
                        cnt++;
                    }
                }
                if (cnt <= 2) {
                    ans.push_back(q);
                    break;
                }
            }
        }
        return ans;
    }
};
char** twoEditWords(char** queries, int queriesSize, char** dictionary, int dictionarySize, int* returnSize) {
    char** ans = malloc(queriesSize * sizeof(char*));
    *returnSize = 0;

    for (int i = 0; i < queriesSize; i++) {
        char* q = queries[i];
        for (int j = 0; j < dictionarySize; j++) {
            char* s = dictionary[j];
            int cnt = 0;
            for (int k = 0; s[k] && cnt <= 2; k++) {
                if (q[k] != s[k]) {
                    cnt++;
                }
            }
            if (cnt <= 2) {
                ans[(*returnSize)++] = q;
                break;
            }
        }
    }

    return ans;
}
func twoEditWords(queries, dictionary []string) (ans []string) {
for _, q := range queries {
next:
for _, s := range dictionary {
cnt := 0
for i := range s {
if q[i] != s[i] {
cnt++
if cnt > 2 {
continue next
}
}
}
ans = append(ans, q)
break
}
}
return
}
var twoEditWords = function(queries, dictionary) {
    const ans = [];
    for (const q of queries) {
        for (const s of dictionary) {
            let cnt = 0;
            for (let i = 0; i < s.length && cnt <= 2; i++) {
                if (q[i] !== s[i]) {
                    cnt++;
                }
            }
            if (cnt <= 2) {
                ans.push(q);
                break;
            }
        }
    }
    return ans;
};
impl Solution {
    pub fn two_edit_words(queries: Vec<String>, dictionary: Vec<String>) -> Vec<String> {
        let mut ans = vec![];
        for q in queries {
            for s in &dictionary {
                let mut cnt = 0;
                for (a, b) in q.bytes().zip(s.bytes()) {
                    if a != b {
                        cnt += 1;
                        if cnt > 2 {
                            break;
                        }
                    }
                }
                if cnt <= 2 {
                    ans.push(q);
                    break;
                }
            }
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(qdn)$,其中 $q$ 是 $\textit{queries}$ 的长度,$d$ 是 $\textit{dictionary}$ 的长度,$n$ 是 $\textit{queries}[i]$ 的长度。题目保证所有字符串长度相等。
  • 空间复杂度:$\mathcal{O}(1)$。返回值不计入。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

随便做

解题思路

代码

###python3

class Solution:
    def twoEditWords(self, queries: List[str], dictionary: List[str]) -> List[str]:
        def check(x,y):
            t=0
            for i in range(len(x)):
                if x[i]!=y[i]:
                    t+=1
            return t<=2
        covered=set()
        lst=[]
        for i in dictionary:
            for j in range(len(queries)):
                t=queries[j]
                if j not in covered and check(t,i):
                    covered.add(j)
                    lst.append(j)
        lst.sort()
        return [queries[i] for i in lst]

一行不解释

class Solution:
    def twoEditWords(self, Q: List[str], D: List[str], f = lambda w, D: min(sum(a != b for a, b in zip(w, t)) for t in D)) -> List[str]:
        return [w for w in Q if f(w, D) <= 2]

每日一题-执行交换操作后的最小汉明距离🟡

给你两个整数数组 sourcetarget ,长度都是 n 。还有一个数组 allowedSwaps ,其中每个 allowedSwaps[i] = [ai, bi] 表示你可以交换数组 source 中下标为 aibi下标从 0 开始)的两个元素。注意,你可以按 任意 顺序 多次 交换一对特定下标指向的元素。

相同长度的两个数组 sourcetarget 间的 汉明距离 是元素不同的下标数量。形式上,其值等于满足 source[i] != target[i]下标从 0 开始)的下标 i0 <= i <= n-1)的数量。

在对数组 source 执行 任意 数量的交换操作后,返回 sourcetarget 间的 最小汉明距离

 

示例 1:

输入:source = [1,2,3,4], target = [2,1,4,5], allowedSwaps = [[0,1],[2,3]]
输出:1
解释:source 可以按下述方式转换:
- 交换下标 0 和 1 指向的元素:source = [2,1,3,4]
- 交换下标 2 和 3 指向的元素:source = [2,1,4,3]
source 和 target 间的汉明距离是 1 ,二者有 1 处元素不同,在下标 3 。

示例 2:

输入:source = [1,2,3,4], target = [1,3,2,4], allowedSwaps = []
输出:2
解释:不能对 source 执行交换操作。
source 和 target 间的汉明距离是 2 ,二者有 2 处元素不同,在下标 1 和下标 2 。

示例 3:

输入:source = [5,1,2,4,3], target = [1,5,4,2,3], allowedSwaps = [[0,4],[4,2],[1,3],[1,4]]
输出:0

 

提示:

  • n == source.length == target.length
  • 1 <= n <= 105
  • 1 <= source[i], target[i] <= 105
  • 0 <= allowedSwaps.length <= 105
  • allowedSwaps[i].length == 2
  • 0 <= ai, bi <= n - 1
  • ai != bi
❌