[Algorithm] Meeting hour optimization (Kanpsack problem) and Dynamic programming
For example we have array of meeting objects:
const data = [ { name: "m1", hours: 2 }, { name: "m2", hours: 4 }, { name: "m3", hours: 3 }, { name: "m4", hours: 3 }, { name: "m5", hours: 1 } ];
For a day, 8 hours, we want to take as any meetings as possible:
const res = optimizeMeetings(data, 8);
You should write function 'optimizeMeetings', get the results of selected meetings to attend.
This problem is the same as Knapack problem, we can construct a table:
hours / total | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
2 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
4 | 0 | 2 | 2 | 4 | 4 | 6 | 6 | 6 |
3 | 0 | 2 | 3 | 4 | 5 | 6 | 7 | 7 |
3 | 0 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
The max hours we can take is the last row & col value, which in the end should be 8.
Then we should trace back the table to find which items should be included.
Final Code:
/**@description * When we have our result for Knapsack problem, we want to back trace to get the selected items. * * What we need to do is trace form last item of the memo, moving up */ const backTrace = (hours, totalHours, memo) => { function helper(memo, row, col) { let current = memo[row][col]; let selected = []; while (current >= 0 && row >= 0 && col >= 0) { // If we reach the first row, then check whether we have the remaining? // If yes then we need to add this row item into final result if (row === 0 && current !== 0) { selected.push(row); break; } let sameRowPrevCol = memo[row][col - 1]; let prevRowSameCol = memo[row - 1][col]; if (current !== sameRowPrevCol && prevRowSameCol !== current) { // Item should be selected if the value with sibling values are differnet selected.push(row); // calcuate the remaining col = current - hours[row] - 1; row = row - 1; } else if (prevRowSameCol === current && current !== sameRowPrevCol) { // current is coming from previous row with the same column, reset row row = row - 1; } else if (current === sameRowPrevCol && prevRowSameCol !== current) { // current is coming from previous column with the same row, reset column col = col - 1; } // Update current with new row and new column current = memo[row][col]; } return selected; } return helper(memo, hours.length - 1, totalHours.length - 1); }; const getMaxHours = (hours, totalHours) => { let memo = [...new Array(hours.length)].map( x => new Array(totalHours.length) ); function helper(hours, totalHours, memo) { for (let row in hours) { const value = hours[row]; for (let col in totalHours) { // Fill in the first row if (!memo[row - 1]) { memo[row][col] = value <= totalHours[col] ? value : 0; continue; } // if the current value is larger than constrain, we use previous value const prevRowSameCol = memo[row - 1][col]; if (value > totalHours[col]) { memo[row][col] = prevRowSameCol; continue; } // if the current value is equal to constrain, then Max{value, prevRowSameCol} if (value === totalHours[col]) { memo[row][col] = Math.max(value, prevRowSameCol); } // if the current value is smaller than constrain // Math {value + memo[row - 1][diff]: where diff is constrain-value, prevRowSameCol} if (value < totalHours[col]) { const diff = totalHours[col] - value - 1; memo[row][col] = Math.max( prevRowSameCol, value + memo[row - 1][diff] ); } } } return memo; } memo = helper(hours, totalHours, memo); const selectedIndex = backTrace(hours, totalHours, memo); return { memo, selectedIndex }; }; function* genearteNumberAry(start, num) { let i = start; while (i <= num) { yield i; i++; } } /** * Main */ /** * @param meetings: [{name: string, hours: number}] * @param haveHours: number * * @returns [meetings] */ function optimizeMeetings(meetings, haveHours) { const hours = meetings.map(m => m.hours); const haveHoursAry = Array.from(genearteNumberAry(1, haveHours)); const { selectedIndex } = getMaxHours(hours, haveHoursAry); return selectedIndex.map(i => meetings[i]); } const data = [ { name: "m1", hours: 2 }, { name: "m2", hours: 4 }, { name: "m3", hours: 3 }, { name: "m4", hours: 3 }, { name: "m5", hours: 1 } ]; const res = optimizeMeetings(data, 8); console.log(JSON.stringify(res)); // [{"name":"m4","hours":3},{"name":"m3","hours":3},{"name":"m1","hours":2}]