Hydro RP详解:从入门到…看源码

Hydro RP详解:从入门到…看源码

很多用Hydro或其高级域的朋友们都问过一个问题:RP是什么?具体计算方法是什么?

本文将具体介绍RP以及其已知的计算方法

注:官方没有公开确切的RP计算方法,本文仅通过经验推测和开源查看。如有不足请指出。


1. RP是什么

RP (Rating Points) 是 Hydro 在线评测系统中的一种​综合评分​,可以理解为用户的“声望值”或“实力评级”(部分朋友也称之为“人品”)。它旨在衡量用户在平台上的活跃度和​解题实力​。

同时,排名也是RP决定的。注意,RP为0的用户不会展示在排行榜上。不同域(domain)的RP不互通。


2. RP的决定因素

在Hydro 帮助,官方给出的解释是这样的:

RP 算法

RP 可由题目,活动,比赛等获得。

——Hydro 帮助

恭喜你,又浪费了人生中宝贵的几秒钟去看了一段废话。。


3. RP更新时间

仔细观察过的朋友们会发现:你每天刷题时排名根本不会变,但是第二天,你可能就变了。

然而,本人曾经是作息极其逆天的熬夜战神。一次,我目睹了凌晨三点RP刷新的那一刻。。。

(壮观吧,我也觉得)

直到最近在 Hydro项目的官方文档(DeepWiki) 中找到:

​Daily Scheduled Tasks​: Executes maintenance routines at ​03:00 AM daily​, including:

  • Deleting temporary records...
  • Updating user RP (Rating Points) via global.Hydro.script.rp.
  • Updating problem statistics...
  • Checking for system updates.

注意到第三行的 Updating user RP (Rating Points),意思是 “更新用户RP(评分点)” (小学英语)。再看上面第一行,写的是 “每日计划任务:每天凌晨03:00执行维护程序” 。

证明了RP的准确更新时间,就是凌晨03:00。


4. 题目难度决定论:基于个人发现

很久很久以前,我在某个域一发通过了四道难度分别为 \(10\)、\(10\)、\(9\)、\(8\) 的题,第二天,我获得了 \(37\) RP。

不难看出,\(10+10+9+8=37\),由此可以严谨推测:一发通过的情况下,你可获得的RP即为该题难度。

为什么强调“一发通过”?因为之前听同学说提交错误会扣RP、或者减少通过后获得的RP数量。(导致当时他们提交都小心翼翼的。。)而这个观点暂时无法验证,所以目前只能针对一次通过的情况给出结论。


5. 开源!

真是受不了了,研究个小心RP还要跟个原始人一样批量测试找规律,不跟你玩了,我直接偷袭你GitHub(

Note:前方高能。


5.1. 三种RP来源

Hydro的RP计算逻辑写在 packages/hydrooj/src/script/rating.ts 里。

源码中定义了三种RP来源:

export const RpTypes: Record<string, RpDef> = {
    problem: { ... },   // 解题获得
    contest: { ... },   // 比赛获得
    delta: { ... },     // 管理员手动调整
};

简单说:你的最终RP = 解题贡献 + 比赛贡献 + 管理员调整值。

下面逐一拆解。


5.2. 解题获得的RP

源码中有一段:

const difficulty = +pdoc.difficulty || difficultyAlgorithm(pdoc.nSubmit, pdoc.nAccept) || 5;
const p = difficulty / 100;
// ...
udict[psdoc.uid] += min(psdoc.score, 100) * p;

这几行告诉我们:

  • 每道题都有一个难度值(difficulty)。这个值要么是管理员手动设定的,要么系统根据提交数和通过率自动算。
  • 你把难度除以100,得到一个系数 p。
  • 你在这道题上的得分(满分100)乘以这个系数,就是本题给你的RP。

举个例子: 一道难度为10的题,你一次通过拿了满分100分。
p = 10 / 100 = 0.1,100 × 0.1 = 10 RP。
这和我之前的个人测试完全吻合:一次通过,RP增量 = 题目难度。

注意,代码中min(psdoc.score, 100)说明得分超过100按100算,不到100按实际得分算。

5.2.1. 防止刷分机制

for (const key in udict) udict[key] = max(0, min(udict[key], log(udict[key]) / log(1.03)));

所有解题RP累加完后,会做一个对数压缩:log(RP) / log(1.03)。

想靠刷大量水题把RP刷爆?没门! 就像很多游戏的等级曲线——前期涨得快,后期越刷收益越低。


5.3. 比赛获得的RP

const contests = await contest.getMulti('', { domainId: { $in: domainIds }, rated: true })
    .limit(10).toArray();
// ...
for (const udoc of rating(users)) udict[udoc.uid] = udoc.new;

这段告诉我们:

  • 系统只取该域下最近10场标记为 rated 的比赛。
  • 每场比赛通过一个 rating() 函数计算参赛者的新RP。这个函数实现的是经典的Elo评分算法——也就是国际象棋、围棋常用的那种:不只看你的排名,还看对手的强弱。(下面会说)
  • 赢了强敌,加分更多;输给弱者,扣分更多。
for (const key in udict) udict[key] = max(1, udict[key] / 4 - 375);

同样有后处理压缩:RP / 4 - 375,保底1。

5.3.1. Elo算法,代码里长什么样?

找到了,核心逻辑其实就浓缩在下面这个函数里(做了精简,保留主干):

function elo(ratings: number[], kFactor = 32, selfRating?: number) {
    const expectedScore = (self: number, opponent: number) => 
        1 / (1 + 10 ** ((opponent - self) / 400));

    const newRating = (rating: number, i: number) => {
        const opponent = ratings[i === 0 ? 1 : 0];
        const actualScore = i === 0 ? 1 : 0;          // 假设只有两人,赢=1,输=0
        const expected = expectedScore(rating, opponent);
        return (selfRating ?? rating) + kFactor * (actualScore - expected);
    };

    return ratings.map((r, i) => newRating(r, i));
}
  1. 预期胜率 —— expectedScore(self, opponent)
    公式是:1 / (1 + 10^((对手积分 - 自己积分) / 400))
    如果你的积分比对手高,这个值就大(表示你赢面大);反之则小。

  2. 实际得分 —— actualScore
    赢是 1,输是 0(Hydro的比赛目前没有平局,所以不考虑0.5)。

  3. 积分更新 —— newRating
    新积分 = 老积分 + K因子 × (实际得分 - 预期胜率)

    • 如果你赢了,实际得分=1,1 - 预期胜率 就是你的意外收益 —— 你赢了预期中不该赢的人,加的分数就多。
    • 如果你输了,实际得分=0,0 - 预期胜率 是负数,扣分。输给弱者扣得更多。
  4. K因子 —— 这个例子里是 32,它控制单场比赛积分变化的幅度。K越大,波动越大;K越小,越稳定。Hydro用的就是经典的32。

5.3.2. 如果比赛不止两个人呢?

Hydro的 rating() 函数还处理了多人混战。它的做法是:把所有人都两两配对,对每一对都跑一遍上面的1v1计算,最后把每个人所有对局的积分变化汇总起来(取平均或累加,具体要看实现,但思想是“多对单场叠加”)。

这样,一场ACM比赛里,你不但赢了同水平的人,还赢了比你强的队 —— 那你的RP就会飞涨,完美体现了Elo“遇强则强,加分更多”的核心思想。

5.4. 管理员手动调整

delta: {
    async run(domainIds, udict) {
        const dudocs = unionWith(
            await domain.getMultiUserInDomain(
                '', { domainId: { $in: domainIds }, rpdelta: { $exists: true } },
            ).toArray(),
            (a, b) => a.uid === b.uid,
        );
        for (const dudoc of dudocs) udict[dudoc.uid] = dudoc.rpdelta;
    },
    hidden: true,
    base: 0,
},

简单粗暴:管理员可以在数据库里给某个用户设置 rpdelta 字段,直接往RP里加或减。
注意 hidden: true——这种操作不对外公开。

这就是有的时候一些老师警告的“禁止作弊,违者xxx……RP清零”的技术操作。不是吓人,真的可以。

5.5. 最终合并

await domain.setMultiUserInDomain(domainId, {}, { rp: 0 });
for (const uid in udict) {
    bulk.find({ domainId, uid: +uid }).upsert().update({ $set: { rp: Math.max(0, udict[uid]) } });
}

三种RP累加后,保底为0,不会出现负RP。

5.6. 排名和等级

export async function calcLevel(domainId: string, report: Report) {
    // 按RP从高到低排序,分配rank
    // 如果两人RP相同,名次并列(1, 2, 2, 4...)
    // 然后根据RP的百分位,分配level(段位)
}

每次RP更新后,系统会重新洗牌:

  • 按RP从高到低排名。
  • 根据你在全域中的排名百分位,给你分配一个等级(默认是Lv. 1~10,但高级域也可以修改成类似于“传奇王者”、“至尊星耀”之类的东西)。

6. 总结

最终RP = (解题得分 × 难度系数,经过对数压缩) + (近10场rated比赛的Elo评分结果,经过压缩) + 管理员手动加减分

来源 核心逻辑 防刷机制 备注
解题 得分 × 难度/100 对数压缩,刷题收益递减 一次通过拿满分 = 获得难度值RP
比赛 Elo算法,看排名和对手实力 自身压缩系数 只计最近10场
手动 直接加减 无 hidden: true,不公开

如何提升RP?

  • 优先做​高难度题​(难度值高的题回报率更高)
  • 认真打​rated比赛​(赢了强敌加分更多)
  • ​不要想着刷水题​(对数压缩让收益快速归零)

感谢观看,记得点赞(开GitHub找了好久,不容易的 → ←)

posted @ 2026-08-10 22:50  Void__114  阅读(386)  评论(1)    收藏  举报