【问题标题】:How can you find the cuboid with the greatest volume in a heightmap? (with low complexity)如何在高度图中找到体积最大的长方体? (复杂度低)
【发布时间】:2020-05-19 06:56:36
【问题描述】:

我需要找到体积最大的长方体,包含在 2D 高度图中。 高度图是一个大小为w*d 的数组,其中w 是宽度,h 是高度,d 是深度。 在 C 中,这看起来是这样的:

unsigned heightmap[w][d]; // all values are <= h

我已经知道有一个简单的算法可以用O(w*d*h) 复杂度解决这个问题。 但是,我怀疑那里有更优化的方法。 它的工作原理如下,在 pythonic 伪代码中:

resultRectangle = None
resultHeight = None
resultVolume = -1

# iterate over all heights
for loopHeight in range(0, h):
    # create a 2D bitmap from our heightmap where a 1 represents a height >= loopHeight
    bool bitmap[w][d]
    for x in range(0, w):
        for y in range(0, d):
            bitmap[x][y] = heightmap[x][y] >= loopHeight

    # obtain the greatest-volume cuboid at this particular height
    maxRectangle = maxRectangleInBitmap(bitmap)
    volume = maxRectangle.area() * loopHeight

    # compare it to our current maximum and replace it if we found a greater cuboid
    if volume > resultVolume:
        resultHeight = loopHeight
        resultVolume = volume
        resultRectangle = maxRectangle

resultCuboid = resultRectangle.withHeight(resultHeight)

在矩形中找到所有1 的最大面积是一个已知问题,O(1) 每像素复杂度或在我们的例子中为O(w*d)。 因此,朴素方法的总复杂度为O(w*h*d)

正如我已经说过的,我想知道我们是否可以克服这种复杂性。 也许我们可以通过更智能地搜索高度而不是“暴力破解”所有高度来将其归结为O(w*d * log(h))

Evgeny Kluev 对这个问题Find largest cuboid containing only 1's in an NxNxN binary array 的回答似乎采取了类似的方法,但它错误地(?)假设我们在这些高度处找到的体积形成单峰函数。 如果是这种情况,我们可以使用黄金分割搜索来更智能地选择高度,但我认为我们不能。

【问题讨论】:

  • 如果高度真的很高,您可以在 sqrt(h) 上进行迭代,这将为您提供 (i) 解决方案的下限和上限以及 (ii) 高度部分的子集(每个大小sqrt(h)) 来检查“天真”。在最坏的情况下,复杂性保持不变,但在实践中,运行速度肯定会稍微快一些。
  • @m.raynal 我不完全确定你的意思。你的意思是省略sqrt(h)上面的数据吗?这可能不会导致最佳解决方案。最好的解决方案可以在任何给定的高度找到,除非它遵循我还不知道的模式。高度图可能是完全平坦的并且高度为 1,这使得位于 h=1 的平面成为最佳解决方案。但它也可能是 1x1 列,使最大体积成为最大高度。因此,如果有一种方法可以省略高度,那不能只是随意跳过高度。
  • 我的意思是在两个高度h1 &lt; h2之间找到的最大长方体肯定小于w1*d1*h2,其中w1*d1代表1在高度h1处的最大面积,并且肯定至少与w1*d1*h1 一样大。这使您可以限制最佳解决方案并丢弃一些潜在的高度(即使在最坏的情况下您最终会迭代所有可能的高度)。我的意思是,您可以按如下方式迭代h,而不是迭代所有可能的值:for h=0; h&lt;=H; h += sqrt(H)
  • @m.raynal 你对上限是正确的,这将允许你在从上到下迭代时提前取消。但是,下限不是w1*d1*h1,因为位图是由高度为&gt;= loopHeight 的所有列构成的。更少的柱子满足更高的要求,因此在高度上升时可以找到更小的体积。考虑一个具有非常宽的底部和从其延伸的 1x1 柱的形状。当增加高度时,可能没有一个底座满足要求,最大的长方体只是这个柱子的一部分,因此体积很小。
  • 我不关注。您能否帮助我理解为什么不只遍历O(n^2) 中高度图中给出的实际高度(即遍历w*d)并检查w*d*map[w][d] 是否大于全局最大值?跨度>

标签: algorithm multidimensional-array 3d voxel heightmap


【解决方案1】:

这是一个想法,有一个重要的假设。伪代码:

P <- points from heightmap sorted by increasing height.
R <- set of rectangles. All maximal empty sub-rectangles for the current height.
R.add(Rectangle(0,0,W,H)
result = last_point_in(P).height()
foreach(p in P):
   RR <- rectangles from R that overlap P (can be found in O(size(RR)), possibly with some logarithmic factors)
   R = R - RR
   foreach(r in RR)
       result = max(result, r.area() * p.height())
       split up r, adding O(1) new rectangles to R.
return result

我有直觉但无法证明的假设是,RR 的平均大小为 O(1)。

编辑:澄清“分裂”,如果我们在 p 点分裂:

AAAAADFFF
AAAAADFFF
AAAAADFFF
BBBBBpGGG
CCCCCEHHH
CCCCCEHHH

我们生成新的矩形,包括: ABC、CEH、FGH、ADF,并将它们添加到 R。

【讨论】:

  • 有些东西我不明白。 R 是否已经用子矩形初始化或者它开始为空?另外RR 的大小到底是多少,你是如何找到这些矩形的?在这种情况下,复杂性如何降低?您需要对所有w*d 点进行排序,这样就是O(w*d*log(w*d)),这很好。 R = R - RR 是什么意思?沿着哪个轴“拆分 r”是什么意思?结果应该是一组坐标,而不仅仅是高度,那你怎么处理呢?
  • 开始为空。 R = R-RR 与 R.removeAll(RR) 相同。要“拆分”,假设您有一个空矩形 r 和一个高点 p。现在,您最多需要将 4 个空(重叠)矩形添加到 R 中。
  • 基本上你像在你的代码中那样循环高度,但是你动态生成像你的“位图”这样的东西,它包含在 R 中(R 必须像四叉树一样有效地找到“矩形R 与 P") 重叠。
  • 如果您以四叉树模式分割矩形,这如何解释那些不与树形对齐的矩形?例如,一个包含少量AFCH 的矩形可能是最大长方体的底边。如果您在所有高度上进行迭代,复杂度仍然是O(w*d*h),这似乎并不比简单算法更好。还是我看错了?
【解决方案2】:

好的,再来一次。大多数“肉”在go 函数中。它使用与我的其他答案相同的“拆分”概念,但使用自上而下的动态编程和记忆。 rmq2d 实现二维范围最小查询。对于 1000x1000 大小,大约需要 30 秒(同时使用 3GB 内存)。

#include <iostream>
#include <vector>
#include <cassert>
#include <set>
#include <tuple>
#include <memory.h>
#include <limits.h>

using namespace std;

constexpr int ilog2(int x){
    return 31 - __builtin_clz(x);
}

const int MAX_DIM = 100;




template<class T>
struct rmq2d{
    struct point{
        int x,y;

        point():x(0),y(0){}
        point(int x,int y):x(x),y(y){}
    };
    typedef point array_t[MAX_DIM][ilog2(MAX_DIM)+1][MAX_DIM];

    int h, logh;
    int w, logw;
    vector<vector<T>> v;

    array_t *A;

    rmq2d(){A=nullptr;}

    rmq2d &operator=(const rmq2d &other){
        assert(sizeof(point)==8);
        if(this == &other) return *this;
        if(!A){
            A = new array_t[ilog2(MAX_DIM)+1];
        }
        v=other.v;
        h=other.h;
        logh = other.logh;
        w=other.w;
        logw=other.logw;
        memcpy(A, other.A, (ilog2(MAX_DIM)+1)*sizeof(array_t));
        return *this;
    }

    rmq2d(const rmq2d &other){
        A = nullptr;
        *this = other;
    }

    ~rmq2d(){
        delete[] A;
    }


    T query(point pos){
        return v[pos.y][pos.x];
    }

    rmq2d(vector<vector<T>> &v) : v(v){
        A = new array_t[ilog2(MAX_DIM)+1];
        h = (int)v.size();
        logh = ilog2(h) + 1;
        w = (int)v[0].size();
        logw = ilog2(w) + 1;

        for(int y=0; y<h; ++y){
            for(int x=0;x<w;x++) A[0][y][0][x] = {x, y};

            for(int jx=1; jx<logw; jx++){
                int sz = 1<<(jx-1);
                for(int x=0; x+sz < w; x++){
                    point i1 = A[0][y][jx-1][x];
                    point i2 = A[0][y][jx-1][x+sz];
                    if(query(i1) < query(i2)){
                        A[0][y][jx][x] = i1;
                    }else{
                        A[0][y][jx][x] = i2;
                    }
                }
            }
        }
        for(int jy=1; jy<logh; ++jy){
            int sz = 1<<(jy-1);
            for(int y=0; y+sz<h; ++y){
                for(int jx=0; jx<logw; ++jx){
                    for(int x=0; x<w; ++x){
                        point i1 = A[jy-1][y][jx][x];
                        point i2 = A[jy-1][y+sz][jx][x];
                        if(query(i1) < query(i2)){
                            A[jy][y][jx][x] = i1;
                        }else{
                            A[jy][y][jx][x] = i2;
                        }

                    }
                }
            }
        }
    }

    point pos_q(int x1, int x2, int y1, int y2){
        assert(A);
        int lenx = ilog2(x2 - x1);
        int leny = ilog2(y2 - y1);

        point idxs[] = {
                 A[leny][y1][lenx][x1],
                 A[leny][y2-(1<<leny)][lenx][x1],
                 A[leny][y1][lenx][x2-(1<<lenx)],
                 A[leny][y2-(1<<leny)][lenx][x2-(1<<lenx)]
                };
        point ret = idxs[0];
        for(int i=1; i<4; ++i){
            if(query(ret) > query(idxs[i])) ret = idxs[i];
        }
        return ret;

    }

    T val_q(int x1, int x2, int y1, int y2){
        point pos = pos_q(x1,x2,y1,y2);
        return v[pos.y][pos.x];
    }
};

rmq2d<long long> rmq;

set<tuple<int, int, int ,int>> cac;
vector<vector<long long>> v(MAX_DIM-5,vector<long long>(MAX_DIM-5,0));

long long ret = 0;
int nq = 0;

void go(int x1, int x2, int y1, int y2){
    if(x1 >= x2 || y1>=y2) return;

    if(!cac.insert(make_tuple(x1,y1,x2,y2)).second) return;
    ++nq;

    auto p = rmq.pos_q(x1, x2, y1, y2);
    long long cur = v[p.y][p.x]*(x2-x1)*(y2-y1);
    if(cur > ret){
        cout << x1 << "-" << x2 << ", " << y1 << "-" << y2 << " h=" << v[p.y][p.x] <<  " :" << cur << endl;
        ret = cur;
    }

    go(p.x+1, x2, y1, y2);
    go(x1, p.x, y1, y2);
    go(x1, x2, p.y+1, y2);
    go(x1, x2, y1, p.y);
}


int main(){
    int W = (int)v[0].size();
    int H=(int)v.size();
    for(int y=0; y<H;++y){
        for(int x=0; x<W; ++x){
            v[y][x] = rand()%10000;
        }
    }
    rmq = rmq2d<long long>(v);
    go(0,W, 0, H);
    cout << "nq:" << nq << endl;
}

【讨论】:

  • 说实话,这有点难以解释,部分原因是变量在很多情况下都是缩写。所以ret 是您的返回值,即音量。当您设置long long cur = v[p.y][p.x]*(x2-x1)*(y2-y1); 时,这假定当前矩形和点的高度包含一个填充的长方体。但是算法如何确保这个矩形中的所有点的高度至少为v[p.y][p.x]?此外,您仍然在go() 中沿 x 轴和沿 y 轴拆分所有内容,那么如何查询所有可能的矩形?
  • 澄清之前的评论。当您在高度图中查询点p 以获取高度h 并选择宽度w 和深度d;生成的长方体的体积可能为w*h*d。但是,只有当i &lt; w, j &lt; d 的每个点p + (i, j) 的高度为h_ij &gt;= h 时,才会出现这种情况。否则它不是一个完全填充的长方体。而且我看不出算法是如何解释这一点的。
  • @J.Schultke 这就是范围最小查询的魔力——rmq 变量。 pos_q 在恒定时间内找到矩形内的最小值。
  • 你的算法对我不起作用。一个重要的问题是lenx 是用独占距离计算的,而不是包含距离。它应该是x2 - x1 + 1,因为同一位置的两个点仍然构成一个1x1x1 长方体,因此具有体积。我已经解决了这个问题,但它会导致下溢问题。与愚蠢算法的实现相比,它显然要快得多,但也找不到最佳结果:。这是我的代码,可能我所做的修改导致了一些问题:gist.github.com/Eisenwave/55b73bdac52f88135a12a468c6447569
  • 我也对分割矩形的类四叉树模式感到担忧。首先,在最低点分裂是什么意思?由于高度图完全是随机噪声,因此在该点拆分似乎与在任何其他点拆分一样好。此外,当您找到最低点时,它将是子矩形中的最小值。所以下半部分也将使用相同的点,因为它仍然是最小值和下半部分,等等,递归。如果您的全局唯一最小值位于 100x100 映射中的 (99,99),则您的算法仅执行一次查询。
猜你喜欢
  • 2012-08-18
  • 2019-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-18
  • 2012-07-09
相关资源
最近更新 更多