寻找矩阵上波浪中心的最佳算法是什么?

23

考虑到我有一个类似这样的矩阵(mxn):

0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0
0 | 1 | 1 | 2 | 1 | 1 | 0 | 0 | 0
0 | 1 | 4 | 9 | 4 | 1 | 0 | 0 | 0
1 | 2 | 9 | # | 9 | 2 | 1 | 0 | 0
0 | 1 | 4 | 9 | 4 | 1 | 0 | 0 | 0
0 | 1 | 1 | 2 | 1 | 1 | 0 | 0 | 0
0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0

有一个随机设置的点#,它会以波的形式影响其附近的值。离这个点越近,值就越接近于10。

假设需要在更大的尺度上使用,哪种算法可以很好地找到#

编辑:我更感兴趣的是如何找到第一个非零数字,而不是找到#本身,这是目的,但不是真正的问题。想象一下一个巨大的矩阵,里面全是零,#就藏在其中某处。该算法的详细部分是找到第一个非零数。


@lc 我不太知道该如何回答这个问题。波的半径始终代表相同的距离,无论矩阵大小如何。 - Patrick Bard
1
这并不是一个确切的答案,理论上来说它不能将复杂度从正常的O(n^2)降低,但它应该会提供显著的加速:除了边界情况外,您可以通过跳过7个条目(或者向右和向下填充#旁边的方框数)来遍历搜索非零数字。找到非零数后,就像其他人在下面的答案中提到的那样跟踪它。 - Jimmy
这是不是超过一个#号? - hutingung
矩阵有多大?“波浪”有多大? - Daenyth
听起来像是SIMD(http://en.wikipedia.org/wiki/SIMD)的一个案例。 - Moop
显示剩余5条评论
12个回答

7
找到第一个非零值仅适用于信号对称且不包含旋转。考虑从互联网借来的以下示例(零=蓝色,最大值=红色),请注意第一个非零值位于右上角附近: enter image description here (来源: mathworks.com)
您可能想看一下梯度下降。一般算法定义为连续函数(您的是离散的),但仍然可以使用它。
基本上,在矩阵中初始化某个位置,查找该点的梯度并沿着该方向移动,然后重复直到收敛。您可以通过随机抽样来初始化它(选择一个随机单元格,直到您到达非零值,您可以期望这比遍历和查找平均情况下的非零值更快,自然取决于您的矩阵和信号大小)
一些属性:
  • 通常比穷举搜索(整个矩阵迭代)要快
  • 搜索空间越大(矩阵),与穷举搜索相比,速度就越快。
  • 即使信号不对称和居中(第一个非零值与最大值对齐),也可以处理更复杂的信号!
  • 相同的方法可用于一维信号或缩放到 n 维(如果你考虑一下会发现很酷,也很有用:])

限制:

  • 它可能在离散函数上永远振荡而不收敛到一个值,因此你需要在代码中处理这种情况。
  • 不能保证找到全局最大值(可能被卡在局部最大值处,有克服这一问题的方法)
  • 你需要插值你的函数(不是全部,只是一些单元格,这不是什么难事,我不会使用线性插值)或对算法进行一些适应(计算离散函数 vs. 连续函数中的梯度,不难)
这可能对你来说有些过度,也可能适当,我不知道,因为你没有提供更多的细节,但是看一下可能是值得的。请注意,有许多变体和优化的算法系列。首先看一下维基百科文章;)

我认为由于此问题的特殊属性,梯度下降不会表现良好。 在波附近,梯度将处处为0,除非在该点附近(取决于半径),问题才得以解决。 - waTeim
它适用于更一般的情况,其中波形的“形状”未知或旋转(在示例中似乎是2D高斯?但未指定是否始终如此)。当然,在任何情况下,您需要首先找到信号,如果信号相对较大,则有更好的机会以比详尽地采样更快的速度找到它,但这并不重要。关键是,在信号形式未知且搜索空间较大时,使用GD可以更快速地找到信号,并且其效益得到回报。 - ButterDog
2
你说得对,它确实涵盖了更广泛的问题集合,而且 OP 没有具体说明。如果波浪很大,那么梯度下降法可能比暴力搜索快得多。一个非常酷的结果是确定半径必须有多大,才能通过计算偏导数来弥补搜索效率的额外成本。 - waTeim
@Xocoatzin,我个人觉得这非常有趣。确实,我没有提供太多信息,只给了一个简单的例子。但是你很棒地寻找到了像这样不同的东西。实际上,我需要这种能力来在非对称波形(想象一下带有“咬痕部分”甚至更糟糕的情况)中查找,但我想先从一个基本的例子开始询问,因为过早的优化是万恶之源。然后,在社区的良好基础上,我可以为最坏的情况创建自己的解决方案。 - Patrick Bard
@PatrickBard 很高兴它对你有用,希望对其他人也有帮助。 - ButterDog

3
在最坏的情况下,您可能无法避免扫描整个矩阵,但是通过逐步增加分辨率来扫描,您可能能够在平均情况下缩短运行时间。例如,您可以从某些(任意选择的)大距离处开始采样,然后有两种可能性:要么找到非零值的点,然后可以使用其他技术在必要时局部“定位”峰值(如其他答案中提到的“梯度上升”),要么搜索结果为空,这意味着扫描分辨率过大,波形“掉进了缝隙”。然后,您的算法将减小分辨率(例如减半),并运行另一个扫描(如果聪明地完成,甚至可以跳过先前运行中已经采样的那些点),以获得更细粒度的扫描。因此,您将继续以逐渐较小的分辨率进行扫描,直到找到所需内容。前几次“粗略”扫描速度较快,但成功的机会较小,但是(取决于一些因素,例如完整矩阵的大小与“小波”的大小相比),平均而言,您有很好的机会在必须逐个元素扫描整个矩阵之前找到目标。举例说明:第一次扫描:
#-------#-------
----------------
----------------
----------------
----------------
----------------
----------------
----------------
#-------#-------
----------------
----------------
----------------
----------------
----------------
----------------
----------------

第二次扫描:
o---#---o---#---
----------------
----------------
----------------
#---#---#---#---
----------------
----------------
----------------
o---#---o---#---
----------------
----------------
----------------
#---#---#---#---
----------------
----------------
----------------

第三次扫描:

o-#-o-#-o-#-o-#-
----------------
#-#-#-#-#-#-#-#-
----------------
o-#-o-#-o-#-o-#-
----------------
#-#-#-#-#-#-#-#-
----------------
o-#-o-#-o-#-o-#-
----------------
#-#-#-#-#-#-#-#-
----------------
o-#-o-#-o-#-o-#-
----------------
#-#-#-#-#-#-#-#-
----------------

如此往复(其中'#'代表新采样的单元格,'o'代表以前采样过的单元格,可以跳过)...


1
如果周围的模式始终相同,那么根据相邻元素的值和我们遇到的第一个非零数字,我们可以始终预测#的正确位置,时间复杂度为O(1)。
 First non zero element-->| 1 | 1 | 2 |
                        0 | 1 | 4 | 9 |
                        1 | 2 | 9 | # |

例如,如果第一个非零数字是1且右侧元素为1,则向下为1,向右-向下对角线为4,因此#位于(i + 2,j + 2)处,其中(i,j)是当前元素的位置。

1
另一种可能的方法是遍历单元格,直到找到第一个非零数字。由于该非零数保证与波浪的中心在同一列,我们可以简单地遍历该列,直到找到 aba 模式(在示例中为 9#9),其中 b 值将是波浪的中心。
假设:
  • 矩阵中存在完整的波浪,以便找到第一个非零数。
  • 中心值在同一列中唯一且不等于其他值,否则算法将找到中心周围的一个值作为中心。
代码:
// iterate through cells
for (int y = 0; y < matrix.length; y++) {
    for (int x = 0; x < matrix[0].length; x++) {
        if (matrix[y][x] > 0) { // first non-zero value, in this case at point 3,3
            int cellCurr = 0;
            int cellOnePrev = 0;
            int cellTwoPrev = 0;
            for (; y < matrix.length; y++) { // iterate down rest of column
                cellCurr = matrix[y][x];
                if (cellCurr == cellTwoPrev) { // aba pattern found, center is b
                    System.out.println("found " + cellOnePrev + " at " + x + "," + (y - 1));
                    return;
                }
                // update necessary values
                cellTwoPrev = cellOnePrev;
                cellOnePrev = cellCurr;
            }
        }
    }
}

将#替换为10的示例输出:

found 10 at 3,6

1
根据编辑,主要意图是找到第一个非零条目。这很有道理。一旦找到了这个,你可以轻松地使用梯度上升法来找到最大值: 只需从当前条目走向每一步中具有最高值的邻居,直到到达顶部。
然而,一些可能重要的细节缺失了。例如,知道波的形状可能很重要。在原始问题中,它似乎具有一定的高斯形式。它也可能更加“平坦”吗? 也就是说,同样的最大值是否会导致填充了更大的矩阵区域与非零条目?
关键点在于-对于第一个简单的优化-要知道包含非零条目的区域的直径。如果你知道带有非零条目的区域的直径是,例如,n,则你可以以步长为n-1遍历矩阵,并确信你不会错过波。

如果关于波可能位置的信息完全没有,那么我怀疑改进的空间将会非常有限。如果它可以出现在任何地方,那么你就必须到处搜索

但是,即使是对于微不足道的搜索(无论步长是1还是n-1),也可能会有影响整体性能的决策,其中最突出的是缓存效应。以下是一个示例,它将“波”放入各种大小的矩阵中。(请注意,“波”实际上是基于具有最大值条目的摩尔邻域的矩形,为简单起见)。

它使用三种方法查找第一个非零条目:

  • findNonZeroSimpleA:按列依次遍历矩阵
  • findNonZeroSimpleB:按行依次遍历矩阵
  • findNonZeroSkipping:按列依次遍历矩阵,步长为n-1

这不是“基准测试”

它只能大致表明性能差异。以下是我的PC的一些结果(不透露设置情况,因为这不是基准测试):对于一个8000x8000矩阵,最大值为10,在(6000,6000)处,三种方法的运行时间如下:
- findNonZeroSimpleA: 28.783毫秒 - findNonZeroSimpleB: 831.323毫秒 - findNonZeroSkipping: 2.203毫秒
可以看出,最大的差异由遍历顺序暗示(只需交换两行 - 确保在此使用正确的行!)。“跳过”方法将运行时间大致减少了与波浪大小相对应的因子。(结果也可能被扭曲,这取决于缓存效应,当波浪大小较大时 - 但幸运的是,这些恰好是跳过方法尤其有益的情况)。
import java.awt.Point;

public class WaveMatrixTest
{
    public static void main(String[] args)
    {
        //basicTest();
        speedTest();
    }

    private static void basicTest()
    {
        int size = 30;
        int maxValue = 10;
        int array[][] = new int[size][size];
        placeValue(array, maxValue, 15, 15);
        System.out.println(toString2D(array));
    }

    private static void speedTest()
    {
        int maxValue = 10;
        int runs = 10;
        for (int size=2000; size<=8000; size*=2)
        {
            for (int run=0; run<runs; run++)
            {
                int x = size/2+size/4;
                int y = size/2+size/4;
                runTestSimpleA(size, maxValue, x, y);
                runTestSimpleB(size, maxValue, x, y);
                runTestSkipping(size, maxValue, x, y);
            }
        }

    }

    private static void runTestSimpleA(int size, int maxValue, int x, int y)
    {
        int array[][] = new int[size][size];
        placeValue(array, maxValue, x, y);

        long before = System.nanoTime();
        Point p = findNonZeroSimpleA(array, maxValue);
        long after = System.nanoTime();

        System.out.printf("SimpleA,  size %5d max at %5d,%5d took %.3f ms, result %s\n",
            size, x, y, (after-before)/1e6, p);
    }

    private static void runTestSimpleB(int size, int maxValue, int x, int y)
    {
        int array[][] = new int[size][size];
        placeValue(array, maxValue, x, y);

        long before = System.nanoTime();
        Point p = findNonZeroSimpleB(array, maxValue);
        long after = System.nanoTime();

        System.out.printf("SimpleB,  size %5d max at %5d,%5d took %.3f ms, result %s\n",
            size, x, y, (after-before)/1e6, p);
    }

    private static void runTestSkipping(int size, int maxValue, int x, int y)
    {
        int array[][] = new int[size][size];
        placeValue(array, maxValue, x, y);

        long before = System.nanoTime();
        Point p = findNonZeroSkipping(array, maxValue);
        long after = System.nanoTime();

        System.out.printf("Skipping, size %5d max at %5d,%5d took %.3f ms, result %s\n",
            size, x, y, (after-before)/1e6, p);
    }

    private static void placeValue(int array[][], int maxValue, int x, int y)
    {
        int sizeX = array.length;
        int sizeY = array[0].length;
        int n = maxValue;
        for (int dx=-n; dx<=n; dx++)
        {
            for (int dy=-n; dy<=n; dy++)
            {
                int cx = x+dx;
                int cy = y+dy;
                if (cx >= 0 && cx < sizeX &&
                    cy >= 0 && cy < sizeY)
                {
                    int v = maxValue - Math.max(Math.abs(dx), Math.abs(dy));
                    array[cx][cy] = v;
                }
            }
        }
    }

    private static Point findNonZeroSimpleA(int array[][], int maxValue)
    {
        int sizeX = array.length;
        int sizeY = array[0].length;
        for (int x=0; x<sizeX; x++)
        {
            for (int y=0; y<sizeY; y++)
            {
                if (array[x][y] != 0)
                {
                    return new Point(x,y);
                }
            }
        }
        return null;
    }

    private static Point findNonZeroSimpleB(int array[][], int maxValue)
    {
        int sizeX = array.length;
        int sizeY = array[0].length;
        for (int y=0; y<sizeY; y++)
        {
            for (int x=0; x<sizeX; x++)
            {
                if (array[x][y] != 0)
                {
                    return new Point(x,y);
                }
            }
        }
        return null;
    }

    private static Point findNonZeroSkipping(int array[][], int maxValue)
    {
        int size = maxValue * 2 - 1;
        int sizeX = array.length;
        int sizeY = array[0].length;
        for (int x=0; x<sizeX; x+=size)
        {
            for (int y=0; y<sizeY; y+=size)
            {
                if (array[x][y] != 0)
                {
                    return new Point(x,y);
                }
            }
        }
        return null;
    }


    private static String toString2D(int array[][])
    {
        StringBuilder sb = new StringBuilder();
        int sizeX = array.length;
        int sizeY = array[0].length;
        for (int x=0; x<sizeX; x++)
        {
            for (int y=0; y<sizeY; y++)
            {
                sb.append(String.format("%3d", array[x][y]));
            }
            sb.append("\n");
        }
        return sb.toString();
    }

}

1

首先将整个矩阵分割成7x7的小矩阵,以最小化矩阵之间的重叠。

一旦将矩阵分割为小矩阵,遍历或随机选择每个小矩阵的一些点,检查是否存在非零值。

如果从小矩阵中找到任何非零值,则从该非零值中找到#


7x7 分区背后是否有任何理由,还是纯粹随意的? - FThompson
也想问一下,我展示的矩阵只是一个例子。 - Patrick Bard
1
@Vulcan 因为示例中的波浪大小为7x7。 - Heejin
@Heejin 我稍后意识到了。但是除法是否可能创建小矩阵,其中找到零的概率更高?因此,在小矩阵内部搜索的方式不是更具体吗? - Patrick Bard
如果您对该环境有一些先前的了解,可以在推断之前计算关于波的先验概率,并检查先验概率比其他点更高的点,但这通常是科学研究中才会做的过度工程。如果您对该主题感兴趣,我建议阅读这篇维基文章http://en.wikipedia.org/wiki/Bayesian_inference,但我不建议将该方法应用于您的问题。 - Heejin

1
从0,0开始遍历矩阵,直至遇到非零值。
一旦遇到非零值,查看上、右、下、左邻居以找到最大值(如果有多个,则随意选择一个)。
然后再在刚刚选择的最大值上执行相同的操作,查看其4个邻居并找到最大值,直至遇到#

1
直到遇到非零值的这个过程是我担心的。看起来我会进行很多不必要的比较。 - Patrick Bard
同时,您不需要以单位1遍历,因为非零值是聚集在一起的,所以根据#的影响的最小范围,您可以跳过某些行和列。对于上面的示例,您有一个5x5的非0块,因此您只需查看每5行和每5列,直到找到非0值。 - Chimeara
这并不是比较会减慢速度(每个比较都在单个CPU指令中计算),而是“访问整个内存块”的速度较慢(这里可能更好地说明了这一点)。如果您从内存中按顺序读取,它仍然相当高效。它强烈依赖于第一个非零值与最大值对齐,这可能并不总是正确的,但仍然比@rosscowar的行/列求和方法平均更快。 - ButterDog

1
假设模式始终相同,您需要检查每个方向上的每五个单元格,从[2][2]开始,直到找到非零值。因此,您将检查[2][2]、[2][7]、[2][12]...、[7][2]、[7][7]、[7][12]...、[12][2]...等等。

一旦找到其中一个具有非零值的单元格,您只需检查其邻居和邻居的邻居即可找到#字符。

这是O(n^2),这是您能够做到的最好的,因为您无法避免必须检查O(n^2)个单元格。


1
在你的情况下,最重要的是达到第一个非零值,有几种方法可以做到这一点,不同的情况需要不同的步骤,因此我将发布两种“通用”的方法。
案例一:波受影响区域相对于所有区域都很大
我的建议是:斜着走。
您应该从角落开始斜着走,直到达到第一个非零值。之后,旅行系统会发生变化。
案例二:波受影响区域相对于所有区域都很小
我的建议是:下棋。
您应该从角落开始像下棋一样移动(见示例)直到达到第一个非零值。这里旅行系统也会发生变化。
达到非零值后,执行以下操作:检查所有8个(实际上5个就足够了,不需要从你来的角落检查,但这个小细节可能难以实现)周围区域并移动到最大值。重复此步骤,直到找到 #

0

因为没有写作业,我会使用多线程先找到一个非零数,然后再用单个线程找到 #。如果矩阵足够大以抵消线程处理的性能下降,这比单线程更有效。

  1. 将矩阵分成多个区域
  2. 顺序或随机查找非零数
  3. 一旦找到任何非零数,停止所有线程
  4. 使用适当的算法仅使用一个线程查找 #

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接