diff --git a/apps/backend/src/app/controllers/transport-problem/services/second-phase.service.test.ts b/apps/backend/src/app/controllers/transport-problem/services/second-phase.service.test.ts new file mode 100644 index 0000000..c342af7 --- /dev/null +++ b/apps/backend/src/app/controllers/transport-problem/services/second-phase.service.test.ts @@ -0,0 +1,32 @@ +import { + secondPhaseFirstResultMock, + secondPhaseSecondResultMock, + vogelKordaFirstResultMock, + vogelKordaSecondResultMock, +} from '@opres/shared/data/mocks'; +import {last} from 'lodash'; + +import {SecondPhaseService} from './second-phase.service'; + +describe('SecondPhaseService', () => { + let service: SecondPhaseService; + + beforeEach(() => { + service = new SecondPhaseService(); + }); + + it('should be created', () => + expect(service).toBeInstanceOf(SecondPhaseService)); + + it("should calculate a transport problem's second phase part 1", () => { + const firstPhaseResult = last(vogelKordaFirstResultMock)?.transportation; + const result = service.calculate(firstPhaseResult, 'explanations'); + expect(result).toEqual(secondPhaseFirstResultMock); + }); + + it("should calculate a transport problem's second phase part 2", () => { + const firstPhaseResult = last(vogelKordaSecondResultMock)?.transportation; + const result = service.calculate(firstPhaseResult, 'explanations'); + expect(result).toEqual(secondPhaseSecondResultMock); + }); +}); diff --git a/apps/backend/src/app/controllers/transport-problem/services/second-phase.service.ts b/apps/backend/src/app/controllers/transport-problem/services/second-phase.service.ts index 6ab4666..92dce8d 100644 --- a/apps/backend/src/app/controllers/transport-problem/services/second-phase.service.ts +++ b/apps/backend/src/app/controllers/transport-problem/services/second-phase.service.ts @@ -1,10 +1,22 @@ import {Injectable} from '@nestjs/common'; import { + AuxiliaryVariables, CalculationMode, SecondPhaseStep, + SelectedCell, TransportTable, } from '@opres/shared/types'; +import { + cloneDeep, + findIndex, + first, + forEach, + last, + map, + minBy, + some, +} from 'lodash'; @Injectable() export class SecondPhaseService { @@ -12,7 +24,167 @@ export class SecondPhaseService { transportTable: TransportTable, mode: CalculationMode, ): Array { - // todo wip - return []; + const process: Array = []; + let nextBase: SelectedCell = {x: 0, y: 0}; + do { + const variables = this.getAuxiliaryVariable(transportTable); + nextBase = this.getNextNewBase(transportTable, variables); + if (nextBase?.value !== undefined) { + const circle = this.getCircle(transportTable, nextBase); + process.push({ + transportation: cloneDeep(transportTable), + auxiliaryVariables: variables, + nextBase: cloneDeep(nextBase), + circle, + }); + this.reduceTransportTable(transportTable, cloneDeep(circle)); + continue; + } + process.push({ + transportation: cloneDeep(transportTable), + auxiliaryVariables: variables, + }); + } while (nextBase?.value !== undefined); + return process; + } + + private getAuxiliaryVariable( + transportTable: TransportTable, + ): AuxiliaryVariables { + const variables: AuxiliaryVariables = { + x: map(transportTable[0], () => null), + y: map(transportTable, () => null), + }; + const initialRow = findIndex(transportTable, (row) => + some(row, (cell) => !!cell.transported), + ); + variables.y[initialRow] = 0; + + const containsNull = (variable) => variable === null; + while (variables.x.some(containsNull) || variables.y.some(containsNull)) { + forEach(transportTable, (row, rowIndex) => + forEach(row, (cell, columnIndex) => { + const currentXVariable = variables.x[columnIndex]; + const currentYVariable = variables.y[rowIndex]; + + if (cell.transported === undefined) return; + if (currentXVariable === null && currentYVariable === null) return; + if (currentXVariable !== null && currentYVariable !== null) return; + + if (currentYVariable === null) + variables.y[rowIndex] = cell.cost - variables.x[columnIndex]; + else if (currentXVariable === null) + variables.x[columnIndex] = cell.cost - variables.y[rowIndex]; + }), + ); + } + return variables; + } + + private getNextNewBase( + transportTable: TransportTable, + variables: AuxiliaryVariables, + ): SelectedCell { + let newBase: SelectedCell = {value: undefined, x: 0, y: 0}; + forEach(transportTable, (row, rowIndex) => + forEach(row, (cell, columnIndex) => { + if (cell.transported !== undefined) + return (cell.reducedCost = undefined); + const reducedCost = + cell.cost - (variables.x[columnIndex] + variables.y[rowIndex]); + transportTable[rowIndex][columnIndex].reducedCost = reducedCost; + if ( + (newBase?.value === undefined && reducedCost < 0) || + newBase?.value > reducedCost + ) + newBase = {value: reducedCost, x: +columnIndex, y: rowIndex}; + }), + ); + return newBase; + } + + private getCircle( + transportTable: TransportTable, + nextBase: SelectedCell, + ): Array { + let baseCells: Array = this.getBaseCellPositions( + transportTable, + nextBase, + ); + let removedCells = Infinity; + while (removedCells > 0) { + const filteredBaseCells = baseCells.filter((value, _, array) => { + const xIndexes = array.filter((v) => v.x === value.x).length; + const yIndexes = array.filter((v) => v.y === value.y).length; + return xIndexes > 1 && yIndexes > 1; + }); + removedCells = baseCells.length - filteredBaseCells.length; + baseCells = filteredBaseCells; + } + return this.sortCircleCells(baseCells); + } + + private getMaxReducableValueFromCircle(circle: Array): number { + return minBy( + circle?.filter((cell, index) => cell?.value !== -1 && index % 2 !== 0), + 'value', + )?.value; + } + + private sortCircleCells(circle: Array): Array { + const comparingIndex = (isX: boolean) => (isX ? 'x' : 'y'); + let isX = true; + const sortedCircle = [circle[0]]; + for (let i = 1; i < circle.length; i++) { + const lastSortedCell = last(sortedCircle); + const baseCell = circle?.find( + (value) => + value?.[comparingIndex(isX)] === + lastSortedCell?.[comparingIndex(isX)] && + value?.[comparingIndex(!isX)] !== + lastSortedCell?.[comparingIndex(!isX)], + ); + sortedCircle.push(baseCell); + isX = !isX; + } + return sortedCircle; + } + + private reduceTransportTable( + transportTable: TransportTable, + circle: Array, + ): void { + const maxReducableValue = this.getMaxReducableValueFromCircle(circle); + first(circle).value = maxReducableValue; + for (let i = 1; i < circle.length; i++) { + if (!circle[i]) continue; + if (i % 2) circle[i].value -= maxReducableValue; + else circle[i].value += maxReducableValue; + } + forEach(circle, (cell) => { + transportTable[cell?.y][cell?.x].transported = + cell?.value === 0 ? undefined : cell?.value; + }); + } + + private getBaseCellPositions( + transportTable: TransportTable, + nextBase?: SelectedCell, + ): Array { + const baseCellPositions: Array = []; + if (nextBase) baseCellPositions.push({...nextBase, value: -1}); + + forEach(transportTable, (row, rowIndex) => + forEach(row, (cell, columnIndex) => { + if (cell.transported !== undefined) + baseCellPositions.push({ + x: +columnIndex, + y: rowIndex, + value: cell.transported, + }); + }), + ); + + return baseCellPositions; } } diff --git a/apps/backend/src/app/controllers/transport-problem/utils/base-cells-counter.util.ts b/apps/backend/src/app/controllers/transport-problem/utils/base-cells-counter.util.ts new file mode 100644 index 0000000..a1ee001 --- /dev/null +++ b/apps/backend/src/app/controllers/transport-problem/utils/base-cells-counter.util.ts @@ -0,0 +1,22 @@ +import {TransportTable} from '@opres/shared/types'; +import {countBy} from 'lodash'; + +import {getColumnUtil} from './get-column.util'; + +export function getBaseCellsInARow( + transportTable: TransportTable, + rowIndex: number, +): number { + return countBy( + transportTable[rowIndex], + (cell) => cell.transported !== undefined, + ).true; +} + +export function getBaseCellsInColumn( + transportTable: TransportTable, + columnIndex: number, +): number { + const column = getColumnUtil(transportTable, columnIndex); + return countBy(column, (cell) => cell.transported !== undefined).true; +} diff --git a/apps/backend/src/app/controllers/transport-problem/utils/get-column.util.ts b/apps/backend/src/app/controllers/transport-problem/utils/get-column.util.ts new file mode 100644 index 0000000..37372c0 --- /dev/null +++ b/apps/backend/src/app/controllers/transport-problem/utils/get-column.util.ts @@ -0,0 +1,8 @@ +import {Cell, TransportTable} from '@opres/shared/types'; + +export function getColumnUtil( + table: TransportTable, + columnIndex: number, +): Array { + return table.map((row) => row[columnIndex]); +} diff --git a/apps/frontend/src/app/pages/transport-problem/transport-problem.service.ts b/apps/frontend/src/app/pages/transport-problem/transport-problem.service.ts index 58392be..45d8ead 100644 --- a/apps/frontend/src/app/pages/transport-problem/transport-problem.service.ts +++ b/apps/frontend/src/app/pages/transport-problem/transport-problem.service.ts @@ -83,7 +83,7 @@ export class TransportProblemService { return this.getEpsilonResult( last(steps)?.transportation || [], mode === 'explanations', - ).pipe(map((epsilon) => ({steps, epsilon} as FirstPhaseResult))); + ).pipe(map((epsilon) => ({steps, epsilon} as SecondPhaseResult))); }), ); } diff --git a/libs/shared/data/mocks/src/lib/transport-problem/index.ts b/libs/shared/data/mocks/src/lib/transport-problem/index.ts index 8aa34b6..e631368 100644 --- a/libs/shared/data/mocks/src/lib/transport-problem/index.ts +++ b/libs/shared/data/mocks/src/lib/transport-problem/index.ts @@ -2,3 +2,4 @@ export * from './transport-problem.mock'; export * from './north-west-result.mock'; export * from './table-minimum-result.mock'; export * from './vogel-korda-result.mock'; +export * from './second-phase-result.mock'; diff --git a/libs/shared/data/mocks/src/lib/transport-problem/second-phase-result.mock.ts b/libs/shared/data/mocks/src/lib/transport-problem/second-phase-result.mock.ts new file mode 100644 index 0000000..ea6edc1 --- /dev/null +++ b/libs/shared/data/mocks/src/lib/transport-problem/second-phase-result.mock.ts @@ -0,0 +1,251 @@ +import {SecondPhaseStep} from '@opres/shared/types'; + +export const secondPhaseFirstResultMock: Array = [ + { + transportation: [ + { + '0': {cost: 8, reducedCost: 11}, + '1': {cost: 7, reducedCost: 9}, + '2': {cost: 3, reducedCost: 5}, + '3': {cost: 2, transported: 15}, + }, + { + '0': {cost: 1, transported: 8}, + '1': {cost: 4, reducedCost: 2}, + '2': {cost: 2, transported: 35}, + '3': {cost: 5, reducedCost: -1}, + }, + { + '0': {cost: 2, transported: 10}, + '1': {cost: 3, transported: 13}, + '2': {cost: 4, reducedCost: 1}, + '3': {cost: 7, transported: 5}, + }, + { + '0': {cost: 1, reducedCost: 1}, + '1': {cost: 1, transported: 19}, + '2': {cost: 4, reducedCost: 3}, + '3': {cost: 4, reducedCost: -1}, + }, + ], + auxiliaryVariables: { + x: [-3, -2, -2, 2], + y: [0, 4, 5, 3], + }, + nextBase: {value: -1, x: 3, y: 1}, + circle: [ + {value: -1, x: 3, y: 1}, + {x: 3, y: 2, value: 5}, + {x: 0, y: 2, value: 10}, + {x: 0, y: 1, value: 8}, + ], + }, + { + transportation: [ + { + '0': {cost: 8, reducedCost: 10}, + '1': {cost: 7, reducedCost: 8}, + '2': {cost: 3, reducedCost: 4}, + '3': {cost: 2, transported: 15}, + }, + { + '0': {cost: 1, transported: 3}, + '1': {cost: 4, reducedCost: 2}, + '2': {cost: 2, transported: 35}, + '3': {cost: 5, transported: 5}, + }, + { + '0': {cost: 2, transported: 15}, + '1': {cost: 3, transported: 13}, + '2': {cost: 4, reducedCost: 1}, + '3': {cost: 7, reducedCost: 1}, + }, + { + '0': {cost: 1, reducedCost: 1}, + '1': {cost: 1, transported: 19}, + '2': {cost: 4, reducedCost: 3}, + '3': {cost: 4, reducedCost: 0}, + }, + ], + auxiliaryVariables: { + x: [-2, -1, -1, 2], + y: [0, 3, 4, 2], + }, + }, +]; + +export const secondPhaseSecondResultMock: Array = [ + { + transportation: [ + { + '0': {cost: 1, transported: 12}, + '1': {cost: 5, reducedCost: 3}, + '2': {cost: 4, reducedCost: 7}, + '3': {cost: 3, transported: 3}, + '4': {cost: 2, reducedCost: 3}, + }, + { + '0': {cost: 1, reducedCost: -3}, + '1': {cost: 1, reducedCost: -4}, + '2': {cost: 0, transported: 3}, + '3': {cost: 3, reducedCost: -3}, + '4': {cost: 7, reducedCost: 5}, + }, + { + '0': {cost: 6, reducedCost: 0}, + '1': {cost: 3, reducedCost: -4}, + '2': {cost: 2, transported: 3}, + '3': {cost: 8, transported: 1}, + '4': {cost: 4, transported: 4}, + }, + { + '0': {cost: 1, reducedCost: 1}, + '1': {cost: 1, transported: 7}, + '2': {cost: 5, reducedCost: 9}, + '3': {cost: 2, transported: 6}, + '4': {cost: 3, reducedCost: 5}, + }, + ], + auxiliaryVariables: { + x: [1, 2, -3, 3, -1], + y: [0, 3, 5, -1], + }, + nextBase: {value: -4, x: 1, y: 1}, + circle: [ + {value: -1, x: 1, y: 1}, + {x: 1, y: 3, value: 7}, + {x: 3, y: 3, value: 6}, + {x: 3, y: 2, value: 1}, + {x: 2, y: 2, value: 3}, + {x: 2, y: 1, value: 3}, + ], + }, + { + transportation: [ + { + '0': {cost: 1, transported: 12}, + '1': {cost: 5, reducedCost: 3}, + '2': {cost: 4, reducedCost: 3}, + '3': {cost: 3, transported: 3}, + '4': {cost: 2, reducedCost: -1}, + }, + { + '0': {cost: 1, reducedCost: 1}, + '1': {cost: 1, transported: 1}, + '2': {cost: 0, transported: 2}, + '3': {cost: 3, reducedCost: 1}, + '4': {cost: 7, reducedCost: 5}, + }, + { + '0': {cost: 6, reducedCost: 4}, + '1': {cost: 3, reducedCost: 0}, + '2': {cost: 2, transported: 4}, + '3': {cost: 8, reducedCost: 4}, + '4': {cost: 4, transported: 4}, + }, + { + '0': {cost: 1, reducedCost: 1}, + '1': {cost: 1, transported: 6}, + '2': {cost: 5, reducedCost: 5}, + '3': {cost: 2, transported: 7}, + '4': {cost: 3, reducedCost: 1}, + }, + ], + auxiliaryVariables: { + x: [1, 2, 1, 3, 3], + y: [0, -1, 1, -1], + }, + nextBase: {value: -1, x: 4, y: 0}, + circle: [ + {value: -1, x: 4, y: 0}, + {x: 4, y: 2, value: 4}, + {x: 2, y: 2, value: 4}, + {x: 2, y: 1, value: 2}, + {x: 1, y: 1, value: 1}, + {x: 1, y: 3, value: 6}, + {x: 3, y: 3, value: 7}, + {x: 3, y: 0, value: 3}, + ], + }, + { + transportation: [ + { + '0': {cost: 1, transported: 12}, + '1': {cost: 5, reducedCost: 3}, + '2': {cost: 4, reducedCost: 4}, + '3': {cost: 3, transported: 1}, + '4': {cost: 2, transported: 2}, + }, + { + '0': {cost: 1, reducedCost: 1}, + '1': {cost: 1, transported: 3}, + '2': {cost: 0, reducedCost: 1}, + '3': {cost: 3, reducedCost: 1}, + '4': {cost: 7, reducedCost: 6}, + }, + { + '0': {cost: 6, reducedCost: 3}, + '1': {cost: 3, reducedCost: -1}, + '2': {cost: 2, transported: 6}, + '3': {cost: 8, reducedCost: 3}, + '4': {cost: 4, transported: 2}, + }, + { + '0': {cost: 1, reducedCost: 1}, + '1': {cost: 1, transported: 4}, + '2': {cost: 5, reducedCost: 6}, + '3': {cost: 2, transported: 9}, + '4': {cost: 3, reducedCost: 2}, + }, + ], + auxiliaryVariables: { + x: [1, 2, 0, 3, 2], + y: [0, -1, 2, -1], + }, + nextBase: {value: -1, x: 1, y: 2}, + circle: [ + {value: -1, x: 1, y: 2}, + {x: 1, y: 3, value: 4}, + {x: 3, y: 3, value: 9}, + {x: 3, y: 0, value: 1}, + {x: 4, y: 0, value: 2}, + {x: 4, y: 2, value: 2}, + ], + }, + { + transportation: [ + { + '0': {cost: 1, transported: 12}, + '1': {cost: 5, reducedCost: 4}, + '2': {cost: 4, reducedCost: 4}, + '3': {cost: 3, reducedCost: 1}, + '4': {cost: 2, transported: 3}, + }, + { + '0': {cost: 1, reducedCost: 0}, + '1': {cost: 1, transported: 3}, + '2': {cost: 0, reducedCost: 0}, + '3': {cost: 3, reducedCost: 1}, + '4': {cost: 7, reducedCost: 5}, + }, + { + '0': {cost: 6, reducedCost: 3}, + '1': {cost: 3, transported: 1}, + '2': {cost: 2, transported: 6}, + '3': {cost: 8, reducedCost: 4}, + '4': {cost: 4, transported: 1}, + }, + { + '0': {cost: 1, reducedCost: 0}, + '1': {cost: 1, transported: 3}, + '2': {cost: 5, reducedCost: 5}, + '3': {cost: 2, transported: 10}, + '4': {cost: 3, reducedCost: 1}, + }, + ], + auxiliaryVariables: { + x: [1, 1, 0, 2, 2], + y: [0, 0, 2, 0], + }, + }, +]; diff --git a/libs/shared/types/src/lib/transport-problem.type.ts b/libs/shared/types/src/lib/transport-problem.type.ts index 2d22cee..2e9109d 100644 --- a/libs/shared/types/src/lib/transport-problem.type.ts +++ b/libs/shared/types/src/lib/transport-problem.type.ts @@ -14,6 +14,7 @@ export interface TPData { export interface Cell { cost: number; transported?: number; + reducedCost?: number; } export type TransportRow = Record; @@ -32,8 +33,16 @@ export interface FirstPhaseStep { explanation?: string; } -// todo: wip +export interface SelectedCell { + x: number; + y: number; + value?: number; +} + export interface SecondPhaseStep { transportation: TransportTable; + circle?: Array; + auxiliaryVariables?: AuxiliaryVariables; + nextBase?: SelectedCell; explanation?: string; }