三元组的最佳合并

2022-09-03 16:10:44

我正在尝试为以下问题提出一种算法:

我有一个整数的三元组的集合 - 让我们称这些整数为A,B,C。存储在其中的值可能很大,因此通常不可能创建大小为 A、B 或 C 的数组。目标是最小化集合的大小。为此,我们提供了一个简单的规则,允许我们合并三元组:

  • 对于两个三元组(A,B,C)和(A',B',C'),删除原始三元组并将三元组(A|A', B, C) 如果 B == B' 且 C = C',其中 |是按位 OR。类似的规则也适用于B和C。

换句话说,如果两个三元组的两个值相等,则删除这两个三元组,按位或第三个值,并将结果放入集合中。

贪婪的方法通常在类似情况下具有误导性,因此对于这个问题,但我找不到一个简单的反例来导致正确的解决方案。对于包含 250 个项目的列表,其中正确的解为 14,贪婪合并计算的平均大小约为 30(从 20 到 70 不等)。次优开销会随着列表大小的增加而变大。

我也尝试过使用设置的位计数,但我没有发现任何有意义的结果。显而易见的事实是,如果记录是唯一的(可以安全地假设),则设置的位计数始终会增加。

这是愚蠢的贪婪实现(这只是一个概念性的东西,请不要考虑代码风格):

public class Record {
    long A;
    long B;
    long C;

    public static void main(String[] args) {
        List<Record> data = new ArrayList<>();
        // Fill it with some data

        boolean found;

        do {
            found = false;
            outer:
            for (int i = 0; i < data.size(); ++i) {
                for (int j = i+1; j < data.size(); ++j) {
                    try {
                        Record r = merge(data.get(i), data.get(j));
                        found = true;
                        data.remove(j);
                        data.remove(i);
                        data.add(r);
                        break outer;
                    } catch (IllegalArgumentException ignored) {
                    }
                }
            }
        } while (found);
    }

    public static Record merge(Record r1, Record r2) {
        if (r1.A == r2.A && r1.B == r2.B) {
            Record r = new Record();
            r.A = r1.A;
            r.B = r1.B;
            r.C = r1.C | r2.C;
            return r;
        }
        if (r1.A == r2.A && r1.C == r2.C) {
            Record r = new Record();
            r.A = r1.A;
            r.B = r1.B | r2.B;
            r.C = r1.C;
            return r;
        }
        if (r1.B == r2.B && r1.C == r2.C) {
            Record r = new Record();
            r.A = r1.A | r2.A;
            r.B = r1.B;
            r.C = r1.C;
            return r;
        }
        throw new IllegalArgumentException("Unable to merge these two records!");
    }

你知道如何解决这个问题吗?


答案 1

这将是一个很长的答案,可悲的是没有最佳解决方案(抱歉)。然而,这是将贪婪的问题解决方案应用于您的问题的严重尝试,因此原则上它可能是有用的。我没有实现最后讨论的方法,也许这种方法可以产生最佳解决方案 - 但我不能保证。

第0级:不是真的贪婪

根据定义,贪婪算法具有启发式方法,用于以局部最优的方式选择下一步,即现在最优,希望达到全局最优,这可能永远可能也不可能。

您的算法选择任何可合并的对并合并它们,然后继续前进。它不评估此合并的含义以及是否有更好的本地解决方案。正因为如此,我根本不会说你的做法是贪婪的。这只是一种解决方案,一种方法。我将它称为盲算法,以便我可以在我的答案中简洁地引用它。我还将使用稍微修改过的算法版本,它不是删除两个三元组并附加合并的三元组,而是仅删除第二个三元组,并将第一个三元组替换为合并的三元组。生成的三元组的顺序是不同的,因此最终结果也可能是不同的。让我在代表性数据集上运行这个修改后的算法,用:*

0: 3 2 3   3 2 3   3 2 3
1: 0 1 0*  0 1 2   0 1 2
2: 1 2 0   1 2 0*  1 2 1
3: 0 1 2*
4: 1 2 1   1 2 1*
5: 0 2 0   0 2 0   0 2 0

Result: 4

第 1 级:贪婪

要拥有贪婪的算法,您需要以一种允许在多个选项可用时进行比较的方式制定合并决策。对我来说,合并决定的直观表述是:

如果我合并这两个三元组,与从当前集合合并任何其他两个三元组的结果相比,结果集是否具有最大可能数量的可合并三元组?

我再说一遍,这对我来说是直观。我没有证据证明这会导致全局最优解决方案,甚至没有证据表明它会导致比盲算法更好或相等的解决方案 - 但它符合贪婪的定义(并且非常容易实现)。让我们在上面的数据集上尝试一下,显示每个步骤之间可能的合并(通过指示三元组对的索引)以及每个可能的合并的可合并数:

          mergables
0: 3 2 3  (1,3)->2
1: 0 1 0  (1,5)->1
2: 1 2 0  (2,4)->2
3: 0 1 2  (2,5)->2
4: 1 2 1
5: 0 2 0

除了合并三元组 1 和 5 之外的任何选择都可以,如果我们采用第一对,我们得到与盲算法相同的中间集(这次我将折叠索引以消除缺口):

          mergables
0: 3 2 3  (2,3)->0
1: 0 1 2  (2,4)->1
2: 1 2 0
3: 1 2 1
4: 0 2 0

这就是该算法的不同之处:它选择三元组2和4,因为与盲算法所做的选择相反,合并它们后仍有一个可能的合并:

          mergables
0: 3 2 3  (2,3)->0   3 2 3
1: 0 1 2             0 1 2
2: 1 2 0             1 2 1
3: 1 2 1

Result: 3

第2级:非常贪婪

现在,从这种直观的启发式方法开始的第二步是进一步展望合并,然后提出启发式问题。概括地,您将进一步展望合并并应用上述启发式,回溯并确定最佳选项。这现在已经变得非常冗长,所以举个例子,我只用 lookahead 1 执行这个新的启发式方法的一个步骤:k

          mergables
0: 3 2 3  (1,3)->(2,3)->0
1: 0 1 0         (2,4)->1*
2: 1 2 0  (1,5)->(2,4)->0
3: 0 1 2  (2,4)->(1,3)->0
4: 1 2 1         (1,4)->0
5: 0 2 0  (2,5)->(1,3)->1*
                 (2,4)->1*

应用此新启发式方法时,标有星号的合并序列是最佳选择。

如果需要口头解释:

而不是检查在开始集的每次可能的合并之后可能有多少个合并;这次我们检查在开始集的每个可能合并之后,每个结果集的每次可能合并后可能有多少个合并。这是为了寻找。对于前瞻,您会看到一个非常长的句子,在每次可能的合并之后,每个结果集时间重复该部分。1nn

第3级:让我们切除贪婪

如果你仔细观察,以前的方法对于适度的输入和前瞻(*)都有灾难性的性能。对于超过 20 个三元组的输入,任何超过 4 合并前瞻的输入都需要不合理的时间。这里的想法是删除似乎比现有解决方案更糟糕的合并路径。如果我们想执行前瞻 10,并且一个特定的合并路径在三次合并后产生的可合并路径比 5 次合并后的另一个路径少,我们不妨剪切当前的合并路径并尝试另一个。这应该可以节省大量时间,并允许大量观察,这将使我们更接近全球最佳解决方案,希望如此。我还没有实现这个进行测试。

(*):假设可以大幅减少输入集,则合并次数与输入大小成正比,并且前瞻大致指示对这些合并的置换程度。因此,您必须从|输入|中选择前瞻,这是二项式系数,对于前瞻≪ |输入|可以近似为O(|input|^lookahead) - 这也是(正确地)写成你彻底搞砸了

将它们放在一起

我对这个问题很感兴趣,所以我坐下来用Python编写了代码。可悲的是,我能够证明不同的前瞻可能会产生不同的结果,甚至盲算法偶尔也会比前瞻1或2更好。这直接证明了该解不是最优的(至少对于 )。请参阅源代码和帮助程序脚本,以及 github 上的校对三元组。请注意,除了对合并结果进行记忆之外,我没有尝试在CPU周期方面优化代码。lookahead ≪ |input|


答案 2

我没有解决方案,但我有一些想法。

表示法

问题的一个有用的视觉表示是将三元组视为3D空间的点。您有整数,因此记录将是网格的节点。当且仅当表示它们的节点位于同一轴上时,两条记录是可合并的。

反例

我发现了一个(最小的)例子,贪婪的算法可能会失败。请考虑以下记录:

(1, 1, 1)   \ 
(2, 1, 1)   |     (3, 1, 1)  \
(1, 2, 1)   |==>  (3, 2, 1)  |==> (3, 3, 1)
(2, 2, 1)   |     (2, 2, 2)  /    (2, 2, 2)
(2, 2, 2)   /

但是,如果选择错误的方式,它可能会卡在三条记录上:

(1, 1, 1)   \ 
(2, 1, 1)   |     (3, 1, 1)
(1, 2, 1)   |==>  (1, 2, 1)
(2, 2, 1)   |     (2, 2, 3)
(2, 2, 2)   /

直觉

我觉得这个问题在某种程度上类似于在图中找到最大匹配。这些算法中的大多数都通过从任意的次优解开始来找到最优解,并通过搜索具有以下属性的增强路径在每次迭代中使其“更优化”:

  1. 它们很容易找到(节点数中的多项式时间),
  2. 一个增强路径和当前解决方案可以制作成一个新的解决方案,这严格优于当前解决方案,
  3. 如果未找到增强路径,则当前解是最佳解。

我认为,在你的问题中,最好的解决方案可以在类似的精神中找到。


推荐