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.1100 × 0.1 = 10 RP
这和我之前的个人测试完全吻合:一次通过,RP增量 = 题目难度。

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

5.2.2 防止刷分机制

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.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比赛​(赢了强敌加分更多)
  • 不要想着刷水题​(对数压缩让收益快速归零)
posted @ 2026-08-10 22:50  Void__114  阅读(4)  评论(0)    收藏  举报