微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

初始化开头的指针与开头的指针和结尾的指针背后的直觉 为什么 L = 0, R = length - 1 适用于 2-sum为什么 L = 0, R = 0 不适用于 2-sum为什么 L = 0, R = length - 1 不适用于 2-difference为什么 L = 0, R = 0 适用于 2-difference

如何解决初始化开头的指针与开头的指针和结尾的指针背后的直觉 为什么 L = 0, R = length - 1 适用于 2-sum为什么 L = 0, R = 0 不适用于 2-sum为什么 L = 0, R = length - 1 不适用于 2-difference为什么 L = 0, R = 0 适用于 2-difference

我几天前解决一个问题:

给定一个包含 A 个整数和一个整数 N 的未排序数组 B,查找数组中是否存在差为 B 的一对元素。如果存在任何此类对,则返回 true,否则返回 false。对于 [2,3,5,10,50,80]; B=40;,它应该返回 true

作为:

int Solution::solve(vector<int> &A,int B) {
    if(A.size()==1) return false;
    int i=0,j=0;     //note: both initialized at the beginning

    sort(begin(A),end(A));
    while(i< A.size() && j<A.size()) {
        if(A[j]-A[i]==B && i!=j) return true;
        if(A[j]-A[i]<B) j++;
        else i++;
    }

    return false;
}

解决这个问题时,我之前犯过的错误是初始化 i=0j=A.size()-1。因此,减少 j增加 i both 减少了差异,因此错过了有效差异。如上所述在开始时初始化两者,我能够解决问题。

现在我正在解决一个后续的 3sum 问题:

给定一个整数数组nums,返回所有的三元组[nums[i],nums[j],nums[k]],使得i != ji != kj != knums[i] + nums[j] + nums[k] == 0。请注意,解决方案集不得包含重复的三元组。如果 nums = [-1,1,2,-1,-4]输出应该是:[[-1,2],[-1,1]](任何订单都有效)。

此问题的 solution 表示为:

vector<vector<int>> threeSum(vector<int>& nums) {
    sort(nums.begin(),nums.end());
    vector<vector<int>> res;
    for (unsigned int i=0; i<nums.size(); i++) {
        if ((i>0) && (nums[i]==nums[i-1]))
            continue;
        int l = i+1,r = nums.size()-1;   //note: unlike `l`,`r` points to the end
        while (l<r) {
            int s = nums[i]+nums[l]+nums[r];
            if (s>0) r--;
            else if (s<0) L++;
            else {
                res.push_back(vector<int> {nums[i],nums[l],nums[r]});
                while (nums[l]==nums[l+1]) L++;
                while (nums[r]==nums[r-1]) r--;
                L++; r--;
            }
        }
    }
    return res;
}

逻辑非常简单:每个nums[i](来自外部循环)都是我们搜索的“目标”,在内部while循环中使用两个指针方法,就像第一个代码中的顶。

我没有遵循的是初始化 r=nums.size()-1 和向后工作背后的逻辑 - 如何不被遗漏有效差异(在这种情况下,实际上是“总和”)?

Edit1:两个问题都包含负数和正数,以及零。

Edit2:我了解这两个片段的工作原理。我的问题特别代码#2中r=nums.size()-1背后的推理:正如我们在上面的代码#1中看到的那样,从最后开始r错过了一些有效的对({{ 3}} - 有效对 (10,50) 错过);那么为什么我们不会错过第二个代码中的有效对呢?

解决方法

重新制定问题

两种算法之间的区别归结为加法和减法,而不是 3 对 2 和。

您的 3-sum 变体要求与目标匹配的 3 个数字的总和。当您在外循环中修复一个数字时,内循环会减少为 2-sum,实际上是 2-sum(即加法)。顶部代码中的“2-sum”变体实际上是 2-(即减法)。

您将 2-sum (A[i] + A[j] == B s.t. i != j) 与 2-difference (A[i] - A[j] == B s.t. i != j) 进行比较。我将继续使用这些术语,而忘记 3-sum 中的外循环作为红鲱鱼。


2-sum

为什么 L = 0,R = length - 1 适用于 2-sum

对于 2-sum,您可能已经看到从末端开始向中间工作的直觉,但值得明确逻辑。

在循环中的任何迭代中,如果总和为 A[L] + A[R] > B,那么我们别无选择,只能将右指针递减到较低的索引。增加左指针肯定会增加我们的总和或保持不变,我们将离目标越来越远,可能会关闭找到解决方案对的可能性,其中很可能仍然包括 A[L]

另一方面,如果 A[L] + A[R] < B,那么您必须通过将左指针向前移动到更大的数字来增加总和。有可能 A[R] 仍然是该总和的一部分——我们不能保证它在 A[L] + A[R] > B 之前不是总和的一部分。

关键点在于,在每一步都无需做出决定:要么找到答案,要么可以从进一步考虑中明确排除任一指数中的两个数字之一。 >

为什么 L = 0,R = 0 不适用于 2-sum

这解释了为什么从 0 开始两个数字对 2-sum 没有帮助。您将使用什么规则来增加指针以找到解决方案?没有办法知道哪个指针需要向前移动,哪个应该等待。两次移动都最多增加总和,但都没有减少总和(开始是最小总和,A[0] + A[0])。移动错误的一个可能会阻止以后找到解决方案,并且没有办法明确消除任何一个数字。

您又回到了将 left 保持在 0 并将右指针向前移动到导致 A[R] + A[L] > B 的第一个元素,然后运行久经考验的原始双指针逻辑。您不妨从 R 开始 length - 1


2-difference

为什么 L = 0,R = length - 1 不适用于 2-difference

现在我们了解了 2-sum 的工作原理,让我们看看 2-difference。为什么同样的方法,从两端开始,向中间工作,行不通?

原因是当你对两个数字相减时,你失去了 2-sum 中最重要的保证,即向前移动左指针将始终增加总和,向后移动右指针将始终减少总和。

在排序数组中的两个数字之间进行减法,A[R] - A[L] s.t. R > L,无论您向前移动 L 还是向后移动 R,总和都会减少,即使在只有正数的数组中也是如此。这意味着在给定的索引处,没有办法知道哪个指针需要移动才能找到正确的对,这与 2-sum 相同的原因破坏了算法,两个指针都从 0 开始。

为什么 L = 0,R = 0 适用于 2-difference

最后,为什么从 0 开始两个指针都适用于 2 差?原因是您回到了 2-sum 保证,即移动一个指针会增加差异,而另一个则减少差异。具体来说,如果 A[R] - A[L] < B,那么 L++ 保证减少差异,而 R++ 保证增加它。

我们又回来了:没有选择或神奇的预言来决定移动哪个索引。我们可以系统地消除过大或过小的值,并专注于目标。该逻辑适用于 L = 0,R = length - 1 适用于 2-sum 的相同原因。


顺便说一句,第一个解决方案是次优的 O(n log(n)) 而不是 O(n) 两次传递和 O(n) 空间。您可以使用无序映射来跟踪到目前为止看到的项目,然后对数组中的每个项目执行查找:如果 B - A[i] 为某些 i 在地图中,您找到了您的配对。

,

考虑一下:

A = {2,3,5,10,50,80}
B = 40

i = 0,j = 5;

当你有类似的东西

while(i<j) {
    if(A[j]-A[i]==B && i!=j) return true;
    if(A[j]-A[i]>B) j--;
    else i++;
}

考虑if(A[j]-A[i]==B && i!=j) 不正确的情况。您的代码做出了一个错误的假设,即如果两个端点的差为 > B,则应该减少 j。给定一个已排序的数组,您不知道是先减少 j 然后取差值会得到目标差值,还是增加 i 然后取差值才能得到目标数,因为它可以双向进行。在您的示例中,当 A[5] - A[0] != 10 您可以双向选择时,A[4] - A[0](这就是您所做的) A[5] - A[1] .两者都会仍然给你一个比目标差异更大的差异。简而言之,您算法中的假设不正确,因此不是正确的方法。

在第二种方法中,情况并非如此。当未找到三元组 nums[i]+nums[l]+nums[r] 时,您知道数组已排序,如果总和大于 0,则意味着 num[r] 需要自递增 {{1} } 只会进一步增加总和,因为 l > num[l + 1]

,

您的问题归结为以下几点:

对于升序排列的数组 A,为什么我们对问题 tA[i] + A[j] == t 执行不同的双指针搜索A[i] - A[j] == t,其中j > i

为什么第一个问题更直观,我们可以将 ij 固定在相反的两端并减少 j 或增加 i,所以我'将重点解决第二个问题。

对于数组问题,有时最容易绘制出解空间,然后从那里提出算法。首先,我们画出解空间B,其中B[i][j] = -(A[i] - A[j])(仅定义为j > i):

B,for A of length N
    i   ---------------------------->
j   B[0][0]     B[0][1]     ... B[0][N - 1]
|   B[1][0]     B[1][1]     ... B[1][N - 1]
|   .           .         .
|   .           .           .
|   .           .             .
v   B[N - 1][0] B[N - 1][1] ... B[N - 1][N - 1]
---
In terms of A:
X           -(A[0] - A[1])  -(A[0] - A[2])  ...  -(A[0] - A[N - 2]) -(A[0] - A[N - 1])
X           X               -(A[1] - A[2])  ...  -(A[1] - A[N - 2]) -(A[1] - A[N - 1])
.           .               .               .                       .
.           .               .                 .                     .
.           .               .                   .                   .
X           X               X               ...  X                  -(A[N - 2] - A[N - 1])
X           X               X               ...  X                  X                       

注意 B[i][j] = A[j] - A[i],所以 B 的行是 升序,B 的列是 降序。让我们为 B 计算 A = [2,80]

B = [
   i------------------------>
j  X    1    3    8    48    78
|  X    X    2    7    47    77
|  X    X    X    5    45    75
|  X    X    X    X    40    70
|  X    X    X    X    X     30
v  X    X    X    X    X     X
]

现在等效的问题是在 B 中搜索 t = 40。请注意,如果我们从 i = 0j = N = 5 开始,则没有好的/保证的方法可以到达 40。但是,如果我们从一个位置开始,我们总是可以在 B 中以小步长递增/递减当前元素,我们可以保证我们将尽可能接近 t

在这种情况下,我们采取的小步骤包括在矩阵中向右/向下遍历,从左上角开始(可以等效地从右下角向左/向上遍历),这对应于递增 A 中原始问题中的 ij

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。