以递增的顺序迭代数对

Posted

技术标签:

【中文标题】以递增的顺序迭代数对【英文标题】:Iterating over pairs of numbers in an increasing order 【发布时间】:2013-12-25 22:03:20 【问题描述】:

我将在底部解释问题的来源,但这是声明。假设我有两个非负整数列表,我将分别写为(A[0] ... A[n])(B[0] ... B[m])。它们是严格增加的,所以A[i+1] > A[i] 对所有iB 类似。我想按总和的升序收集所有 n * m 元素对。

所以,例如如果A = (0 1 2)B = (1 4),那么我想最终收集((0 1) (1 1) (2 1) (0 4) (1 4) (2 4))。如果出现平局,我不在乎我以哪个顺序收集这两个元素。例如,如果A = (0 1)B = (0 1),那么我不介意哪个混合术语,(0 1) 或@987654333 @,我先接。

显然,我希望这样的效率相当高。我希望有可能在时间上渐近到m * n。具体来说,如果我对输入一无所知,我希望有序输入比等效问题更容易解决这个问题。当我第一次问这个问题时,我在想的是我们必须存储的状态量。我希望以恒定的数量有可能,但也许这是不现实的。 (后来我尝试过的东西都失败了!)

代码实际上是用 Lisp 编写的,但我认为问题陈述几乎与 Lisp 无关。输入最自然地以单链表的形式出现,但无论如何我都必须提前反转它们,所以如果随机访问是相关的,我可以将它们制作成数组。如果它是相关的,我希望这主要是在非常小的列表上调用的,因此运行时中的大量常数项/常数因子可能会排除解决方案。 (虽然我很想知道算法的想法!)

背景:我一直在查看计算机代数系统 Maxima 的源代码,尤其是用于两个多项式相乘的代码。多项式以“稀疏格式”表示,因此x^5 + x^2 + 2 可能显示为(5 1 2 1 0 2),其指数下降后跟它们各自的系数。为了有效地计算产品,我真正想做的是收集零度项,然后是度1项等。当前代码通过半心半意地尝试解决这个问题来避免解决这个问题,然后做一个一种通用多项式加法,以它不期望的顺序处理系数。我觉得我们应该可以做得更好!

【问题讨论】:

您使用什么语言?数组有多大? 波西米亚:阅读第二段。其他人也是,如果你想回答这个问题,我猜。 正如下面几个人所指出的,当我说“迭代每个列表一次”时,我很傻。我将进行编辑,以便上面的声明更明智。 阵列的备件有多少?例子 ?因为如果我们在多项式 (x^500+3*x^120+x) 中有大洞,我们要么最好进行分治法(这将跳过空白部分),要么像手动处理乘法一样。 Aside:等效的 Python 表达式:sorted([(a,b) for a in A for b in B], key=sum) 【参考方案1】:

我想知道您的多项式应该有多稀疏?

对于密集多项式的乘法可能值得考虑的一个选项是计算多项式的傅立叶变换并将它们的傅立叶系数相乘。

这允许您在 O(nlogn) 中乘以多项式,其中 n 是多项式的次数。

这不适用于稀疏多项式,例如 (1+x^1000000)*(1+x^1000000),因为 n 大约为 1000000,而当前算法只需要几个周期。

可以在these lecture notes 中找到对这种傅立叶方法的一个很好的解释。

【讨论】:

我认为我希望成本以项的数量而不是程度来衡量,因此可能应该假设多项式非常稀疏。不过,我不知道这种傅立叶方法——它很迷人! 傅里叶方法需要浮点数和舍入。这可能不适用于计算机代数系统。【参考方案2】:

就我的理解而言,由于以下逻辑,您甚至不能指望具有复杂性O(N*M) 的解决方案。

假设数组是(a1, a2, a3, a4)(b1, b2, b3, b4, b5)

可能的配对如下:

a1b1 a1b2 a1b3 a1b4 a1b5
     a2b1 a2b2 a2b3 a2b4 a2b5
          a3b1 a3b2 a3b3 a3b4 a3b5
               a4b1 a4b2 a4b3 a4b4 a4b5

现在对于每一行,左边的对总是在右边的对之前被拾起。

    所以第一个候选人是a1b1。 然后一旦a1b1 被选中,下一个候选者是a1b2a2b1。 说a1b2 被选中。那么候选人是a1b3a2b1。 现在说a2b1 被选中。那么候选人是a1b3a2b2a3b1

因此我们看到,随着我们的前进,该职位的候选人数量呈线性增加。

因此,在最坏的情况下,您将不得不按照N*M*M 的顺序进行比较。

O(N*Mlog(M*N)) 方法。 (where N = a.size() and M = b.size())

for ( int i = 0; i < a.size(); i++ )
  for ( int j = 0; j < b.size(); j++ )
    sumVector.push_back( a[i] + b[j] );

sort( sumVector ); // using merge sort or quicksort.

【讨论】:

嗯,是的,我想是的。但这也适用于完全未排序的输入。当然我们可以做得更好?! 为什么不: 1. 有 m 个已排序的向量: AiBj (i 是向量的索引)(j=0...m) [O(nm)] 2.然后递归地合并每两个向量以形成一个排序向量。每个向量合并都可以采用 [O(mn)],因为这是结果向量的长度然后添加前缀 O(log(m)),这是此类合并的数量。我们得到了 O(log(m)*(m*n))。我一定是错的,因为它对于 n 和 m 必须是对称的,但我不确定我的错误在哪里。 为什么是 logm?应该是 m 因为你正在合并 m 个向量。 看我的回答。因为您已经对输入进行了排序,所以您实际上不必对每个生成的列表成员进行一次以上的比较。将其视为冒泡排序,您总是试图在当前计算出的最高数字下插入一个数字。【参考方案3】:

我希望它可以只存储恒定数量的状态,并且只对每个列表进行一次迭代。

我认为这是不可能的。我无法提供严格的数学证明,但请考虑一下:您的问题有两个部分:

1) 从 A 和 B 生成所有对。 2) 确保它们按总和递增的顺序排列。

让我们删除第二部分以使其更容易。实现这一点的最简单方法是

foreach a in A:
  foreach b in B:
    emit (a, b)

我希望您会同意,没有比这更有效的方法了。但是这里我们已经迭代了 B length(A) 次。

所以这个的最小时间复杂度是 O(A*B),但你想要 O(A+B)。

【讨论】:

OP自称:我要收集所有n * m。然后他要求O(n+m)。这显然是不可能的。 是的,你是对的。我的意思是我想避免前后移动,但我显然没有仔细考虑我的意思。恒定数量的状态部分是有道理的。我想我的意思是我希望算法的复杂度约为 n*m(但不是 n^2 * m)【参考方案4】:

如this question 中所述,使用算法在排序数组中找到一对数的总和为特定常数K 非常简单。

总结

最终的解决方案将是时间复杂度O((M+N)*(M+N)),最多比M*N 的下限差4倍(仅输出所有对)。所以常数因子只有4。

编辑:哎呀,这不是我想的O((M+N)*(M+N)),显然,它是O(K*(M+N)),其中K 是两个数组中的最高和。我不确定这个算法是否可以改进,但似乎该解决方案将类似于 Peter de Rivaz 描述的快速傅里叶变换方法。

排序数组求和算法

在该算法中,我们将低位指针设置为 0,将高位指针设置为数组的末尾。那么如果这两个位置的和大于K,我们就减少higher指针。如果它较低,我们增加较低的指针。这是有效的,因为在任何迭代中,arr[low] 是迄今为止可能得出答案的最低元素。同样,arr[high] 是最高的。因此,如果我们取最低和最高元素,并且总和大于K,我们知道与arr[high] 的任何其他组合将大于K。所以arr[high] 不能成为任何解决方案的一部分。因此我们可以将它从我们的数组中移除(这是通过减少high 指针来实现的)。

将其应用于您的问题

将其应用于您的问题,我们的想法是,我们迭代可能的总和,即从 0 到 A[len(A)-1]+B[len(B)-1]。对于每个可能的总和,我们运行上述算法。对于您的问题,我们设置了指向数组A 的低指针和指向数组B 的高指针。

对于原始算法,它会在找到与常数之和的对时立即中断。对于您的问题,您将增加ptr_A 和减少ptr_B 1。这是因为您的数组严格增加。因此,如果我们找到 A[ptr_A]+B[ptr_B]==K,那么对于所有 low_B&lt;ptr_BA[high_A]+B[ptr_B]&gt;K 对于所有 high_A&gt;ptr_A 都是 A[ptr_A]+B[low_B]&lt;K

这将找到所有对,因为对于每个可能的总和 K,它会找到总和为 K 的所有对,并且我们遍历所有可能的总和 K

作为奖励,该算法将根据列表A中的递增值对输出进行排序(您可以通过交换指针根据列表B中递增的值进行排序),并且我们不需要随机访问数组。

Python 中的代码

在 Python 中的实现:

def pair_sum(A,B):
    result = []
    max_sum = A[-1]+B[-1]
    for cur_sum in range(max_sum+1): # Iterate over all possible sum
        ptr_A = 0        # Lower pointer
        ptr_B = len(B)-1 # Higher pointer
        while True:
            if A[ptr_A]+B[ptr_B]>cur_sum:
                ptr_B -= 1
            elif A[ptr_A]+B[ptr_B]<cur_sum:
                ptr_A += 1
            else:
                result.append((A[ptr_A],B[ptr_B]))
                ptr_A += 1
                ptr_B -= 1
            if ptr_A==len(A):
                break
            if ptr_B==-1:
                break
    return result

def main():
    print pair_sum([0,1,2],[1,4])
    print pair_sum([0,1],[0,1])
    print pair_sum([0,1,3],[1,2])

if __name__=='__main__':
    main()

将打印:

[(0, 1), (1, 1), (2, 1), (0, 4), (1, 4), (2, 4)] [(0, 0), (0, 1), (1, 0), (1, 1)] [(0, 1), (0, 2), (1, 1), (1, 2), (3, 1), (3, 2)]

根据需要。

【讨论】:

这很简洁,但我不想假设指数的密度。不过感谢您的仔细撰写!【参考方案5】:

这个问题与排序 X + Y 只是表面上的不同,这是多年来对计算几何的主要刺激。 (见Joseph O’Rourke’s (open) Problem 41。)总结一下实现者的链接,当指数可以相加和比较时,已知最快的算法是O(m n log (m n)),这是显而易见的。如果指数是有界整数,则适用彼得的傅立叶方法。很多聪明人已经思考这个问题很长时间了,所以我不会期望很快会有更好的算法。

【讨论】:

这是一个绝妙的答案,谢谢!我希望“是的,这很容易,只要这样做......”,但“不,这很难,这就是为什么”也很酷。我要去看看一些论文。 哎呀,我才意识到我还没有正式接受这个答案。对此感到抱歉。【参考方案6】:

为什么不直接生成未排序的二维数组然后对其使用快速排序?

ED:看起来如果您稍微智能地生成最终数组(在初始迭代和数组生成期间使用比较算法),您以后可以使用更具体的排序算法(例如,smoothsort)并最终获得接近 O( 2(m*n)) 最坏情况为 O(m*n + (m*n) log (m*n))

ED2:我绝对确定您可以编写一个算法来在 O(m*n) 中执行此操作。 你想要做的是干净地生成数组。我没有时间写伪代码,但如果你要 1. 为每个数组设置一个最小迭代器、最大迭代器和当前迭代器,并为它们设置一个当前最大和。 2. 生成第一对 3. 迭代一个变量,将其与另一个可能的第三对变量(将其保存在临时变量中)进行比较,然后将其保存到输出或将另一个数组保存到输出并重新开始。

我将其想象为两排具有三种颜色的块:红色表示完全完成,蓝色表示“迄今为止最高”和未完成。您一次处理其中一行块,为结果数组生成下一个值,始终比较当前侧基线的组合是否低于当前处理的总和。每次总和较低时,将新的最低结果设置为刚刚计算的结果,将先前的最低结果添加到(已排序的)输出数组中,并使用先前的最低值(及其行)作为基线开始累加.永远不要回到红色块中,每次找到新的对面低点时都切换哪一边是蓝色的。

你永远不应该计算一个结果两次,然后得出一个排序数组。

本质上是一种浮顶冒泡排序。您知道计算的值高于您当前的总和,直到它不是您处理数组中较低的寄存器。当它不再更高时,您将最高值向上切换并返回到您迭代当前成员较小的数组的点。 例如:

答:(1 5 10)

B:(1 6 9)

(1, 1) -> (1, 6) 高于 (1, 5) 所以加上 (1, 5) 使 (1, 6) 最高

(5, 6) 高于 (1, 6) 所以加上 (1, 6) 使 (5, 6) 最高移动到 (1, 9)

(1, 9) 低于 (5, 6) 所以 [添加 (1, 9) 并在 A 上增加完成]

当前数组:(1, 1), (1, 5), (1, 6), (1,9)

(1, 10) 等于 (5,6) 所以将两者相加,在 B 上增加完成,从 (5,9) 开始

(5, 9) 高于 (10, 1) 所以加上 (10, 1) 使 (5, 9) 最高移动到 (10, 6)

(10, 6) 高于 (5, 9) 所以....

无论如何。如果设置正确,则每次添加到最终数组时都会进行一次比较,并且可以即时构建它而无需分支。如果您不需要内存中的最终数组,则 FFT 可能会更快,但这应该可以。

【讨论】:

【参考方案7】:

因此,让我们采用两个“scares 数组”sA 和 sB,它们仅包含原始帖子中描述的非空度数/系数数字。 示例:

A   =  x^5 + 3*x^2 + 4
sA  = [ 5, 1, 2, 3, 0, 4 ]

B = 2*x^6 + 5*x^3 + 8*x
sB = [ 6, 2, 3, 5, 1, 8]

我的建议是按照我们手动操作的方式进行操作,因此它们需要 m*n 的时间,其中 m,n 是非空系数的数量,而不是 p*q,其中 p 和 q 是 A、B 的度数。 既然你说 m 和 n 很小,那么 m*n 没什么大不了的。 要在计算时存储系数,请使用稀疏数组(可能成本很高)或哈希表。索引或键是度数,值是对应的系数。 这是一个使用哈希表在 javascript 中实现的示例:

http://jsbin.com/ITIgokiJ/2/edit?js,console

一个例子:

A     =   "x^5+3x^2+4"
B     =   "2x^6+5x^3+8x^1"
A * B =   "2x^11+11x^8+16x^6+15x^5+44x^3+32x^1"

代码:

function productEncodedPolynoms( sA, sB) 
   var aIndex = 0 ;
   var bIndex = 0 ;
   var resIndex = 0 ;
   var resHash =  ;
   // for loop within sA, moving 2 items at a time
   for (aIndex = 0; aIndex < sA.length ; aIndex+=2) 
       // for loop within sB, moving 2 items at a time
       for (bIndex = 0; bIndex < sB.length ; bIndex+=2 ) 
           resIndex = sA[aIndex]+sB[bIndex] ;
           // create key/value pair if none created
           if (resHash[resIndex]===undefined)  resHash[resIndex]=0;
           // add this product to right coefficient
           resHash[resIndex] += sA[aIndex+1]*sB[bIndex+1];
       
    
   // now unpack the hash into an encoded sparse array
   // get hash keys
   var coeff = Object.keys(resHash);
   // sort keys in reverse order
   coeff.sort(reverseSort);
   encodedResult = [];
   for (var i=0; i<coeff.length; i++ ) 
     if (resHash[coeff[i]]) 
       encodedResult.push(+coeff[i]);          // (+ converts to int)
       encodedResult.push(+resHash[coeff[i]]);
     
   
   return encodedResult;

示例:

sA = [ 5, 1, 2, 3, 0, 4 ] ;

sB = [ 6, 2, 3, 5, 1, 8] ;

sAB = productEncodedPolynoms ( sA, sB );

打印结果的实用程序:

function printEncodedArray(sA) 
  res='';
  for (var i=0; i<sA.length; i+=2) 
    if (sA[i+1]) 
        if (sA[i+1] != 1 || sA[i]==0) res+=sA[i+1];
        if (sA[i]!=0) res+='x^'+sA[i];
        if (i!=sA.length-2) res+='+';
    
  
  return res;


// utilities
function reverseSort(a,b)  return b-a ; 

【讨论】:

以上是关于以递增的顺序迭代数对的主要内容,如果未能解决你的问题,请参考以下文章

以递增的顺序将列表拆分为多个列表

LeetCode Algorithm 1403. 非递增顺序的最小子序列

LeetCode Algorithm 1403. 非递增顺序的最小子序列

LeetCode Algorithm 1403. 非递增顺序的最小子序列

按递增/递减顺序对包含 datetime.date 的列表进行排序以创建漂亮的表/csv 文件

设顺序表中的数据元素递增有序,试着写一算法,将x插入到顺序表上的适当位置上,以保持该表的有序性。