【问题标题】:Intuition behind initializing both the pointers at the beginning versus one at the beginning and other at the ending初始化两个指针在开头与一个在开头和另一个在结尾的直觉
【发布时间】:2021-09-30 00:58:20
【问题描述】:

我前几天解决了一个问题:

给定一个包含N 整数和一个整数B 的未排序数组A,找出数组中是否存在一对元素的差为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,0,1,2,-1,-4],则输出应为:[[-1,-1,2],[-1,0,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]s 中的每一个(来自外部循环)都是我们搜索的“目标”,在内部 while 循环中使用两个指针方法,就像在顶部的第一个代码中一样。

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

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

Edit2:我了解这两个 sn-ps 的工作原理。我的问题特别是代码#2 中r=nums.size()-1 背后的原因:正如我们在上面的代码#1 中看到的,从末尾开始的r 错过了一些有效对(http://cpp.sh/36y27 - 有效对 (10,50) 错过了);那么为什么我们不会错过第二个代码中的有效对呢?

【问题讨论】:

  • 如果将三和问题简化为二和问题,您的问题归结为“为什么要在排序数组 A st A[i] - A [j] == 目标不同于 A[i] + A[j] == 目标”。因为这两个问题是不同的,很明显它们需要一种(稍微)不同的方法,经过一些实验/思考后可能会弄明白。
  • @wLui155,你可以把“A[i] + A[j] == target”写成“A[i] - (-A[j]) == target”,然后2 个问题会变得相同,对吧?
  • 嗯,是的,A[i] + A[j] == A[i] - (-A[j]),但在大多数情况下A[i] - (-A[j]) =/= A[i] - A[j]...无论如何在第一个问题中,你已经明白为什么你不能从r = nums.size() - 1开始;因为“减少j 和增加i 都减少了差异。”另一方面,如果您从 ij 在同一位置开始,您将通过基本上对定义的 差异矩阵 B 进行二进制搜索来找到与目标的(最接近的)差异by B[i][j] = A[i] - A[j],按行降序排序,按列升序排序(因为A是排序)。
  • @ggorlen,哦,是的,我明白你的意思了。您能否详细说明这些不同的不变量是什么?
  • @ggorlen 什么时候是 6?

标签: c++ algorithm initialization sliding-window


【解决方案1】:

重新表述问题

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

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

您将 2 和 (A[i] + A[j] == B s.t. i != j) 与 2 差 (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] &gt; B,那么我们别无选择,只能将右指针递减到较低的索引。增加左指针可以保证增加我们的总和或保持不变,我们将越来越远离目标,可能会关闭找到解决方案对的潜力,其中可能仍然包括A[L]

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

关键的一点是,在每一步都没有要做出的决定:要么找到答案,要么可以从进一步考虑中明确排除任一索引上的两个数字之一。 p>

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

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

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


二差

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

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

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

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

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

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

我们又回来了:没有选择或神奇的神谕来决定移动哪个索引。我们可以系统地消除过大或过小的值,并针对目标进行磨练。该逻辑的工作原理与 L = 0, R = length - 1 对 2-sum 的工作原理相同。


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

【讨论】:

  • 我不太同意:here's 我设置的第一个代码,j=nums.size()-1。它在测试用例上失败:[2, 3, 5, 10, 50, 80]; B=40。另一个例子是:[-533, -666, -500, 169, 724, 478, 358, -38, -536, 705, -855, 281, -173, 961, -509, -5, 942, -173, 436, -609, -396, 902, -847, -708, -618, 421, -284, 718, 895, 447, 726, -229, 538, 869, 912, 667, -701, 35, 894, -297, 811, 322]; B=369。你能告诉我为什么它会失败吗?
  • 谢谢。我会更深入地研究它并通过一些例子来工作;诚然,这对我来说是一个粗略的分析,但事情是这样的:3sum 问题的内部循环与您使用 sort() 和两个指针完成的 2sum 基本相同,所以我看不出是什么导致了差异-- 3sum 希望您返回所有解决方案而 2sum 只需要 true/false 的事实并不重要。
  • 是的,这就是我通过设置r=nums.size()-1 解决第一个问题的原因,但在上述测试用例上得到了一个WA。进一步看,我意识到我的错误(如上面代码 1 所示)。谢谢你帮助我。感谢您的帮助!
  • 感谢您的支持和帮助!它澄清了我的问题。我感谢您的帮助! :)
  • 没问题——非常有趣的问题,花了一些时间才弄清楚发生了什么。
【解决方案2】:

考虑一下:

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 &amp;&amp; i!=j)不正确的情况。您的代码做出了一个错误的假设,即如果两个端点的差异是&gt; 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] 需要递减,因为递增l 只会进一步增加从num[l + 1] > num[l] 开始进一步求和。

【讨论】:

  • 对不起,这不能回答我的问题。我完全理解它是如何工作的。我的问题具体是代码#2 中r=nums.size()-1 背后的原因:正如我们在上面的代码#1 中看到的,从最后开始错过了一些有效的对(cpp.sh/36y27);那么为什么我们不会错过第二个代码中的有效对呢?
  • @Someone 查看更新后的答案是否解决了您的问题。
【解决方案3】:

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

对于升序排列的数组A,为什么我们对问题t 执行不同的两指针搜索A[i] + A[j] == tA[i] - A[j] == t,其中j &gt; 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的列是降序的。让我们计算BA = [2, 3, 5, 10, 50, 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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 2019-07-23
    • 2020-06-10
    • 2014-06-01
    相关资源
    最近更新 更多