Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/common/subtask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ interface ParsedSubtask {
score?: number;
id?: number;
if?: number[];
if_score?: number[];
}

export function readSubtasksFromFiles(files: string[], config) {
Expand Down Expand Up @@ -162,6 +163,7 @@ export function normalizeSubtasks(
id: id + 1,
type: 'min',
if: [],
if_score: [],
...s,
score,
time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate,
Expand Down
1 change: 1 addition & 0 deletions packages/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface SubtaskConfig {
memory?: string;
score?: number;
if?: number[];
if_score?: number[];
id?: number;
type?: SubtaskType;
cases?: TestCaseConfig[];
Expand Down
76 changes: 57 additions & 19 deletions packages/hydrojudge/src/flow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Queue from 'p-queue';
import {
JudgeResultBody, NormalizedCase, NormalizedSubtask, STATUS,
JudgeResultBody, NormalizedCase, NormalizedSubtask, STATUS, type SubtaskResult, type SubtaskType,
} from '@hydrooj/common';
import { getConfig } from './config';
import { FormatError } from './error';
Expand All @@ -19,7 +19,7 @@ const Score = {
min: Math.min,
};

function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['judgeCase']) {
function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['judgeCase'], skip = false) {
return async (ctx: Context) => {
subtask.type ||= 'min';
const ctxSubtask = {
Expand All @@ -33,7 +33,8 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['
for (const cid in subtask.cases) {
const runner = judgeCase(subtask.cases[cid]);
cases.push(ctx.queue.add(async () => {
const res = (ctx.errored
const res = (skip
|| ctx.errored
|| (subtask.type === 'min' && ctxSubtask.score === 0)
|| (subtask.type === 'max' && ctxSubtask.score === subtask.score)
|| (subtask.if || []).filter((i) => ctx.failed[i]).length)
Expand Down Expand Up @@ -71,13 +72,47 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['
}
ctx.total_status = Math.max(ctx.total_status, ctxSubtask.status);
return {
type: ctxSubtask.subtask.type,
type: ctxSubtask.subtask.type as SubtaskType,
score: ctxSubtask.score,
status: ctxSubtask.status,
};
};
}

async function judgeScoreDependentSubtasks(ctx: Context, task: Task) {
const subtasks: Record<string, NormalizedSubtask> = {};
for (const [key, value] of Object.entries(ctx.config.subtasks)) {
subtasks[value.id?.toString() || key] = value;
}
const pending = new Set(Object.keys(subtasks));
const infos: Record<string, SubtaskResult> = {};
while (pending.size) {
const ready = [...pending].filter((sid) => {
const subtask = subtasks[sid];
return [...(subtask.if || []), ...(subtask.if_score || [])]
.every((id) => !subtasks[id] || !pending.has(id.toString()));
});
if (!ready.length) throw new FormatError('Circular dependency between subtasks.');
for (const sid of ready) pending.delete(sid);
// eslint-disable-next-line no-await-in-loop
await Promise.all(ready.map(async (sid) => {
const subtask = subtasks[sid];
const accepted = (subtask.if || []).every((id) => (
!subtasks[id] || (infos[id] && infos[id].status <= STATUS.STATUS_ACCEPTED)
));
const scored = (subtask.if_score || []).every((id) => infos[id]?.score > 0);
if (!accepted || !scored) {
ctx.failed[sid] = true;
await judgeSubtask(subtask, sid, task.judgeCase, true)(ctx);
return;
}
infos[sid] = await judgeSubtask(subtask, sid, task.judgeCase)(ctx);
}));
}
for (const info of Object.values(infos)) ctx.total_score += info.score;
return infos;
}

export const runFlow = async (ctx: Context, task: Task) => {
if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.');
ctx.next({ status: STATUS.STATUS_COMPILING });
Expand Down Expand Up @@ -111,21 +146,24 @@ export const runFlow = async (ctx: Context, task: Task) => {
ctx.end({ nop: true });
}
} else {
const infos = {};
await Promise.all(Object.entries(ctx.config.subtasks).map(async ([key, value]) => {
const sid = value.id?.toString() || key;
infos[sid] = await judgeSubtask(value, sid, task.judgeCase)(ctx);
}));
for (const [key, value] of Object.entries(ctx.config.subtasks)) {
let effective = true;
const sid = value.id?.toString() || key;
for (const required of value.if || []) {
if (ctx.failed[required.toString()]) effective = false;
}
if (effective) ctx.total_score += infos[sid].score;
else {
ctx.failed[sid] = true;
delete infos[sid];
const hasScoreDependencies = ctx.config.subtasks.some((i) => i.if_score?.length);
const infos = hasScoreDependencies ? await judgeScoreDependentSubtasks(ctx, task) : {};
if (!hasScoreDependencies) {
await Promise.all(Object.entries(ctx.config.subtasks).map(async ([key, value]) => {
const sid = value.id?.toString() || key;
infos[sid] = await judgeSubtask(value, sid, task.judgeCase)(ctx);
}));
for (const [key, value] of Object.entries(ctx.config.subtasks)) {
let effective = true;
const sid = value.id?.toString() || key;
for (const required of value.if || []) {
if (ctx.failed[required.toString()]) effective = false;
}
if (effective) ctx.total_score += infos[sid].score;
else {
ctx.failed[sid] = true;
delete infos[sid];
}
}
}
ctx.end({
Expand Down
1 change: 1 addition & 0 deletions packages/hydrojudge/src/judge/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const judge = async (ctx: Context) => {
time: ctx.config.time,
memory: ctx.config.memory,
if: [],
if_score: [],
cases: ctx.input.map((i, idx) => ({
id: idx + 1,
time: ctx.config.time,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const problemConfigSchema: JSONSchema7 = {
score: { $ref: '#/definitions/score', description: 'score' },
cases: { $ref: '#/definitions/cases' },
if: { type: 'array', items: { type: 'integer' } },
if_score: { type: 'array', items: { type: 'integer' } },
id: { type: 'integer' },
},
required: ['score'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const configKey = [
];

const subtasksKey = [
'time', 'memory', 'score', 'if', 'id',
'time', 'memory', 'score', 'if', 'if_score', 'id',
'type', 'cases',
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ export default function reducer(state = {
if (action.payload.memory) subtask.memory = action.payload.memory;
if (action.payload.score) subtask.score = +action.payload.score || 0;
if (action.payload.if) subtask.if = action.payload.if;
if ('if_score' in action.payload) {
if (action.payload.if_score?.length) subtask.if_score = action.payload.if_score;
else delete subtask.if_score;
}
if (action.payload.type) subtask.type = action.payload.type;
if (!subtask.time) delete subtask.time;
if (!subtask.memory) delete subtask.memory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface SubtaskSettingsProps {
memory: string;
}

function parseDependencies(value: string) {
return value.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i);
}

export function SubtaskSettings(props: SubtaskSettingsProps) {
const [open, setOpen] = React.useState(false);
const [depsOpen, setDepsOpen] = React.useState(false);
Expand All @@ -22,12 +26,14 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
const time = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).time);
const memory = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).memory);
const deps = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).if || [], isEqual);
const scoreDeps = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).if_score || [], isEqual);
const type = useSelector((state: RootState) => state.config.subtasks.find((i) => i.id === props.subtaskId).type || 'min');

const [ctime, setTime] = React.useState(time);
const [cmemory, setMemory] = React.useState(memory);
const [cscore, setScore] = React.useState(score);
const [cdeps, setDeps] = React.useState(deps.join(', '));
const [cscoreDeps, setScoreDeps] = React.useState(scoreDeps.join(', '));
const [ctype, setType] = React.useState(type);

const dispatch = useDispatch();
Expand All @@ -49,7 +55,8 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
time: ctime,
memory: cmemory,
score: cscore,
if: cdeps.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i),
if: parseDependencies(cdeps),
if_score: parseDependencies(cscoreDeps),
},
});
setOpen(false);
Expand Down Expand Up @@ -88,13 +95,29 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
</div>
</Modal>
<Modal opened={depsOpen} onClose={() => setDepsOpen(false)} title={i18n('Set dependencies')}>
<CustomSelectAutoComplete
data={subtaskIds.map((i) => ({ _id: i, name: `${i18n('Subtask {0}', i)}` }))}
setSelectItems={cdeps.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i)}
onChange={(items) => setDeps(items)}
placeholder="dependencies"
multi
/>
<div style={{ marginBottom: 16 }}>
<Text fw={600} style={{ marginBottom: 4 }}>{i18n('Dependencies')}</Text>
<CustomSelectAutoComplete
data={subtaskIds.map((i) => ({ _id: i.toString(), name: `${i18n('Subtask {0}', i)}` }))}
selectedKeys={parseDependencies(cdeps).map((i) => i.toString())}
onChange={(items) => setDeps(items)}
placeholder={i18n('Dependencies')}
multi
/>
</div>
<div>
<Text fw={600} style={{ marginBottom: 4 }}>{i18n('Positive-score dependencies')}</Text>
<Text c="dimmed" size="sm" style={{ marginBottom: 4 }}>
{i18n('The current subtask is judged only when all selected subtasks have a score greater than 0.')}
</Text>
<CustomSelectAutoComplete
data={subtaskIds.map((i) => ({ _id: i.toString(), name: `${i18n('Subtask {0}', i)}` }))}
selectedKeys={parseDependencies(cscoreDeps).map((i) => i.toString())}
onChange={(items) => setScoreDeps(items)}
placeholder={i18n('Positive-score dependencies')}
multi
/>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
<Button color="blue" onClick={onConfirm}>{i18n('Save')}</Button>
</div>
Expand All @@ -111,6 +134,10 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
<Text><i className="icon icon-diagram-tree" /></Text>
<Text>{i18n('Dependencies')}: {deps.length ? deps.join(', ') : i18n('(None)')}</Text>
</div>
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', cursor: 'pointer', gap: 8 }} onClick={() => setDepsOpen(true)}>
<Text><i className="icon icon-diagram-tree" /></Text>
<Text>{i18n('Positive-score dependencies')}: {scoreDeps.length ? scoreDeps.join(', ') : i18n('(None)')}</Text>
</div>
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', gap: 8 }}>
<i className="icon icon-asterisk" />
<Text>{i18n('Scoring method')}</Text>
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-default/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@ Please select at least one user to perform this operation.: 请选择至少一
Please set the balloon color for each problem first.: 请先为每道题设置气球颜色。
Please wait until contest host unfreeze the scoreboard.: 请等待比赛主办方解除封榜。
Polyhedron supports managing problem version history, testing solutions, checking time limits, composing contest statements, cooperation and much more.: Polyhedron 支持题目版本管理,代码测试,时限检验,制作比赛题面,多人协作等等功能。
Positive-score dependencies: 正分依赖
Preference Settings: 偏好设置
preferredPrefix_hint: 此选项用于重排题号。例如,若题目包中所给的题号分别是 P1001, P1002, P1003,而此选项填写了 T,则导入后三道题的题号分别为 T1001, T1002 和 T1003。
Preparing Upload...: 准备上传...
Expand Down Expand Up @@ -950,6 +951,7 @@ Text: 文本
The 'default' role applies to ALL REGISTERED USER.: default 角色作用于<b>所有已注册用户</b>
The contest is a flexible time contest. You need to complete the contest within a specified time after you attended.: 本场比赛采用灵活时间模式,你需要在参加后的指定时间内完成比赛。
The contest is ended. New submissions will be treated as correction submissions and will not be counted in the contest.: 比赛已经结束。新提交将被视为补题提交,不计入比赛成绩。
The current subtask is judged only when all selected subtasks have a score greater than 0.: 仅当所有选中的子任务得分均大于 0 时,才评测当前子任务。
The group to join when user joining the domain.: 加入域时自动加入的小组。
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 作业已超过截止时间,但仍在延期时间内。您递交题目将无法获得全部分数。
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 个字符。
Expand Down
Loading