XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEEDToolBox.Core.Models;

namespace XFEEDToolBox.Core.Planning;

public sealed record CargoCandidate(string Commodity, string Name, long BuyPrice, long SellPrice, int Limit)
{
    public long ProfitPerTon => SellPrice - BuyPrice;
}
public sealed record CargoSolution(IReadOnlyList<TradeOrder> Orders, bool ProvenOptimal, int Nodes)
{
    public decimal Profit => Orders.Sum(x => x.Profit);
}

/// <summary>Integer bounded knapsack with both cash and tonnage constraints, plus explicit node budget.</summary>
public static class CargoOptimizer
{
    public static CargoSolution Optimize(IEnumerable<CargoCandidate> source, int capacity, decimal budget, int nodeBudget = 2500, CancellationToken token = default)
    {
        var goods = source.Where(x => x.BuyPrice > 0 && x.ProfitPerTon > 0 && x.Limit > 0)
            .OrderByDescending(x => x.ProfitPerTon).ThenBy(x => x.BuyPrice).ThenBy(x => x.Commodity, StringComparer.Ordinal).ToArray();
        if (capacity <= 0 || budget <= 0 || goods.Length == 0) return new([], true, 0);
        var current = new int[goods.Length];
        var best = new int[goods.Length];
        decimal bestProfit = 0, bestCost = 0;
        var nodes = 0;
        var truncated = false;

        void Remember(decimal profit, decimal cost)
        {
            if (profit > bestProfit || profit == bestProfit && cost < bestCost)
            { bestProfit = profit; bestCost = cost; Array.Copy(current, best, current.Length); }
        }

        void Seed(IEnumerable<int> order)
        {
            Array.Clear(current);
            var hold = capacity; var cash = budget; decimal profit = 0;
            foreach (var i in order)
            {
                var q = (int)Math.Min(Math.Min(goods[i].Limit, hold), Math.Min(int.MaxValue, decimal.Floor(cash / goods[i].BuyPrice)));
                current[i] = q; hold -= q; cash -= (decimal)q * goods[i].BuyPrice; profit += (decimal)q * goods[i].ProfitPerTon;
            }
            Remember(profit, budget - cash);
        }
        Seed(Enumerable.Range(0, goods.Length));
        Seed(Enumerable.Range(0, goods.Length).OrderByDescending(i => (decimal)goods[i].ProfitPerTon / goods[i].BuyPrice));
        Seed(Enumerable.Range(0, goods.Length).OrderBy(i => goods[i].BuyPrice));
        Array.Clear(current);

        void Search(int index, int hold, decimal cash, decimal profit, decimal cost)
        {
            token.ThrowIfCancellationRequested();
            if (++nodes > nodeBudget) { truncated = true; return; }
            Remember(profit, cost);
            if (index >= goods.Length || hold == 0 || cash <= 0) return;
            decimal byHold = 0, bestRoi = 0;
            var remaining = hold;
            for (var i = index; i < goods.Length; i++)
            {
                var q = Math.Min(remaining, goods[i].Limit);
                byHold += (decimal)q * goods[i].ProfitPerTon; remaining -= q;
                bestRoi = Math.Max(bestRoi, (decimal)goods[i].ProfitPerTon / goods[i].BuyPrice);
            }
            var upper = profit + Math.Min(byHold, cash * bestRoi);
            if (upper <= bestProfit) return;
            var g = goods[index];
            var max = (int)Math.Min(Math.Min(g.Limit, hold), Math.Min(int.MaxValue, decimal.Floor(cash / g.BuyPrice)));
            for (var q = max; q >= 0; q--)
            {
                if (nodes >= nodeBudget) { truncated = true; break; }
                current[index] = q;
                Search(index + 1, hold - q, cash - (decimal)q * g.BuyPrice, profit + (decimal)q * g.ProfitPerTon, cost + (decimal)q * g.BuyPrice);
                current[index] = 0;
            }
        }
        Search(0, capacity, budget, 0, 0);
        var orders = goods.Select((g, i) => new TradeOrder(g.Commodity, g.Name, best[i], g.BuyPrice, g.SellPrice)).Where(x => x.Quantity > 0).ToArray();
        return new(orders, !truncated, nodes);
    }
}