使用奇怪的分类器查找具有相同“颜色”的9个元素

时间:2019-06-28 02:09:14

标签: arrays algorithm search

假设我有8种类型的元素,每种元素都有不同的“颜色”,并且当类型均匀分布时,大型“ bank”数组包含所有类型的随机元素。

现在,我的目标是找到最快的方法来从相同的“颜色”中获取9个元素,但是只能使用以下分类器: 给定n个元素组成的数组A,如果A包含至少9个具有相同“颜色”元素的子组,则返回“ True”,否则返回False。

我所能做的就是通过添加/删除元素来组织一个数组并将其发送到分类器,只有“颜色”的知识是它是统一的。

截至目前,我正在从那家银行取一个65号的阵列。这意味着至少有一种颜色会出现9次(因为8 * 8 = 64),并且如果逐个检查元素是否从数组中删除会更改分类器答案。如果答案从“真”更改为“假”,则所讨论的元素是唯一的“非重复”的一部分,应重新插入到数组中。如果答案保持为“ True”,则该元素将被简单丢弃。

有更好的方法吗?也许一次删除2个?某种二进制搜索?

谢谢。

2 个答案:

答案 0 :(得分:3)

获取65个元素然后删除k并进行测试的第一步与从65-k个元素开始然后进行测试并添加k(如果您没有至少9个相同颜色的子组)基本相同。我会用例如蒙特卡洛(Monte Carlo)测试以查看对于65-k尺寸的每个可能集合,至少有9个相同颜色的可能性。如果该概率为p,则第一阶段的预期元素数量为65-k +(1-p)k。对所有可能的k进行蒙特卡洛测试,并在第一次测试后看看能为您提供最少预期元素数量的方法,应该是可行的。

如果您有一个元素数组并依次从左到右测试删除每个元素,那么您应该在第一遍之后以9个元素结束,因为保证每个测试删除都可以工作,直到只有一个数组中由9个颜色相同的元素组成的集合,然后每个测试删除都起作用,除非删除了9个元素之一。

当您沿着数组移动时,如果仅剩下9组颜色相同的元素中的一组,并且经受住了测试的元素是该颜色,则测试删除最有可能失败。假设您决定要依次删除下一个元素以及在右边随机选择的k个其他元素。因为您知道尚未找到的9个元素集中的元素数量,所以您可以计算出此删除成功的可能性。如果失败,则仅测试下一个元素。您可以算出成功和失败的概率,并找到在第一次测试中删除了最大预期元素数量的k,并查看此k值是否比一次仅删除一个元素更好。

答案 1 :(得分:2)

您当前使用的算法将调用分类器65次,对于您从原始数组中删除的每个元素(可能再插入其中),一次。因此,目的是降低该数字。

最坏情况下分类器调用的理论最小次数

最坏情况的理论最小值是35倍:

从65个数组开始时,可​​能确实只有9个元素具有相同颜色的集合,所有其他元素都具有不同的颜色,并且每个元素出现8次。在那种情况下,这9个元素可以按 C(65 of 9)方式放置(忽略顺序),即65!/(56!9!)= 31 966 749 880。

由于调用分类器有两种可能的结果,因此它将仍被认为可能的配置数目分为两部分:一组仍然可能,另一组不再可能。最有效地使用分类器的目的在于进行拆分,以消除50%的配置,无论返回值是(false / true)。

为了使可能的配置数变为1(最终解决方案),您需要将35除以2。否则,将2 35 设为2的第一个乘方,即超过310亿。

因此,即使我们知道如何以最佳方式使用分类器,在最坏的情况下我们仍然需要35次调用。

说实话,我不知道这样一种算法会如此有效,但是由于具有一定的随机性并通过更大的步幅减少了数组大小,因此平均可以获得比65更好的数字。

猜测策略的想法

大约有50%的机会随机排列42个对象中的9个颜色相同。您可以通过运行大量仿真来验证这一点。

那么为什么不从最初选择的65个对象中只有42个调用分类器开始呢?幸运的是,您在一次呼叫后就知道9个对象隐藏在一个由42个数组(而不是65个)组成的数组中,这实际上会减少您总共进行的呼叫数量。如果没有,您只是“丢失”了一个电话。

在后一种情况下,您可以尝试从该数组中重新选择42个对象。现在,您肯定会包括其他23个对象,因为这些对象在9个搜索元素中的可能性将稍有增加。也许分类器再次返回false。运气不好,但是您将继续使用不同的42组,直到获得成功。平均而言,您将在2.7次通话后获得成功。

在成功地由42个数组组成的数组之后,您显然会丢弃其余的23个其他对象。现在,按照与上述相同的原理,将38个对象用作尝试的对象。

要在每次迭代中驱动对象的选择,请向不属于先前选择的对象添加功劳,并按降序对所有对象进行排序。当未选择对象的数量较少时,增加的功劳应该更高。如果功劳相同,则使用随机顺序。

通过记录分类器返回false的所有子集,可以采取其他预防措施。如果您要再次调用分类器,请首先检查它是否是这些较早进行的调用之一的子集。如果是这样,则调用分类器是没有意义的,因为可以确定它将返回false。平均而言,这将节省一个对分类器的调用。

实施

这是该想法的JavaScript实现。此实现还包括“ bank”(get方法)和分类器。有效地隐藏了算法中的颜色。

此代码将清除1000个随机测试用例的工作,然后输出在找到答案之前调用分类器所需的最小,平均和最大次数。

我使用了BigInt原语,因此只能在support BigInt的JS引擎上运行。

// This is an implementation of the bank & classifier (not part of the solution)
const {get, classify} = (function () {
    const colors = new WeakMap; // Private to this closure
    return {
        get(length) {
            return Array.from({length}, () => {
                const obj = {};
                colors.set(obj, Math.floor(Math.random()*8)); // assign random color (0..7)
                return obj; // Only return the (empty) object: its color is secret
            });
        },
        classify(arr) {
            const counts = Array(8).fill(9); // counter for 8 different colors
            return !arr.every(obj => --counts[colors.get(obj)]);
        }
    }
})();

function shuffle(a) {
    var j, x, i;
    for (i = a.length - 1; i > 0; i--) {
        j = Math.floor(Math.random() * (i + 1));
        x = a[i];
        a[i] = a[j];
        a[j] = x;
    }
    return a;
}

// Solution:
function randomReduce(get, classify) {
    // Get 65 objects from the bank, so we are sure there are 9 with the same type
    const arr = get(65); 
    // Track some extra information per object: 
    const objData = new Map(arr.map((obj, i) => [obj, { rank: 0, bit: 1n << BigInt(i) } ]));
    // Keep track of all subsets that the classifier returned false for
    const failures = [];
    let numClassifierCalls = 0;
    
    function myClassify(arr) {
        const pattern = arr.reduce((pattern, obj) => pattern | objData.get(obj).bit, 0n);
        if (failures.some(failed => (failed & pattern) === pattern)) {
            // This would be a redundant call, as we already called the classifier on a superset
            // of this array, and it returned false; so don't make the call. Just return false
            return false;
        }
        numClassifierCalls++;
        const result = classify(arr);
        if (!result) failures.push(pattern);
        return result;
    }
    
    for (let length of [42,38,35,32,29,27,25,23,21,19,18,17,16,15,14,13,12,11,10,9]) {
        while (!myClassify(arr.slice(0, length))) {
            // Give the omited entries an increased likelihood of being seleced in the future
            let addRank = 1/(arr.length - length - 1); // Could be Infinity: then MUST use
            for (let i = length; i < arr.length; i++) {
                objData.get(arr[i]).rank += addRank;
            }
            // Randomise the array, but ensure that the most promising objects are put first
            shuffle(arr).sort((a,b) => objData.get(b).rank - objData.get(a).rank);
        }
        arr.length = length; // Discard the non-selected objects
    }
    // At this point arr.length is 9, and the classifier returned true. So we have the 9 elements.
    return numClassifierCalls; 
}

let total = 0, min = Infinity, max = 0;
let numTests = 1000;
for (let i = 0; i < numTests; i++) {
    const numClassifierCalls = randomReduce(get, classify);
    total += numClassifierCalls;
    if (numClassifierCalls < min) min = numClassifierCalls;
    if (numClassifierCalls > max) max = numClassifierCalls;
}
console.log("Number of classifier calls:");
console.log("- least: ", min);
console.log("- most: ", max);
console.log("- average: ", total/numTests);

效果统计信息

以上算法平均需要35次分类程序调用才能解决此问题。在最佳情况下,它对分类器的调用始终返回true,然后将进行20次调用。不利的一面是,最坏的情况确实很糟糕,可以达到170甚至更高。但是这种情况很少见。

这是概率分布图:

enter image description here

在99%的情况下,算法最多可以在50个分类器调用中找到解决方案。