Skip to content

Commit

Permalink
backtrack
Browse files Browse the repository at this point in the history
  • Loading branch information
inker committed Sep 29, 2024
1 parent dd9a516 commit d349d70
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 52 deletions.
222 changes: 170 additions & 52 deletions src/engine/dfs/ls/generateSchedule/splitMatchdaysIntoDays.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { countBy, minBy, orderBy, pull, shuffle, sumBy } from 'lodash';
import { countBy, difference, orderBy, shuffle } from 'lodash';

import { type UefaCountry } from '#model/types';
import type Tournament from '#model/Tournament';
import { findFirstSolution } from '#utils/backtrack';
import combine from '#utils/combine';

interface Team {
readonly name: string;
Expand All @@ -24,13 +26,11 @@ export default ({
const numMatchdays = matchdays.length;

const numTeamsByCountry = countBy(teams, team => team.country);
const countriesWithMultipleTeams = Object.keys(numTeamsByCountry).filter(
c => numTeamsByCountry[c] > 1,
) as UefaCountry[];
const countriesWithMultipleTeamsSet = new Set(countriesWithMultipleTeams);
const allCountries = Object.keys(numTeamsByCountry) as UefaCountry[];

const newMatchdays: (readonly [number, number])[][][] = [];
for (const [matchdayIndex, md] of matchdays.entries()) {
const shuffledMd = shuffle(md);
const days = Array.from(
{
// TODO: remove this hardcode
Expand All @@ -45,59 +45,177 @@ export default ({
);
const numGamesPerDay = matchdaySize / days.length;

// TODO: build a graph of paired teams

for (const pair of tvPairings) {
for (const t of shuffle(pair)) {
const pairedT = t === pair[0] ? pair[1] : pair[0];
const team = teams[t];
const teamCountry = team.country;
const match = md.find(m => m[0] === t || m[1] === t);
if (!match) {
// Already allocated
continue;
}
const nonFullDays = days.filter(day => day.length < numGamesPerDay);
const minDay = minBy(shuffle(nonFullDays), day =>
sumBy(day, m =>
m[0] === pairedT || m[1] === pairedT
? 1000000
: (teams[m[0]].country === teamCountry ? 1 : 0) +
(teams[m[1]].country === teamCountry ? 1 : 0),
let solution;
for (
let numEliminatedPairings = 0;
numEliminatedPairings < tvPairings.length;
++numEliminatedPairings
) {
for (const eliminatedTvPairings of combine(
tvPairings.toReversed(),
numEliminatedPairings,
)) {
// eslint-disable-next-line no-console
console.log('Eliminating the following:', eliminatedTvPairings);
const remainingTvPairings = difference(
tvPairings,
eliminatedTvPairings,
);

const getPairedTeam = new Map([
...remainingTvPairings,
...remainingTvPairings.map(
pair => pair.toReversed() as [number, number],
),
)!;
minDay.push(match);
pull(md, match);
]);

const s = findFirstSolution(
{
matchIndex: 0,
pickedDay: 0,
schedule: [] as number[],
numMatchesByDay: Array.from(
{
length: days.length,
},
() => 0,
),
dayByTeam: {} as Record<number, number>,
countryTeamsByDay: Object.fromEntries(
allCountries.map(
country => [country, days.map(() => 0)] as const,
),
) as Record<UefaCountry, number[]>,
},
{
reject: c => {
if (days.length === 1) {
return false;
}

if (c.numMatchesByDay[c.pickedDay] === numGamesPerDay) {
return true;
}

const match = shuffledMd[c.matchIndex];
const [firstTeam, secondTeam] = match;
const firstPairedTeam = getPairedTeam.get(firstTeam);
if (
firstPairedTeam !== undefined &&
c.dayByTeam[firstPairedTeam] === c.pickedDay
) {
return true;
}
const secondPairedTeam = getPairedTeam.get(secondTeam);
if (
secondPairedTeam !== undefined &&
c.dayByTeam[secondPairedTeam] === c.pickedDay
) {
return true;
}

return false;
},

accept: c => c.matchIndex === shuffledMd.length - 1,

generate: c => {
const match = shuffledMd[c.matchIndex];

const newSchedule = [...c.schedule, c.pickedDay];

const newNumMatchesByDay = c.numMatchesByDay.with(
c.pickedDay,
c.numMatchesByDay[c.pickedDay] + 1,
);

const newDayByTeam = {
...c.dayByTeam,
[match[0]]: c.pickedDay,
[match[1]]: c.pickedDay,
};

const newCountryTeamsByDay = {
...c.countryTeamsByDay,
[teams[match[0]].country]: c.countryTeamsByDay[
teams[match[0]].country
].with(
c.pickedDay,
c.countryTeamsByDay[teams[match[0]].country][c.pickedDay] + 1,
),
[teams[match[1]].country]: c.countryTeamsByDay[
teams[match[1]].country
].with(
c.pickedDay,
c.countryTeamsByDay[teams[match[1]].country][c.pickedDay] + 1,
),
};

const candidates: (typeof c)[] = [];
for (let dayIndex = 0; dayIndex < days.length; ++dayIndex) {
candidates.push({
matchIndex: c.matchIndex + 1,
pickedDay: dayIndex,
schedule: newSchedule,
numMatchesByDay: newNumMatchesByDay,
dayByTeam: newDayByTeam,
countryTeamsByDay: newCountryTeamsByDay,
});
}
return orderBy(shuffle(candidates), newCandidate => {
const [h, a] = shuffledMd[newCandidate.matchIndex];
const numTeamsFromHomeCountry =
newCandidate.countryTeamsByDay[teams[h].country][
newCandidate.pickedDay
];
const numTeamsFromAwayCountry =
newCandidate.countryTeamsByDay[teams[a].country][
newCandidate.pickedDay
];
return (
(numTeamsFromHomeCountry === 0
? -1000000
: numTeamsFromHomeCountry) +
(numTeamsFromAwayCountry === 0
? -1000000
: numTeamsFromAwayCountry)
);
});
},
},
);

if (s) {
if (numEliminatedPairings > 0) {
// eslint-disable-next-line no-console
console.log(
`solution found after eliminating ${numEliminatedPairings} pairings:`,
eliminatedTvPairings,
);
}
solution = s;
break;
}

// eslint-disable-next-line no-console
console.warn('No solution found');
}
}

const remainingTeams = md.flat();
const orderedRemainingTeams = orderBy(
shuffle(remainingTeams),
t => countriesWithMultipleTeamsSet.has(teams[t].country),
'desc',
);
for (const t of orderedRemainingTeams) {
const team = teams[t];
const teamCountry = team.country;
const match = md.find(p => p[0] === t || p[1] === t);
if (!match) {
// Already allocated
continue;
if (solution) {
break;
}
const nonFullDays = days.filter(day => day.length < numGamesPerDay);
const minDay = minBy(shuffle(nonFullDays), day =>
sumBy(
day,
m =>
(teams[m[0]].country === teamCountry ? 1 : 0) +
(teams[m[1]].country === teamCountry ? 1 : 0),
),
)!;
minDay.push(match);
pull(md, match);
}

if (!solution) {
throw new Error('No solution found after all');
}

for (const [i, dayIndex] of solution.schedule.entries()) {
const match = shuffledMd[i];
days[dayIndex].push(match);
}
days[solution.pickedDay].push(shuffledMd[solution.matchIndex]);

newMatchdays.push(shuffle(days.map(day => shuffle(day))));
}

Expand Down
15 changes: 15 additions & 0 deletions src/utils/combine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function* combine<T>(arr: readonly T[], k: number): Generator<T[]> {
if (k === 0) {
yield [];
return;
}

for (let i = 0; i <= arr.length - k; ++i) {
const rest = arr.slice(i + 1);
for (const combination of combine(rest, k - 1)) {
yield [arr[i], ...combination];
}
}
}

export default combine;

0 comments on commit d349d70

Please sign in to comment.