基于1对1选择的协同排序算法

时间:2015-08-12 22:31:16

标签: algorithm sorting

我不知道这是否是一个更具数学性的对象,但我潜伏着mathexchange而且看起来并不是面向算法的,所以我更愿意在这里问。

我想知道以下问题是否已经解决:

让我们说我们有10个对象,我们想根据它们对它们进行排序。如果排序属于一个人,没问题,我们请他回答我们的问题(使用bubblesort或类似的)并回答,在一堆问题之后,他将获得最终排名。

现在让我们说有10个人。我们希望在全球排名。变得困难,任何人都可以找到解决问题的方法(例如,向所有人询问"第一个最喜欢的三个"然后分配点,然后进行排名);

我希望更科学,因此更具算法性,换句话说,使用冒泡排序(其实现,就像一系列问题1vs1对象,并询问你最喜欢的是什么,然后进行排名)为十个人,尽量减少要问的问题。

因此,我们应该有一种方法对对象进行全球排名,同时分配给那些将要排序的人,重要的,并且如果可能的话,不要等待任何人进行排名但是要按百分比和统计数据进行排序基础。

希望能够很好地解释我的问题,如果你感觉不适合这个团体,请告诉我并转发其他服务。谢谢!

2 个答案:

答案 0 :(得分:5)

你的问题是Arrow's Theorem的主题。简而言之,一般来说,你想做的事情是不可能的。

如果你还想尝试,我建议在有向图中使用有向边来表示偏好;类似于多数的东西更喜欢A到B,包括边缘A-> B,并且在关系的情况下没有边缘。如果结果是有向无环图,恭喜您,您可以使用toposort订购商品。否则使用Tarjan的算法识别强连通组件,这是麻烦点。

总的来说,在我看来,摆脱这个难题的最好方法是获得分数而不是对项目进行排名。然后你只是平均分数。

答案 1 :(得分:4)

在我之前回答的结果不尽如人意之后,我决定开始研究问题的实际方面:如何最佳地提出问题以建立一个人的偏好。

跳过不必要的问题

如果要订购10件商品,则需要比较45对商品。这45个决定构成了一个三角矩阵:

   0  1  2  3  4  5  6  7  8
1  >
2  >  <
3  <  >  =
4  =  >  <  =
5  >  <  <  <  >
6  <  >  >  <  >  <
7  <  >  <  =  =  <  >
8  <  <  =  >  =  <  <  <
9  =  >  >  <  <  >  >  =  >

在最糟糕的情况下,您需要先询问一个人45个问题,然后才能填写整个矩阵并了解他对10个项目的排名。但是,如果某人喜欢第1项到第2项,第2项到第3项,您可以推断他更喜欢第1项到第3项,并跳过该问题。事实上,在最好的情况下,只有9个问题足以填写整个矩阵。

回答二进制问题以推断项目在有序列表中的位置与填充二叉搜索树非常相似;然而,在一个10项b树中,最好的情况是16个问题,而不是我们的理论最小值9;所以我决定尝试寻找另一种解决方案。

以下是基于三角矩阵的算法。它以随机顺序询问问题,但在每个答案之后检查可以推断出哪些其他答案,并避免提出不必要的问题。

在实践中,填写45个问题矩阵所需的问题数量平均为25.33,其中90.5%的实例在20-30范围内,最小值为12,最大值为40(在100,000上测试)样本,随机问题顺序,否&#34; =&#34;答案)。
distribution: number of questions (random order)

当系统地询问问题时(从上到下,从左到右填充矩阵),分布完全不同,平均值较低,为24.44,奇数截止值低于19,少数样本达到最大值45,奇数和偶数之间的明显差异。
distribution: number of questions (systematic)

我并不期待这种差异,但它让我意识到这里有优化的机会。我正在考虑与b-tree概念相关的策略,但没有固定的根。这将是我的下一步。 (更新:见下文)

&#13;
&#13;
function PrefTable(n) {
    this.table = [];
    for (var i = 0; i < n; i++) {
        this.table[i] = [];
        for (var j = 0; j < i; j++) {
            this.table[i][j] = null;
        }
    }

    this.addAnswer = function(x, y, pref, deduced) {
        if (x < y) {
            var temp = x; x = y; y = temp; pref *= -1;
        }
        if (this.table[x][y] == null) {
            this.table[x][y] = pref;
            if (! deduced) this.deduceAnswers();
            return true;
        }
        else if (this.table[x][y] != pref) {
            console.log("INCONSISTENT INPUT: " + x + ["<", "=", ">"][pref + 1] + y);
        }
        return false;
    }

    this.deduceAnswers = function() {
        do {
            var changed = false;
            for (var i = 0; i < this.table.length; i++) {
                for (var j = 0; j < i; j++) {
                    var p = this.table[i][j];
                    if (p != null) {
                        for (var k = 0; k < j; k++) {
                            var q = this.table[j][k];
                            if (q != null && p * q != -1) {
                                changed |= this.addAnswer(i, k, p == 0 ? q : p, true);
                            }
                        }
                        for (var k = i + 1; k < this.table.length; k++) {
                            var q = this.table[k][j];
                            if (q != null && p * q != 1) {
                                changed |= this.addAnswer(i, k, p == 0 ? -q : p, true);
                            }
                        }
                        for (var k = j + 1; k < i; k++) {
                            var q = this.table[i][k];
                            if (q != null && p * q != 1) {
                                changed |= this.addAnswer(j, k, p == 0 ? q : -p, true);
                            }
                        }
                    }
                }
            }
        }
        while (changed);
    }

    this.getQuestion = function() {
        var q = [];
        for (var i = 0; i < this.table.length; i++) {
            for (var j = 0; j < i; j++) {
                if (this.table[i][j] == null) q.push({a:i, b:j});
            }
        }
        if (q.length) return q[Math.floor(Math.random() * q.length)]
        else return null;
    }

    this.getOrder = function() {
        var index = [];
        for (i = 0; i < this.table.length; i++) index[i] = i;
        index.sort(this.compare.bind(this));
        return(index);
    }

    this.compare = function(a, b) {
        if (a > b) return this.table[a][b]
        else return 1 - this.table[b][a];
    }
}

// CREATE RANDOM ORDER THAT WILL SERVE AS THE PERSON'S PREFERENCE
var fruit = ["orange", "apple", "pear", "banana", "kiwifruit", "grapefruit", "peach", "cherry", "starfruit", "strawberry"];
var pref = fruit.slice();
for (i in pref) pref.push(pref.splice(Math.floor(Math.random() * (pref.length - i)),1)[0]);
pref.join(" ");

// THIS FUNCTION ACTS AS THE PERSON ANSWERING THE QUESTIONS
function preference(a, b) {
    if (pref.indexOf(a) - pref.indexOf(b) < 0) return -1
    else if (pref.indexOf(a) - pref.indexOf(b) > 0) return 1
    else return 0;
}

// CREATE TABLE AND ASK QUESTIONS UNTIL TABLE IS COMPLETE
var t = new PrefTable(10), c = 0, q;
while (q = t.getQuestion()) {
    console.log(++c + ". " + fruit[q.a] + " or " + fruit[q.b] + "?");
    var answer = preference(fruit[q.a], fruit[q.b]);
    console.log("\t" + [fruit[q.a], "whatever", fruit[q.b]][answer + 1]);
    t.addAnswer(q.a, q.b, answer);
}

// PERFORM SORT BASED ON TABLE
var index = t.getOrder();

// DISPLAY RESULT
console.log("LIST IN ORDER:");
for (var i in index) console.log(i + ". " + fruit[index[i]]);
&#13;
&#13;
&#13;

更新1:按正确的顺序提问题

如果你按顺序提出问题,从上到下填写三角形矩阵,你实际做的是:保留你已经问过的项目的初步顺序,引入新的一次一个项目,将其与之前的项目进行比较,直到您知道在初步订单中将其插入的位置,然后转到下一个项目。

这个算法有一个明显的优化机会:如果你想将一个新项目插入一个有序列表,而不是依次将它与每个项目进行比较,你可以将它与de middle中的项目进行比较:它告诉你哪一半新项目进入;然后你将它与那一半中间的项目进行比较,依此类推...这限制了log2(n)+1的最大步数。

以下是使用此方法的代码版本。在实践中,它提供了非常一致的结果,所需问题的数量平均为22.21,不到最大值45的一半。所有结果都在19到25范围内(在100,000个样本上测试,没有&#34; =&#34;答案)。

distribution: number of questions (optimised order)

随着物品数量的增加,这种优化的优势变得更加明显;对于20个项目,在可能的190个问题中,随机方法给出平均77(40.5%),而优化方法给出平均62(32.6%)。 50项,即300/1225(24.5%),而217/1225(17.7%)。

&#13;
&#13;
function PrefList(n) {
    this.size = n;
    this.items = [{item: 0, equals: []}];
    this.current = {item: 1, try: 0, min: 0, max: 1};

    this.addAnswer = function(x, y, pref) {
        if (pref == 0) {
            this.items[this.current.try].equals.push(this.current.item);
            this.current = {item: ++this.current.item, try: 0, min: 0, max: this.items.length};
        } else {
            if (pref == -1) this.current.max = this.current.try
            else this.current.min = this.current.try + 1;
            if (this.current.min == this.current.max) {
                this.items.splice(this.current.min, 0, {item: this.current.item, equals: []});
                this.current = {item: ++this.current.item, try: 0, min: 0, max: this.items.length};
            }
        }
    }

    this.getQuestion = function() {
        if (this.current.item >= this.size) return null;
        this.current.try = Math.floor((this.current.min + this.current.max) / 2);
        return({a: this.current.item, b: this.items[this.current.try].item});
    }

    this.getOrder = function() {
        var index = [];
        for (var i in this.items) {
            index.push(this.items[i].item);
            for (var j in this.items[i].equals) {
                index.push(this.items[i].equals[j]);
            }
        }
        return(index);
    }
}

// PREPARE TEST DATA
var fruit = ["orange", "apple", "pear", "banana", "kiwifruit", "grapefruit", "peach", "cherry", "starfruit", "strawberry"];
var pref = fruit.slice();
for (i in pref) pref.push(pref.splice(Math.floor(Math.random() * (pref.length - i)),1)[0]);
pref.join(" ");

// THIS FUNCTION ACTS AS THE PERSON ANSWERING THE QUESTIONS
function preference(a, b) {
    if (pref.indexOf(a) - pref.indexOf(b) < 0) return -1
    else if (pref.indexOf(a) - pref.indexOf(b) > 0) return 1
    else return 0;
}

// CREATE TABLE AND ASK QUESTIONS UNTIL TABLE IS COMPLETE
var t = new PrefList(10), c = 0, q;
while (q = t.getQuestion()) {
    console.log(++c + ". " + fruit[q.a] + " or " + fruit[q.b] + "?");
    var answer = preference(fruit[q.a], fruit[q.b]);
    console.log("\t" + [fruit[q.a], "whatever", fruit[q.b]][answer + 1]);
    t.addAnswer(q.a, q.b, answer);
}

// PERFORM SORT BASED ON TABLE
var index = t.getOrder();

// DISPLAY RESULT

console.log("LIST IN ORDER:");
for (var i in index) console.log(i + ". " + fruit[index[i]]);
&#13;
&#13;
&#13;

我认为这可以为一个人优化二进制问题流程。下一步是弄清楚如何询问几个人的偏好并将它们组合起来,而不会在矩阵中引入冲突的数据。

更新2:根据多个人的偏好进行排序

在尝试(在我之前的回答中)使用不同的人会回答每个问题的算法时,显然冲突的偏好会创建一个数据不一致的偏好表,这对于比较的基础并不有用。排序算法。

本答案前面的两种算法提供了处理这个问题的可能性。一种选择是在&#34;之前&#34;&#34;&#34;之后用百分比而不是&#34;填写偏好表。和#34;平等&#34;作为唯一的选择。之后,您可以搜索不一致性,并通过更改最近投票的决定来修复它们,例如:如果苹果与橘子的比例为80/20%,则橘子与梨的比例为70/30%,梨与苹果的比例为60/40%,从而改变了苹果之前的优先选择。梨之前的苹果#34;将是解决不一致的最佳方法。

另一种选择是跳过不必要的问题,从而消除偏好表中不一致的可能性。这将是最简单的方法,但问题的顺序会对最终结果产生更大的影响。

第二种算法将每个项目插入初步顺序,首先检查它是进入第一个还是最后一个,然后是否进入该半部分的第一个或最后一个部分,依此类推......稳定地放大在不断减少的步骤中的正确位置。这意味着用于确定每个项目位置的决策顺序的重要性日益降低。这可能是一个系统的基础,在这个系统中,更多的人被要求为重要的决定投票,而较少的人则为不太重要的决策投票,从而减少了每个人必须回答的问题的数量。

如果人数远远大于项目数,你可以使用这样的东西:对于每一个新项目,第一个问题都放在一半的人身上,然后每一个问题都放到一半剩下的人。这样,每个项目每个人最多只能回答一个问题,对于整个列表,每个人最多可以回答与项目数量相等的问题数量。

同样,对于大量人群,有可能使用统计数据。这可以决定某个答案在哪个阶段已经形成统计上显着的领先优势,并且可以将该问题视为已回答,而无需再询问任何人。它还可以用来决定投票必须被认为与“平等”的接近程度。答案。

更新3:根据问题的重要性询问小组

此代码版本通过向较大的子群体提出重要问题而向较小的子群体提出不太重要的问题来减少每人的问题数量,如更新2中所述。 例如当在已经包含7个项目的列表中找到第八个项目的位置时,需要最多3个问题才能找到正确的位置;因此人口将分为3组,相对大小为4:2:1 该示例根据20人的偏好订购10个项目;任何人被问到的最大问题数是9。

&#13;
&#13;
function GroupPref(popSize, listSize) {	// CONSTRUCTOR
    if (popSize < steps(listSize)) return {};
    this.population = popSize;
    this.people = [];
    this.groups = [this.population];
    this.size = listSize;
    this.items = [{item: 0, equals: []}];
    this.current = {item: 1, question: 0, try: 0, min: 0, max: 1};

    this.getQuestion = function() {
        if (this.current.item >= this.size) return null;
        if (this.current.question == 0) this.populate();
        var group = this.people.splice(0, this.groups[this.current.question++]);
        this.current.try = Math.floor((this.current.min + this.current.max) / 2);
        return({people: group, a: this.current.item, b: this.items[this.current.try].item});
    }

    this.processAnswer = function(pref) {
        if (pref == 0) {
            this.items[this.current.try].equals.push(this.current.item);
        } else {
            if (pref < 0) this.current.max = this.current.try
            else this.current.min = this.current.try + 1;

            if (this.current.min == this.current.max) {
                this.items.splice(this.current.min, 0, {item: this.current.item, equals: []});
            } else return;
        }
        this.current = {item: ++this.current.item, question: 0, try: 0, min: 0, max: this.items.length};
        this.distribute();
    }

    function steps(n) {
        return Math.ceil(Math.log(n) / Math.log(2));
    }

    this.populate = function() {
        for (var i = 0; i < this.population; i++) this.people.splice(Math.floor(Math.random() * (i + 1)), 0, i);
    }

    this.distribute = function() {
        var total = this.population, groups = steps(this.current.item + 1);
        this.groups.length = 0;
        for (var i = 0; i < groups; i++) {
            var size = Math.round(Math.pow(2, i) * total / (Math.pow(2, groups) - 1));
            if (size == 0) ++size, --total;
            this.groups.unshift(size);
        }
    }

    this.getOrder = function() {
        var index = [];
        for (var i in this.items) {
            var equal = [this.items[i].item];
            for (var j in this.items[i].equals) {
                equal.push(this.items[i].equals[j]);
            }
            index.push(equal);
        }
        return(index);
    }
}

// PREPARE TEST DATA
var fruit = ["orange", "apple", "pear", "banana", "kiwifruit", "grapefruit", "peach", "cherry", "starfruit", "strawberry"];
var pref = [];
for (i = 0; i < 20; i++) {
    var temp = fruit.slice();
    for (j in temp) temp.push(temp.splice(Math.floor(Math.random() * (temp.length - j)), 1)[0]);
    pref[i] = temp.join(" ");
}

// THIS FUNCTION ACTS AS THE PERSON ANSWERING THE QUESTIONS
function preference(person, a, b) {
    if (pref[person].indexOf(a) - pref[person].indexOf(b) < 0) return -1
    else if (pref[person].indexOf(a) - pref[person].indexOf(b) > 0) return 1
    else return 0;
}

// CREATE LIST AND ANSWER QUESTIONS UNTIL LIST IS COMPLETE
var t = new GroupPref(20, 10), c = 0, q;
while (q = t.getQuestion()) {
    var answer = 0;
    console.log(++c + ". ask " + q.people.length + " people (" + q.people + ")\n\tq: " + fruit[q.a] + " or " + fruit[q.b] + "?");
    for (i in q.people) answer += preference(q.people[i], fruit[q.a], fruit[q.b]);
    console.log("\ta: " + [fruit[q.a], "EQUAL", fruit[q.b]][answer != 0 ? answer / Math.abs(answer) + 1 : 1]);
    t.processAnswer(answer);
}

// GET ORDERED LIST AND DISPLAY RESULT
var index = t.getOrder();
console.log("LIST IN ORDER:");
for (var i = 0, pos = 1; i < index.length; i++) {
    var pre = pos + ". ";
    for (var j = 0; j < index[i].length; j++) {
        console.log(pre + fruit[index[i][j]]);
        pre = "   ";
    }
    pos += index[i].length;
}
&#13;
&#13;
&#13;