【问题标题】:print all the triplets (3 Sum) leetcode,https://leetcode.com/problems/3sum/ [closed]打印所有三元组 (3 Sum) leetcode,https://leetcode.com/problems/3sum/ [关闭]
【发布时间】:2022-01-19 12:14:07
【问题描述】:
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<int> v;
        vector<vector<int>> ans;
        int n=nums.size();
        sort(nums.begin(),nums.end());
        for(int i=0;i<n;i++){
            for(int j=i+1;j<n;j++){
                for(int k=j+1;k<n;k++){
                    if(nums[i]+nums[j]+nums[k]==0 && i!=j && i!=k && j!=k){
                        v.push_back(nums[i]);
                        v.push_back(nums[j]);
                        v.push_back(nums[k]);                        
                        ans.push_back(v);
                    }
                }
            }
        }
         return ans;
    }
};

它没有显示错误,但它显示了我在附件中给出的错误答案

Input: [-1, 0, 1, 2, -1, 4]

Your output: [[-1, -1, 2], [-1, -1, 2, -1, 0, 1], [-1, -1, 2, -1, 0, 1, -1, 0, 1]]

Expected output: [[-1, -1, 2], [-1, 0, 1]]

我可以理解将越来越多的值推回我的向量 v 的问题。好的。

但也许,有人可以给我一个提示,告诉我如何解决重复的问题?

非常欢迎和感谢对我作为新用户的任何帮助。

【问题讨论】:

  • 你的问题是什么?
  • 你有两个主要问题,你没有重置v(它应该只代表一个三元组)。你不处理重复。
  • 至少你看到了输入。现在轮到你学习一些相当有用的东西了(IMO 这些问题不是):调试! 创建一个合适的 minimal reproducible example 并在你的本地构建和运行代码自己的。然后使用debugging 找出问题所在。
  • 请注意,存在比O(n³) 更好的解决方案,存在O(n²) 解决方案。
  • 这里真正考的是对计算机科学和算法的基础知识和理解。如果有人不知道答案,那么实现这一点的裸代码转储将不会真正帮助他们理解任何东西或学习任何东西。相反,这里的正确答案应该是去学习实现这一点所需的计算机科学和算法的相关领域。不幸的是,stackoverflow.com 不能替代 good C++ and computer science algorithms textbook

标签: c++ arrays vector


【解决方案1】:

当然,我们会在这里为您提供帮助。

从一门新语言开始从来都不是那么容易,而且可能有些事情一开始并没有立即清楚。此外,对于您可能看到的任何粗鲁的 cmets,我深表歉意,但您可以放心,SO 的绝大多数成员都非常支持。

我想先给你一些关于 Leetcode 和 Codeforces 等页面的信息。通常也称为“竞争性编程”页面。有时人们会误解这一点,他们认为您提交代码的时间有限。但事实并非如此。有这样的比赛,但通常不在提到的页面上。不好的是,在线页面上也使用了真实比赛中使用的编码风格。这真的很糟糕。因为这种编码风格太可怕了,以至于没有认真的开发人员能在一家真正的公司里生存一天,需要通过软件赚钱,然后为此负责。

因此,这些页面永远不会教您或指导您如何编写好的 C++ 代码。更糟糕的是,如果新手开始学习该语言并看到这些糟糕的代码,那么他们就会养成坏习惯。

但是这些页面的目的是什么?

目的是找到一个好的算法,主要针对运行时执行速度进行优化,并且通常还针对低内存消耗。

所以,我们的目标是一个好的设计。语言或编码风格对他们来说并不重要。所以,你甚至可以提交完全混淆的代码或“代码高尔夫”的解决方案,只要它快,没关系。

因此,切勿在第一步就立即开始编码。首先,考虑 3 天。然后,拿一些设计工具,比如一张纸,画一个设计草图。然后重构您的设计,然后重构您的设计,然后重构您的设计,然后重构您的设计,然后重构您的设计等等。这可能需要一周时间。

接下来,寻找一种你知道并且可以处理你的设计的合适的编程语言。

最后,开始编码。因为你之前做了一个很好的设计,你可以使用长而有意义的变量名,写很多很多cmets,这样其他人(一个月后还有你)可以理解你的代码AND你的设计。

好的,大概明白了。


现在,让我们分析您的代码。您选择了具有三重嵌套循环的蛮力解决方案。这可能适用于少量元素,但在大多数情况下会导致所谓的 TLE(超出时间限制)错误。这些页面上的几乎所有问题都无法通过蛮力解决。蛮力解决方案始终表明您没有执行上述设计步骤。这会导致额外的错误。

您的代码存在过多的语义错误。

您在开头定义了一个名为“v”的std::vector。然后,在循环中,当你找到满足给定条件的三元组后,push_back 将得到std::vector 中的结果。这意味着,您将 3 个值添加到 std::vector“v”,现在其中有 3 个元素。在下一个循环运行中,在找到下一个合适的位置后,您再次将 push_back 3 个附加值添加到您的 std::vector ”v” 中,现在其中有 6 个元素。在下一轮 9 个元素等等。

如何解决?

您可以使用std::vectorclear 函数从最内层循环开始处if 语句之后的std::vector 中删除旧元素。但这基本上不是那么好,而且还很耗时。更好的是遵循一般习语,尽可能晚地定义变量,并在需要的时候。所以,如果你在 if 语句之后定义你的std::vector“v”,那么问题就消失了。但是,您还会注意到它仅在此处使用,而在其他任何地方都没有。因此,您根本不需要它。

您可能已经看到可以通过使用初始化列表向std::vector 添加值。比如:

std::vector<int> v {1,2,3};

有了这个诀窍,你可以删除你的std::vector“v”和所有相关代码,直接写:

ans.push_back( { nums[i], nums[j], nums[k] } );

那么您将节省 3 个不必要的 push_back(和一个 clear)操作,更重要的是,您不会获得超过 3 个元素的结果集。

下一个问题。重复。您尝试通过编写 &amp;&amp; i!=j &amp;&amp; i!=k &amp;&amp; j!=k 来防止存储重复项。但这通常不起作用,因为您比较的是索引而不是值,而且比较也是错误的。布尔表达式是重言式。它总是正确的。您使用i+1 初始化变量j,因此“i”永远不会等于“j”。因此,条件i != j 始终为真。其他变量也是如此。

但是如何防止重复条目。您可以进行一些逻辑比较,或者首先存储所有三元组,然后使用std::unique(或其他函数)来消除重复项,或者使用只存储唯一元素的容器,例如std::set。对于给定的设计,时间复杂度为 O(n^3),这意味着它已经非常慢,添加 std::set 不会让事情变得更糟。我在一个小型基准测试中检查了这一点。因此,唯一的解决方案是完全不同的设计。我们稍后会谈到。让我们先修复代码,仍然使用蛮力方法。

请看下面的简短而优雅的解决方案。

    vector<vector<int>> threeSum(vector<int>& nums) {
        std::set<vector<int>> ans;
        int n = nums.size();
        sort(nums.begin(), nums.end());
        for (int i = 0; i < n; i++) 
            for (int j = i + 1; j < n; j++) 
                for (int k = j + 1; k < n; k++) 
                    if (nums[i] + nums[j] + nums[k] == 0) 
                        ans.insert({ nums[i], nums[j], nums[k] });
        return { ans.begin(), ans.end() };
    }

但是,不幸的是,由于不幸的设计决策,大输入比更好的设计慢 20000 倍。而且,由于在线测试程序将使用大输入向量,因此程序不会通过运行时约束。

如何找到更好的解决方案。我们需要仔细分析需求,也可以使用一些现有的知识来解决类似的问题。

如果您阅读一些书籍或互联网文章,那么您经常会得到提示,即所谓的“滑动窗口”是获得合理解决方案的正确方法。

您将找到有用的信息here。但你当然也可以在 SO 上搜索答案。


对于这个问题,我们会使用典型的 2 指针方法,但是针对这个问题的具体要求进行了修改。基本上是一个起始值和一个移动和关闭窗口。 . .

需求分析得出以下思路。

  • 如果所有计算的数字都 > 0,那么我们永远不会有 0 的总和。
  • 很容易识别重复的数字,如果它们彼此位于下方

--> 对输入值进行排序会非常有益。

这将消除对随机分布输入向量的一半值的测试。见:

std::vector<int> nums { 5, -1, 4, -2, 3, -3, -1, 2, 1, -1 };
std::sort(nums.begin(), nums.end());

// Will result in
// -3, -2, -1, -1, -1, 1, 2, 3, 4, 5

我们看到,如果我们将窗口向右移动,那么只要窗口的开始达到正数,我们就可以停止评估。此外,我们可以立即识别重复的数字。

接下来。如果我们从排序向量的开头开始,这个值很可能非常小。如果我们以当前窗口的开头加一开始下一个窗口,那么我们将得到“非常”的负数。要通过将 2 个“非常”负数相加得到 0,我们需要一个非常正数。这是std::vector的末尾。

开始

startPointerIndex 0,值 -3 窗口开始 = startPointerIndex + 1 --> 值 -2 窗口结束 = lastIndexInVector --> 5

是的,我们已经找到了解决方案。现在我们需要检查重复项。如果倒数第二个位置还有 5 个,那么我们可以跳过。它不会添加额外的不同解决方案。因此,在这种情况下,我们可以减少结束窗口指针。同样有效,如果窗口开头会有一个额外的-2。然后我们需要增加起始窗口指针,以避免从那一端重复发现。

Some 对起始指针索引有效。示例:startPointerIndex = 3(从 0 开始计数索引),值为 -1。但之前的值,在索引 2 处也是 -1。所以,没必要评价。因为我们已经评估过了。

以上方法将防止创建重复条目。

但是如何继续搜索。如果我们找不到解决方案,我们将缩小窗口。我们也会以一种聪明的方式做到这一点。如果总和太大,显然右边的窗口值太大了,我们最好使用下一个较小的值进行下一次比较。

在窗口的起始侧也是如此,如果总和很小,那么我们显然需要更大的值。所以,让我们增加开始窗口指针。我们这样做(使窗口变小)直到我们找到解决方案或直到窗口关闭,这意味着开始窗口指针不再小于结束窗口指针。

现在,我们已经开发出某种不错的设计,可以开始编码了。

我们还尝试实现良好的编码风格。并重构代码以实现更快的实现。

请看:


class Solution {
public:
    // Define some type aliases for later easier typing and understanding
    using DataType = int;
    using Triplet = std::vector<DataType>;
    using Triplets = std::vector<Triplet>;
    using TestData = std::vector<DataType>;

    // Function to identify all unique Triplets(3 elements) in a given test input
    Triplets threeSum(TestData& testData) {

        // In order to save function oeverhead for repeatingly getting the size of the test data,
        // we will store the size of the input data in a const temporary variable
        const size_t numberOfTestDataElements{ testData.size()};

        // If the given input test vector is empty, we also immediately return an empty result vector
        if (!numberOfTestDataElements) return {};

        // In later code we often need the last valid element of the input test data
        // Since indices in C++ start with 0 the value will be size -1 
        // With taht we later avoid uncessary subtractions in the loop
        const size_t numberOfTestDataElementsMinus1{ numberOfTestDataElements -1u };

        // Here we will store all the found, valid and unique triplets
        Triplets result{};

        // In order to save the time for later memory reallocations and copying tons of data, we reserve 
        // memory to hold all results only one time. This will speed upf operations by 5 to 10%
        result.reserve(numberOfTestDataElementsMinus1);

        // Now sort the input test data to be able to find an end condition, if all elements are 
        // greater than 0 and to easier identify duplicates
        std::sort(testData.begin(), testData.end());

        // This variables will define the size of the sliding window
        size_t leftStartPositionOfSlidingWindow, rightEndPositionOfSlidingWindow;

        // Now, we will evaluate all values of the input test data from left to right
        // As an optimization, we additionally define a 2nd running variable k, 
        // to avoid later additions in the loop, where i+1 woild need to be calculated.
        // This can be better done with a running variable that will be just incremented
        for (size_t i = 0, k = 1; i < numberOfTestDataElements; ++i, ++k) {

            // If the current value form the input test data is greater than 0,
            // As um with the result of 0 will no longer be possible. We can stop now
            if (testData[i] > 0) break;

            // Prevent evaluation of duplicate based in the current input test data
            if (i and (testData[i] == testData[i-1])) continue;

            // Open the window and determin start and end index
            // Start index is always the current evaluate index from the input test data
            // End index is always the last element
            leftStartPositionOfSlidingWindow = k;
            rightEndPositionOfSlidingWindow = numberOfTestDataElementsMinus1;

            // Now, as long as if the window is not closed, meaning to not narrow, we will evaluate
            while (leftStartPositionOfSlidingWindow < rightEndPositionOfSlidingWindow) {

                // Calculate the sum of the current addressed values
                const int sum = testData[i] + testData[leftStartPositionOfSlidingWindow] + testData[rightEndPositionOfSlidingWindow];

                // If the sum is t0o small, then the mall value on the left side of the sorted window is too small
                // Therefor teke next value on the left side and try again. So, make the window smaller
                if (sum < 0) {
                    ++leftStartPositionOfSlidingWindow;
                }

                // Else, if the sum is too biig, the the value on the right side of the window was too big
                // Use one smaller value. One to the left of the current closing address of the window
                //  So, make the window smaller
                else if (sum > 0) {
                    --rightEndPositionOfSlidingWindow;
                }
                else {
                    // Accodring to above condintions, we found now are triplet, fulfilling the requirements.
                    // So store this triplet as a result
                    result.push_back({ testData[i], testData[leftStartPositionOfSlidingWindow], testData[rightEndPositionOfSlidingWindow] });

                    // We know need to handle duplicates at the edges of the window. So, left and right edge
                    // For this, we remember to c
                    const DataType lastLeftValue = testData[leftStartPositionOfSlidingWindow];
                    const DataType lastRightValue = testData[rightEndPositionOfSlidingWindow];

                    // Check left edge. As long as we have duplicates here, we will shift the opening position of the window to the right
                    // Because of boolean short cut evaluation we will first do the comparison for duplicates. This will give us 5% more speed
                    while (testData[leftStartPositionOfSlidingWindow] == lastLeftValue && leftStartPositionOfSlidingWindow < rightEndPositionOfSlidingWindow)
                        ++leftStartPositionOfSlidingWindow;

                    // Check right edge. As long as we have duplicates here, we will shift the closing position of the window to the left
                    // Because of boolean short cut evaluation we will first do the comparison for duplicates. This will give us 5% more speed
                    while (testData[rightEndPositionOfSlidingWindow] == lastRightValue && leftStartPositionOfSlidingWindow < rightEndPositionOfSlidingWindow)
                        --rightEndPositionOfSlidingWindow;
                }
            }
        }
        return result;
    }
};



上述解决方案将优于 99% 的其他解决方案。我做了很多基准测试来证明这一点。

它还包含大量的 cmets 来解释那里发生了什么。如果我选择了“会说话”且有意义的变量名以便更好地理解。

希望能帮到你一点。

最后:我将这个答案献给 Sam Varshavchik 和 PaulMcKenzie。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-10-03
    • 1970-01-01
    • 2017-08-22
    • 1970-01-01
    • 2015-01-31
    • 2022-06-30
    • 2022-06-17
    相关资源
    最近更新 更多