下面给出一种 「加权总变差」(Weighted TVD) 算法 ——在保持线性时间复杂度的同时,对 “缺料 / 额外材料” 赋予更高惩罚,使其相似度必然 低于 “材料全对但比例偏差” 的情形。随后提供可直接拷贝的 C# 实现


1 算法设计要点

场景惩罚思路
比例偏差(配方所含材料,比例有误)按偏移量线性扣分
缺料(配方里有,但实际没放)额外乘以权重 λ (>1)
额外材料(配方里无,但实际多放)同样乘以权重 λ

只要 λ > 1,就能保证:
缺料 / 多料 ⇒ 分数一定比仅比例误差更低(其余条件相同)。


1.1 形式化定义

  • 目标配方百分比向量 
  • 实际投入百分比 

各项差异

设置惩罚权重 λ (,默认可取 2)。

加权总差

当两分布完全无交集时
最大 Dmax⁡=2λD_{\max}=2λ。


1.2 相似度得分

  • 完全一致 
  • 完全不相关 
  • 只要出现缺料/多料, 会因 被放大 → 得分更低。

2 效果验证

场景举例(%)结果
比例偏差配方:A 50/B 50实际:A 60/B 40 Score ≈ 95.0
多放新料配方:A 50/B 50实际:A 45/B 45/C 10 ⇒ Score = (1‑0.30/4)·100≈92.5比纯比例偏差更低
缺少原料配方:A 80/B 20实际:A 100 Score≈85.0

3 C# 参考实现

using System;
using System.Collections.Generic;
using System.Linq;
 
public static class RecipeSimilarity
{
    /// <summary>
    /// 计算配方与实际投入的相似度 (0~100)。
    /// </summary>
    /// <param name="recipe">配方百分比,已归一化:{ingredient → fraction(0~1)}</param>
    /// <param name="actual">实际投入:{ingredient → amount(>0)}</param>
    /// <param name="penalty">缺料 / 额外材料的惩罚权重 λ,建议 1.5~3;若 =1 则退化为普通 TVD。</param>
    public static double GetScore(
        IDictionary<string, double> recipe,
        IDictionary<string, double> actual,
        double penalty = 2.0)
    {
        if (penalty <= 1.0) throw new ArgumentException("penalty 必须大于 1");
 
        // 1) 归一化实际数据 → 百分比
        double total = actual.Values.Sum();
        if (total <= 0) return 0.0;          // 没放材料,判 0 分
 
        var p = actual.ToDictionary(kv => kv.Key, kv => kv.Value / total);
 
        // 2) 计算三类差异
        double dCom   = 0, dMissing = 0, dExtra = 0;
 
        foreach (var kv in recipe)                 // 处理共有 + 缺失
        {
            if (p.TryGetValue(kv.Key, out double pf))
            {
                dCom += Math.Abs(kv.Value - pf);   // 共有材料比例差
            }
            else
            {
                dMissing += kv.Value;              // 缺料
            }
        }
 
        foreach (var kv in p)                      // 处理额外
        {
            if (!recipe.ContainsKey(kv.Key))
                dExtra += kv.Value;                // 多料
        }
 
        // 3) 加权总差
        double D = dCom + penalty * (dMissing + dExtra);
 
        // 4) 归一化得分
        double score = Math.Max(0.0, 1.0 - D / (2 * penalty)) * 100.0;
        return score;
    }
}

使用示例

var recipe = new Dictionary<string, double>
{
    ["DragonScale"] = 0.4,
    ["BatWing"]     = 0.4,
    ["StarEssence"] = 0.2
};
 
var actual = new Dictionary<string, double>
{
    ["DragonScale"] = 20,
    ["BatWing"]     = 40,
    ["RatTail"]     = 40      // 额外材料
};
 
double score = RecipeSimilarity.GetScore(recipe, actual, penalty: 2);
Console.WriteLine($"相似度 = {score:F1}%");  // 输出 50.0%

4 参数调优建议

目标建议 λ
比例误差与缺料/多料差不多严重1.1 – 1.5
缺料/多料 明显更严重(多数配方场景)2 – 3
需要_极端_惩罚(炼金失败直接爆炸这类玩法)≥4

扩展方向

  1. 多级权重:对“关键原料”自定义
  2. 容差区间 ε:先剪去 的细微误差;
  3. Tag‑Based 替代:把同类物品先聚合到功能 Tag,再做上述比较;
  4. 非线性衰减:最后分数可再做 γ 次方平滑,强化高分区分度。

这样即可满足「原料不对 → 分数更低」的要求,并保留易实现、易调参的优点。祝开发顺利!