【问题标题】:Nearest permutation to given array给定数组的最近排列
【发布时间】:2025-11-26 02:35:01
【问题描述】:

问题

我有两个整数数组A[]B[]。数组B[] 是固定的,我需要找到A[] 的排列,它在字典上小于B[],并且排列最接近B[]。我的意思是:

对于 i in (0 A[] 应该小于 B[] lexiographically

例如:

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 一样大

为了更好地理解

【问题讨论】:

  • 我认为这个问题可以使用动态规划来解决。在使用 O(2^n * n) 空间时,时间复杂度可以优化到 O(2^n * n^2)。
  • n 可以大到 10^5
  • @ruakh 你是对的我会编辑我的问题
  • 您能描述一下如何计算排列与 B 的距离吗?
  • 好的,根据您的图片,您希望 B 的字典前身仅由 A 的元素组成。我建议更正 abs(B[i]-A[i]) 东西,因为这意味着偏差的元素定义。我有一个代码解决方案给你,但只能稍后发布。

标签: c++ algorithm


【解决方案1】:

首先,构建A 的不同元素计数的有序映射。

然后,向前遍历数组索引(0 到 n-1),从该映射中“撤出”元素。在每一点上,都有三种可能性:

  • 如果i < n-1,并且可以选择A[i] == B[i],请这样做并继续向前迭代。
  • 否则,如果可以选择A[i] < B[i],请为A[i] < B[i] 选择尽可能大的值。然后继续为所有后续数组索引选择最大的可用值。 (此时您不再需要担心维护A[i] <= B[i],因为我们已经在A[i] < B[i] 所在的索引之后。)返回结果。
  • 否则,我们需要回溯到可以选择A[i] < B[i] 的最后一个索引,然后使用上一个要点中的方法。
    • 请注意,尽管需要回溯,但这里最糟糕的情况是三遍:使用第一个要点中的逻辑进行一次前向遍历,在回溯中向后遍历一次以查找可能出现 A[i] < B[i] 的最后一个索引,然后使用第二个要点中的逻辑进行最后的前向传递。

由于维护有序映射的开销,这需要 O(n log m) 时间和 O em>(m) 额外空间,其中nAm 的元素的总数distinct 元素的数量。 (由于 m ≤ n,我们也可以将其表示为 O(n log n em>) 时间和 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 修改了 标准。问题变得简单多了。但是你的方法可以调整。我还准备了一个具有第一个标准的(不同的)解决方案......(未发布)
  • 我真的很喜欢你的方法,但是你能解释一下 O(nlogm) 的时间复杂度是多少,在最坏的情况下我们需要回溯 n 所以时间复杂度应该是 O(n^logm),如果我错了,请纠正我
  • @PunitJain:我确实解释过了。请阅读以“请注意,尽管需要回溯”开头的要点。
【解决方案2】:

根据评论部分对您的问题的讨论,您寻找一个完全由向量 A 的元素组成的数组,即 - 按字典顺序 - 最接近向量 B

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

  • 排序 A
  • 遍历 B 并选择 A 中最接近 B[i] 的元素。从列表中删除该元素。
  • 如果 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;
}

应用于OP一得到的问题:

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(这是A 的所有排列中字典顺序上最大的,在字典上小于@987654331 @)。
  • @ruakh:你说得对,谢谢指点。正如您所写,在 de 是一个具有 A[i]
【解决方案3】:

A[n]n! 排列(如果有重复元素则更少)。

0..n!-1 范围内使用二分搜索来确定A[] (arbitrary found example) 的第k 个字典排列,它最接近B[] 的低1。

也许在 C++ 中你可以利用 std::lower_bound

【讨论】:

  • 这需要介于 O(n² log n) 和 O(n³ log n) 之间的时间,如果 n 与 10⁵ 一样大,这似乎有点高。
  • @ruakh 是的,我评估为至少 O(n² log n) 并在阅读 10^5 编辑之前写了这个