三胞胎的最佳合并

时间:2014-03-06 11:23:06

标签: java algorithm optimization

我正在尝试针对以下问题提出算法:

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

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

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

贪婪的方法在类似的情况下通常会产生误导,所以这是针对这个问题,但我找不到一个简单的反例,它会导致正确的解决方案。对于包含正确解为14的250项的列表,通过贪婪合并计算的平均大小约为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!");
    }

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

3 个答案:

答案 0 :(得分:2)

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

等级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:非常贪心

现在,这个直观启发式的第二步是向前看进一步合并,然后询问启发式问题。通用,你会向前看k进一步合并并应用上述启发式,回溯并确定最佳选项。这个现在变得非常冗长,所以为了举例说明,我只用前瞻1执行这个新启发式的一步:

          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*

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

如果需要口头解释:

  

而不是检查起始集的每次可能合并后可能的合并次数;这次我们检查在每个可能的合并之后每个可能的合并之后可能的合并数量。这是预示1。对于预测n,您可能会看到一个非常长的句子,在每个可能的合并之后为每个结果集 n次重复部分。

<3> 3级:让我们减少贪婪

如果仔细观察,即使是适度的输入和前瞻(*),之前的方法仍然具有灾难性的表现。对于超过20个三元组的输入,超出4-merge-lookahead的任何东西都会花费不合理的长时间。这里的想法是删除合并路径,这些路径似乎比现有解决方案更糟糕。如果我们想执行前瞻10,并且特定的合并路径在三次合并之后产生的mergables少于5次合并之后的另一个路径,我们也可以 cut 当前的合并路径并尝试另一个路径。这应该可以节省大量时间并允许大量前瞻,这将使我们更接近全局最优解决方案,希望如此。我还没有实施这个测试。

  

(*):假设输入集大量减少,合并的数量是   与输入大小成正比,和   预测近似表示您对这些合并进行了多少排列。   因此,您可以从lookahead中选择|input|   lookahead ≪ |input|的二项式系数可以近似为   O(|input|^lookahead) - 这也是(正确地)写成你被彻底搞砸了

全部放在一起

我对这个问题很感兴趣,我坐下来用Python编写了这个问题。遗憾的是,我能够证明不同的前瞻产生了可能不同的结果,甚至盲目算法偶尔也会比前瞻1或2更好。这是解决方案不是最优的直接证据(至少{{1} })。 See the source code and helper scripts, as well as proof-triplets on github。请注意,除了合并结果的记忆之外,我没有尝试优化代码CPU周期。

答案 1 :(得分:0)

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

表示

问题的一个有用的直观表示是将三元组视为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. 如果未找到扩充路径,则当前解决方案是最佳的。
  4. 我认为你的问题的最佳解决方案可以在类似的精神中找到。

答案 2 :(得分:0)

根据您的问题说明:

  

我及时给出了一堆事件,通常会有一些模式。   目标是找到模式。整数中的每个位   代表&#34;事件发生在这个特定的年/月/日&#34;。对于   例如,2014年3月7日的表示将是[1&lt;   (2014-1970),1&lt;&lt; 3,1 <&lt; 7]。上面描述的模式允许我们   压缩这些事件,以便我们可以说每1日发生一次事件   在2000 - 2010年&#39;。 - Danstahr 3月7日10:56

我想鼓励你听取MicSim指出的答案,特别是

  

根据您的问题描述,您应该查看此SO   答案(如果你还没有这样做):   stackoverflow.com/a/4202095/44522和   stackoverflow.com/a/3251229/44522 - MicSim 3月7日15:31

您的目标描述比您使用的方法更清晰。我很害怕你不知道合并的想法。听起来很吓人。您得到的答案取决于您操纵数据的顺序。你不想要那个。

您似乎需要保留数据并进行总结。因此,您可以尝试计算这些位而不是合并它们。当然,尝试聚类算法,但更具体地说是尝试回归分析。如果您创建一些辅助数据,我认为使用相关性分析可以获得很好的结果。例如,如果您为&#34;星期一&#34;,&#34;星期二&#34;,&#34;该月的第一个星期一&#34;,&#34;该月的第一个星期二&#34创建数据;,...&#34;本月的第二个星期一&#34;,...&#34;甚至几年&#34;,&#34;每四年&#34;,&#34;闰年&#34; ;,&#34;没有闰日的年份&#34;,...&#34;以3&#34;结束的年份,......

您现在所拥有的是&#34;该月的第一天&#34;,&#34;该月的第二天&#34;,...&#34;一年中的第一个月&#34; ,&#34;一年中的第二个月&#34;,......这些听起来不够精致,无法找到模式。

如果您觉得有必要继续您已经开始的方法,那么您可能会将其视为搜索而非合并。我的意思是你需要一个成功的标准/衡量标准。您可以对原始数据进行合并,同时严格要求A == A&#39;。然后在原始数据上重复合并,同时要求B == B&#39;。同样C == C&#39;。最后比较结果(使用标准/度量)。你知道这是怎么回事吗?您对位计数的想法可以用作衡量标准。

另一点,你可以在表现上做得更好。我不是将所有数据和匹配对进行双循环,而是鼓励您对数据进行单次传递并将其分类到容器中。 HashMap是你的朋友。确保同时实现hashCode()和equals()。使用地图,您可以按键对数据进行排序(比如月份和日期都匹配),然后在值中累计年份。哦,伙计,这可能是很多编码。

最后,如果执行时间不是问题而你不需要表现,那么这里有一些尝试。您的算法取决于数据的顺序。您可以根据不同的排序获得不同的答案。成功的标准是合并后最小尺寸的答案。所以,通过这个算法反复循环:随机播放原始数据,进行合并,保存结果。现在,每次循环都保持到目前为止最小的结果。每当您得到小于先前最小值的结果时,打印出迭代次数和大小。这是一个非常简单的算法,但如果有足够的时间,它将找到小的解决方案。根据您的数据大小,可能需要很长时间......

亲切的问候,

-JohnStosh