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.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比赛(赢了强敌加分更多)
- 不要想着刷水题(对数压缩让收益快速归零)

浙公网安备 33010602011771号