const VALID_TOKEN = "9876543210";
function evaluateCombo(coalData, combo, weighted, constraints) { const [calMin, calMax, sulMin, sulMax, ashMin, ashMax, waterMin, waterMax, volMin, volMax] = constraints; let tw=0, cal=0, sul=0, ash=0, water=0, vol=0, cost=0; for (let i=0; i<coalData.length; i++) { if (combo[i] === 0) continue; const w = weighted ? coalData[i].weightPerShovel : 1; tw += w * combo[i]; cal += coalData[i].calorie * w * combo[i]; sul += coalData[i].sulfur * w * combo[i]; ash += coalData[i].ash * w * combo[i]; water += coalData[i].water * w * combo[i]; vol += coalData[i].volatile * w * combo[i]; cost += coalData[i].price * combo[i]; } if (tw === 0) return null; const avgCal = Math.round(cal/tw), avgSul = sul/tw, avgAsh = ash/tw, avgWater = water/tw, avgVol = vol/tw; if (avgCal < calMin || avgCal > calMax || avgSul < sulMin || avgSul > sulMax) return null; if (avgAsh < ashMin || avgAsh > ashMax || avgWater < waterMin || avgWater > waterMax) return null; if (avgVol < volMin || avgVol > volMax) return null; let maxTons = 0, tbw = 0; const dem = []; for (let i=0; i<coalData.length; i++) { if (combo[i] === 0) continue; const w = weighted ? coalData[i].weightPerShovel : 1; dem.push({idx:i, need: wcombo[i]}); tbw += wcombo[i]; } if (tbw > 0) { let mb = Infinity; for (const d of dem) { const s = coalData[d.idx].stock; if (s <= 0) { mb = 0; break; } mb = Math.min(mb, Math.floor(s/d.need)); } maxTons = Math.round(mbtbw100)/100; } return {totalShovels: combo.reduce((a,b)=>a+b,0), combo, avgCalorie: avgCal, avgSulfur: Math.round(avgSul1e4)/1e4, avgAsh: Math.round(avgAsh100)/100, avgWater: Math.round(avgWater100)/100, avgVolatile: Math.round(avgVol100)/100, totalCost: Math.round(cost*100)/100, maxTonnage: maxTons}; }
function branchAndBound(coalData, params) { const {maxShovels, calorieMin, calorieMax, sulfurMin, sulfurMax, ashMin, ashMax, waterMin, waterMax, volatileMin, volatileMax, useWeighted} = params; const sulMin = sulfurMin!==''?+sulfurMin:-Infinity, sulMax = sulfurMax!==''?+sulfurMax:Infinity; const ashMin_ = ashMin!==''?+ashMin:-Infinity, ashMax_ = ashMax!==''?+ashMax:Infinity; const waterMin_ = waterMin!==''?+waterMin:-Infinity, waterMax_ = waterMax!==''?+waterMax:Infinity; const volMin = volatileMin!==''?+volatileMin:-Infinity, volMax = volatileMax!==''?+volatileMax:Infinity; const constraints = [calorieMin, calorieMax, sulMin, sulMax, ashMin_, ashMax_, waterMin_, waterMax_, volMin, volMax]; const indices = Array.from({length: coalData.length}, (_,i)=>i).sort((a,b)=>coalData[a].price - coalData[b].price); const sortedCoals = indices.map(i=>coalData[i]); const minPrices = []; let minPP = Infinity; for (let i=sortedCoals.length-1; i>=0; i--) { if (sortedCoals[i].price < minPP) minPP = sortedCoals[i].price; minPrices[i] = minPP; } let totalCombos = 0; const allResults = []; for (let n=1; n<=maxShovels; n++) { let upperBound = Infinity; const greedy = (()=>{ const combo = Array(sortedCoals.length).fill(0); let rem = n; for (let i=0; i<sortedCoals.length&&rem>0; i++) { combo[i] = rem; const r = evaluateCombo(sortedCoals, combo, useWeighted, constraints); if (r) { rem=0; break; } combo[i] = 0; } return rem>0 ? null : evaluateCombo(sortedCoals, combo, useWeighted, constraints); })(); if (greedy) upperBound = greedy.totalCost; const current = Array(sortedCoals.length).fill(0); (function dfs(idx, rem, curCost) { if (rem === 0) { const r = evaluateCombo(sortedCoals, current, useWeighted, constraints); if (r && r.totalCost < upperBound) { upperBound = r.totalCost; allResults.push({...r, combo: current.slice()}); totalCombos++; } return; } if (idx === sortedCoals.length-1) { current[idx] = rem; const r = evaluateCombo(sortedCoals, current, useWeighted, constraints); if (r && r.totalCost < upperBound) { upperBound = r.totalCost; allResults.push({...r, combo: current.slice()}); totalCombos++; } current[idx] = 0; return; } const minCost = minPrices[idx+1] || 0; for (let k=0; k<=rem; k++) { current[idx] = k; const est = curCost + k*sortedCoals[idx].price + (rem-k)minCost; if (est >= upperBound) { current[idx]=0; continue; } dfs(idx+1, rem-k, curCost + ksortedCoals[idx].price); } current[idx] = 0; })(0, n, 0); } return {totalCombos, filteredCount: allResults.length, allResults: allResults.map(r=>{ const orig = Array(coalData.length).fill(0); indices.forEach((oidx, sidx)=>{ orig[oidx] = r.combo[sidx]; }); return {...r, combo: orig}; })}; }
export default async function(req) { if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization" } }); }
const auth = req.headers.get("Authorization");
if (auth !== Bearer ${VALID_TOKEN}) {
return new Response(JSON.stringify({ error: "未授权" }), {
status: 403,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
}
try { const data = await req.json(); const { coalData, params, topN, currentSort } = data; const start = Date.now(); const result = branchAndBound(coalData, params); const elapsed = Date.now() - start; const all = result.allResults; if (currentSort === 'cost') all.sort((a,b)=>a.totalCost-b.totalCost); else if (currentSort === 'tonnage') all.sort((a,b)=>b.maxTonnage-a.maxTonnage); else all.sort((a,b)=>(a.totalCost/a.maxTonnage)-(b.totalCost/b.maxTonnage));
return new Response(JSON.stringify({
totalCombos: result.totalCombos,
filteredCount: result.filteredCount,
elapsed,
allResults: all,
topResults: all.slice(0, topN)
}), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
} catch (e) { return new Response(JSON.stringify({ error: e.message }), { status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }); } }