$value) { $form_list[htmlspecialchars($key)] = htmlspecialchars($value); } /* Variable List - User Inputs */ $r = intval($form_list["r"]); // R = # of Roommates to a room/pairing $k = intval($form_list["k"]); // K = # of students applicants $a = intval($form_list["a"]); // A = # of choosen applicants $b = intval($form_list["b"]); // B = # of banned pairings $q = $a/$r; // Q = # of required pairings $q_list = array(); // The List of approved students. // Show all array values and processes $GLOBALS['show_debug'] = false; if (isset($form_list['show_debug'])) { $GLOBALS['show_debug'] = true; } if ($GLOBALS['show_debug']) { echo "

Form List

";
                print_r($form_list);
                echo "
"; } // Extract List of Applicants to Array if (isset($form_list["applicants"]) && strlen($form_list["applicants"]) > 0) { $applicants = array_map('trim', explode(PHP_EOL, $form_list["applicants"])); // List of applicants from user $num_submissions = count($applicants); // Limit User Inputs to Max of their applicant entries $k = min($k, $num_submissions); // K = # of students applicants $rand_finalist_limit = (int) ($num_submissions * (rand(70, 100) / 100)); //(create a random percentage (between 70% - 100%) to generate number of accepted applicants ) $a = min($a, $rand_finalist_limit); // A = # of choosen applicants // B = # of Possible banned pairings based on permutation of n! / (n-r)! // Also generate number of groupings based on percentage. // Example 500 possible pairings * 90% will generate a ban list of 450 pairings. $lower_perm = $k - $r; $factk = fact($k); $factl = fact($lower_perm); $q = (int)($a/$r); // Q = # of required pairings $b = (int)(($factk / $factl) - $q ); //Total possible pairings - required pairings = possible max banned list } else { try { /* Variable List - Default Values */ $applicants_text = explode(PHP_EOL, file_get_contents("student_list.txt")); shuffle($applicants_text); $applicants = array_slice($applicants_text, 0, $k); if ($GLOBALS['show_debug']) { echo "

Applicants text from student_list.txt

".print_r($applicants, true)."
"; } } catch(Exception $e) { echo "Could Not Find File student_list.txt: " . $e->getMessage(); exit(); } } // Extract or Generate List of Banned Pairings if (isset($form_list["ban_relations"]) && strlen($form_list["ban_relations"]) > 0) { $ban_relations_list = explode(PHP_EOL, $form_list["ban_relations"]); // List of applicants from user if ($GLOBALS['show_debug']) { echo "

Form List

";
                print_r($ban_relations_list);
                echo "
"; echo "

applicants List

";
                print_r($applicants);
                echo "
"; } $ban_relations = array(); //List of User Inputs foreach ($ban_relations_list as $pairing) { $applicant = array_map('trim', explode(",", $pairing)); if ($GLOBALS['show_debug']) { echo "

applicant pairing

";
                print_r($applicant);
                echo "
"; } $temp_arr = array(); for ($v=0; $v < count($applicant); $v++) { $key_app = array_search($applicant[$v], $applicants); if ($key_app == false) { echo " "; exit(); } $temp_arr[$key_app] = $applicant[$v]; } $ban_relations[] = $temp_arr; } } else { try { // Create realistic set of deans ban list. // Generate all possible combinations of students based on number of students per pairing $ban_relations = $combinatorics->combinations($applicants, $r); } catch(Exception $e) { echo "Could Not Create Combination List: " . $e->getMessage(); exit(); } } // Randomize and Slice a Selection of the Total Combinations to make a realistic Ban List (subset of total combination list). shuffle($ban_relations); $trim_ban_relations = array_slice($ban_relations, 0, $b); if ($GLOBALS['show_debug']) { echo '

Debug - List of Banned Combinations

'.print_r($trim_ban_relations, true).'
'; } // create list of ranked applicants & sets their initial ranked values to zero. $ranked_list = array(); for ($i = 0; $i < count($applicants); $i++) { $ranked_list[$applicants[$i]] = 0; } /* If the Number of students per pairing was > 2, Then you would only need to make a function that breaks the pairings into groupings of 2, before processing them. Ex: Banned pairing: student_1, student_2, student_3 would become : student_1, student_3 student_1, student_2 student_3, student_2 */ //Create Ranked List and Sort List of Applicants By # of Bad Relations $ranked_list = rank_users($trim_ban_relations); if ($GLOBALS['show_debug']) { echo '

Debug - List of Ranked Users

Create Ranked List and Sort List of Applicants By # of Bad Pairings each applicant is listed in

'; echo "
";
                print_r($ranked_list);
                echo "
"; } // Show Results renderApplicants($k, $r, $a, $q, $b, $applicants); renderCombinations($k, $ban_relations); renderBannedList($b, $ban_relations, $trim_ban_relations); pair_delist($ranked_list, $q_list, $q, $trim_ban_relations, $applicants); } /* Functions */ // Create rank_list from banned applicant relationships function rank_users($arr_banned) { $ranked_list = array(); foreach($arr_banned as $banned) { //check and increment first and last element of each random pair found in ban list. if (empty($ranked_list[$banned[array_key_first($banned)]])) { $ranked_list[$banned[array_key_first($banned)]] = 1; } else { $ranked_list[$banned[array_key_first($banned)]]++; } if (empty($ranked_list[$banned[array_key_last($banned)]])) { $ranked_list[$banned[array_key_last($banned)]] = 1; } else { $ranked_list[$banned[array_key_last($banned)]]++; } } asort($ranked_list); return $ranked_list; } // Splits ranked list into groupings based on # appearances on the ban list. function rank_grouping($arr) { $rank_sort = array(); foreach ($arr as $applicant => $value) { $rank_sort[$value][] = $applicant; } return $rank_sort; } function pairingCheck($temparr,$active_ban_relations) { // If false, then pair is from ban list // If true then a match is not found and approved if ($GLOBALS['show_debug']) { echo '

Debug - Pairing Check

Checks Potential Pairing against Banned List of Pairings


'; } foreach ($active_ban_relations as $ban_pair) { $bf_key = array_key_first($ban_pair); $bl_key = array_key_last($ban_pair); if ( (($temparr[0] == $ban_pair[$bf_key]) && ($temparr[1] == $ban_pair[$bl_key])) || (($temparr[0] == $ban_pair[$bl_key]) && ($temparr[1] == $ban_pair[$bf_key])) ) { if ($GLOBALS['show_debug']) { echo "

found a similarity between ((".$temparr[0]." == ".$ban_pair[$bf_key].") && (".$temparr[1]." == ".$ban_pair[$bl_key].")) || ((".$temparr[0]." == ".$ban_pair[$bl_key].") && (".$temparr[1]." == ".$ban_pair[$bf_key]."))

"; } return false; } } return true; } function pair_delist($ranked_list, $q_list, $q, $active_ban_relations,$applicants){ if ($GLOBALS['show_debug']) { echo '

Current List of Approved Students

'.print_r($q_list).'
'; } // Group List By Ranks $rank_group = rank_grouping($ranked_list); $f_key = array_key_first($rank_group); $l_key = array_key_last($rank_group); if (count($q_list) >= $q) { // If count of qualified list >= quota size $list = ""; foreach ($q_list as $accepted_pairing) { $list .= implode(', ', $accepted_pairing)."
"; } $ban_list = ""; foreach ($active_ban_relations as $b_pair) { $ban_list .= implode(', ', $b_pair)."
"; } echo "
"; if ($GLOBALS['show_debug']) { echo "
"; } } else if ($f_key == $l_key) { /* when all the keys left are existing in same rank list level - these are the 'worst' problems to sort for the program. */ for ($i = 0; $i < (count($rank_group[$f_key])/2); $i+=2) { // Take all the lowest value keys applicants and combine them with the highest value keys applicants . IE: add them to the completed approved list. //When array doesnt intersect with the ban list, then add it as approved. $temparr = array($rank_group[$f_key][$i],$rank_group[$f_key][$i+1]); if ($GLOBALS['show_debug']) { echo '
Testing pair: '.implode(', ', $temparr); } if (($f_key == 0) || pairingCheck($temparr,$active_ban_relations) == true) { $q_list[] = $temparr; if ($GLOBALS['show_debug']) { echo "

New Pairing Accepted: ".$rank_group[$f_key][$i].", ".$rank_group[$f_key][$i+1]."

"; } unset($ranked_list[$rank_group[$f_key][$i]]); unset($ranked_list[$rank_group[$f_key][$i+1]]); unset($rank_group[$f_key][$i]); unset($rank_group[$f_key][$i+1]); // If it makes it past here, its returned all the checks, and should now remove all occurences from the active ban list. $rf_key = array_key_first($temparr); $rl_key = array_key_last($temparr); $count = 0; foreach ($active_ban_relations as $bankey => $ban) { $count++; if ($GLOBALS['show_debug']) { echo '
Checking Pairing: '.print_r($temparr).'

'; echo '
Testing against ban pair: '.print_r($ban).'

--
'; } if (in_array($temparr[$rf_key], $ban) || in_array($temparr[$rl_key], $ban)) { if ($GLOBALS['show_debug']) { echo "

found a match to remove: ".$count."

"; echo '
Checking Pairing: '.print_r($temparr).'
'; echo '
Ban List Item: '.print_r($ban).'
'; } unset($active_ban_relations[$bankey]); } } if ($GLOBALS['show_debug']) { echo "
checked student matching ".$count. " times: ".print_r($temparr, true); } } else { // When the 1st item in the first group is a bad match with the 1st item in last group, shuffle the ranked_list if ($GLOBALS['show_debug']) { echo "
this matching: ".$rank_group[$f_key][$i]." and ".$rank_group[$l_key][$i+1]." was banned
"; } //https://stackoverflow.com/questions/4102777/php-random-shuffle-array-maintaining-key-value //https://stackoverflow.com/users/2350217/mohamed-bouallegue uksort($ranked_list, function() { return rand() > getrandmax() / 2; }); } } if (count($q_list) < $q) { /* Edge Case: Ban list wasn't enough, so you'll need to Take students from main pool that aren't already added and add them to the list. */ $squashed_list = array_merge(...$q_list); if ($GLOBALS['show_debug']) { echo "

Ban list didnt meet requirements, here is the squashed qlist

"; echo '
'.print_r($squashed_list, true).'
'; echo "

here is the original applicant list

"; echo '
'.print_r($applicants, true).'
'; echo "

After doing an array diff with the original list, here are some students that are level 0 we can pair up.

"; } $total_remaining_apps = array_diff($applicants, $squashed_list); if ($GLOBALS['show_debug']) { echo '
'.print_r($total_remaining_apps, true).'
'; echo "

And since we know they werent generated from the ban list, then these are all level 0 and can be combined with each other.
so we randomly shuffle slice this array by number remaining *2

"; } shuffle($total_remaining_apps); $selected_apps = array_slice($total_remaining_apps, 0, ((($q-count($q_list))*2))); if ($GLOBALS['show_debug']) { echo '
'.print_r($selected_apps, true).'
'; } for ($i = 0; $i < (count($selected_apps)/2); $i+=2) { // Take all the lowest value keys applicants and combine them with the highest value keys applicants . IE: add them to the completed approved list. //When array doesnt intersect with the ban list, then add it as qualified. $temparr = array($selected_apps[$i],$selected_apps[$i+1]); $q_list[] = $temparr; } } pair_delist($ranked_list, $q_list, $q, $active_ban_relations,$applicants); } else { // completed approved list is less than the student pair quota. if (empty($q_list) || count($q_list) < $q) { for ($i = 0; $i < min(count($rank_group[$f_key]),count($rank_group[$l_key])); $i++) { // Take all the lowest value keys applicants and combine them with the highest value keys applicants . IE: add them to the completed approved list. // Set both applicants to rank -1 on the active rank list. //When array doesnt intersect with the ban list, then add it as approved. $temparr = array($rank_group[$f_key][$i],$rank_group[$l_key][$i]); if ($GLOBALS['show_debug']) { echo '
Testing pair: '.implode(', ', $temparr); } if (($f_key == 0) || pairingCheck($temparr,$active_ban_relations) == true) { if ($GLOBALS['show_debug']) { echo "

New Pairing Accepted: ".$rank_group[$f_key][$i].", ".$rank_group[$l_key][$i]."

"; } // Add approved pairing to qualified list, and remove it from the ranked list and ranked group. $q_list[] = $temparr; unset($ranked_list[$rank_group[$f_key][$i]]); unset($ranked_list[$rank_group[$l_key][$i]]); unset($rank_group[$f_key][$i]); unset($rank_group[$l_key][$i]); // If it makes it past here, its returned all the checks, and should now remove all occurences from the active ban list. $rf_key = array_key_first($temparr); $rl_key = array_key_last($temparr); $count = 0; foreach ($active_ban_relations as $bankey => $ban) { if ($GLOBALS['show_debug']) { echo '
Checking Pairing: '.print_r($temparr, true).'

'; echo '
Testing against ban pair: '.print_r($ban, true).'

--
'; } $count++; if (in_array($temparr[$rf_key], $ban) || in_array($temparr[$rl_key], $ban)) { if ($GLOBALS['show_debug']) { $count++; echo "

found a match to remove: ".$count."

"; echo '
Checking Pairing: '.print_r($temparr, true).'
'; echo '
Ban List Item: '.print_r($ban, true).'
'; } unset($active_ban_relations[$bankey]); } } if ($GLOBALS['show_debug']) { echo "
checked student matching ".$count. " times: ".print_r($temparr, true); } } else { if ($GLOBALS['show_debug']) { echo "
this matching: ".$rank_group[$f_key][$i]." and ".$rank_group[$l_key][$i]." was banned
"; } // When the 1st item in the first group is a bad match with the 1st item in last group, shuffle the ranked_list //https://stackoverflow.com/questions/4102777/php-random-shuffle-array-maintaining-key-value //https://stackoverflow.com/users/2350217/mohamed-bouallegue uksort($ranked_list, function() { return rand() > getrandmax() / 2; }); } } } pair_delist($ranked_list, $q_list, $q, $active_ban_relations,$applicants); } } // Get Factorial // https://www.geeksforgeeks.org/php-factorial-number/ function fact($n){ if ($n <= 1){return 1;} else {return $n * fact($n - 1);} } /* START - Template functions */ function renderApplicants($k, $r, $a, $q, $b, $applicants) { echo '

Explained Process & Results

Student List ('.$k.' Applicants | '.$r.' Students Per Room | '.$a.' Applicants Can Be Chosen | '.$q.' Acceptable Pairings)

Applicant List: '.$k.' Students:
'; } function renderCombinations($k, $ban_relations) { echo '

All-Combinations of '.$k.' applicants ('.count($ban_relations).' Possible Pairings Found)

This is using all possible combinations to create a list of bad pairings to pull from.

'; } function renderBannedList($b, $ban_relations, $trim_ban_relations) { echo '

Deans Bad List of Groupings ('.min($b,count($ban_relations)).' / '.count($ban_relations).' Possible Pairs)

This shuffles the All-Combinations applicants list, and selects the first '.min($b,count($ban_relations)).' pairs to create a dean ban list.

'; if (!$GLOBALS['show_debug'] || ($GLOBALS['show_debug']) == false) { echo '

'; } } /* END - Template functions */ ?>

Roommate Matching Demo

Solution to the Roommate Matching Puzzle from Clay Math Institute
Source Code Available Via: Github




Custom - Use Your Own Applicants Lists: