给定数组的最接近排列

7

问题

我有两个整数数组A[]B[]。 数组B[]是固定的,我需要找到比B[]字典序小且最接近B[]A[]排列。这里我的意思是:

对于i在(0 <= i < n)之间 abs(B[i]-A[i]) 最小,并且A[]应该比B[]字典序小。

例如:

A[]={1,3,5,6,7}

B[]={7,3,2,4,6}

因此,A[] 可能的最近排列是 B[]

A[]={7,3,1,6,5}

我的方法

尝试对 A[] 进行所有排列,然后将其与 B[] 进行比较。但时间复杂度将为 (n! * n)

那么有没有什么方法可以优化这个过程呢?

编辑

n 可以达到 10^5

为了更好地理解 enter image description here


@ruakh 你说得对,我会编辑我的问题。 - Punit Jain
什么是排列 C,使得对其使用 next_permutation 函数可以得到 B - user58697
如果你选择对A进行排列,那么A[]={7,3,1,6,5}会在A[]={7,3,1,5,6}之后出现,因此这两个排列都比B小,但{7,3,1,6,5}会更接近B,在数字线上想象排列可能会有所帮助。 - Punit Jain
1
你能描述一下如何计算排列与B的相似度吗? - ciamej
1
好的,根据您的图片,您想要由A中的元素组成的B的字典序前驱。我建议您更正abs(B[i]-A[i])的内容,因为这意味着偏差的逐个元素定义。我已经为您编写了一个解决方案的代码,但只能稍后发布。 - davidhigh
显示剩余9条评论
3个回答

5
首先,构建一个有序映射表,记录 A 中不同元素的计数。
然后,通过数组索引向前迭代(从 0 到 n-1),从该映射表中“撤回”元素。在每个点上,有三种情况:
1. 如果 i < n-1,并且可以选择 A[i] == B[i],则这么做并继续向前迭代。 2. 否则,如果可以选择 A[i] < B[i],则选择最大的可选值 A[i] < B[i]。然后,选择所有后续数组指数的最大可用值。(此时您无需再担心维护 A[i] <= B[i],因为我们已经在 A[i] < B[i] 的索引之后了)。返回结果。 3. 否则,我们需要回溯到上次可以选择 A[i] < B[i] 的索引处,然后使用上一个项目中的方法。
请注意,尽管需要回溯,但此处的最坏情况是三次遍历:一次前向遍历使用第一个项目中的逻辑,一次后向遍历回溯到找到最后一个可以选择 A[i] < B[i] 的索引处,然后使用第二个项目中的逻辑进行最终前向遍历。
由于维护有序映射表的开销,这需要 O(n log m) 时间和 O(m) 额外空间,其中 n 是 A 的总元素数,m 是不同元素的数量。(由于 m ≤ n,我们也可以将其表达为 O(n log n) 时间和 O(n) 额外空间。)
请注意,如果没有解决方案,则回溯步骤将一直到达 i == -1。如果发生这种情况,您可能需要引发异常。
编辑后添加(2019-02-01):
现在已删除的答案中,גלעד ברקן总结了这个目标:
要使字典顺序更小,该数组必须具有从左到右的初始可选部分,其中 A[i] = B[i],并以元素 A[j] < B[j] 结束。为了最接近 B,我们希望最大化该部分长度,然后最大化数组的剩余部分。
因此,考虑到这个总结,另一种方法是进行两个独立的循环,第一个循环确定初始段的长度,第二个循环实际填充A。这相当于上面的方法,但可能会使代码更清晰。所以:
  1. 构建一个有序映射,记录A中不同元素的计数。
  2. 初始化initial_section_length := -1
  3. 遍历数组索引0到n−1,“撤回”这个映射中的元素。对于每个索引:
    • 如果可以选择一个尚未使用过的A元素,该元素小于当前B元素,则将initial_section_length设置为当前数组索引。(否则,不执行此操作。)
    • 如果无法选择一个尚未使用过的A元素,该元素等于当前B元素,则退出此循环。(否则,继续循环。)
  4. 如果initial_section_length == -1,则没有解决方案;抛出异常。
  5. 重复步骤#1:重新构建有序映射。
  6. 遍历数组索引从0到initial_section_length-1,“撤回”映射中的元素。对于每个索引,选择一个尚未使用过的A元素,该元素等于当前B元素。(第一个循环确保了这样的元素的存在。)
  7. 对于数组索引initial_section_length,选择最大的尚未使用的A元素,该元素小于当前B元素(并从映射中“撤回”它)。 (第一个循环确保了这样的元素的存在。)
  8. 遍历数组索引从initial_section_length+1n−1,继续从映射中“撤回”元素。对于每个索引,选择尚未使用的A元素中的最大元素。
此方法具有与基于回溯的方法相同的时间和空间复杂度。

请注意,OP 修改了标准。问题变得简单得多。但是您的方法可以适应。我还准备了一个(不同的)解决方案,符合第一个标准...(未发布) - Damien
我非常喜欢你的方法,但是你能解释一下O(nlogm)的时间复杂度吗?在最坏情况下,我们需要回溯n次,所以时间复杂度应该是O(n^ logm),如果我错了请纠正我。 - Punit Jain
@PunitJain:我已经解释过了。请阅读以“请注意,尽管需要回溯”开头的项目符号。 - ruakh

2
n!A[n]的排列(如果有重复元素,则排列数目较少)。
使用二分查找在范围0..n!-1内确定A[]的第k个字典序排列,该排列最接近B[]的较小者 (任意找到的例子)。
也许在C++中可以利用std::lower_bound
注:Original Answer翻译成“最初的回答”。

这需要O(n² log n)到O(n³ log n)的时间,如果n达到10⁵这么大,似乎有点高。 - ruakh
@ruakh 是的,我评估为至少是O(n² log n),并且在阅读10^5编辑之前就写下了这个。 - MBo

0

根据您问题的评论部分的讨论,您正在寻找一个完全由向量A元素组成的数组,该数组在词典顺序上最接近向量B

对于这种情况,算法变得非常简单。这个想法与@ruakh的答案中已经提到的一样(尽管他的答案是针对您早期和更复杂的问题版本的 -- 仍然显示在OP中 -- 因此更加复杂):

  • 对A进行排序
  • 循环B并选择最接近B[i]的A元素。从列表中删除该元素。
  • 如果A中没有小于或等于B[i]的元素,则选择最大的元素。

以下是基本实现:

#include <string>
#include <vector>
#include <algorithm>

auto get_closest_array(std::vector<int> A, std::vector<int> const& B)
{
    std::sort(std::begin(A), std::end(A), std::greater<>{});

    auto select_closest_and_remove = [&](int i)
    {
        auto it = std::find_if(std::begin(A), std::end(A), [&](auto x) { return x<=i;});
        if(it==std::end(A))
        {
            it = std::max_element(std::begin(A), std::end(A));            
        }
        auto ret = *it;
        A.erase(it);
        return ret;
    };

    std::vector<int> ret(B.size());
    for(int i=0;i<(int)B.size();++i)
    {
        ret[i] = select_closest_and_remove(B[i]);
    }
    return ret;
}

适用于原帖中的问题,得到:
int main()
{
    std::vector<int> A ={1,3,5,6,7};
    std::vector<int> B ={7,3,2,4,6};

    auto C = get_closest_array(A, B);

    for(auto i : C)
    {
        std::cout<<i<<" ";
    }
    std::cout<<std::endl;
}

它会显示出来

7 3 1 6 5

这似乎是期望的结果。


我认为这不是OP想要的; 对于 A = {1,3,4,6,7}B = {7,3,2,4,6},它给出了 7 3 1 4 6,但我认为OP想要的答案是 7 3 1 6 4(它是所有比 B 字典序小的 A 的排列中字典序最大的)。 - ruakh
@ruakh:你说得对,谢谢指出。正如你所写的,在thede之后,如果有一个元素满足A[i]<B[i],那么必须始终选择A条目中的最大值。 - davidhigh

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