消除不可能的选择

8
我在尝试编写程序时遇到了一些困难,很难理解这个问题。为了简化问题,我们假设有一定数量的球和人数。每个人必须选择一个球,而人们可以被限制只能选择特定类型的球。目标是在消除所有不可能的组合后确定人们可以选择哪些选项。

示例1:

在一个简单的情况下,假设我们有两个人,一个红球和一个绿球。第一个人可以选择任何一个球,但第二个人只能选择绿色的球。可以用以下方式表示:

Person 1: RG
Person 2: G

由于我们知道第二个人必须选择绿色球,这意味着第一个人不能选择那个球,因此必须选择红色球。所以可以简化为:

Person 1: R
Person 2: G

在这种情况下,我们知道每个人会选择什么。
例子2:
现在让我们增加一些复杂性。现在有4个人需要从2个红球、1个绿球和4个蓝球中选择。第1个人可以选择任何球,第2个和第3个人可以选择红色或绿色球,第4个人必须选择一个红球。因此,我们有以下选择:
Person 1: RRGBBBB
Person 2: RRG
Person 3: RRG
Person 4: RR

由于第四个人只有一种选项,我们知道他必须选择红球。因此,我们可以从其他所有人中消除一个红球:

Person 1: RGBBBB
Person 2: RG
Person 3: RG
Person 4: RR

然而这就是问题所在。我们可以看到,第二个人和第三个人必须选择一个红色球和一个绿色球,我们只是不知道哪个人会选择哪个颜色的球。但由于我们只剩下一个红球和一个绿球,所以红色和绿色也可以从第一个人的选择中排除掉:

Person 1: BBBB
Person 2: RG
Person 3: RG
Person 4: RR

现在,我们可以从每个条目中删除重复项,以得到以下选项:
Person 1: B
Person 2: RG
Person 3: RG
Person 4: R

我们知道第一和第四个人的选择,而第二和第三个人在红色和绿色之间做出选择。
为了解决这个问题,我会循环遍历每个人,首先如果某个人只有一种球类型,就把这个人从数组中删除并将其放入结果数组中(因为我知道这个人必须选择这种类型的球),然后从数组中的其他人那里删除一个相同类型的球(如果有的话)。
完成这些操作后,我认为规则是:
如果有$n$个人都具有相同的$n$个选项(或其中的子集),则可以从所有其他人中删除这些选项,其中$n$小于总人数。
接下来,我再次遍历每个人,并检查其他拥有相同选项(或这些选项的子集)的人,如果这与该人的总选项数相等,则从所有其他人的选项中删除它们。
这是我目前已经解决这两种情况的方法:

// The quantity of each ball
$balls = array(
    'r' => 1,
    'g' => 1,
    'b' => 1,
    'k' => 1,
);
// The options available for each person
$options = array(
    array('r', 'g', 'b', 'k'),
    array('r', 'g'),
    array('r', 'b'),
    array('b', 'g'),
);

// Put both of these together into one array
$people = [];
foreach ($options as $option) {
    $person = [];
    foreach ($option as $ball_key) {
        $person[$ball_key] = $balls[$ball_key];
    }
    $people[] = $person;
}

print_r($people);
// This produces an array like:
// Array
// (
//     [0] => Array
//         (
//             [r] => 2
//             [g] => 1
//             [b] => 4
//         )
//
//     [1] => Array
//         (
//             [r] => 2
//             [g] => 1
//         )
//
//     [2] => Array
//         (
//             [r] => 2
//             [g] => 1
//         )
//
//     [3] => Array
//         (
//             [r] => 2
//         )
//
// )

// This will be used to hold the final result
$results = [];

do {
    // If anything changes, this needs to be set to true. Any time anything
    // changes we loop through everything again in case it caused a cascading
    // effect
    $has_change = false;

    // Step 1:
    // Find out if there are any people who have only one option and remove it
    // from the array and add it to the result and subtract one from all other
    // people with this option
    foreach ($people as $p_index => $p_options) {
        if (count($p_options) === 1) {
            $color = key($p_options);

            foreach ($people as $p_index_tmp => $p_options_tmp) {
                // It's the current person, so skip it
                if ($p_index_tmp === $p_index) {
                    continue;
                }

                if (isset($p_options_tmp[$color])) {
                    // Subtract 1 from this color from this person and if zero,
                    // remove it.
                    if (--$people[$p_index_tmp][$color] === 0) {
                        unset($people[$p_index_tmp][$color]);
                    }

                    $has_change = true;
                }
            }

            // Add to results array and remove from people array
            $results[$p_index] = array($color => 1);
            unset($people[$p_index]);
        }
    }

    // Step 2:
    // If there are $n number of people who all have the same $n number of
    // options (or a subset of them), these options can be removed
    // from all other people, where $n is less than the total number of people
    foreach ($people as $p_index => $p_options) {
        $num_options = array_sum($p_options);
        if ($num_options < count($people)) {
            // Look for other people with no different options from the ones
            // that this person has
            $people_with_same_options = [];
            foreach ($people as $p_index_tmp => $p_options_tmp) {
                foreach (array_keys($p_options_tmp) as $color) {
                    if (array_search($color, array_keys($p_options)) === false) {
                        // This color was not found in the options, so we can
                        // skip this person.
                        // (Make sure we break out of both foreach loops)
                        continue 2;
                    }
                }
                // This person has all the same options, so append to the array
                $people_with_same_options[] = $p_index_tmp;
            }

            // Remove these options from the other people if the number of
            // people with only these options is equal to the number of options
            if (count($people_with_same_options) === $num_options) {
                foreach ($people as $p_index_tmp => $p_options_tmp) {
                    if (array_search($p_index_tmp, $people_with_same_options) === false) {
                        foreach (array_keys($p_options) as $option) {
                            unset($people[$p_index_tmp][$option]);

                            $has_change = true;
                        }
                    }
                }
            }
        }
    }
}
while ($has_change === true);

// Combine any remaining people into the result and sort it
$results = $results + $people;
ksort($results);

print_r($results);

This produces the following result:

Array
(
    [0] => Array
        (
            [b] => 1
        )

    [1] => Array
        (
            [r] => 1
            [g] => 1
        )

    [2] => Array
        (
            [r] => 1
            [g] => 1
        )

    [3] => Array
        (
            [r] => 1
        )

)

Example 3:

This example does not work with the above code. Suppose there is 1 red ball, 1 green ball, 1 blue ball, one yellow ball and 4 people. Person 1 can choose any ball, person 2 can choose red or green, person 3 can choose green or blue, person 4 can choose red or blue.

Visually this would look like:

Person 1: RGBY
Person 2: RG
Person 3: GB
Person 4: RB

Since the 3 colors red, green and blue are the only options for persons 2, 3 and 4, they are completely contained within these 3 people and they can therefore can all be eliminated from person 1, meaning person 1 must choose yellow. If person 1 were to choose anything but yellow, it would be impossible for the other people to choose their balls.

Putting it into my PHP program, I have these input values:

// The quantity of each ball
$balls = array(
    'r' => 1,
    'g' => 1,
    'b' => 1,
    'y' => 1,
);
// The options available for each person
$options = array(
    array('r', 'g', 'b', 'y'),
    array('r', 'g'),
    array('r', 'b'),
    array('b', 'g'),
);

However I can't think of how to loop through to find cases like this without iterating through every single possible combination of people. Any ideas how this could be done?


2
我很想看到这个问题的结果。你可以使用蛮力技术,但这样做相当不规范。 - Luke Joshua Park
2个回答

2

我更喜欢一种更像面向对象编程的方法。所以我基本上是从头开始的。希望你没关系。

因此,下面的代码看起来(诚然)相当丑陋,而且我还没有用除了你的三个示例之外的任何东西进行过测试,但是这就是它:

class Ball {
    private $color;
    public function __construct($color) {
        $this->color = $color;
    }
    public function getColor() {
        return $this->color;
    }
}
class Ball_resource extends Ball {
    private $num_available;

    public function __construct($color, $number) {
        parent::__construct($color);
        $this->num_available = $number;
    }
    public function take() {
        $this->num_available--;
    }

    public function isExhausted() {
        return $this->num_available <= 0;
    }
}
class Person {
    /**
     *
     * @var Ball
     */
    private $allowed_balls = array();

    public function addConstraint($color) {
        $this->allowed_balls[$color] = new Ball($color);
        return $this;
    }
    public function getConstraints() {
        return $this->allowed_balls;
    }

    public function getNumberOfConstraints() {
        return count($this->allowed_balls);
    }

    /**
     * return true if removal was successful; false otherwise
     */
    public function removeConstraint(Ball $ball) { // todo remove
        if (isset ($this->allowed_balls [$ball->getColor()])) {
            unset ($this->allowed_balls [$ball->getColor()]);
            return true;
        }
        else {
            // this means our puzzle isn't solvable
            return false;
        }
    }
}
class Simplifier {
    /**
     *
     * @var Person
     */
    private $persons = array ();
    /**
     *
     * @var Ball_resource
     */
    private $availableBalls = array ();

    public function addPerson(Person $person) {
        $this->persons[] = $person;
        return $this;
    }
    public function addBallRessource(Ball_resource $ball_resource) {
        $this->availableBalls[] = $ball_resource;
        return $this;
    }


    public function getChoices() {      
        $queue = $this->persons; // shallow copy

        while (count($queue) > 0) {
            // find most constrained person(s)
            $numberOfConstraints = 1; // each person must have at least one constraint
            while (true) {
                $resolve_queue = array();
                foreach($queue as $person) {
                    if ($person->getNumberOfConstraints() === $numberOfConstraints) {
                        $resolve_queue[] = $person;
                    }
                }
                // find mutually dependent constraint groups connected with a person
                $first_run = true;
                foreach ($resolve_queue as $startPerson) {
                    // check if we havent already been removed via dependencies
                    if ($first_run || !self::contains($queue, $startPerson)) {
                        $dependent_persons = $this->findMutuallyDependentPersons($startPerson, $resolve_queue);
                        // make a set out of their combined constraints
                        $combinedConstraints = $this->getConstraintsSet($dependent_persons);
                        $this->adjustResources($dependent_persons);
                        $this->removeFromQueue($dependent_persons, $queue);
                        // substract each ball of this set from all less constrained persons
                        $this->substractConstraintsFromLessConstrainedPersons($queue, $combinedConstraints, $numberOfConstraints);
                        $first_run = false;
                        continue 3;
                    }
                }
                $numberOfConstraints++;
            }

        }
        return $this->persons; // has been altered implicitly   
    }

    private static function contains(array $haystack, Person $needle) {
        foreach ($haystack as $person) {
            if ($person === $needle) return true;
        }
        return false;
    }

    private function findMutuallyDependentPersons(Person $startPerson, array $persons) {
        // add recursion
        $output = array();
        //$output[] = $startPerson;
        foreach($persons as $person) {
            foreach ( $person->getConstraints () as $constraint ) {
                foreach ( $startPerson->getConstraints () as $targetConstraint ) {
                    if ($constraint->getColor () === $targetConstraint->getColor ()) {
                        $output [] = $person;
                        continue 3;
                    }
                }
            }   
        }
        return $output;
    }

    private function getConstraintsSet(array $persons) {
        $output = array();
        foreach ($persons as $person) {
            foreach ($person->getConstraints() as $constraint) {
                foreach($output as $savedConstraint) {
                    if ($savedConstraint->getColor() === $constraint->getColor()) continue 2;                   
                }
                $output[] = $constraint;
            }
        }
        return $output;
    }

    private function substractConstraintsFromLessConstrainedPersons(array $persons, array $constraints, $constraintThreshold) {
        foreach ($persons as $person) {
            if ($person->getNumberOfConstraints() > $constraintThreshold) {
                foreach($constraints as $constraint) {
                    foreach($this->availableBalls as $availableBall) {
                        if ($availableBall->isExhausted()) {
                            $person->removeConstraint($constraint);
                        }
                    }                   
                }
            }
        }
    }

    private function adjustResources(array $persons) {
        foreach($persons as $person) {
            foreach($person->getConstraints() as $constraint) {
                foreach($this->availableBalls as &$availableBall) {
                    if ($availableBall->getColor() === $constraint->getColor()) {
                        $availableBall->take();
                    }
                }
            }
        }
    }

    private function removeFromQueue(array $persons, array &$queue) {       
        foreach ($persons as $person) {
            foreach ($queue as $key => &$availablePerson) {
                if ($availablePerson === $person) {
                    unset($queue[$key]);
                }
            }
        }
    }
}

整个东西被称为这样:
// Example 2
{
    $person1 = new Person();
    $person1->addConstraint("R")->addConstraint("R")->addConstraint("G")->addConstraint("B")->addConstraint("B")->addConstraint("B")->addConstraint("B");
    $person2 = new Person();
    $person2->addConstraint("R")->addConstraint("R")->addConstraint("G");
    $person3 = new Person();
    $person3->addConstraint("R")->addConstraint("R")->addConstraint("G");
    $person4 = new Person();
    $person4->addConstraint("R")->addConstraint("R");

    $redBalls = new Ball_resource("R", 2);
    $greenBalls = new Ball_resource("G", 1);
    $blueBalls = new Ball_resource("B", 4);

    $simplifier = new Simplifier();
    $simplifier->addPerson($person1)->addPerson($person2)->addPerson($person3)->addPerson($person4);
    $simplifier->addBallRessource($redBalls)->addBallRessource($greenBalls)->addBallRessource($blueBalls);
    $output = $simplifier->getChoices();

    print_r($output);
}

同样适用于示例3:
// Example 3
{
    $person1 = new Person();
    $person1->addConstraint("R")->addConstraint("G")->addConstraint("B")->addConstraint("Y");
    $person2 = new Person();
    $person2->addConstraint("R")->addConstraint("G");
    $person3 = new Person();
    $person3->addConstraint("G")->addConstraint("B");
    $person4 = new Person();
    $person4->addConstraint("R")->addConstraint("B");

    $redBalls = new Ball_resource("R", 1);
    $greenBalls = new Ball_resource("G", 1);
    $blueBalls = new Ball_resource("B", 1);
    $yellowBalls = new Ball_resource("Y", 1);   

    $simplifier = new Simplifier();
    $simplifier->addPerson($person1)->addPerson($person2)->addPerson($person3)->addPerson($person4);
    $simplifier->addBallRessource($redBalls)->addBallRessource($greenBalls)->addBallRessource($blueBalls)->addBallRessource($yellowBalls);
    $output = $simplifier->getChoices();

    print_r($output);
}

为了简洁起见,我省略了原始输出。但是对于第二个示例,它基本上等同于您在问题中列出的最后一个相应列表;对于第三个示例,它生成相当于:
Person 1: Y
Person 2: RG
Person 3: GB
Person 4: RB

我还没有机会仔细查看你的代码,但在我进行的几个测试中,它似乎正常工作。然而,也许你可以添加一些解释说明它是如何实现的,而不仅仅是发布代码。 - Mike
上面的代码远非完美(例如,removeConstraint() 的返回值从未被使用)...但如果我有时间,我会添加更多的解释。 - morido
我不确定是你的逻辑有误还是代码,然而在 Simplifier::getChoices() 方法中有一些看起来像错误的奇怪东西,例如 $first_run 总是为真(因此 self::contains() 从未执行),以及所有这些循环似乎只是为了找到第一个拥有最少约束的人。它似乎接近正确,但我想知道你的头脑中是否有正确的想法,只是没有在代码中表达出来。就像我之前说的,如果你解释一下它应该如何工作的逻辑,可能会帮助我更好地理解它。 - Mike
还有一件事需要记住,看起来你的回答是基于一个人所拥有的约束数量,但请记住这些约束可能是无关的。例如,人1可以选择球A和B,人2可以选择球D、E、F,而人3只能选择球G。因此,即使一个人比其他人更受限制,但在这种情况下,这些约束都是完全无关的,一个人的约束对另一个人没有影响。 - Mike
就像我之前说的,也许如果你解释一下它应该如何工作的逻辑,那么我就能更好地理解它了。- 你到底不理解现在的逻辑是什么?实际上,我觉得它非常简单明了...它确实有一些小问题和限制。但这是一个开始。 - morido
显示剩余2条评论

1
最终我决定采用的方法是一种半暴力算法,但我对其进行了调整,使其不必遍历每个组合,在大多数情况下,迭代次数应该比可能的每个组合要少得多。
总组合数等于exp(2, count(balls))(即2^{球的类型}),所以如果我们有20种球,那么就有1048576种不同的球组合需要检查。如果要检查每一个组合,这将是太多的迭代,甚至在只有16个球颜色时我就已经耗尽了内存。
要获取所有可能的组合,可以使用此函数(来源):
function getAllCombinations($balls) {
    // initialize by adding the empty set
    $results = array(array( ));

    foreach ($balls as $color => $number) {
        foreach ($results as $combination) {
            $results[] = array_merge(array($color), $combination);
        }
    }

    return $results;
}

然而,如果我们回到原始规则:如果有$n$个人都有相同的$n$个选项(或其子集),则可以从所有其他人中删除这些选项,其中$n$小于总人数。如我们所见,如果我们已经超过了$n$个选项,我们可以跳过任何未来的迭代。请注意,在此函数中,当我们有多个相同颜色的球时,这将计算为多个球。一旦我们得到了不同的可能子集,我们循环遍历人员以查看是否有$n$个不同的人使用该子集,并从所有其他人中删除这些值。这就是我最终想出的方法。
/**
 * This just gets a sum of the number of balls a person can choose
 * from
 */
function getNumberOfOptions($person, $balls) {
    $n = 0;
    foreach ($person as $allowed_ball) {
        $n += $balls[$allowed_ball];
    }
    return $n;
}

/**
 * This function finds all the subsets of the balls array that we can look for
 * in the people array later to remove options from them
 */
function getBallSubsets($balls, $max_n) {
    // initialize by adding the empty set
    $results = array(array( ));

    // Remove all balls that have more options than max_n
    foreach ($balls as $color => $number) {
        if ($number > $max_n) {
            unset($balls[$color]);
        }
    }

//    $n = 0;
    foreach ($balls as $color => $number) {
        foreach ($results as $combination) {
//            $n++;
            $set = array_merge(array($color), $combination);
            if (getNumberOfOptions($set, $balls) <= $max_n) {
                $results[] = $set;
            }
        }
    }

//    echo $n; exit;  
    return $results;
}

function removeFromOthers($colors, $people, $skip_indexes) {
    foreach ($people as $p_index => $person) {
        if (array_search($p_index, $skip_indexes) === false) {
            foreach ($colors as $color) {
                $index = array_search($color, $person);
                if ($index !== false) {
                    unset($people[$p_index][$index]);
                }
            }
        }
    }
    return $people;
}

function getOptions($people, $balls) {
    $ball_subsets = getBallSubsets($balls, count($people) -1);

    foreach ($ball_subsets as $sub) {
        $num_colors = count($sub);
        $num_people = getNumberOfOptions($sub, $balls);

        // Loop through people and if we find $n people with only the elements
        // from this subset, we can remove these elements from all other people
        $found = [];
        $found_colors = [];
        foreach ($people as $p_index => $person_arr) {
            foreach ($person_arr as $color) {
                // Contains another color, so skip this one
                if (array_search($color, $sub) === false) {
                    continue 2;
                }
            }
            $found[] = $p_index;
            foreach ($person_arr as $color) {
                if (array_search($color, $found_colors) === false) {
                    $found_colors[] = $color;
                }
            }
        }
        if (count($found) === $num_people && count($found_colors) == $num_colors) {
            $people = removeFromOthers($sub, $people, $found);
        }
        else {
            unset($found);
        }
    }
    return $people;
}

// The quantity of each ball
$balls = array(
    'r' => 3,
    'g' => 2,
    'b' => 1,
    'k' => 16,
);
// The options available for each person
$people = array(
    array('r', 'g', 'b', 'k'),
    array('r', 'g'),
    array('r', 'b'),
    array('b', 'g'),
);

print_r($people);
$options = getOptions($people, $balls);
print_r($options);

这似乎适用于我尝试过的任何值。在上面的例子中,我们有4种不同的球颜色(2 ^ 4 = 16种组合),但是我们只需要在getBallSubsets()函数中进行6次迭代,因此这比强制执行每个可能的组合要高效得多。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接