假设我有8种类型的元素,每种元素都有不同的“颜色”,并且当类型均匀分布时,大型“ bank”数组包含所有类型的随机元素。
现在,我的目标是找到最快的方法来从相同的“颜色”中获取9个元素,但是只能使用以下分类器: 给定n个元素组成的数组A,如果A包含至少9个具有相同“颜色”元素的子组,则返回“ True”,否则返回False。
我所能做的就是通过添加/删除元素来组织一个数组并将其发送到分类器,只有“颜色”的知识是它是统一的。
截至目前,我正在从那家银行取一个65号的阵列。这意味着至少有一种颜色会出现9次(因为8 * 8 = 64),并且如果逐个检查元素是否从数组中删除会更改分类器答案。如果答案从“真”更改为“假”,则所讨论的元素是唯一的“非重复”的一部分,应重新插入到数组中。如果答案保持为“ True”,则该元素将被简单丢弃。
有更好的方法吗?也许一次删除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甚至更高。但是这种情况很少见。
这是概率分布图:
在99%的情况下,算法最多可以在50个分类器调用中找到解决方案。