【问题标题】:Cartesian product with specific criteria具有特定标准的笛卡尔积
【发布时间】:2017-03-31 12:15:52
【问题描述】:

我正在尝试查找笛卡尔积并附加特定标准。

我有四个池,每个池 25 人。每个人都有一个分数和一个价格。每个池子里的每个人看起来都是这样的。

[0] => array(
    "name" => "jacob",
    "price" => 15,
    "score" => 100
),
[1] => array(
    "name" => "daniel",
    "price" => 22,
    "score" => 200
)

我想找到最佳的人员组合,从每个池中挑选一个人。但是,有一个上限价格,任何分组都不能超过某个价格。

我一直在弄乱笛卡尔和排列函数,似乎无法弄清楚如何做到这一点。我知道如何编写代码的唯一方法是嵌套 foreach 循环,但这非常费力。

如您所见,下面的这段代码效率非常低。特别是如果池增加!

foreach($poolA as $vA) {
   foreach($poolb as $vB) {
       foreach($poolC as $vC) {
            foreach($poolD as $vD) {

                // calculate total price and check if valid
                // calculate total score and check if greatest
                // if so, add to $greatest array

            }
        }
    }    
}      

我还认为我可以找到一种方法来计算总价格/得分比率并将其用于我的优势,但我不知道我错过了什么。

【问题讨论】:

  • 您可以通过按价格对每个池中的人进行排序来改进算法。当您达到价格超过限制的价格时,您不必尝试该池中的其余部分。
  • @Barmar 真是天才!这绝对应该减少许多循环。谢谢。
  • 你的蛮力方法与排列无关(也不应该)。

标签: php arrays algorithm permutation cartesian-product


【解决方案1】:

正如Barmar 所指出的,对每个池中的人进行排序可以让您在总价格超过限制时提前停止循环,从而减少您需要检查的案例数量。但是,应用这种改进的渐近复杂度仍然是 O(n4)(其中n 是池中的人数)。

我将概述一种具有更好渐近复杂度的替代方法,如下所示:

  1. 构建一个池 X,其中包含所有对的人,其中一个来自池 A,另一个来自池 B
  2. 构建一个池 Y,其中包含所有对的人,其中一个来自池 C,另一个来自池 D
  3. 按总价对池X 中的货币对进行排序。然后对于价格相同的任何一对,保留得分最高的一对并丢弃剩余的一对。
  4. 按总价对池Y 中的货币对进行排序。然后对于价格相同的任何一对,保留得分最高的一对并丢弃剩余的一对。
  5. 用两个指针循环检查所有满足价格约束的可能组合,其中head 指针从池X 中的第一项开始,tail 指针从池中的最后一项开始池Y。下面给出示例代码来说明这个循环是如何工作的:

================================================ =============================

$head = 0;
$tail = sizeof($poolY) - 1;

while ($head < sizeof($poolX) && $tail >= 0) {
    $total_price = $poolX[$head].price + $poolY[$tail].price;

    // Your logic goes here...

    if ($total_price > $price_limit) {
        $tail--;
    } else if ($total_price < $price_limit) {
        $head++;
    } else {
        $head++;
        $tail--;
    }
}

for ($i = $head; $i < sizeof($poolX); $i++) {
    // Your logic goes here...
}

for ($i = $tail; $i >= 0; $i--) {
    // Your logic goes here...
}

================================================ =============================

步骤1和2的复杂度是O(n2),步骤3和4的复杂度可以在O(n2 log(n )) 使用平衡二叉树。而第5步本质上是对n2项的线性扫描,所以复杂度也是O(n2)。因此这种方法的总体复杂度是 O(n2 log(n))。

【讨论】:

  • 这是一个很酷的解决方案。有点不相关,但是找到两个列表组合的最佳方法是什么?它仍然是 foreach 中的 foreach 吗?当我需要为一个列表执行此操作时,我将列表分成两半并循环遍历两者。另外,最后两个 for 循环有什么作用?
  • @JacobRaccuia (1) 对于大小为mn 的列表,总共有mn 对,所以它需要Ω(mn) 时间(打印输出),所以两个嵌套for 循环已经是你能做的最好的了。 (2) 请注意,有可能$head 没有到达$poolX 的结尾或$tail 没有到达$poolY 的开头,所以最后两个for 循环是为了确保它们扫描所有可能的情况。
  • 我明白了。那是因为$head$tail 可以是不同的长度。如果它们的长度相同,那么它们不会触发?
  • @JacobRaccuia while 循环的指针基于$total_price 的值与$price_limit 相比得到更新,因此$head 有可能到达$poolX 的末尾而$tail 仍然挂在$poolY 的中间,反之亦然,所以这与$poolX$poolY 的长度没有直接关系。
  • 我不会撒谎,我还是不明白。我将数据放入其中,但我不知道如何找出 $poolX$poolY 中的哪一个会产生最佳组合。
【解决方案2】:

关于您的方法有几点需要注意。严格来说,从数学的角度来看,您计算的排列比得出明确答案实际需要的要多。

在组合学中,为了得出产生所有可能组合所需的确切排列数量,需要提出两个重要问题。

  1. 顺序重要吗? (对于你的情况,它没有)
  2. 是否允许重复? (对于您的情况,无需重复)

由于这两个问题的答案都是,因此您只需要您当前对嵌套循环进行的一小部分迭代。目前您正在执行pow(25, 4) 排列,即390625。您实际上只需要n! / r! (n-r)!gmp_fact(25) / (gmp_fact(4) * gmp_fact(25 - 4)),这只是12650 所需的总排列。

这是一个简单的函数示例,它使用 PHP 中的生成器(取自 this SO answer)生成不重复的组合(并且顺序无关紧要)。

function comb($m, $a) {
    if (!$m) {
        yield [];
        return;
    }
    if (!$a) {
        return;
    }
    $h = $a[0];
    $t = array_slice($a, 1);
    foreach(comb($m - 1, $t) as $c)
        yield array_merge([$h], $c);
    foreach(comb($m, $t) as $c)
        yield $c;
}

$a = range(1,25); // 25 people in each pool
$n = 4; // 4 pools

foreach(comb($n, $a) as $i => $c) {
    echo $i, ": ", array_sum($c), "\n";
}

修改生成器函数以检查价格总和是否达到/超过所需阈值并仅从那里返回有效结果(即在需要时提前放弃)非常容易。 p>

对于您的用例来说,重复和顺序在这里并不重要的原因是,无论您添加$price1 + $price2 还是$price2 + $price1,结果无疑在两种排列中都是相同的。因此,您只需将每个唯一集合相加一次即可确定所有可能的总和。

【讨论】:

  • 谢谢。我会看看这个。在我的具体情况下,每个池中有不同数量的人。我该如何解释呢?我也有点困惑如何使用这个功能。我应该把保存数据的四个数组放在哪里? (将数据合并到一个数组中会更容易吗?)
  • 泳池中有多少人并不重要。该解决方案不考虑对每个集合进行选择,而是将集合中的所有可用选择唯一地组合到给定大小(即$m)。如果您想将集合中的所有成员组合成 4 的组合,则这样做不会重复任何成员。如果您需要一个独特的过滤标准来避免内置组合,您可以从生成器函数本身中强加这样的过滤器,尽管我发现它可能是不必要的。
  • 我仍然不明白我的数组在您的示例中的位置。如何在函数中调用它们?
【解决方案3】:

类似于 chiwangs 的解决方案,您可以预先消除每个组成员,如果该组中存在另一个组成员,则以较低的价格获得相同或更高的分数。 也许您可以使用这种方法消除每个组中的许多成员。

然后,您可以使用此技术构建两个对并重复过滤(消除对,如果存在另一个对,则以相同或更低的成本获得更高的分数),然后以相同的方式组合这些对,或者添加一个成员一步一步(一对,三重,四重奏)。

如果有会员自己超过了允许的总价,可以提前淘汰。

如果您按分数降序对 4 个组进行排序,并找到一个解决方案 abcd,其中总价格是合法的,那么您找到了给定 abc 集合的最优解决方案。

【讨论】:

  • 我做了很多你说的事情来帮助优化我的代码。谢谢!
【解决方案4】:

这里的回复帮助我找到了最好的方法来做到这一点。

我还没有优化这个功能,但基本上我一次遍历两个结果,以找到两个池中每个组合的组合工资/分数。

我将组合的薪水 -> 分数组合存储在一个新数组中,如果薪水已经存在,我将比较分数并删除较低的。

$results = array();
foreach($poolA as $A) {
    foreach($poolB as $B) {
        $total_salary = $A['Salary'] + $B['Salary'];
        $total_score =  $A['Score'] + $B['Score'];
        $pids = array($A['pid'], $B['pid']);

        if(isset($results[$total_salary]) {
             if($total_score > $results[$total_salary]['Score']) {
                 $results[$total_salary]['Score'] => $total_score;
                 $results[$total_salary]['pid'] => $pids; 
        } else {
            $results[$total_salary]['Score'] = $total_score;
            $results[$total_salary]['pid'] = $pids;
        }
    }         
}

在这个循环之后,我有另一个相同的循环,除了我的 foreach 循环在 $results 和 $poolC 之间。

foreach($results as $R) {
    foreach($poolC as $C) {

最后,我为 $poolD 做了最后一次。

我正在通过将所有四个 foreach 循环合二为一来优化代码。

感谢大家的帮助,我能够遍历 9 个列表,每个列表有 25 人以上,并在极快的处理时间内找到最佳结果!

【讨论】:

    猜你喜欢
    • 2021-11-20
    • 2021-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多