Skip to content

Commit 5e0b297

Browse files
committed
judge: support score-based subtask dependencies
1 parent 06640c9 commit 5e0b297

9 files changed

Lines changed: 103 additions & 27 deletions

File tree

packages/common/subtask.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ interface ParsedSubtask {
9090
score?: number;
9191
id?: number;
9292
if?: number[];
93+
if_score?: number[];
9394
}
9495

9596
export function readSubtasksFromFiles(files: string[], config) {
@@ -162,6 +163,7 @@ export function normalizeSubtasks(
162163
id: id + 1,
163164
type: 'min',
164165
if: [],
166+
if_score: [],
165167
...s,
166168
score,
167169
time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate,

packages/common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface SubtaskConfig {
3131
memory?: string;
3232
score?: number;
3333
if?: number[];
34+
if_score?: number[];
3435
id?: number;
3536
type?: SubtaskType;
3637
cases?: TestCaseConfig[];

packages/hydrojudge/src/flow.ts

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Queue from 'p-queue';
22
import {
3-
JudgeResultBody, NormalizedCase, NormalizedSubtask, STATUS,
3+
JudgeResultBody, NormalizedCase, NormalizedSubtask, STATUS, type SubtaskResult,
44
} from '@hydrooj/common';
55
import { getConfig } from './config';
66
import { FormatError } from './error';
@@ -19,7 +19,7 @@ const Score = {
1919
min: Math.min,
2020
};
2121

22-
function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['judgeCase']) {
22+
function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['judgeCase'], skip = false) {
2323
return async (ctx: Context) => {
2424
subtask.type ||= 'min';
2525
const ctxSubtask = {
@@ -33,7 +33,8 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['
3333
for (const cid in subtask.cases) {
3434
const runner = judgeCase(subtask.cases[cid]);
3535
cases.push(ctx.queue.add(async () => {
36-
const res = (ctx.errored
36+
const res = (skip
37+
|| ctx.errored
3738
|| (subtask.type === 'min' && ctxSubtask.score === 0)
3839
|| (subtask.type === 'max' && ctxSubtask.score === subtask.score)
3940
|| (subtask.if || []).filter((i) => ctx.failed[i]).length)
@@ -78,6 +79,40 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['
7879
};
7980
}
8081

82+
async function judgeScoreDependentSubtasks(ctx: Context, task: Task) {
83+
const subtasks: Record<string, NormalizedSubtask> = {};
84+
for (const [key, value] of Object.entries(ctx.config.subtasks)) {
85+
subtasks[value.id?.toString() || key] = value;
86+
}
87+
const pending = new Set(Object.keys(subtasks));
88+
const infos: Record<string, SubtaskResult> = {};
89+
while (pending.size) {
90+
const ready = [...pending].filter((sid) => {
91+
const subtask = subtasks[sid];
92+
return [...(subtask.if || []), ...(subtask.if_score || [])]
93+
.every((id) => !subtasks[id] || !pending.has(id.toString()));
94+
});
95+
if (!ready.length) throw new FormatError('Circular dependency between subtasks.');
96+
for (const sid of ready) pending.delete(sid);
97+
// eslint-disable-next-line no-await-in-loop
98+
await Promise.all(ready.map(async (sid) => {
99+
const subtask = subtasks[sid];
100+
const accepted = (subtask.if || []).every((id) => (
101+
!subtasks[id] || (infos[id] && infos[id].status <= STATUS.STATUS_ACCEPTED)
102+
));
103+
const scored = (subtask.if_score || []).every((id) => infos[id]?.score > 0);
104+
if (!accepted || !scored) {
105+
ctx.failed[sid] = true;
106+
await judgeSubtask(subtask, sid, task.judgeCase, true)(ctx);
107+
return;
108+
}
109+
infos[sid] = await judgeSubtask(subtask, sid, task.judgeCase)(ctx);
110+
}));
111+
}
112+
for (const info of Object.values(infos)) ctx.total_score += info.score;
113+
return infos;
114+
}
115+
81116
export const runFlow = async (ctx: Context, task: Task) => {
82117
if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.');
83118
ctx.next({ status: STATUS.STATUS_COMPILING });
@@ -111,21 +146,24 @@ export const runFlow = async (ctx: Context, task: Task) => {
111146
ctx.end({ nop: true });
112147
}
113148
} else {
114-
const infos = {};
115-
await Promise.all(Object.entries(ctx.config.subtasks).map(async ([key, value]) => {
116-
const sid = value.id?.toString() || key;
117-
infos[sid] = await judgeSubtask(value, sid, task.judgeCase)(ctx);
118-
}));
119-
for (const [key, value] of Object.entries(ctx.config.subtasks)) {
120-
let effective = true;
121-
const sid = value.id?.toString() || key;
122-
for (const required of value.if || []) {
123-
if (ctx.failed[required.toString()]) effective = false;
124-
}
125-
if (effective) ctx.total_score += infos[sid].score;
126-
else {
127-
ctx.failed[sid] = true;
128-
delete infos[sid];
149+
const hasScoreDependencies = ctx.config.subtasks.some((i) => i.if_score?.length);
150+
const infos = hasScoreDependencies ? await judgeScoreDependentSubtasks(ctx, task) : {};
151+
if (!hasScoreDependencies) {
152+
await Promise.all(Object.entries(ctx.config.subtasks).map(async ([key, value]) => {
153+
const sid = value.id?.toString() || key;
154+
infos[sid] = await judgeSubtask(value, sid, task.judgeCase)(ctx);
155+
}));
156+
for (const [key, value] of Object.entries(ctx.config.subtasks)) {
157+
let effective = true;
158+
const sid = value.id?.toString() || key;
159+
for (const required of value.if || []) {
160+
if (ctx.failed[required.toString()]) effective = false;
161+
}
162+
if (effective) ctx.total_score += infos[sid].score;
163+
else {
164+
ctx.failed[sid] = true;
165+
delete infos[sid];
166+
}
129167
}
130168
}
131169
ctx.end({

packages/hydrojudge/src/judge/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const judge = async (ctx: Context) => {
6060
time: ctx.config.time,
6161
memory: ctx.config.memory,
6262
if: [],
63+
if_score: [],
6364
cases: ctx.input.map((i, idx) => ({
6465
id: idx + 1,
6566
time: ctx.config.time,

packages/ui-default/components/monaco/schema/problemconfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const problemConfigSchema: JSONSchema7 = {
3232
score: { $ref: '#/definitions/score', description: 'score' },
3333
cases: { $ref: '#/definitions/cases' },
3434
if: { type: 'array', items: { type: 'integer' } },
35+
if_score: { type: 'array', items: { type: 'integer' } },
3536
id: { type: 'integer' },
3637
},
3738
required: ['score'],

packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const configKey = [
3535
];
3636

3737
const subtasksKey = [
38-
'time', 'memory', 'score', 'if', 'id',
38+
'time', 'memory', 'score', 'if', 'if_score', 'id',
3939
'type', 'cases',
4040
];
4141

packages/ui-default/components/problemconfig/reducer/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ export default function reducer(state = {
127127
if (action.payload.memory) subtask.memory = action.payload.memory;
128128
if (action.payload.score) subtask.score = +action.payload.score || 0;
129129
if (action.payload.if) subtask.if = action.payload.if;
130+
if ('if_score' in action.payload) {
131+
if (action.payload.if_score?.length) subtask.if_score = action.payload.if_score;
132+
else delete subtask.if_score;
133+
}
130134
if (action.payload.type) subtask.type = action.payload.type;
131135
if (!subtask.time) delete subtask.time;
132136
if (!subtask.memory) delete subtask.memory;

packages/ui-default/components/problemconfig/tree/SubtaskSettings.tsx

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ interface SubtaskSettingsProps {
1414
memory: string;
1515
}
1616

17+
function parseDependencies(value: string) {
18+
return value.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i);
19+
}
20+
1721
export function SubtaskSettings(props: SubtaskSettingsProps) {
1822
const [open, setOpen] = React.useState(false);
1923
const [depsOpen, setDepsOpen] = React.useState(false);
@@ -22,12 +26,14 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
2226
const time = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).time);
2327
const memory = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).memory);
2428
const deps = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).if || [], isEqual);
29+
const scoreDeps = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).if_score || [], isEqual);
2530
const type = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).type || 'min');
2631

2732
const [ctime, setTime] = React.useState(time);
2833
const [cmemory, setMemory] = React.useState(memory);
2934
const [cscore, setScore] = React.useState(score);
3035
const [cdeps, setDeps] = React.useState(deps.join(', '));
36+
const [cscoreDeps, setScoreDeps] = React.useState(scoreDeps.join(', '));
3137
const [ctype, setType] = React.useState(type);
3238

3339
const dispatch = useDispatch();
@@ -49,7 +55,8 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
4955
time: ctime,
5056
memory: cmemory,
5157
score: cscore,
52-
if: cdeps.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i),
58+
if: parseDependencies(cdeps),
59+
if_score: parseDependencies(cscoreDeps),
5360
},
5461
});
5562
setOpen(false);
@@ -88,13 +95,29 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
8895
</div>
8996
</Modal>
9097
<Modal opened={depsOpen} onClose={() => setDepsOpen(false)} title={i18n('Set dependencies')}>
91-
<CustomSelectAutoComplete
92-
data={subtaskIds.map((i) => ({ _id: i, name: `${i18n('Subtask {0}', i)}` }))}
93-
setSelectItems={cdeps.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i)}
94-
onChange={(items) => setDeps(items)}
95-
placeholder="dependencies"
96-
multi
97-
/>
98+
<div style={{ marginBottom: 16 }}>
99+
<Text fw={600} style={{ marginBottom: 4 }}>{i18n('Dependencies')}</Text>
100+
<CustomSelectAutoComplete
101+
data={subtaskIds.map((i) => ({ _id: i.toString(), name: `${i18n('Subtask {0}', i)}` }))}
102+
selectedKeys={parseDependencies(cdeps).map((i) => i.toString())}
103+
onChange={(items) => setDeps(items)}
104+
placeholder={i18n('Dependencies')}
105+
multi
106+
/>
107+
</div>
108+
<div>
109+
<Text fw={600} style={{ marginBottom: 4 }}>{i18n('Positive-score dependencies')}</Text>
110+
<Text c="dimmed" size="sm" style={{ marginBottom: 4 }}>
111+
{i18n('The current subtask is judged only when all selected subtasks have a score greater than 0.')}
112+
</Text>
113+
<CustomSelectAutoComplete
114+
data={subtaskIds.map((i) => ({ _id: i.toString(), name: `${i18n('Subtask {0}', i)}` }))}
115+
selectedKeys={parseDependencies(cscoreDeps).map((i) => i.toString())}
116+
onChange={(items) => setScoreDeps(items)}
117+
placeholder={i18n('Positive-score dependencies')}
118+
multi
119+
/>
120+
</div>
98121
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
99122
<Button color="blue" onClick={onConfirm}>{i18n('Save')}</Button>
100123
</div>
@@ -111,6 +134,10 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
111134
<Text><i className="icon icon-diagram-tree" /></Text>
112135
<Text>{i18n('Dependencies')}: {deps.length ? deps.join(', ') : i18n('(None)')}</Text>
113136
</div>
137+
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', cursor: 'pointer', gap: 8 }} onClick={() => setDepsOpen(true)}>
138+
<Text><i className="icon icon-diagram-tree" /></Text>
139+
<Text>{i18n('Positive-score dependencies')}: {scoreDeps.length ? scoreDeps.join(', ') : i18n('(None)')}</Text>
140+
</div>
114141
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', gap: 8 }}>
115142
<i className="icon icon-asterisk" />
116143
<Text>{i18n('Scoring method')}</Text>

packages/ui-default/locales/zh.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ Please select at least one user to perform this operation.: 请选择至少一
700700
Please set the balloon color for each problem first.: 请先为每道题设置气球颜色。
701701
Please wait until contest host unfreeze the scoreboard.: 请等待比赛主办方解除封榜。
702702
Polyhedron supports managing problem version history, testing solutions, checking time limits, composing contest statements, cooperation and much more.: Polyhedron 支持题目版本管理,代码测试,时限检验,制作比赛题面,多人协作等等功能。
703+
Positive-score dependencies: 正分依赖
703704
Preference Settings: 偏好设置
704705
preferredPrefix_hint: 此选项用于重排题号。例如,若题目包中所给的题号分别是 P1001, P1002, P1003,而此选项填写了 T,则导入后三道题的题号分别为 T1001, T1002 和 T1003。
705706
Preparing Upload...: 准备上传...
@@ -950,6 +951,7 @@ Text: 文本
950951
The 'default' role applies to ALL REGISTERED USER.: default 角色作用于<b>所有已注册用户</b>
951952
The contest is a flexible time contest. You need to complete the contest within a specified time after you attended.: 本场比赛采用灵活时间模式,你需要在参加后的指定时间内完成比赛。
952953
The contest is ended. New submissions will be treated as correction submissions and will not be counted in the contest.: 比赛已经结束。新提交将被视为补题提交,不计入比赛成绩。
954+
The current subtask is judged only when all selected subtasks have a score greater than 0.: 仅当所有选中的子任务得分均大于 0 时,才评测当前子任务。
953955
The group to join when user joining the domain.: 加入域时自动加入的小组。
954956
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。
955957
The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 加入此域的邀请码。您只能使用字母和数字,并且不能长于 64 个字符。

0 commit comments

Comments
 (0)