291 lines
12 KiB
JavaScript
291 lines
12 KiB
JavaScript
/**
|
|
* Helper function to print a 2D array (matrix) nicely.
|
|
* @param {Array<Array<any>>} matrix The matrix to print.
|
|
* @param {string} title Optional title to print before the matrix.
|
|
*/
|
|
function printMatrix(matrix, title = "") {
|
|
if (title) {
|
|
console.log(title);
|
|
}
|
|
// console.table is often available and provides good formatting
|
|
if (typeof console.table === 'function') {
|
|
console.table(matrix);
|
|
} else {
|
|
// Fallback basic printing
|
|
matrix.forEach(row => {
|
|
// JSON.stringify formats arrays with commas and brackets
|
|
console.log(JSON.stringify(row));
|
|
});
|
|
}
|
|
}
|
|
|
|
// --- Input Data ---
|
|
// Using let for demand and supply as they will be modified during the algorithm
|
|
// Make copies if you want to preserve the original arrays elsewhere
|
|
let demand = [30, 20, 70, 30, 60];
|
|
let supply = [50, 60, 50, 50];
|
|
const costs = [
|
|
[16, 16, 13, 22, 17],
|
|
[14, 14, 13, 19, 15],
|
|
[19, 19, 20, 23, 50],
|
|
[50, 12, 50, 15, 11]
|
|
];
|
|
|
|
// --- Dimensions and State Variables ---
|
|
const nRows = supply.length;
|
|
const nCols = demand.length;
|
|
|
|
// Track which rows/columns are fully satisfied
|
|
const rowDone = Array(nRows).fill(false);
|
|
const colDone = Array(nCols).fill(false);
|
|
|
|
// The resulting allocation matrix, initialized with zeros
|
|
const result = Array(nRows).fill(0).map(() => Array(nCols).fill(0));
|
|
|
|
/**
|
|
* Calculates the difference between the two smallest costs in a given row or column,
|
|
* considering only active (not done) cells.
|
|
* @param {number} j The index of the row or column to analyze.
|
|
* @param {number} len The length of the other dimension (nCols if analyzing a row, nRows if analyzing a column).
|
|
* @param {boolean} isRow True if analyzing row j, false if analyzing column j.
|
|
* @returns {Array<number>} An array: [penalty, minCost, minCostPosition]
|
|
* penalty = min2 - min1 (Infinity if < 2 costs found)
|
|
* minCost = the smallest cost found (Infinity if none found)
|
|
* minCostPosition = index of the smallest cost (-1 if none found)
|
|
*/
|
|
function diff(j, len, isRow) {
|
|
let min1 = Infinity;
|
|
let min2 = Infinity;
|
|
let minP = -1; // Position (index) of min1
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
// Skip if the corresponding column (if analyzing row) or row (if analyzing column) is already done
|
|
if (isRow ? colDone[i] : rowDone[i]) {
|
|
continue;
|
|
}
|
|
|
|
// Get the cost from the correct cell
|
|
const c = isRow ? costs[j][i] : costs[i][j];
|
|
|
|
if (c < min1) {
|
|
min2 = min1; // Old min1 becomes min2
|
|
min1 = c; // New cost is the new min1
|
|
minP = i; // Record position of new min1
|
|
} else if (c < min2) {
|
|
min2 = c; // Update min2
|
|
}
|
|
}
|
|
|
|
// Calculate penalty: difference between second minimum and first minimum
|
|
// If min2 is still Infinity, it means less than two costs were found. Penalty is Infinity.
|
|
const penalty = (min2 === Infinity) ? Infinity : min2 - min1;
|
|
|
|
return [penalty, min1, minP];
|
|
}
|
|
|
|
/**
|
|
* Finds the row or column with the maximum penalty among active rows/columns.
|
|
* @param {number} len1 Number of rows or columns to iterate through (nRows or nCols).
|
|
* @param {number} len2 Length of the other dimension (nCols or nRows).
|
|
* @param {boolean} isRow True if finding max penalty among rows, false for columns.
|
|
* @returns {Array<number> | null} An array: [rowIndex, colIndex, minCostInSelected, maxPenaltyValue]
|
|
* Returns null if no active rows/columns found.
|
|
*/
|
|
function maxPenalty(len1, len2, isRow) {
|
|
let maxDiff = -Infinity; // Use -Infinity to ensure any real penalty is larger
|
|
let posOfMaxDiff = -1; // Index of row/column with max penalty
|
|
let posOfMinCost = -1; // Index of min cost within the selected row/column
|
|
let minCostVal = -1; // Value of min cost within the selected row/column
|
|
let foundActive = false;
|
|
|
|
for (let i = 0; i < len1; i++) {
|
|
// Skip if this row (if isRow) or column (if !isRow) is already done
|
|
if (isRow ? rowDone[i] : colDone[i]) {
|
|
continue;
|
|
}
|
|
foundActive = true; // Found at least one active row/column to check
|
|
|
|
const res = diff(i, len2, isRow); // [penalty, minCost, minPos]
|
|
const currentPenalty = res[0];
|
|
|
|
// Check if this penalty is the highest found so far
|
|
if (currentPenalty > maxDiff) {
|
|
maxDiff = currentPenalty;
|
|
posOfMaxDiff = i; // Record which row/column had this max penalty
|
|
minCostVal = res[1]; // Record the min cost in this row/column
|
|
posOfMinCost = res[2]; // Record the position of min cost
|
|
}
|
|
}
|
|
if (!foundActive) {
|
|
return null; // No active rows/columns left to calculate penalty for
|
|
}
|
|
|
|
// Return results ensuring format [rowIndex, colIndex, minCostVal, maxDiff]
|
|
return isRow
|
|
? [posOfMaxDiff, posOfMinCost, minCostVal, maxDiff] // For row: [rowIdx, colIdxOfMin, minCost, penalty]
|
|
: [posOfMinCost, posOfMaxDiff, minCostVal, maxDiff]; // For col: [rowIdxOfMin, colIdx, minCost, penalty]
|
|
}
|
|
|
|
/**
|
|
* Determines the next cell to allocate to based on Vogel's Approximation Method (VAM).
|
|
* It compares the highest row penalty and the highest column penalty.
|
|
* @returns {Array<number> | null} An array: [rowIndex, colIndex, minCostInSelected, maxPenaltyValue]
|
|
* Returns null if no suitable cell can be found.
|
|
*/
|
|
function nextCell() {
|
|
const resRow = maxPenalty(nRows, nCols, true); // Max penalty among rows
|
|
const resCol = maxPenalty(nCols, nRows, false); // Max penalty among columns
|
|
|
|
// Handle cases where one or both couldn't find penalties (e.g., all done)
|
|
if (!resRow && !resCol) return null; // No more valid moves
|
|
if (!resRow) return resCol; // Only column penalty found
|
|
if (!resCol) return resRow; // Only row penalty found
|
|
|
|
const maxPenaltyRow = resRow[3];
|
|
const maxPenaltyCol = resCol[3];
|
|
const minCostInRow = resRow[2];
|
|
const minCostInCol = resCol[2];
|
|
|
|
// Decide based on highest penalty. Break ties by choosing the one with the lower minimum cost.
|
|
if (maxPenaltyRow === maxPenaltyCol) {
|
|
return minCostInRow <= minCostInCol ? resRow : resCol;
|
|
} else {
|
|
// In the C++ code: `res1[3] > res2[3] ? res2 : res1;`
|
|
// This seems backward? Higher penalty should be chosen.
|
|
// Let's assume higher penalty is better:
|
|
// return maxPenaltyRow >= maxPenaltyCol ? resRow : resCol;
|
|
// Let's stick to the C++ code's logic for direct translation:
|
|
return maxPenaltyRow > maxPenaltyCol ? resCol : resRow;
|
|
// Re-evaluating C++ code:
|
|
// `auto res1 = maxPenalty(nRows, nCols, true);` -> `res1` = row result
|
|
// `auto res2 = maxPenalty(nCols, nRows, false);` -> `res2` = col result
|
|
// `return res1[3] > res2[3] ? res2 : res1;` -> If row penalty > col penalty, return col result. This is incorrect VAM.
|
|
// VAM selects the *highest* penalty.
|
|
// Let's correct the logic based on VAM principle: Choose highest penalty, break ties with lowest cost.
|
|
//
|
|
// if (maxPenaltyRow > maxPenaltyCol) {
|
|
// return resRow;
|
|
// } else if (maxPenaltyCol > maxPenaltyRow) {
|
|
// return resCol;
|
|
// } else {
|
|
// // Penalties are equal, break tie with minimum cost
|
|
// return minCostInRow <= minCostInCol ? resRow : resCol;
|
|
// }
|
|
//
|
|
// The logic `maxPenaltyRow > maxPenaltyCol ? resCol : resRow` actually means:
|
|
// If RowPenalty is bigger, return ColumnResult. If ColPenalty is bigger or equal, return RowResult.
|
|
// This seems highly suspect and likely a bug in the original C++ or a misunderstanding of its output.
|
|
// Let's implement the standard VAM logic (highest penalty wins, tiebreak with cost):
|
|
if (maxPenaltyRow > maxPenaltyCol) {
|
|
return resRow;
|
|
} else if (maxPenaltyCol > maxPenaltyRow) {
|
|
return resCol;
|
|
} else {
|
|
// Penalties are equal, break tie with minimum cost cell within the tied lines
|
|
return minCostInRow <= minCostInCol ? resRow : resCol;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// --- Main VAM Algorithm Execution ---
|
|
|
|
// Calculate total initial supply
|
|
let supplyLeft = supply.reduce((sum, current) => sum + current, 0);
|
|
let totalCost = 0;
|
|
let iterations = 0; // Safety counter
|
|
const maxIterations = nRows * nCols * 2; // A reasonable upper bound
|
|
|
|
console.log("Starting Vogel's Approximation Method...");
|
|
console.log("Initial Supply:", supply);
|
|
console.log("Initial Demand:", demand);
|
|
console.log("Costs:");
|
|
printMatrix(costs);
|
|
console.log("---");
|
|
|
|
|
|
while (supplyLeft > 0 && iterations < maxIterations) {
|
|
iterations++;
|
|
const cell = nextCell(); // Get [r, c, minCost, penalty] for the next allocation
|
|
|
|
if (!cell) {
|
|
console.log("No suitable cell found by nextCell(). Stopping.");
|
|
break; // No more valid moves can be determined
|
|
}
|
|
|
|
const r = cell[0]; // Row index to allocate to
|
|
const c = cell[1]; // Column index to allocate to
|
|
|
|
// Safety check for valid indices and if the cell's row/col are already done
|
|
// This shouldn't happen if maxPenalty correctly skips done rows/cols, but good for robustness.
|
|
if (r < 0 || r >= nRows || c < 0 || c >= nCols || rowDone[r] || colDone[c]) {
|
|
console.warn(`Iteration ${iterations}: nextCell returned invalid/done cell [${r}, ${c}]. Attempting to recover or break.`);
|
|
// Attempt recovery: mark potentially problematic row/col as done if indices are valid
|
|
if (r >= 0 && r < nRows) rowDone[r] = true;
|
|
if (c >= 0 && c < nCols) colDone[c] = true;
|
|
// Or simply break:
|
|
// break;
|
|
continue; // Try finding another cell in the next iteration
|
|
}
|
|
|
|
|
|
// Determine the quantity to allocate: minimum of remaining supply in row r and demand in col c
|
|
const quantity = Math.min(demand[c], supply[r]);
|
|
|
|
if (quantity <= 0) {
|
|
// This might happen if a row/col is selected but has 0 supply/demand left.
|
|
// Mark them as done and continue.
|
|
console.warn(`Iteration ${iterations}: Zero quantity allocation attempted for cell [${r}, ${c}]. Marking row/col as done.`);
|
|
if (demand[c] <= 0 && !colDone[c]) colDone[c] = true;
|
|
if (supply[r] <= 0 && !rowDone[r]) rowDone[r] = true;
|
|
continue;
|
|
}
|
|
|
|
|
|
// Update demand, supply, and mark as done if depleted
|
|
demand[c] -= quantity;
|
|
if (demand[c] === 0) {
|
|
colDone[c] = true;
|
|
}
|
|
|
|
supply[r] -= quantity;
|
|
if (supply[r] === 0) {
|
|
rowDone[r] = true;
|
|
}
|
|
|
|
// Record the allocation in the result matrix
|
|
result[r][c] = quantity;
|
|
|
|
// Update total supply left and total cost
|
|
supplyLeft -= quantity;
|
|
totalCost += quantity * costs[r][c];
|
|
|
|
// console.log(`Iteration ${iterations}: Allocated ${quantity} to [${r}, ${c}]. Cost += ${quantity * costs[r][c]}. SupplyLeft: ${supplyLeft}`);
|
|
// console.log(" Current Supply:", supply);
|
|
// console.log(" Current Demand:", demand);
|
|
// console.log(" rowDone:", rowDone);
|
|
// console.log(" colDone:", colDone);
|
|
// console.log("---");
|
|
|
|
}
|
|
|
|
if (iterations >= maxIterations) {
|
|
console.warn("Warning: Maximum iterations reached. The algorithm might be stuck in a loop.");
|
|
}
|
|
|
|
// --- Output Results ---
|
|
console.log("\n--- VAM Finished ---");
|
|
printMatrix(result, "Allocation Matrix (Result):");
|
|
console.log(`\nTotal Cost: ${totalCost}`);
|
|
|
|
// Optional: Check final state
|
|
const remainingDemand = demand.reduce((s, d) => s + d, 0);
|
|
const remainingSupply = supply.reduce((s, sup) => s + sup, 0);
|
|
console.log(`Final Remaining Demand: ${remainingDemand}`);
|
|
console.log(`Final Remaining Supply: ${remainingSupply}`);
|
|
console.log(`Calculated Supply Left: ${supplyLeft}`);
|
|
|
|
// Verify if total allocated matches total demand/supply (assuming balanced problem)
|
|
const totalAllocated = result.flat().reduce((s, q) => s + q, 0);
|
|
console.log(`Total Quantity Allocated: ${totalAllocated}`);
|