使用(x * y)的降序枚举2D平面上的网格点

时间:2012-07-11 16:47:19

标签: algorithm math

给定N > 0M > 0,我想枚举所有(x,y)对,使得1< = x< = N并且1< = y< = M下降(x * y)的顺序。 例如:给定N = 3且M = 2,枚举序列应为:

1. (3, 2) -- 3 * 2 = 6
2. (2, 2) -- 2 * 2 = 4
3. (3, 1) -- 3 * 1 = 3
4. (2, 1) -- 2 * 1 = 2
5. (1, 2) -- 1 * 2 = 2
6. (1, 1) -- 1 * 1 = 1

可以交换(2, 1)(1, 2)的顺序。一种显而易见的方法是将它们全部列出,插入vector<pair<int, int> >,并使用我自己的比较函数调用std::sort()。但是,由于N和M可能很大,而且大多数时候我只需要序列的前几个项,我希望有一些更聪明的方法来生成这样的序列而不是生成它们全部排序,需要尽可能多的N*M数组元素。

更新:我忘了提及虽然大部分时间我只需要前几个术语,但在枚举之前所需的术语数量是未知的。

8 个答案:

答案 0 :(得分:6)

如果您只是希望节省空间,同时保持时间或多或少相等,那么您可以指望每个连续较小的元素必须相邻(在您提到的2-D网格中)到一个你已经遇到过的元素。 (你可以通过归纳来证明这一点,这并不是特别困难。我将假设其余的M> = N.)

基本算法类似于:

Start with a list (Enumerated Points) containing just the maximum element, M*N
Create a max heap (Candidates) containing (M-1),N and M,(N-1).
Repeat this:
    1.Pick the largest value (x,y) in Candidates and append it to Enumerated Points
    2.Add (x-1),y and x,(y-1) to Candidates if they are not there already

只要您想要枚举点中的更多元素,就可以重复此操作。候选人的最大人数应该是M + N,所以我认为这是O(k log(M + N)),其中k是你想要的分数。

附录: 避免重复的问题并不完全困难,但值得一提。我将在这个算法中假设你将网格放下,这样当你向下和向右移动时数字会下降。无论如何,它是这样的:

在算法开始时,创建一个数组(Column Size),每列有一个元素。您应该使此数组包含每列中的行数,这些行是枚举点列表的一部分。

添加新元素并更新此数组后,您将检查两侧列的大小,以确定此新枚举点的右侧和下方的网格正方形是否已在候选列表中

检查左侧列的大小 - 如果它大于此列,则无需在新的枚举点下方添加元素。

检查右侧列的大小 - 如果它小于此列的相同大小,则不需要更新此列右侧的元素。

为了明白这一点,让我们看看这个部分完成的图表,其中M = 4,N = 2:

4  3  2  1
*  *  *  2 |2
*  3  2  1 |1

元素(4,2),(3,2),(2,2)和(4,1)已经在列表中。 (第一个坐标为M,第二个坐标为N.)“列大小”数组为[2 1 1 0],因为这是“枚举点”列表中每列中的项数。我们将要添加(3,1)到新列表 - 我们可以查看右边的列大小并得出结论,不需要添加(2,1)因为M = 2的列的大小更大比1-1。视觉上的推理非常清晰 - 当我们添加(2,2)时,我们已经添加了(2,1)。

答案 1 :(得分:1)

这是一个O(K logK)解决方案,其中K是您要生成的术语数。
编辑:Q只保留每个元素的一个副本;如果元素已经存在,则插入失败。

priority_queue Q
Q.insert( (N*M, (N,M)) ) // priority, (x,y) 
repeat K times:
    (v, (x,y)) = Q.extract_max()
    print(v, (x,y))
    Q.insert( (x-1)*y, (x-1,y) )
    Q.insert( x*(y-1), (x,y-1) )

这是有效的,因为在访问(x,y)之前,您必须访问(x + 1,y)或(x,y + 1)。复杂性是O(KlogK),因为Q最多2K元素被推入其中。

答案 2 :(得分:0)

这实际上相当于枚举素数;你想要的数字都是不是素数的数字(除了xy等于1的所有数字)。

我不确定method of enumerating primes会比你提议的更快(至少在算法复杂性方面)。

答案 3 :(得分:0)

因为你提到大多数时候你需要序列的前几个术语;在生成它们之后,您不需要对它们进行排序以找到前几个术语。您可以根据所需的术语数量使用最大堆,例如k。因此,如果堆的大小为k(&lt;&lt; N&amp;&amp;&lt;&lt;&lt; M),那么在nlogk之后你可以拥有最大的k项,这比排序的nlogn好。

这里n = N * M

答案 4 :(得分:0)

一种虚拟方法,从NxM循环到1,搜索成对时产生当前数字的对:

#!/usr/bin/perl

my $n = 5;
my $m = 4;

for (my $p = $n * $m; $p > 0; $p--) {
    my $min_x = int(($p + $m - 1) / $m);
    for my $x ($min_x..$n) {
        if ($p % $x == 0) {
            my $y = $p / $x;
            print("x: $x, y: $y, p: $p\n");
        }
    }
}

对于N = M,复杂度为O(N 3 ),但内存使用率为O(1)。

更新:请注意,复杂性并不像看起来那么糟糕,因为要生成的元素数量已经是N 2 。为了进行比较,生成所有对和排序方法是O(N 2 logN),其中O(N 2 )内存使用。

答案 5 :(得分:0)

这是给你的算法。我会试着给你一个英文描述。

在我们正在使用的矩形中,我们总是可以假设某个点P(x, y)的“区域”比其下方的点P(x, y-1)更大。因此,当寻找最大面积点时,我们只需要比较每列中最顶端的未点(即每个可能的x)。例如,在考虑原始3 x 5网格

5 a b c
4 d e f
3 g h i
2 j k l
1 m n o
  1 2 3

我们实际上只需要比较abc。保证所有其他点的面积比至少其中一个点少。因此,构建一个仅包含每列中最高点的最大堆。当你从堆中弹出一个点时,推入它正下方的点(如果该点存在)。重复直到堆为空。这为您提供了以下算法(已测试,但它在Ruby中):

def enumerate(n, m)
    heap = MaxHeap.new
    n.times {|i| heap.push(Point.new(i+1, m))}

    until(heap.empty?)
        max = heap.pop
        puts "#{max} : #{max.area}"

        if(max.y > 1)
            max.y -= 1
            heap.push(max)
        end
    end
end

这使您的运行时间为O((2k + N) log N)。堆操作成本log N;我们在构建初始堆时执行N,然后在我们拉出2k最大区域点时k(假设每个弹出点后面跟着下面的点,则为{2}}它)。

它具有额外的优势,无需构建所有积分,这与构建整个集合然后排序的原始提议不同。您只需构建尽可能多的点来保持堆的准确性。

最后:可以进行改进!我没有做过这些,但是通过以下调整你可以获得更好的性能:

  1. 仅下降到每列中的y = x而不是y = 1。要生成您不再检查的点,请使用P(x, y)区域等于P(y, x)区域的事实。 注意:如果您使用此方法,则需要两个版本的算法。列在M >= N时有效,但如果M < N您需要按行执行此操作。
  2. 仅考虑可能包含最大值的列。在我给出的示例中,没有理由从一开始就在堆中包含a,因为它保证小于b。因此,只需在弹出邻居列的顶点时向列中添加列。
  3. 这变成了一篇小文章......无论如何 - 希望它有所帮助!

    编辑:完整的算法,包含我上面提到的两项改进(但仍然在Ruby中,因为我很懒)。请注意,不需要任何额外的结构来避免插入重复项 - 除非它是一个“顶部”点,每个点只会在它的行/列中插入另一个点。

    def enumerate(n, m, k)
        heap = MaxHeap.new
        heap.push(Point.new(n, m))
        result = []
    
        loop do
            max = heap.pop
            result << max
            return result if result.length == k
    
            result << Point.new(max.y, max.x) if max.x <= m && max.y <= n && max.x != max.y
            return result if result.length == k
    
            if(m < n) # One point per row
                heap.push(Point.new(max.x, max.y - 1)) if max.x == n && max.y > 1
                heap.push(Point.new(max.x - 1, max.y)) if max.x > max.y
            else # One point per column
                heap.push(Point.new(max.x - 1, max.y)) if max.y == m && max.x > 1
                heap.push(Point.new(max.x, max.y - 1)) if max.y > max.x
            end
        end
    end
    

答案 6 :(得分:0)

我明白了!

将网格视为一组M列,其中每列都是一个堆栈,其中包含从底部的1到顶部的N的元素。每列都标有x坐标。

每个列堆栈中的元素按其y值排序,因此也按x * y排序,因为x对所有列都具有相同的值。

所以,你只需要选择顶部有较大x * y值的堆栈,弹出并重复。

实际上,您不需要堆栈,只需要顶部值的索引,您可以使用优先级队列来获取具有更大x * y值的列。然后,递减索引的值,如果它大于0(表示堆栈尚未耗尽),则将堆栈重新插入具有新优先级x * y的队列。

该算法对N = M的复杂度为O(N 2 logN)及其内存使用量O(N)。

更新:在Perl中实施...

use Heap::Simple;

my ($m, $n) = @ARGV;

my $h = Heap::Simple->new(order => '>', elements => [Hash => 'xy']);
# The elements in the heap are hashes and the priority is in the slot 'xy':

for my $x (1..$m) {
    $h->insert({ x => $x, y => $n, xy => $x * $n });
}

while (defined (my $col = $h->extract_first)) {
    print "x: $col->{x}, y: $col->{y}, xy: $col->{xy}\n";
    if (--$col->{y}) {
        $col->{xy} = $col->{x} * $col->{y};
        $h->insert($col);
    }
}

答案 7 :(得分:0)

在Haskell中,它立即生成输出 。这是一个例子:

         -------
        -*------
       -**------
      -***------
     -****------
    -*****------
   -******------
  -*******------

每个加星标的点都会产生(x,y)和(y,x)。算法从右上角“吃掉”这个东西,比较每列中的顶部元素。边界的长度绝不会超过N(我们假设为N >= M)。

enuNM n m | n<m = enuNM m n                    -- make sure N >= M
enuNM n m = let
    b = [ [ (x*y,(x,y)) | y<- [m,m-1..1]] | x<-[n,n-1..m+1]]
    a = [ (x*x,(x,x)) : 
          concat [ [(z,(x,y)),(z,(y,x))]       -- two symmetrical pairs,
                           | y<- [x-1,x-2..1]  --  below and above the diagonal
                           , let z=x*y  ] | x<-[m,m-1..1]]
 in
    foldi (\(x:xs) ys-> x : merge xs ys) [] (b ++ a)

merge a@(x:xs) b@(y:ys) = if (fst y) > (fst x) 
                            then  y : merge  a ys 
                            else  x : merge  xs b
merge a [] = a 
merge [] b = b

foldi f z []     = z
foldi f z (x:xs) = f x (foldi f z (pairs f xs))

pairs f (x:y:t)  = f x y : pairs f t
pairs f t        = t

foldi构建一个倾斜的无限深化树作为堆,连接所有生成器流,每个生成器流为每个x创建,这些生成器流已经按降序排序。由于生产者流的所有初始值都保证按递减顺序排列,因此可以在不进行比较的情况下弹出每个初始值,从而允许延迟构建树。

a的代码使用对角线下方的对应对(假设为N >= M)生成对角线以上的对,对于(x,y) x <= M & y < x(y,x)也将被制作出来。)

对于产生的少数第一个值中的每一个,它实际上应该是O(1),这些值非常接近比较树的顶部。

Prelude Main> take 10 $ map snd $ enuNM (2000) (3000)
[(3000,2000),(2999,2000),(3000,1999),(2998,2000),(2999,1999),(3000,1998),(2997,2
000),(2998,1999),(2999,1998),(2996,2000)]
(0.01 secs, 1045144 bytes)

Prelude Main> let xs=take 10 $ map (log.fromIntegral.fst) $ enuNM (2000) (3000)
Prelude Main> zipWith (>=) xs (tail xs)
[True,True,True,True,True,True,True,True,True]

Prelude Main> take 10 $ map snd $ enuNM (2*10^8) (3*10^8)
[(300000000,200000000),(299999999,200000000),(300000000,199999999),(299999998,20
0000000),(299999999,199999999),(300000000,199999998),(299999997,200000000),(2999
99998,199999999),(299999999,199999998),(299999996,200000000)]
(0.01 secs, 2094232 bytes)

我们可以评估经验运行时复杂性:

Prelude Main> take 10 $ drop 50000 $ map (log.fromIntegral.fst) $ enuNM (2*10^8)
 (3*10^8)
[38.633119670465554,38.633119670465554,38.63311967046555,38.63311967046554,38.63
311967046554,38.63311967046553,38.63311967046553,38.633119670465526,38.633119670
465526,38.63311967046552]
(0.17 secs, 35425848 bytes)

Prelude Main> take 10 $ drop 100000 $ map (log.fromIntegral.fst) $ enuNM (2*10^8
) (3*10^8)
[38.63311913546512,38.633119135465115,38.633119135465115,38.63311913546511,38.63
311913546511,38.6331191354651,38.6331191354651,38.633119135465094,38.63311913546
5094,38.63311913546509]
(0.36 secs, 71346352 bytes)

*Main> let x=it
*Main> zipWith (>=) x (tail x)
[True,True,True,True,True,True,True,True,True]

Prelude Main> logBase 2 (0.36/0.17)
1.082462160191973     -- O(n^1.08) for n=100000 values produced

这可以翻译成例如Python使用生成器以明确的方式为Haskell流提供here