PascalABC.NET

Модуль PT4TaskMakerNET: разработка заданий, связанных с ЕГЭ по информатике


Группы заданий Exam и их особенности

Начиная с версии 4.10, в базовый набор задачника Programming Taskbook для языков Pascal и C++ входят специальные группы заданий, связанные с ЕГЭ по информатике: ExamBegin и ExamTaskC. Порядок выполнения заданий из этих групп имеет ряд особенностей, основной из которых является отказ от применения специальных средств ввода-вывода, входящих в состав задачника. В заданиях групп Exam для ввода-вывода надо применять стандартные средства используемого языка программирования. Это позволяет максимально приблизить вид программы, выполняющей задание, к виду, требуемому на экзамене, а также учесть при выполнении задания его дополнительные особенности, связанные с организацией ввода исходных данных и форматированием результатов.

С использованием конструктора учебных заданий PT4TaskMaker преподаватель может разрабатывать новые группы заданий, связанные с ЕГЭ по информатике. При этом необходимо следовать дополнительным правилам, основные из которых приводятся ниже.

  1. Любые группы заданий, связанные с ЕГЭ по информатике, должны иметь имена, начинающиеся с префикса Exam (для групп с этим префиксом задачник генерирует специальные программы-заготовки, позволяющие использовать при выполнении задания стандартные средства ввода-вывода).
  2. Необходимо проверять номер текущей версии задачника и текущий язык программирования, создавая новую группу тольков случае, если версия имеет номер не ниже 4.10, а языком программирования является Pascal или C++.
  3. В преамбуле к группе заданий желательно отметить тот факт, что для ввода-вывода необходимо использовать стандартные средства языка.
  4. В новые группы Exam следует импортировать только те задания, которые также относятся к группам Exam.
  5. Набор исходных и контрольных данных надо сохранять в текстовых файлах, передавая задачнику информацию об именах этих файлов (процедурами DataS) и связывая содержимое этих файлов с разделами исходных и результирующих данных (процедурами DataFileT и ResultFileT соответственно).
  6. При любых вариантах наборов исходных данных соответствующие контрольные файлы не должны быть пустыми (при наличии пустого файла результатов задачник считает запуск программы ознакомительным).

Проиллюстрируем эти правила, разработав в среде PascalABC.NET демонстрационную группу заданий ExamDemo. Задания, связанные с ЕГЭ, вполне допустимо разрабатывать и на других языках, поддерживаемых конструктором учебных заданий, в частности, на языке C++ или на языке Pascal в средах Delphi или Lazarus, причем полученные реализации не будут иметь никаких существенных отличий от реализации, приведенной ниже. Задания можно разрабатывать даже на языке C#, несмотря на то что выполнять их на этом языке будет нельзя.

Реализация сводной группы заданий

Напомним, что сводной группой называется группа, все задания которой импортированы из уже имеющихся групп. Сводные группы оказываются очень полезными при составлении вариантов проверочных работ, поскольку позволяют дать заданиям новые имена и тем самым затрудняют применение разного рода шпаргалок.

Так как в сводных группах отсутствуют новые задания, при разработке сводных групп для заданий, связанных с ЕГЭ по информатике, достаточно учесть правила 1–4, приведенные в предыдущем пункте.

Будем предполагать, что общие правила разработки новых групп заданий в среде PascalABC.NET читателю известны (см. раздел «Примеры»).

Следуя правилам именования групп (имя должно состоять из латинских букв и цифр, иметь длину не более 9 символов и не оканчиваться цифрой), а также правилу 1 из предыдущего пункта (наличие префикса Exam), назовем нашу группу ExamDemo. Динамическая библиотека в этом случае должна иметь имя PT4ExamDemo.

Импортируем в группу ExamDemo несколько заданий из обеих групп Exam, входящих в базовый набор. Из группы ExamBegin возьмем задания ExamBegin71 и ExamBegin72, входящие в подгруппу «Преобразование массивов» и связанные перестановкой элементов массива. Из группы ExamTaskC возьмем серию из 12 заданий ExamTaskC25–ExamTaskC36, объединенных общей предметной областью: сведениями об абитуриентах из различных школ.

Учитывая правила подготовки динамических библиотек с группами учебных заданий, а также правила 2 и 3 из предыдущего пункта, получим следующий вариант нашей библиотеки (файл PT4ExamDemo.pas):

library PT4ExamDemo;

uses PT4TaskMakerNET;

procedure InitTask(num: integer);
begin
  case num of
  1..2:  UseTask('ExamBegin', 70 + num);
  3..14: UseTask('ExamTaskC', 22 + num);
  end;
end;

procedure inittaskgroup;
begin
  if (CurrentVersion < '4.10') or
     (CurrentLanguage and (lgPascal or lgCPP) = 0) then
    exit;
  CreateGroup('ExamDemo', '^ЕГЭ по информатике: примеры различных задач',
    'М. Э. Абрамян, 2013', 'qdfedsag33gbg45j', 14, InitTask);
  CommentText('\PПри выполнении заданий данной группы вместо');
  CommentText('специальных операций ввода-вывода, предоставляемых');
  CommentText('задачником, необходимо применять стандартные операции');
  CommentText('используемого языка программирования: процедуры');
  CommentText('\MRead\m/\MReadln\m\:\MWrite\m/\MWriteln\m для языка');
  CommentText('Pascal, потоки \Mcin\m\:\Mcout\m для языка C++.');
end;

procedure activate(S: string);
begin
  ActivateNET(S);
end;

begin
end.

Кратко опишем полученную программу. Вначале к ней подключается модуль PT4TaskMakerNET, в котором реализован конструктор учебных заданий для среды PascalABC.NET. Затем следует описание основной процедуры группы заданий InitTask, определяющей задание по его номеру. Поскольку мы не создавали своих заданий, в данной процедуре используется только стандартная процедура UseTask, позволяющая импортировать задания из имеющихся групп. В нашем случае импортируются задания с номерами 71 и 72 из группы ExamBegin и задания с номерами 25–36 из группы ExamTaskC (всего 14 заданий).

Затем описывается процедура инициализации данной группы заданий. Она имеет стандартное имя inittaskgroup (набранное строчными, т. е. маленькими буквами). В этой процедуре вызывается процедура CreateGroup, в которой задаются характеристики создаваемой группы: имя ('ExamDemo'), описание ('^ЕГЭ по информатике: примеры различных задач'), сведения об авторе, строковый ключ, число заданий (14) и основная процедура группы (InitTask).

Поскольку надо гарантировать, что группа будет создана только в случае использования задачника версии не ниже 4.10 и только для языков Pascal и С++, перед вызовом процедуры инициализации группы CreateGroup выполняется проверка перечисленных выше условий. Если хотя бы одно из условий нарушено, то выполняется немедленный выход из процедуры inittaskgroup, и группа ExamDemo не создается.

Следует обратить внимание на наличие символа-метки «^» в начале строки-описания группы. Этот символ отменяет автоматическое преобразование к нижнему регистру первой буквы описания при его выводе в программных модулях PT4Demo и PT4Load (если бы символ «^» отсутствовал, то строка с описанием данной группы имела бы вид «Тема: еГЭ по информатике: примеры различных задач»).

Наконец, в соответствии с правилом 3, мы включили в создаваемую группу преамбулу, в которой отмечается основная особенность данной группы: необходимость использования стандартных средств ввода-вывода (для определения текста преамбулы вызываются процедуры CommentText). Обратите внимание на управляющие последовательности \M\m, позволяющие отобразить фрагмент текста моноширинным шрифтом.

После процедуры inittaskgroup описывается вспомогательная процедура activate (ее имя также должно быть набрано строчными буквами), в которой необходимо вызвать процедуру ActivateNET, описанную в модуле PT4TaskMakerNET.

Для успешной компиляции программы с созданной группой необходимо, чтобы ей был доступен модуль PT4TaskMakerNET. Этот модуль входит в число стандартных модулей библиотеки системы PascalABC.NET и размещается в подкаталоге LIB системного каталога PascalABC.NET, поэтому копировать его в рабочий каталог не требуется.

Для того чтобы при успешной компиляции можно было сразу просмотреть содержимое созданной группы, достаточно использовать вспомогательную тестирующую программу, являющуюся заготовкой для выполнения заданий из созданной группы. Эту программу проще всего создать с помощью программного модуля PT4Load, нажав комбинацию клавиш [Shift]+[Ctrl]+[L] и введя в поле «Задание»; имя первого задания группы: ExamDemo1. В результате будет создан файл с именем ExamDemo1.pas со следующим содержимым:

uses PT4Exam;

begin
  Task('ExamDemo1');

end.

Следует обратить внимание на то, что к созданной программе подключается не традиционный модуль задачника PT4, а специальный его вариант PT4Exam, используемый для заданий, связанных с ЕГЭ.

Для того чтобы тестирующая программа отображала задания в демонстрационном режиме, причем при ее запуске на экране отображалось последнее из заданий, включенных в группу, достаточно заменить в процедуре Task номер задания на символ «?»: Task('ExamDemo?').

Теперь при запуске тестирующей программы на экране отобразится окно задачника с заданием ExamDemo14:

По умолчанию окно задачника отображается в режиме с динамической компоновкой, который появился в версии 4.11 и является более наглядным, чем режим с фиксированной компоновкой. Однако при разработке заданий желательно применять режим с фиксированной компоновкой, поскольку он позволит выявить недостатки форматирования (в частности, вертикального выравнивания данных), присущие только этому режиму. Для переключения между режимами отображения данных достаточно нажать клавишу [F4]. После выполнения этого действия окно задачника изменится следующим образом:

В окне задачника можно просматривать все имеющиеся задания данной группы (нажимая клавиши [Enter] и [Backspace], а также генерировать различные варианты исходных данных и связанных с ними контрольных (т. е. «правильных») результатов. При закрытии окна программа немедленно завершит работу, и мы вернемся в редактор среды PascalABC.NET. Заметим, что при последующих запусках программы будет автоматически выбираться тот режим окна задачника, в котором оно находилось в момент его предшествующего закрытия.

Для того чтобы cгенерировать html-страницу с описанием созданной группы (это позволяет, в частности, увидеть текст преамбулы группы), достаточно в процедуре Task тестирующей программы заменить символ «?» на символ «#»: Task('ExamDemo#'). Теперь при запуске этой программы на экране вместо окна задачника с заданием ExamDemo14 появится html-браузер с описанием созданной группы:

Добавление новых заданий

Добавим к нашей группе новые задания. Подобно заданиям, импортированным из группы ExamBegin, они будут посвящены преобразованию массивов путем перестановки их элементов. Если импортированные задания были посвящены инвертированию массива (или его части), то в новых заданиях надо будет выполнить перестановку всех пар элементов или перестановку первой и второй половины массива. Чтобы не уточнять действия в случае массивов нечетного размера, добавим в задание условие о том, что исходный массив всегда имеет четный размер. Тип элементов массива для подобных заданий является несущественным, поэтому будем обрабатывать массивы вещественных чисел. Таким образом, набор исходных данных будет иметь вид, подобный набору из задания ExamDemo2 (см. формулировку этого задания, приведенную на предыдущем рисунке). Оформление вывода результатов также не будет отличаться от оформления, требуемого в задании ExamDemo2.

Прежде чем приступать к реализации заданий, добавим в начало нашей библиотеки описание ряда вспомогательных переменных, процедур и функций.

Первая функция упрощает генерацию случайных исходных данных вещественного типа с не более чем двумя дробными знаками:

function RandR(a, b: integer): real;
begin
  result := RandomN(a*100, b*100)/100;
end;

При реализации функции RandR мы использовали функцию RandomN, входящую в состав конструктора учебных заданий, начиная с версии 4.11 (функция RandomN(M, N) возвращает случайное целое число, лежащее в диапазоне M..N, включая границы диапазона). Функция RandR(a, b) (a и b — целые) возвращает вещественное число, лежащее в диапазоне a..b и имеющее не более двух дробных знаков. Такие числа можно без потери точности записывать в текстовый файл в формате с двумя дробными знаками; таким образом, программа учащегося прочтет из файла именно то число, которое было сгенерировано при инициализации задания. Напомним, что в версии 4.11 конструктора имеется функция RandomR, также предназначенная для генерации случайных вещественных чисел, однако она не позволяет фиксировать число дробных знаков, и поэтому менее пригодна для генерации данных, предназначенных для записи в текстовые файлы.

Поскольку в обоих заданиях нам потребуется выполнять обмен значений, содержащихся в вещественных переменных, опишем процедуру, которая выполняет подобный обмен:

procedure SwapR(var a, b: real);
var
  c: real;
begin
  c := a;
  a := b;
  b := c;
end;

Следующая группа вспомогательных переменных и процедур связана с особенностью заданий типа Exam, описанной в правиле 5 (см. первый пункт данного раздела). Ввиду важности этого правила приведем его еще раз:

  • Набор исходных и контрольных данных надо сохранять в текстовых файлах, передавая задачнику информацию об именах этих файлов (процедурами DataS) и связывая содержимое этих файлов с разделами исходных и результирующих данных (процедурами DataFileT и ResultFileT соответственно).

Таким образом, при инициализации каждого задания группы Exam надо выполнить следующие действия:

  1. Сгенерировать имена файлов, содержащих исходные и контрольные данные (эти имена должны быть различными и меняться при каждом тестовом испытании программы; кроме того, подобно всем файлам, используемым в заданиях, они должны иметь расширение .tst).
  2. Связать созданные имена с файловыми переменными и открыть эти файлы на запись.
  3. Заполнить файлы необходимыми данными.
  4. Закрыть файлы с исходными и контрольными данными.
  5. Передать задачнику информацию об именах созданных файлов, чтобы при выполнении задания эта информация была использована при связывании файлов со стандартными потоками ввода-вывода.
  6. Передать задачнику информацию о том, что первый из созданных файлов должен быть включен в раздел исходных данных, а второй — в раздел результатов; это, во-первых, позволит отобразить содержимое файлов в окне задачника и, во-вторых, обеспечит проверку правильности результирующего файла, созданного программой учащегося (путем его сравнения с данными контрольного файла).

От условий конкретного задания будет зависеть только действие 3, связанное с заполнением файлов нужными данными. Все остальные действия являются стандартными и должны выполняться при инициализации любого задания групп Exam. Поэтому удобно оформить эти действия в виде двух вспомогательных процедур, одна из которых (StartExam) выполняет начальные действия 1–2, а другая (EndExam) — завершающие действия 4–6. Поскольку в каждой из этих процедур необходимо использовать имена созданных файлов и связанные с ними файловые переменные, эти переменные удобно описать как глобальные:

var
  f1,f2: text;
  f1name, f2name: string;

procedure StartExam;
var
  s: string;
begin
  Str(RandomN(10000, 99999), s);
  f1name := 'pt1' + s + '.tst';
  f2name := 'pt2' + s + '.tst';
  Assign(f1, f1name);
  Rewrite(f1);
  Assign(f2, f2name);
  Rewrite(f2);
end;

procedure EndExam;
begin
  Close(f1);
  Close(f2);
  DataS(f1name, 3, 1);
  DataS(f2name, 45, 1);
  DataFileT(f1name, 1, 5);
  ResultFileT(f2name, 1, 5);
end;

Обсудим особенности этих процедур. Имена файлов, создаваемых в процедуре StartExam, имеют вид pt1#####.tst (для файла с исходными данными) и pt2#####.tst (для файла с контрольными данными), причем в позициях, помеченных символом «#», располагаются цифры, выбираемые случайным образом. Тем самым обеспечиваются все требования к именам файлов: они генерируются случайным образом, имеют расширение .tst, и имя файла с исходными данными всегда отличается от имени контрольного файла. Напомним, что все файлы с расширением .tst автоматически удаляются из рабочего каталога после проверки учебного задания.

При анализе процедуры EndExam следует обратить внимание на то, что информация о содержимом исходного файла занимает всю область исходных данных (строки с первой по пятую — см. вызов процедуры DataFileT) и, таким образом, она скрывает информацию об именах файлов, ранее выведенную в первой строке области исходных данных (см. вызовы процедур DataS). В обычном задании такая реализация была бы ошибочной, поскольку учащийся не увидел бы на экране имена файлов и не понял бы, что эти имена необходимо ввести и обработать в его программе. Однако в задании групп Exam именно такая реализация является правильной, поскольку ввод имен файлов и связывание этих файлов со стандартными потоками ввода-вывода выполняется автоматически («незаметно» для программы учащегося), и поэтому информацию об именах файлов на экране отображать не следует.

Итак, наличие процедур StartExam и EndExam позволяет нам упростить реализацию заданий: после определения формулировки любого задания нам достаточно вызвать процедуру StartExam, заполнить файлы f1 и f2 исходными и, соответственно, контрольными данными и вызвать процедуру EndExam.

Приступим к непосредственной реализации заданий. Поскольку эти задания являются однотипными, реализуем их в одной процедуре Exam1, снабдив ее параметром m: при m = 1 будет инициализироваться первое задание, а при m = 2 — второе:

procedure Exam1(m: integer);
var
  n, i: integer;
  a: array[1..10] of real;
begin
  CreateTask('Преобразование массивов');
  case m of
  1:
  begin
    TaskText('На вход в первой строке подается целое положительное четное число {N},', 0, 1);
    TaskText('а во второй строке \= массив из {N} вещественных чисел. Поменять местами', 0, 2);
    TaskText('его первый элемент со вторым, третий с четвертым, и т.\,д. Вывести', 0, 3);
    TaskText('преобразованный массив в одной строке, для каждого элемента', 0, 4);
    TaskText('отводить 7 экранных позиций.', 0, 5);
  end;
  2:
  begin
    TaskText('На вход в первой строке подается целое положительное четное число {N},', 0, 2);
    TaskText('а во второй строке \= массив из {N} вещественных чисел. Поменять местами', 0, 3);
    TaskText('первую и вторую половину элементов массива. Вывести преобразованный массив', 0, 4);
    TaskText('в одной строке, для каждого элемента отводить 7 экранных позиций.', 0, 5);
  end;
  end;
  StartExam;
  n := 2 * RandomN(1, 5);
  for i := 1 to n do
    a[i] := RandR(-99, 99);
  writeln(f1,n);
  for i := 1 to n - 1 do
    write(f1, a[i]:0:2, ' ');
  writeln(f1, a[n]:0:2);
  for i := 1 to n div 2 do
    case m of
    1: SwapR(a[2*i - 1], a[2*i]);
    2: SwapR(a[i], a[i + n div 2]);
    end;
  for i := 1 to n do
    write(f2, a[i]:7:2);
  writeln(f2);
  EndExam;
  SetTestCount(3);
end;

Обратите внимание на то, что при вызове процедуры CreateTask ей передается строковый параметр, содержащий имя подгруппы «Преобразование массивов». Это обеспечивает включение новых заданий в подгруппу, с которой связаны ранее импортированные в нашу группу задания ExamBegin71 и ExamBegin72.

Размер исходного массива всегда будет четным и не превосходящим 10; последнее условие необходимо для того, чтобы все исходные данные можно было разместить на одной экранной строке.

В обоих заданиях значения элементов исходного массива можно выбирать произвольным образом из некоторого диапазона. Мы выбрали диапазон от -99 до 99, поскольку в этом случае при отображении чисел с двумя дробными знаками они будут занимать не более 6 экранных позиций.

При записи в файл элементов исходного массива между ними всегда располагается по одному пробелу, поскольку такой порядок организации исходных данных принят во всех заданиях групп ExamBegin и ExamTaskC. Чтобы обеспечить при этом отображение вещественных чисел с двумя дробными знаками, используется специальный набор форматирующих атрибутов: «:0:2». При выводе результатов, согласно формулировке задания, необходимо отводить для каждого элемента массива по 7 экранных позиций и выводить его с двумя дробными знаками (последнее условие принято по умолчанию во всех заданиях групп ExamBegin и ExamTaskC, использующих вещественные данные). Поэтому при выводе применяются другие форматирующие атрибуты: «:7:2».

Так как алгоритм решения обеих задач не содержит ветвлений, для проверки его правильности достаточно небольшого числа тестовых запусков. Мы установили это число равным трем, указав его в качестве параметра процедуры SetTestCount.

Нам осталось включить вызовы процедуры Exam1 (с параметрами, равными 1 и 2) в основную процедуру группы InitTask, связав эти вызовы с номерами заданий. Следует разместить новые задания сразу после импортированных заданий ExamBegin71 и ExamBegin72, так как все эти задания относятся к одной и той же подгруппе «Преобразование массивов». При этом номера последних 12 заданий увеличатся на 2:

procedure InitTask(num: integer);
begin
  case num of
  1..2:   UseTask('ExamBegin', 70 + num);
  3..4:   Exam1(num - 2);
  5..16:  UseTask('ExamTaskC', 20 + num);
  end;
end;

Необходимо также увеличить на 2 пятый параметр процедуры CreateGroup, определяющий общее количество заданий в группе (теперь это количество должно быть равно 16).

Для просмотра новых заданий в окне задачника надо заменить в параметре процедуры Task тестирующей программы символ «#» на «?»: Task('ExamDemo?').

При нажатии клавиши [F9] мы увидим на экране окно задачника в демо-режиме, в котором можно выбрать и просмотреть все задания, включенные к настоящему моменту в нашу группу. Приведем вид окна для задания ExamDemo4 (напомним, что это задание инициализируется посредством вызова процедуры Exam1 с параметром, равным 2):

Добавление заданий повышенной сложности

Наша группа ExamDemo к настоящему моменту содержит 12 заданий повышенной сложности, импортированных из группы ExamTaskC. Все эти задания связаны с общей предметной областью; они содержат сведения об абитуриентах и включают их фамилии, номера школ и годы поступления в вузы. Для того чтобы проиллюстрировать некоторые особенности, связанные с разработкой подобных заданий, дополним набор уже имеющихся заданий двумя новыми заданиями из той же предметной области.

Новые задания будут связаны с группировкой абитуриентов по школам: для каждой школы надо найти связанный с ней минимальный (или максимальный) год поступления абитуриента. Второе из двух заданий мы усложним, дополнительно потребовав, чтобы полученные результаты были отсортированы по убыванию максимального года (а для одинаковых годов — по возрастанию номера школы). Первое задание сделаем более простым: в нем результирующие данные надо располагать по возрастанию номеров школ.

При генерации наборов исходных данных нам потребуются не только числа (номера школ и годы поступления), но и строковые данные — фамилии абитуриентов (хотя для выполнения этих заданий они не требуются). Проще всего определить массив возможных фамилий достаточно большого размера, из которого выбирать элементы случайным образом. Заметим, что в условии заданий не говорится о том, что все фамилии в исходном наборе должны быть различными, поэтому совпадения фамилий вполне допустимы (если в некоторой группе заданий все фамилии должны быть уникальными, то целесообразно дополнять их инициалами, чтобы обеспечить большее разнообразие; кроме того, для таких заданий при добавлении к набору исходных данных новой фамилии необходимо проверять, что среди уже имеющихся элементов набора отсутствует данная фамилия с теми же инициалами).

Добавим к нашей библиотеке вспомогательный массив фамилий из 40 элементов (обратите внимание на то, что по правилам языка PascalABC.NET между описанием массива и списком инициализирующих значений указывается знак присваивания):

const
  famcount = 40;
var
  fam: array[1..famcount] of string :=
  ('Иванов', 'Петров', 'Сидоров', 'Кузнецов', 'Филиппов',
   'Сергеев', 'Александров', 'Петухов', 'Пономарев', 'Яшин',
   'Греков', 'Иванова', 'Кузнецова', 'Алексеева', 'Зайцев',
   'Волкова', 'Фролов', 'Юрьев', 'Бондарев', 'Семенов',
   'Семенова', 'Федченко', 'Марченко', 'Борисова', 'Петровский',
   'Беляева', 'Белкин', 'Лысенко', 'Сорокина', 'Пастухов',
   'Юрьева', 'Кондратьев', 'Тимофеев', 'Степанова', 'Якимов',
   'Юсов', 'Степанов', 'Руденко', 'Демидов', 'Леонидов');

Оба новых задания, как и два предыдущих, мы реализуем в виде одной процедуры с параметром m, принимающим значения 1 или 2:

procedure Exam2(m: integer);
var
  n, i, y, num, max, k: integer;
  a: array[1..100] of integer;
  nums: array[1..10] of integer;
begin
  CreateTask('Обработка сложных наборов данных');
  case m of
  1:
  begin
    TaskText('На вход подаются сведения об абитуриентах. В первой строке указывается',0,1);
    TaskText('количество абитуриентов {N}, каждая из последующих {N} строк имеет формат',0,2);
    TaskText('\(\M<Год поступления> <Фамилия> <Номер школы>\m\)',0,3);
    TaskText('Номер школы содержит не более двух цифр, годы лежат в диапазоне от 1990',0,4);
    TaskText('до 2010. Для каждого номера школы, присутствующего в исходных данных,',0,5);
    TaskText('определить связанный с ним минимальный год поступления (вначале указывать',0,0);
    TaskText('номер школы, затем минимальный год). Сведения о каждой школе выводить',0,0);
    TaskText('на новой строке и упорядочивать по возрастанию номера школы.',0,0);
    k := 1;
    for i := 1 to 100 do
      a[i] := 2100;
  end;
  2:
  begin
    TaskText('На вход подаются сведения об абитуриентах. В первой строке указывается',0,1);
    TaskText('количество абитуриентов {N}, каждая из последующих {N} строк имеет формат',0,2);
    TaskText('\(\M<Номер школы> <Фамилия> <Год поступления>\m\)',0,3);
    TaskText('Номер школы содержит не более двух цифр, годы лежат в диапазоне от 1990',0,4);
    TaskText('до 2010. Для каждого номера школы, присутствующего в исходных данных,',0,5);
    TaskText('определить связанный с ним максимальный год поступления (вначале указывать',0,0);
    TaskText('максимальный год, затем номер школы). Сведения о каждой школе выводить',0,0);
    TaskText('на новой строке и упорядочивать по убыванию максимального года,',0,0);
    TaskText('а для совпадающих годов \= по возрастанию номера школы.',0,0);
    k := -1;
    for i := 1 to 100 do
      a[i] := 0;
  end;
  end;
  StartExam;
  if Random(2)=0 then
    n := RandomN(50, 100)
  else
    n := RandomN(10, 20);
  case CurrentTest of
  1: n := RandomN(10, 20);
  2: n := RandomN(50, 100);
  end;
  if n <= 20 then
    for i := 1 to 10 do
      nums[i] := RandomN(1, 100);
  writeln(f1,n);
  for i := 1 to n do
  begin
    y := RandomN(1990, 2010);
    if n <= 20 then
      num := nums[RandomN(1, 10)]
    else
      num := RandomN(1, 100);
    case m of
    1: writeln(f1, y, ' ', fam[RandomN(1, famcount)],' ', num);
    2: writeln(f1, num, ' ', fam[RandomN(1, famcount)],' ', y);
    end;
    if k*a[num] > k*y then
      a[num] := y;
  end;
  case m of
  1: for i := 1 to 100 do
       if a[i] < 2100 then
         writeln(f2, i, ' ', a[i]);
  2: while true do
     begin
       max := 0;
       for i := 1 to 100 do
         if a[i] > max then
         begin
           max := a[i];
           num := i;
         end;
       if max = 0 then
         break
       else
       begin
         writeln(f2, max, ' ', num);
         a[num] := 0;
       end;
     end;
  end;
  EndExam;
  SetTestCount(5);
end;

Обсудим детали реализации этих заданий. Начальная часть их формулировки посвящена описанию предметной области и является стандартной для данной серии заданий. Обратите внимание на то, что поля исходных записей в заданиях указываются в различном порядке. Этот прием используется во всех сериях группы ExamTaskC, чтобы обеспечить большее разнообразие входящих в них задач.

При определении завершающей части формулировки заданий последний параметр процедур TaskText полагается равным 0. Это означает, что в режиме окна с фиксированной компоновкой данные строки при первоначальном отображении задания не видны на экране, однако их можно просмотреть, используя прокрутку раздела с формулировкой задания. В режиме с динамической компоновкой полный текст формулировки задания сразу отображается на экране.

В процедуре используются два массива: массив a предназначен для хранения контрольных (правильных) результатов, а массив nums является вспомогательным (его назначение описывается далее). Для хранения исходных данных массив не предусматривается, поскольку после генерации полей очередной записи они будут немедленно записываться в исходный файл и обрабатываться.

Количество записей в исходном наборе данных записывается в переменную n. Наборы, содержащие небольшое число записей, удобны при отладке программы (благодаря своей «обозримости»), в то время как большие наборы позволяют проверить программу в «реальной» ситуации и тем самым окончательно убедиться в правильности алгоритма. Используя функцию CurrentTest, добавленную в версию 4.11 конструктора PT4TaskMaker, мы обеспечили дополнительную «настройку» процесса генерации исходных данных: при первом тестовом запуске программы с решением задачи ей всегда предлагается набор из небольшого количества записей (что упрощает поиск и исправление ошибок), а при втором тестовом запуске — большой набор записей (что позволяет проверить предложенный алгоритм «на прочность»). При последующих тестовых запусках (а также при демонстрационном и ознакомительном запуске программы) значение n с равной вероятностью выбирается либо из диапазона 10..20, либо из диапазона 50..100.

В случае генерации исходных данных для указанных заданий при небольших значениях n (10–20) возникает дополнительная проблема: если выбирать случайным образом номера школ из всего допустимого диапазона 1–100, то с большой вероятностью каждый номер школы появится в наборе исходных данных всего по одному разу, что не позволит проверить правильность реализованного в программе алгоритма нахождения минимального/максимального значения. Чтобы решить эту проблему, используется вспомогательный массив nums из 10 элементов, в который заносятся 10 случайно выбранных номеров школ, после чего номера школ для исходного набора записей выбираются уже из этого набора номеров.

В любом задании, связанном с нахождением набора записей, обычно требуется отсортировать полученный набор. Задание должно быть сформулировано таким образом, чтобы обеспечить однозначный порядок вывода полученных данных. В частности, если поле, по которому выполняется сортировка (главный ключ сортировки), может содержать одинаковые значения, то обязательно следует указать дополнительное поле (подчиненный ключ сортировки), по которому надо сортировать записи с одинаковым главным ключом. При выводе отсортированных данных вначале надо располагать главный ключ, после него — подчиненные ключи (если они имеются), затем — остальные поля (такой порядок вывода принят во всех заданиях группы ExamTaskC).

Для упорядочивания результатов во втором задании вместо сортировки массива a по убыванию используется другой алгоритм, связанный с последовательным нахождением максимального элемента, выводом этого элемента и его «порчей» (заменой его значения на 0). Обычная сортировка массива в данном случае не позволит получить требуемый набор данных, так как при перемене местами значений элементов в массиве a будет потеряна связь с номером школы (который определяется по индексу элемента). Заметим, что возможен и вариант получения упорядоченного набора данных с помощью сортировки, однако для этого надо использовать массив записей, полями которых являются максимальный год и номер школы.

Во втором задании результаты должны упорядочиваться по набору ключей: первый (главный) ключ — максимальный год (сортируется по убыванию), второй (подчиненный) ключ — номер школы (сортируется по возрастанию). Использованный нами способ упорядочивания обеспечивает автоматическую сортировку по подчиненному ключу, так как при поиске очередного максимума массив a перебирается по возрастанию индексов, и поэтому в результате находится номер первого максимального элемента.

Прочие фрагменты процедуры Exam2 дополнительных комментариев не требуют.

Вызов процедуры Exam2 надо добавить в конец оператора case процедуры InitTask, связав его с номерами 17 и 18:

procedure InitTask(num: integer);
begin
  case num of
  1..2:   UseTask('ExamBegin', 70 + num);
  3..4:   Exam1(num - 2);
  5..16:  UseTask('ExamTaskC', 20 + num);
  17..18: Exam2(num - 16);
  end;
end;

Кроме того, необходимо опять откорректировать параметр процедуры CreateGroup, определяющий количество заданий, положив его равным 18.

При нажатии клавиши [F9] на экране появится окно задачника с последним заданием данной группы (на рисунке приведен вид окна после прокрутки раздела с формулировкой задания):

Если теперь опять заменить символ «?» на символ «# » в параметре процедуры Task тестирующей программы, то при нажатии [F9] мы увидим html-страницу с описанием группы, которое теперь содержит не только импортированные, но и реализованные нами задания:

Поскольку при создании новых заданий мы указали в качестве параметра процедур CreateTask названия подгрупп (и разместили новые задания после заданий из данных подгрупп), новые задания отображаются в составе этих подгрупп: ExamDemo3 и ExamDemo4 — в подгруппе «Преобразование массивов», а ExamDemo17 и ExamDemo18 — в подгруппе «Обработка сложных наборов данных».