如何找到小于N的所有出租车数字?

时间:2012-09-03 07:22:42

标签: algorithm

出租车编号是一个整数,可以用两种不同的方式表示为两个整数立方体的总和:a^3+b^3 = c^3+d^3。设计一种算法,找出a,b,c和d小于N的所有出租车编号。

请以N为单位给出空间和时间复杂度。 我可以在o(N^2.logN)空间O(N^2)时间内完成此操作。

到目前为止我发现的最佳算法:

形成所有对:N^2
对总和进行排序:N^2 logN
查找小于N的重复项

但这需要N^2个空格。我们可以做得更好吗?

9 个答案:

答案 0 :(得分:18)

  

但这需要N ^ 2个空间。我们可以做得更好吗?

存在基于优先级队列 O(N)空间解决方案。时间复杂度 O(N ^ 2 logN)。为了勾勒出算法的思想,这里是矩阵M,使得M [i] [j] = i ^ 3 + j ^ 3(当然,矩阵永远不会在内存中创建) :

0 1 8 27 64 125 1 2 9 28 65 126 8 9 16 35 72 133 27 28 35 54 91 152 64 65 72 91 128 189 125 126 133 152 189 250
观察每行和每行按升序排序。让PQ成为优先级队列。首先,我们将最大元素放在优先级队列中。然后执行以下操作,只要PQ不为空:

  1. 弹出PQ中最大的元素
  2. 如果PQ没有该行中的任何元素,则在上面添加adajcent元素
  3. 如果PQ没有该列中的任何元素,并且它不在矩阵的对角线下(以避免冗余元素),则在左侧添加adajcent元素
  4. 请注意

    1. 您不需要在内存中创建矩阵来实现算法
    2. 元素将以降序从PQ中弹出,从矩阵的最大元素到最小元素(避免矩阵冗余半部分的元素)。
    3. 每当PQ发出相同的值两次,我们就会找到一个出租车编号。

      作为一个例子,这是一个C ++实现。 时间复杂度为O(N ^ 2 logN)空间复杂度O(N)

      #include <iostream>
      #include <cassert>
      #include <queue>
      
      using namespace std;
      
      typedef unsigned int value_type;
      
      struct Square
      {
         value_type i;
         value_type j;
         value_type sum_of_cubes;
         Square(value_type i, value_type j) : i(i), j(j), sum_of_cubes(i*i*i+j*j*j) {}
         friend class SquareCompare;
         bool taxicab(const Square& sq) const
         {
           return sum_of_cubes == sq.sum_of_cubes && i != sq.i && i != sq.j;
         }
         friend ostream& operator<<(ostream& os, const Square& sq);
      };
      
      class SquareCompare
      {
      public:
        bool operator()(const Square& a, const Square& b)
        {
          return a.sum_of_cubes < b.sum_of_cubes;
        }
      };
      
      ostream& operator<<(ostream& os, const Square& sq)
      {
        return os << sq.i << "^3 + " << sq.j << "^3 = " << sq.sum_of_cubes;
      }
      
      int main()
      {
        const value_type N=2001;
        value_type count = 0;
        bool in_i [N];
        bool in_j [N];
      
        for (value_type i=0; i<N; i++) {
          in_i[i] = false;
          in_j[i] = false;
        }
      
        priority_queue<Square, vector<Square>, SquareCompare> p_queue;
      
        p_queue.push(Square(N-1, N-1));
        in_i[N-1] = true;
        in_j[N-1] = true;
      
        while(!p_queue.empty()) {
          Square sq = p_queue.top();
      
          p_queue.pop();
          in_i[sq.i] = false;
          in_j[sq.j] = false;
      
          // cout << "pop " << sq.i << " " << sq.j << endl;
      
          if (sq.i > 0 && !in_i[sq.i - 1] && sq.i-1 >= sq.j) {
            p_queue.push(Square(sq.i-1, sq.j));
            in_i[sq.i-1] = true;
            in_j[sq.j] = true;
            // cout << "push " << sq.i-1 << " " << sq.j << endl;
          }
          if (sq.j > 0 && !in_j[sq.j-1] && sq.i >= sq.j - 1) {
            p_queue.push(Square(sq.i, sq.j-1));
            in_i[sq.i] = true;
            in_j[sq.j - 1] = true;
            // cout << "push " << sq.i << " " << sq.j-1 << endl;
          }
      
          if (sq.taxicab(p_queue.top())) {
            /* taxicab number */
            cout << sq << " " << p_queue.top() << endl;
            count++;
          }
        }
        cout << endl;
      
        cout << "there are " << count << " taxicab numbers with a, b, c, d < " << N << endl;
      
        return 0;
      }
      

答案 1 :(得分:7)

算法的时间复杂度在任何情况下都不能小于 O(N 2 ,因为您可能会打印到 O(N 2 出租车编号。

为减少空间使用,理论上可以使用此处提到的建议:little link。基本上,我们的想法是,首先尝试所有可能的对 a,b 并找到解决方案:

  

a = 1 - (p - 3 * q)(p 2 + 3 * q 2

     

b = -1 +(p + 3 * q)(p 2 + 3q 2

然后您可以使用以下方法找到相应的 c,d

  

c =(p + 3 * q) - (p 2 + 3 * q 2

     

d = - (p - 3 * q)+(p 2 + 3 * q 2

并检查它们是否都小于N.这里的问题是解决这个方程组可能会有点混乱('有点'我的意思是非常乏味)。

O(N 2 空间解决方案要简单得多,并且它可能足够有效,因为任何可以在合理时间限制内运行的二次时间复杂度二次空间使用可能会很好。

我希望有所帮助!

答案 2 :(得分:7)

Novneet Nov和user3017842给出的答案都是使用minHeap查找具有存储O(N)的出租车编号的正确方法。 再简单解释为什么N大小的minHeap有效。 首先,如果你有所有的总和(O(N ^ 2))并且可以对它们进行排序(O(N ^ 2lgN)),你只需在遍历排序的数组时选择重复项。好吧,在我们使用minHeap的情况下,我们可以按顺序遍历所有总和:我们只需要确保minHeap始终包含最小的未处理总和。

现在,我们有大量的金额(O(N ^ 2))。但是,请注意,这个数字可以分成N组,每组都有一个容易定义的最小值! (修复a,从b =&gt;更改0 to N-1这里是您的N群组。一个群组中b小于1的总和小于1在同一组中使用较大的b - 因为a是相同的)。

  

这些群体的最小结合是这些群体的联合   组。因此,如果你保留这些组的所有最小值   minHeap你保证在minHeap中有最小值。

现在,当您从堆中提取Min时,只需添加此提取的min组中的下一个最小元素(因此,如果您提取(a, b),则添加(a, b+1))并保证您的minHeap仍然包含所有总和的下一个未处理的最小值。

答案 3 :(得分:6)

我在这里找到了解决方案/代码:Time complexity O(N^2 logN), space complexity O(N) 该解决方案是通过优先级队列实现的。

通过查看代码可以轻松完成反向思考。它可以在大小为N的数组中完成,因为在与下一个最小值进行比较后从数组中删除了最小值,然后通过添加新的总和使数组大小为N - (i ^ 3 +(j + 1) ^ 3)。

这里有一个直观的证据:

最初,我们在最小优先级队列中添加了(1,1),(2,2),(3,3),...,(N,N)。

假设^ + b ^ 3 = c ^ 3 + d ^ 3,并且(a,b)是接下来将从优先级队列中取出的最小值。为了能够检测这个出租车编号,(c,d)也必须在(a,b)之后取出的优先级队列中。

注意:我们将在提取(a,b)后添加(a,b + 1),因此无法提取(a,b)会导致(c,d)添加到优先级队列,因此它必须已存在于优先级队列中。

现在让我们假设(c,d)不在优先级队列中,因为我们还没有完成它。相反,在优先级队列中存在一些(c,d-k),其中k> 0。

由于(a,b)被取出, 一个^ 3 + B ^3≤c^ 3 +(d-K)^ 3

然而,a ^ 3 + b ^ 3 = c ^ 3 + d ^ 3

因此,

C 1-6 3 + d ^3≤c^ 3 +(d-K)^ 3 d≤d-K k≤0

由于k> 0,这是不可能的。因此,我们的假设永远不会成真。 因此,对于从min-PQ中移除的每个(a,b),如果a ^ 3 + b ^ 3 = c ^ 3 + d,则(c,d)已经在min-PQ中(或者刚被移除) ^ 3

答案 4 :(得分:1)

  1. 创建一个数组:1 ^ 3,2 ^ 3,3 ^ 3,4 ^ 3,....... k ^ 3。这样k ^ 3 < N和(k + 1)^ 3> N.数组大小为〜(N)^(1/3)。数组按顺序排列。
  2. 在与阵列大小成比例的线性时间中使用2sum技术(link)。如果我们找到2对数字,那就是命中。
  3. 通过每次减少N来循环到步骤2。
  4. 这将使用O(N ^(1/3))额外空间和~O(N ^(4/3))时间。

答案 5 :(得分:0)

理解Time complexity O(N^2 logN), space complexity O(N)的一种简单方法是将其视为"".*"" return TOK_STRING; 排序数组的合并以及之前合并元素的簿记。

答案 6 :(得分:0)

version1使用List和排序
(n ^ 2 * logn)时间和O(n ^ 2)空间

    public static void Taxicab1(int n)
    {
        // O(n^2) time and O(n^2) space
        var list = new List<int>();
        for (int i = 1; i <= n; i++)
        {
            for (int j = i; j <= n; j++)
            {
                list.Add(i * i * i + j * j * j);
            }
        }

        // O(n^2*log(n^2)) time
        list.Sort();

        // O(n^2) time
        int prev = -1;
        foreach (var next in list)
        {
            if (prev == next)
            {
                Console.WriteLine(prev);
            }
            prev = next;
        }
    }

version2使用HashSet
(n ^ 2)时间和O(n ^ 2)空间

    public static void Taxicab2(int n)
    {
        // O(n^2) time and O(n^2) space
        var set = new HashSet<int>();
        for (int i = 1; i <= n; i++)
        {
            for (int j = i; j <= n; j++)
            {
                int x = i * i * i + j * j * j;
                if (!set.Add(x))
                {
                    Console.WriteLine(x);
                }
            }
        }
    }

版本3使用面向最小的Priority Queue
(n ^ 2 * logn)时间和O(n)空间

    public static void Taxicab3(int n)
    {
        // O(n) time and O(n) space
        var pq = new MinPQ<SumOfCubes>();
        for (int i = 1; i <= n; i++)
        {
            pq.Push(new SumOfCubes(i, i));
        }

        // O(n^2*logn) time
        var sentinel = new SumOfCubes(0, 0);
        while (pq.Count > 0)
        {
            var current = pq.Pop();

            if (current.Result == sentinel.Result)
                Console.WriteLine($"{sentinel.A}^3+{sentinel.B}^3 = {current.A}^3+{current.B}^3 = {current.Result}");

            if (current.B <= n)
                pq.Push(new SumOfCubes(current.A, current.B + 1));

            sentinel = current;
        }
    }

其中SummOfCubes

public class SumOfCubes : IComparable<SumOfCubes>
{
    public int A { get; private set; }
    public int B { get; private set; }
    public int Result { get; private set; }

    public SumOfCubes(int a, int b)
    {
        A = a;
        B = b;
        Result = a * a * a + b * b * b;
    }

    public int CompareTo(SumOfCubes other)
    {
        return Result.CompareTo(other.Result);
    }
}

github

答案 7 :(得分:0)

似乎是一种简单的带有适当范围的蛮力算法,可以按时间与 n ^ 1.33 成比例,而空间与 n 成比例。还是有人可以将我指向我误会的地方?

考虑4个嵌套循环,每个循环从1到n的立方根。使用这些循环,我们可以遍历4个值的所有可能组合,并找到形成出租车编号的对。这意味着每个循环所花的时间都与 n的立方根 n ^(1/3)成比例。将该值乘以4倍即可得到:

(n^(1/3)^4 = n^(4/3) = n^1.33

我用JavaScript编写了一个解决方案并对其进行了基准测试,它似乎正在工作。一个警告是,结果仅被部分排序

这是我的JavaScript代码(尚不是最佳代码,可以进一步优化):

function taxicab(n) {
  let a = 1, b = 1, c = 1, d = 1,
  cubeA = a**3 + b**3,
  cubeB = c**3 + d**3,
  results = [];

  while (cubeA < n) { // loop over a
    while (cubeA < n) { // loop over b
      // avoid running nested loops if this number is already in results
      if (results.indexOf(cubeA) === -1) {
       while (cubeB <= cubeA) { // loop over c
        while (cubeB <= cubeA) { // loop over d
          if (cubeB === cubeA && a!=c && a!=d) { // found a taxicab number!
            results.push(cubeA);
          }
          d++;
          cubeB = c**3 + d**3;
        } // end loop over d
        c++;
        d = c;
        cubeB = c**3 + d**3;
       } // end loop over c
      }
      b++;
      cubeA = a**3 + b**3;
      c = d = 1;
      cubeB = c**3 + d**3;
    } // end loop over d
    a++;
    b = a;
    cubeA = a**3 + b**3;
  } // end loop over a

  return results;
}

在浏览器控制台中运行taxicab(1E8)大约需要30秒,因此会产生485个数字。较小的值taxicab(1E7)的十倍(一千万)花费了将近1.4秒并产生了150个数字。 10^1.33 * 1.4 = 29.9,即将 n 乘以 10 导致运行时间增加了 10 ^ 1.33 倍。结果数组未排序,但是在快速排序之后,我们得到了正确的结果,如下所示:

[1729, 4104, 13832, 20683, 32832, 39312, 40033, 46683, 64232, 65728,
110656, 110808, 134379, 149389, 165464, 171288, 195841, 216027, 216125,
262656, 314496, 320264, 327763, 373464, 402597, 439101, 443889, 513000, 
513856, 515375, 525824, 558441, 593047, 684019, 704977, 805688, 842751, 
885248, 886464, 920673, 955016, 984067, 994688, 1009736, 1016496, 1061424,
1073375, 1075032, 1080891, 1092728, 1195112, 1260441, 1323712, 1331064,
1370304, 1407672, 1533357, 1566728, 1609272, 1728216, 1729000, 1734264,
1774656, 1845649, 2048391, 2101248, 2301299, 2418271, 2515968, 2562112,
2585375, 2622104, 2691451, 2864288, 2987712, 2991816, 3220776, 3242197,
3375001, 3375008, 3511872, 3512808, 3551112, 3587409, 3628233, 3798613,
3813992, 4033503, 4104000, 4110848, 4123000, 4174281, 4206592, 4342914,
4467528, 4505949, 4511808, 4607064, 4624776, 4673088, …]

这是基准测试的代码:

// run taxicab(n) for k trials and return the average running time
function benchmark(n, k) {
  let t = 0;
  k = k || 1; // how many times to repeat the trial to get an averaged result

  for(let i = 0; i < k; i++) {
    let t1 = new Date();
    taxicab(n);
    let t2 = new Date();
    t += t2 - t1;
  }
  return Math.round(t/k);
}

最后,我对其进行了测试:

let T = benchmark(1E7, 3); // 1376 - running time for n = 10 million
let T2 = benchmark(2E7, 3);// 4821 - running time for n = 20 million
let powerLaw = Math.log2(T2/T); // 1.3206693816701993

因此,在此测试中,时间与 n ^ 1.32 成正比。以不同的值重复多次,总是会得到相同的结果:从 1.3 1.4

答案 8 :(得分:0)

首先,我们将构建出租车号码,而不是搜索它们。我们将用于构建出租车号码的范围,即 Ta(2) 将上升到 n^1/3 而不是 n。因为如果你对一个大于 n^1/3 的数字进行立方,它会比 n 大,而且我们也不能对负数进行立方以防止 definition 出现这种情况。我们将使用 HashSet 来记住算法中两个立方数的和。这将帮助我们在 O(1) 时间内查找先前的立方和,同时我们在我之前提到的范围内迭代每个可能的数字对。

时间复杂度:O(n^2/3)

空间复杂度:O(n^1/3)

def taxicab_numbers(n: int) -> list[int]:
    taxicab_numbers = []
    max_num = math.floor(n ** (1. / 3.))
    seen_sums = set()
    for i in range(1, max_num + 1):
        for j in range(i, max_num + 1):
            cube_sum = i ** 3 + j ** 3
            if cube_sum in seen_sums:
                taxicab_numbers.append(cube_sum)
            else:
                seen_sums.add(cube_sum)
    return taxicab_numbers