java: Gale-Shapley Algorithm

项目结构

java 复制代码
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:51
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : AppConfig.java
 * explain   : 学习  类
 **/
 
package GaleShapley.config;
 
import java.io.File;
 
public class AppConfig {
    // 匹配模式:true=反向(供给方求婚企业优先) false=正向
    public static boolean REVERSE_MODE = true;
    public static int MAX_MATCH_ROUND = 20000;
    public static boolean ENABLE_PERFORMANCE_LOG = true;
    public static boolean EXPORT_EXCEL = true;
    public static boolean EXPORT_CHART = true;
 
    // 路径配置
    public static final String BASE_DIR = System.getProperty("user.dir");
    public static final String OUTPUT_DIR = BASE_DIR + File.separator + "output";
 
    // 绘图配置 对标gonum、ScottPlot
    public static final int CHART_WIDTH = 1400;
    public static final int CHART_HEIGHT = 700;
    public static final int HIST_BIN_COUNT = 25;
    public static final String FONT_NAME = "SimHei"; // 黑体解决中文乱码
 
    static {
        File dir = new File(OUTPUT_DIR);
        if (!dir.exists()) dir.mkdirs();
    }
}
 
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:51
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : MatchSubject.java
 * explain   : 学习  类
 **/
 
package GaleShapley.model;
 
import java.util.HashMap;
import java.util.Map;
 
public class MatchSubject {
    private String id;
    private String name;
    private double score;
    private Map<String, Double> attrs = new HashMap<>();
 
    // getter setter
    public String getId() {return id;}
    public void setId(String id) {this.id = id;}
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    public double getScore() {return score;}
    public void setScore(double score) {this.score = score;}
    public Map<String, Double> getAttrs() {return attrs;}
    public void setAttrs(Map<String, Double> attrs) {this.attrs = attrs;}
}
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:55
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : PreferenceList.java
 * explain   : 学习  类
 **/
 
package GaleShapley.model;
 
import java.util.ArrayList;
import java.util.List;
 
public class PreferenceList {
    private String ownerId;
    private List<String> rankedIds = new ArrayList<>();
 
    public String getOwnerId() {return ownerId;}
    public void setOwnerId(String ownerId) {this.ownerId = ownerId;}
    public List<String> getRankedIds() {return rankedIds;}
    public void setRankedIds(List<String> rankedIds) {this.rankedIds = rankedIds;}
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:55
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : MatchResultItem.java
 * explain   : 学习  类
 **/
 
package GaleShapley.model;
 
public class MatchResultItem {
    private String proposerId;
    private String proposerName;
    private String acceptorId;
    private String acceptorName;
    private double matchScore;
 
    // getter setter
    public String getProposerId() {return proposerId;}
    public void setProposerId(String proposerId) {this.proposerId = proposerId;}
    public String getProposerName() {return proposerName;}
    public void setProposerName(String proposerName) {this.proposerName = proposerName;}
    public String getAcceptorId() {return acceptorId;}
    public void setAcceptorId(String acceptorId) {this.acceptorId = acceptorId;}
    public String getAcceptorName() {return acceptorName;}
    public void setAcceptorName(String acceptorName) {this.acceptorName = acceptorName;}
    public double getMatchScore() {return matchScore;}
    public void setMatchScore(double matchScore) {this.matchScore = matchScore;}
}
 
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:56
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : MatchOutput.java
 * explain   : 学习  类
 **/
 
package GaleShapley.model;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
public class MatchOutput {
    private String sceneName;
    private boolean reverseMode;
    private List<MatchResultItem> matchList = new ArrayList<>();
    private List<MatchSubject> unmatchedProposers = new ArrayList<>();
    private List<MatchSubject> unmatchedAcceptors = new ArrayList<>();
    private double totalCostMs;
    private Date createTime = new Date();
 
    // getter setter
    public String getSceneName() {return sceneName;}
    public void setSceneName(String sceneName) {this.sceneName = sceneName;}
    public boolean isReverseMode() {return reverseMode;}
    public void setReverseMode(boolean reverseMode) {this.reverseMode = reverseMode;}
    public List<MatchResultItem> getMatchList() {return matchList;}
    public void setMatchList(List<MatchResultItem> matchList) {this.matchList = matchList;}
    public List<MatchSubject> getUnmatchedProposers() {return unmatchedProposers;}
    public void setUnmatchedProposers(List<MatchSubject> unmatchedProposers) {this.unmatchedProposers = unmatchedProposers;}
    public List<MatchSubject> getUnmatchedAcceptors() {return unmatchedAcceptors;}
    public void setUnmatchedAcceptors(List<MatchSubject> unmatchedAcceptors) {this.unmatchedAcceptors = unmatchedAcceptors;}
    public double getTotalCostMs() {return totalCostMs;}
    public void setTotalCostMs(double totalCostMs) {this.totalCostMs = totalCostMs;}
    public Date getCreateTime() {return createTime;}
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:56
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : GaleShapleySolver.java
 * explain   : 学习  类
 **/
 
package GaleShapley.core;
 
import GaleShapley.config.AppConfig;
import GaleShapley.model.PreferenceList;
 
import java.util.*;
 
public class GaleShapleySolver {
    private final boolean reverseMode;
    private final int maxRound;
 
    public GaleShapleySolver(boolean reverseMode) {
        this.reverseMode = reverseMode;
        this.maxRound = AppConfig.MAX_MATCH_ROUND;
    }
 
    /**
     * 返回:匹配Map、未匹配求婚方、未匹配接收方、耗时ms
     */
    public SolveResult solve(List<PreferenceList> proposerPrefs, List<PreferenceList> acceptorPrefs) {
        long start = System.nanoTime();
 
        Map<String, List<String>> propPrefMap = new HashMap<>();
        Map<String, List<String>> accPrefMap = new HashMap<>();
        for (PreferenceList pl : proposerPrefs) propPrefMap.put(pl.getOwnerId(), pl.getRankedIds());
        for (PreferenceList pl : acceptorPrefs) accPrefMap.put(pl.getOwnerId(), pl.getRankedIds());
 
        // 接收方偏好排名缓存
        Map<String, Map<String, Integer>> accRankCache = new HashMap<>();
        for (PreferenceList pl : acceptorPrefs) {
            Map<String, Integer> rank = new HashMap<>();
            List<String> ids = pl.getRankedIds();
            for (int i = 0; i < ids.size(); i++) rank.put(ids.get(i), i);
            accRankCache.put(pl.getOwnerId(), rank);
        }
 
        Set<String> freeProposers = new HashSet<>(propPrefMap.keySet());
        Map<String, Integer> nextIndex = new HashMap<>();
        for (String pid : propPrefMap.keySet()) nextIndex.put(pid, 0);
 
        Map<String, String> acceptorMatched = new HashMap<>();
        Map<String, String> finalMatch = new HashMap<>();
        int roundCnt = 0;
 
        while (!freeProposers.isEmpty() && roundCnt < maxRound) {
            roundCnt++;
            String proposerId = freeProposers.iterator().next();
            freeProposers.remove(proposerId);
 
            List<String> prefList = propPrefMap.get(proposerId);
            int idx = nextIndex.get(proposerId);
            if (idx >= prefList.size()) continue;
            String targetAcc = prefList.get(idx);
            nextIndex.put(proposerId, idx + 1);
 
            if (!acceptorMatched.containsKey(targetAcc)) {
                acceptorMatched.put(targetAcc, proposerId);
                finalMatch.put(proposerId, targetAcc);
            } else {
                String oldProp = acceptorMatched.get(targetAcc);
                int rankNew = accRankCache.get(targetAcc).get(proposerId);
                int rankOld = accRankCache.get(targetAcc).get(oldProp);
                if (rankNew < rankOld) {
                    freeProposers.add(oldProp);
                    acceptorMatched.put(targetAcc, proposerId);
                    finalMatch.put(proposerId, targetAcc);
                    finalMatch.remove(oldProp);
                } else {
                    freeProposers.add(proposerId);
                }
            }
        }
 
        Set<String> unmatchedAcceptors = new HashSet<>();
        for (String aid : accPrefMap.keySet()) {
            if (!acceptorMatched.containsKey(aid)) unmatchedAcceptors.add(aid);
        }
        double costMs = (System.nanoTime() - start) / 1000000.0;
        if (AppConfig.ENABLE_PERFORMANCE_LOG) {
            System.out.printf("[GS算法耗时] %.2f ms | 迭代轮数:%d%n", costMs, roundCnt);
        }
        return new SolveResult(finalMatch, freeProposers, unmatchedAcceptors, costMs);
    }
 
    public static class SolveResult {
        public Map<String, String> matchMap;
        public Set<String> unmatchProposers;
        public Set<String> unmatchAcceptors;
        public double costMs;
        public SolveResult(Map<String, String> m, Set<String> up, Set<String> ua, double c) {
            matchMap = m;
            unmatchProposers = up;
            unmatchAcceptors = ua;
            costMs = c;
        }
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:57
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : ScoreEngine.java
 * explain   : 学习  类
 **/
 
package GaleShapley.service;
 
import GaleShapley.model.MatchSubject;
import GaleShapley.model.PreferenceList;
 
import java.util.List;
import java.util.stream.Collectors;
 
@FunctionalInterface
interface ScoreFunc {
    double score(MatchSubject self, MatchSubject candidate);
}
 
public class ScoreEngine {
    public static PreferenceList buildPreference(MatchSubject self, List<MatchSubject> pool, ScoreFunc func) {
        List<String> sortedIds = pool.stream()
                .map(c -> new Item(func.score(self, c), c.getId()))
                .sorted((a, b) -> Double.compare(b.score, a.score))
                .map(item -> item.id)
                .collect(Collectors.toList());
        PreferenceList pl = new PreferenceList();
        pl.setOwnerId(self.getId());
        pl.setRankedIds(sortedIds);
        return pl;
    }
 
    public static List<PreferenceList> batchGenerate(List<MatchSubject> main, List<MatchSubject> pool, ScoreFunc func) {
        return main.stream().map(s -> buildPreference(s, pool, func)).collect(Collectors.toList());
    }
 
    static class Item {
        double score;
        String id;
        Item(double s, String id) {this.score = s; this.id = id;}
    }
 
    // ========== 四大场景打分权重和C#完全一致 ==========
    // 场景1 工匠-订单
    public static double artisanOrder(MatchSubject artisan, MatchSubject order) {
        return 0.6 * order.getAttrs().get("profit") + 0.4 * order.getAttrs().get("skill_match");
    }
    public static double orderArtisan(MatchSubject order, MatchSubject artisan) {
        return 0.5 * artisan.getAttrs().get("delivery_score") + 0.5 * artisan.getAttrs().get("craft_level");
    }
 
    // 场景2 销售-客户
    public static double salesCustomer(MatchSubject sales, MatchSubject customer) {
        return 0.7 * customer.getAttrs().get("consume_level") + 0.3 * customer.getAttrs().get("style_fit");
    }
    public static double customerSales(MatchSubject customer, MatchSubject sales) {
        return 0.5 * sales.getAttrs().get("service_score") + 0.5 * sales.getAttrs().get("profession");
    }
 
    // 场景3 供应商-工厂
    public static double supplierFactory(MatchSubject supplier, MatchSubject factory) {
        return 0.7 * factory.getAttrs().get("order_amount") + 0.3 * factory.getAttrs().get("payment_score");
    }
    public static double factorySupplier(MatchSubject factory, MatchSubject supplier) {
        return 0.6 * supplier.getAttrs().get("price_score") + 0.4 * supplier.getAttrs().get("stable_score");
    }
 
    // 场景4 设计师-新品需求
    public static double designerDemand(MatchSubject designer, MatchSubject demand) {
        return 0.65 * demand.getAttrs().get("project_bonus") + 0.35 * demand.getAttrs().get("style_familiar");
    }
    public static double demandDesigner(MatchSubject demand, MatchSubject designer) {
        return designer.getAttrs().get("success_rate");
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:57
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : JewelrySceneService.java
 * explain   : 学习  类
 **/
 
package GaleShapley.service;
 
import GaleShapley.config.AppConfig;
import GaleShapley.core.GaleShapleySolver;
import GaleShapley.model.MatchOutput;
import GaleShapley.model.MatchResultItem;
import GaleShapley.model.MatchSubject;
import GaleShapley.model.PreferenceList;
 
import java.util.*;
import java.util.stream.Collectors;
 
public class JewelrySceneService {
    private static final Random RAND = new Random();
 
    private static String newId() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 8);
    }
 
    private static double randDouble(double min, double max) {
        return min + RAND.nextDouble() * (max - min);
    }
 
    // 场景1 工匠-定制订单
    public static MatchOutput scene1ArtisanOrder(int scale) {
        System.out.printf("%n=====【场景1:工匠-定制珠宝订单匹配】规模:%d =====%n", scale);
        List<MatchSubject> artisans = new ArrayList<>();
        List<MatchSubject> orders = new ArrayList<>();
        for (int i = 1; i <= scale; i++) {
            MatchSubject art = new MatchSubject();
            art.setId(newId());
            art.setName(String.format("工匠_%04d", i));
            art.setScore(randDouble(60, 95));
            Map<String, Double> aAttr = new HashMap<>();
            aAttr.put("delivery_score", randDouble(50, 100));
            aAttr.put("craft_level", randDouble(50, 100));
            art.setAttrs(aAttr);
            artisans.add(art);
 
            MatchSubject ord = new MatchSubject();
            ord.setId(newId());
            ord.setName(String.format("定制订单_%04d", i));
            ord.setScore(randDouble(50, 98));
            Map<String, Double> oAttr = new HashMap<>();
            oAttr.put("profit", randDouble(10, 100));
            oAttr.put("skill_match", randDouble(0, 100));
            ord.setAttrs(oAttr);
            orders.add(ord);
        }
        List<MatchSubject> proposers, acceptors;
        ScoreFunc propScore, accScore;
        if (AppConfig.REVERSE_MODE) {
            proposers = artisans; acceptors = orders;
            propScore = ScoreEngine::artisanOrder;
            accScore = ScoreEngine::orderArtisan;
        } else {
            proposers = orders; acceptors = artisans;
            propScore = ScoreEngine::orderArtisan;
            accScore = ScoreEngine::artisanOrder;
        }
        return runMatch("工匠_定制订单", proposers, acceptors, propScore, accScore);
    }
 
    // 场景2 销售-客户
    public static MatchOutput scene2SalesCustomer(int scale) {
        System.out.printf("%n=====【场景2:销售顾问-高端客户匹配】规模:%d =====%n", scale);
        List<MatchSubject> sales = new ArrayList<>();
        List<MatchSubject> customers = new ArrayList<>();
        for (int i = 1; i <= scale; i++) {
            MatchSubject s = new MatchSubject();
            s.setId(newId());
            s.setName(String.format("销售_%04d", i));
            s.setScore(randDouble(60, 95));
            Map<String, Double> sa = new HashMap<>();
            sa.put("service_score", randDouble(50, 100));
            sa.put("profession", randDouble(50, 100));
            s.setAttrs(sa);
            sales.add(s);
 
            MatchSubject c = new MatchSubject();
            c.setId(newId());
            c.setName(String.format("高端客户_%04d", i));
            c.setScore(randDouble(55, 99));
            Map<String, Double> ca = new HashMap<>();
            ca.put("consume_level", randDouble(10, 100));
            ca.put("style_fit", randDouble(0, 100));
            c.setAttrs(ca);
            customers.add(c);
        }
        List<MatchSubject> proposers, acceptors;
        ScoreFunc propScore, accScore;
        if (AppConfig.REVERSE_MODE) {
            proposers = sales; acceptors = customers;
            propScore = ScoreEngine::salesCustomer;
            accScore = ScoreEngine::customerSales;
        } else {
            proposers = customers; acceptors = sales;
            propScore = ScoreEngine::customerSales;
            accScore = ScoreEngine::salesCustomer;
        }
        return runMatch("销售_高端客户", proposers, acceptors, propScore, accScore);
    }
 
    // 场景3 供应商-工厂
    public static MatchOutput scene3SupplierFactory(int scale) {
        System.out.printf("%n=====【场景3:原料供应商-加工厂采购】规模:%d =====%n", scale);
        List<MatchSubject> suppliers = new ArrayList<>();
        List<MatchSubject> factories = new ArrayList<>();
        for (int i = 1; i <= scale; i++) {
            MatchSubject sp = new MatchSubject();
            sp.setId(newId());
            sp.setName(String.format("原料供应商_%04d", i));
            sp.setScore(randDouble(60, 96));
            Map<String, Double> spa = new HashMap<>();
            spa.put("price_score", randDouble(40, 100));
            spa.put("stable_score", randDouble(40, 100));
            sp.setAttrs(spa);
            suppliers.add(sp);
 
            MatchSubject fac = new MatchSubject();
            fac.setId(newId());
            fac.setName(String.format("加工厂采购_%04d", i));
            fac.setScore(randDouble(50, 97));
            Map<String, Double> faca = new HashMap<>();
            faca.put("order_amount", randDouble(20, 100));
            faca.put("payment_score", randDouble(30, 100));
            fac.setAttrs(faca);
            factories.add(fac);
        }
        List<MatchSubject> proposers, acceptors;
        ScoreFunc propScore, accScore;
        if (AppConfig.REVERSE_MODE) {
            proposers = suppliers; acceptors = factories;
            propScore = ScoreEngine::supplierFactory;
            accScore = ScoreEngine::factorySupplier;
        } else {
            proposers = factories; acceptors = suppliers;
            propScore = ScoreEngine::factorySupplier;
            accScore = ScoreEngine::supplierFactory;
        }
        return runMatch("原料供应商_加工厂", proposers, acceptors, propScore, accScore);
    }
 
    // 场景4 设计师-新品需求
    public static MatchOutput scene4DesignerDemand(int scale) {
        System.out.printf("%n=====【场景4:珠宝设计师_新品开发需求】规模:%d =====%n", scale);
        List<MatchSubject> designers = new ArrayList<>();
        List<MatchSubject> demands = new ArrayList<>();
        for (int i = 1; i <= scale; i++) {
            MatchSubject dsg = new MatchSubject();
            dsg.setId(newId());
            dsg.setName(String.format("设计师_%04d", i));
            dsg.setScore(randDouble(65, 96));
            Map<String, Double> dsa = new HashMap<>();
            dsa.put("success_rate", randDouble(40, 100));
            dsg.setAttrs(dsa);
            designers.add(dsg);
 
            MatchSubject dm = new MatchSubject();
            dm.setId(newId());
            dm.setName(String.format("新品项目_%04d", i));
            dm.setScore(randDouble(55, 98));
            Map<String, Double> dma = new HashMap<>();
            dma.put("project_bonus", randDouble(10, 90));
            dma.put("style_familiar", randDouble(0, 100));
            dm.setAttrs(dma);
            demands.add(dm);
        }
        List<MatchSubject> proposers, acceptors;
        ScoreFunc propScore, accScore;
        if (AppConfig.REVERSE_MODE) {
            proposers = designers; acceptors = demands;
            propScore = ScoreEngine::designerDemand;
            accScore = ScoreEngine::demandDesigner;
        } else {
            proposers = demands; acceptors = designers;
            propScore = ScoreEngine::demandDesigner;
            accScore = ScoreEngine::designerDemand;
        }
        return runMatch("设计师_新品需求", proposers, acceptors, propScore, accScore);
    }
 
    // 通用匹配组装
    private static MatchOutput runMatch(String sceneName, List<MatchSubject> proposers, List<MatchSubject> acceptors,
                                        ScoreFunc propScore, ScoreFunc accScore) {
        List<PreferenceList> propPrefs = ScoreEngine.batchGenerate(proposers, acceptors, propScore);
        List<PreferenceList> accPrefs = ScoreEngine.batchGenerate(acceptors, proposers, accScore);
        GaleShapleySolver solver = new GaleShapleySolver(AppConfig.REVERSE_MODE);
        GaleShapleySolver.SolveResult res = solver.solve(propPrefs, accPrefs);
 
        Map<String, MatchSubject> propMap = proposers.stream().collect(Collectors.toMap(MatchSubject::getId, s -> s));
        Map<String, MatchSubject> accMap = acceptors.stream().collect(Collectors.toMap(MatchSubject::getId, s -> s));
 
        List<MatchResultItem> matchList = new ArrayList<>();
        for (Map.Entry<String, String> entry : res.matchMap.entrySet()) {
            MatchSubject p = propMap.get(entry.getKey());
            MatchSubject a = accMap.get(entry.getValue());
            MatchResultItem item = new MatchResultItem();
            item.setProposerId(p.getId());
            item.setProposerName(p.getName());
            item.setAcceptorId(a.getId());
            item.setAcceptorName(a.getName());
            item.setMatchScore((p.getScore() + a.getScore()) / 2.0);
            matchList.add(item);
        }
        List<MatchSubject> unProp = res.unmatchProposers.stream().map(propMap::get).collect(Collectors.toList());
        List<MatchSubject> unAcc = res.unmatchAcceptors.stream().map(accMap::get).collect(Collectors.toList());
        System.out.printf("匹配成功数量:%d | 求婚方未匹配:%d | 接收方未匹配:%d%n", matchList.size(), unProp.size(), unAcc.size());
 
        MatchOutput output = new MatchOutput();
        output.setSceneName(sceneName);
        output.setReverseMode(AppConfig.REVERSE_MODE);
        output.setMatchList(matchList);
        output.setUnmatchedProposers(unProp);
        output.setUnmatchedAcceptors(unAcc);
        output.setTotalCostMs(res.costMs);
        return output;
    }
}
 
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 22:59
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : ExcelExporter.java
 * explain   : 学习  类
 **/
 
package GaleShapley.util;
 
import GaleShapley.config.AppConfig;
import GaleShapley.model.MatchOutput;
import GaleShapley.model.MatchResultItem;
import GaleShapley.model.MatchSubject;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
 
public class ExcelExporter {
    public static String export(MatchOutput output) {
        String time = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String fileName = "匹配结果_" + output.getSceneName() + "_" + time + ".xlsx";
        String fullPath = AppConfig.OUTPUT_DIR + File.separator + fileName;
        try (Workbook workbook = new XSSFWorkbook()) {
            // 1.匹配明细
            Sheet sheet1 = workbook.createSheet("匹配明细");
            String[] header1 = {"求婚方ID", "求婚方名称", "接收方ID", "接收方名称", "匹配综合得分"};
            createHeader(sheet1, header1);
            int rowIdx = 1;
            for (MatchResultItem item : output.getMatchList()) {
                Row row = sheet1.createRow(rowIdx++);
                row.createCell(0).setCellValue(item.getProposerId());
                row.createCell(1).setCellValue(item.getProposerName());
                row.createCell(2).setCellValue(item.getAcceptorId());
                row.createCell(3).setCellValue(item.getAcceptorName());
                row.createCell(4).setCellValue(item.getMatchScore());
            }
            // 2.未匹配求婚方
            Sheet sheet2 = workbook.createSheet("未匹配求婚方");
            String[] h2 = {"ID", "名称", "综合分数"};
            createHeader(sheet2, h2);
            rowIdx = 1;
            for (MatchSubject s : output.getUnmatchedProposers()) {
                Row r = sheet2.createRow(rowIdx++);
                r.createCell(0).setCellValue(s.getId());
                r.createCell(1).setCellValue(s.getName());
                r.createCell(2).setCellValue(s.getScore());
            }
            //3.未匹配接收方
            Sheet sheet3 = workbook.createSheet("未匹配接收方");
            createHeader(sheet3, h2);
            rowIdx = 1;
            for (MatchSubject s : output.getUnmatchedAcceptors()) {
                Row r = sheet3.createRow(rowIdx++);
                r.createCell(0).setCellValue(s.getId());
                r.createCell(1).setCellValue(s.getName());
                r.createCell(2).setCellValue(s.getScore());
            }
            //4.摘要
            Sheet sheet4 = workbook.createSheet("运行摘要");
            String[][] summary = {
                    {"场景名称", output.getSceneName()},
                    {"反向模式(企业优先)", String.valueOf(output.isReverseMode())},
                    {"成功匹配数", String.valueOf(output.getMatchList().size())},
                    {"未匹配求婚方", String.valueOf(output.getUnmatchedProposers().size())},
                    {"未匹配接收方", String.valueOf(output.getUnmatchedAcceptors().size())},
                    {"算法耗时(ms)", String.format("%.2f", output.getTotalCostMs())}
            };
            for (int i = 0; i < summary.length; i++) {
                Row row = sheet4.createRow(i);
                row.createCell(0).setCellValue(summary[i][0]);
                row.createCell(1).setCellValue(summary[i][1]);
            }
            try (FileOutputStream out = new FileOutputStream(fullPath)) {
                workbook.write(out);
            }
            System.out.println("✅ Excel已导出:" + fullPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return fullPath;
    }
 
    private static void createHeader(Sheet sheet, String[] headers) {
        Row row = sheet.createRow(0);
        for (int i = 0; i < headers.length; i++) {
            row.createCell(i).setCellValue(headers[i]);
        }
    }
}
 
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 23:00
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : ChartVisualizer.java
 * explain   : 学习  类
 **/
 
package GaleShapley.util;
 
import GaleShapley.config.AppConfig;
import GaleShapley.model.MatchOutput;
import GaleShapley.model.MatchResultItem;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.data.statistics.HistogramDataset;
import org.jfree.data.statistics.HistogramType;
 
import java.awt.*;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
 
public class ChartVisualizer {
    public static String drawScoreHistogram(MatchOutput output) {
        String time = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String name = "得分分布_" + output.getSceneName() + "_" + time + ".png";
        String path = AppConfig.OUTPUT_DIR + File.separator + name;
        double[] scores = output.getMatchList().stream().mapToDouble(MatchResultItem::getMatchScore).toArray();
        if (scores.length == 0) {
            System.out.println("⚠️ 无匹配数据,跳过绘图");
            return "";
        }
        // 构建直方图数据集
        HistogramDataset dataset = new HistogramDataset();
        dataset.setType(HistogramType.FREQUENCY);
        dataset.addSeries("匹配得分", scores, AppConfig.HIST_BIN_COUNT);
        // 创建图表
        JFreeChart chart = ChartFactory.createHistogram(
                "【" + output.getSceneName() + "】匹配综合得分分布",
                "匹配综合得分",
                "匹配对数",
                dataset,
                PlotOrientation.VERTICAL,
                false, true, false
        );
        // 统一字体,解决中文乱码
        Font font = new Font(AppConfig.FONT_NAME, Font.PLAIN, 16);
        chart.getTitle().setFont(font);
        XYPlot plot = chart.getXYPlot();
        NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
        NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
        xAxis.setLabelFont(font);
        yAxis.setLabelFont(font);
        xAxis.setTickLabelFont(font);
        yAxis.setTickLabelFont(font);
        XYBarRenderer renderer = (XYBarRenderer) plot.getRenderer();
        renderer.setSeriesPaint(0, new Color(70, 130, 180, 180));
        // 保存图片
        try {
            ChartUtils.saveChartAsPNG(new File(path), chart, AppConfig.CHART_WIDTH, AppConfig.CHART_HEIGHT);
            System.out.println("✅ 得分分布图已导出:" + path);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return path;
    }
}

调用:

java 复制代码
/**
 * encoding: utf-8
 * 版权所有 2026 ©涂聚文有限公司 ®
 * 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
 * 描述:Gale-Shapley Algorithm
 * Author    : geovindu,Geovin Du 涂聚文.
 * IDE       : IntelliJ IDEA 2024.3.6 Java 17
 * # database  : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
 * # OS        : window10
 * Datetime  : 2026 - 2026/8/1 - 23:00
 * User      : geovindu
 * Product   : IntelliJ IDEA
 * Project   : JavaAlgorithms
 * File      : ChartVisualizer.java
 * explain   : 学习  类
 **/
 
package GaleShapley.util;
 
import GaleShapley.config.AppConfig;
import GaleShapley.model.MatchOutput;
import GaleShapley.model.MatchResultItem;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.data.statistics.HistogramDataset;
import org.jfree.data.statistics.HistogramType;
 
import java.awt.*;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
 
public class ChartVisualizer {
    public static String drawScoreHistogram(MatchOutput output) {
        String time = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String name = "得分分布_" + output.getSceneName() + "_" + time + ".png";
        String path = AppConfig.OUTPUT_DIR + File.separator + name;
        double[] scores = output.getMatchList().stream().mapToDouble(MatchResultItem::getMatchScore).toArray();
        if (scores.length == 0) {
            System.out.println("⚠️ 无匹配数据,跳过绘图");
            return "";
        }
        // 构建直方图数据集
        HistogramDataset dataset = new HistogramDataset();
        dataset.setType(HistogramType.FREQUENCY);
        dataset.addSeries("匹配得分", scores, AppConfig.HIST_BIN_COUNT);
        // 创建图表
        JFreeChart chart = ChartFactory.createHistogram(
                "【" + output.getSceneName() + "】匹配综合得分分布",
                "匹配综合得分",
                "匹配对数",
                dataset,
                PlotOrientation.VERTICAL,
                false, true, false
        );
        // 统一字体,解决中文乱码
        Font font = new Font(AppConfig.FONT_NAME, Font.PLAIN, 16);
        chart.getTitle().setFont(font);
        XYPlot plot = chart.getXYPlot();
        NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
        NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
        xAxis.setLabelFont(font);
        yAxis.setLabelFont(font);
        xAxis.setTickLabelFont(font);
        yAxis.setTickLabelFont(font);
        XYBarRenderer renderer = (XYBarRenderer) plot.getRenderer();
        renderer.setSeriesPaint(0, new Color(70, 130, 180, 180));
        // 保存图片
        try {
            ChartUtils.saveChartAsPNG(new File(path), chart, AppConfig.CHART_WIDTH, AppConfig.CHART_HEIGHT);
            System.out.println("✅ 得分分布图已导出:" + path);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return path;
    }
}

介绍了一个基于Gale-Shapley算法的双边匹配系统实现,包含以下核心内容:

  1. 系统架构分为配置层(AppConfig)、模型层(MatchSubject等)、核心算法层(GaleShapleySolver)和服务层(JewelrySceneService)
  2. 实现了Gale-Shapley稳定匹配算法,支持正向/反向匹配模式,最大匹配轮次可配置
  3. 提供四种业务场景的匹配实现:
    • 工匠-定制订单
    • 销售顾问-高端客户
    • 原料供应商-加工厂
    • 设计师-新品需求
  4. 包含完整的结果输出功能:
    • Excel导出匹配结果和统计摘要
    • JFreeChart生成得分分布直方图
  5. 系统特点:
    • 采用函数式接口实现灵活的打分策略
    • 支持大规模数据匹配(测试规模达20000轮次)
    • 提供性能监控日志

该项目实现了经典稳定匹配算法在珠宝行业多个业务场景中的实际应用,具有完整的输入输出处理能力。

输出:

相关推荐
LUCKY-LIVING1 小时前
ELF File in linux
android·java·linux
冻柠檬飞冰走茶2 小时前
PTA基础编程题目集 7-34 通讯录的录入与显示(C语言实现)
c语言·开发语言·数据结构·算法
zander2582 小时前
LeetCode 79. 单词搜索
算法·深度优先
环境栈笔记2 小时前
指纹浏览器安全评测方法:核对环境、数据与权限后再选型
前端·人工智能·后端·自动化
星核0penstarry2 小时前
DeepSeek-V4-Flash 正式公测:大模型行业进入「极速平价普惠时代」
java·开发语言·人工智能
老洋葱Mr_Onion2 小时前
【C++】高精度模板
开发语言·c++·算法
卷无止境2 小时前
LangExtract:让LLM从杂乱文本中"抠"出结构化数据的开源工具
后端·python
脚踏实地皮皮晨2 小时前
003003002_WPF Grid 基类官方类定义逐行深度解析
开发语言·windows·算法·c#·wpf·visual studio