繁体   English   中英

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

[英]Enumerate grid points on 2D plane with descending order of (x * y)

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

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

如果您只是希望节省空间,同时保持时间大致相等,那么您可以指望每个连续较小的元素必须与其中一个元素相邻(在您提到的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是你想要的点数。

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

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

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

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

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

为了使这一点显而易见,让我们看看这个部分完成的图表,其中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)。

这是一个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元素被推入其中。

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

我不确定是否有一种枚举素数方法比你已经提出的更快(至少在算法复杂度方面)。

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

这里n = N * M.

一种虚拟方法,从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 )存储器使用。

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

在我们正在使用的矩形中,我们总是可以假设点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 ,然后在我们拉出最大区域的k个点时执行2k (假设每个pop后面跟着它下面的点,则为2)。

它的另一个优点是不需要构建所有的点,不像建立整个集合然后排序的原始提议。 您只需构建尽可能多的点来保持堆的准确性。

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

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

这变成了一篇小文章......无论如何 - 希望它有所帮助!

编辑:完整的算法,包含我上面提到的两个改进(但仍然在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

我知道了!

将网格视为一组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);
    }
}

在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     

这可以通过使用发电机哈斯克尔在strightforward地流可以看出被翻译成如Python的位置

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM