import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.HashSet;
import java.util.stream.Collectors; // Stream API用に追加
/**
* 基本情報技術者試験の合格確率をモンテカルロシミュレーションで計算するシミュレーター。
* 各問題の配点設定に基づいた「素点計算」により合格率を算出します。
* 本シミュレーターはIRT(項目応答理論)採点を再現するものではありません。
*/
public class FeSimulator {
// 基本情報技術者試験の定数(素点計算に基づく)
private static final int TOTAL_QUESTIONS_A = 60; // 科目A 総問題数
private static final int CHOICES_A = 4; // 科目Aの選択肢数 (固定)
private static final int SCORE_PER_QUESTION_A = 1; // 科目Aの各問題の配点 (素点)
private static final int REQUIRED_RAW_SCORE_A = 36; // 科目Aの合格に必要な素点 (60点満点中36点)
private static final int TOTAL_QUESTIONS_B = 20; // 科目B 総問題数
private static final int SCORE_PER_QUESTION_B = 1; // 科目Bの各問題の配点 (素点)
private static final int REQUIRED_RAW_SCORE_B = 12; // 科目Bの合格に必要な素点 (20点満点中12点)
private static final int TOTAL_EXAM_QUESTIONS = TOTAL_QUESTIONS_A + TOTAL_QUESTIONS_B; // 試験全体の総問題数
private static final int DEFAULT_TRIALS = 10000; // 各シナリオでのデフォルト試行回数
/**
* 各問題の定義。問題番号(※実際にはインデックス)、選択肢の数、配点を保持します。
*/
public static class Problem {
private final int no; // 問題番号(デバッグ用など)
private final int choices; // 選択肢の数
private final int score; // 配点
public Problem(int no, int choices, int score) {
this.no = no;
this.choices = choices;
this.score = score;
}
public int getNo() { return no; }
public int getChoices() { return choices; }
public int getScore() { return score; }
}
// 全問題データ(科目Aと科目Bを結合して、通し番号で管理)
private static final List<Problem> PROBLEMS = new ArrayList<>();
static {
// 科目Aの問題 (No.1-60): 4択1点問題
for (int i = 0; i < TOTAL_QUESTIONS_A; i++) {
PROBLEMS.add(new Problem(i + 1, CHOICES_A, SCORE_PER_QUESTION_A));
}
// 科目Bの問題 (No.61-80, 元のNo.1-20): 選択肢数変動1点問題
// 科目B選択肢数リスト = [6, 5, 7, 4, 7, 4, 6, 4, 4, 6, 4, 4, 4, 8, 4, 9, 10, 10, 5, 7]
int[] choicesB = {6, 5, 7, 4, 7, 4, 6, 4, 4, 6, 4, 4, 4, 8, 4, 9, 10, 10, 5, 7};
for (int i = 0; i < TOTAL_QUESTIONS_B; i++) {
PROBLEMS.add(new Problem(TOTAL_QUESTIONS_A + i + 1, choicesB[i], SCORE_PER_QUESTION_B));
}
}
/**
* ランダム解答方式を定義する列挙型。
* 各方式で、元の選択肢数から計算される有効な選択肢数を保持し、正答率を決定します。
*/
public enum AnswerMethod {
// 完全ランダム(不正解を0%消す力)
FULL_RANDOM("0%の不正解を消す力(完全ランダム)", 0.0),
// 不正解を25%消す力
ELIMINATE_25_PERCENT("25%の不正解を消す力", 0.25),
// 不正解を50%消す力
ELIMINATE_50_PERCENT("50%の不正解を消す力", 0.50);
private final String name;
private final double eliminationRate; // 除外する不正解の割合
AnswerMethod(String name, double eliminationRate) {
this.name = name;
this.eliminationRate = eliminationRate;
}
public String getName() {
return name;
}
/**
* 指定された問題の有効な選択肢数を計算します。
* Math.round() を使用し、最小値は1とします。
* @param originalChoices 元の選択肢の数
* @return 計算された有効な選択肢の数
*/
public int calculateEffectiveChoices(int originalChoices) {
// 除外後の選択肢数を計算
double remainingChoices = originalChoices * (1.0 - eliminationRate);
// Math.round で丸め、小数点以下を四捨五入して整数にする
int effectiveChoices = (int) Math.round(remainingChoices);
// 少なくとも1つの選択肢は残るようにする
return Math.max(1, effectiveChoices);
}
/**
* ランダムに正解するかどうかを判定します。
* @param random Randomインスタンス
* @param problem 問題オブジェクト
* @return 正解であればtrue、そうでなければfalse
*/
public boolean isCorrect(Random random, Problem problem) {
int effectiveChoices = calculateEffectiveChoices(problem.getChoices());
// 0からeffectiveChoices-1までの乱数を生成し、0であれば正解とみなす
return random.nextInt(effectiveChoices) == 0;
}
}
/**
* シミュレーション結果を保持するクラス。
*/
public static class SimulationResult {
private final int knownCorrectQuestionsOverall;
private final double successRate;
public SimulationResult(int knownCorrectQuestionsOverall, double successRate) {
this.knownCorrectQuestionsOverall = knownCorrectQuestionsOverall;
this.successRate = successRate;
}
public int getKnownCorrectQuestionsOverall() { return knownCorrectQuestionsOverall; }
public double getSuccessRate() { return successRate; }
}
private final Random random;
/**
* FeSimulator の新しいインスタンスを生成します。
*/
public FeSimulator() {
this.random = new Random();
}
/**
* 指定されたシナリオでモンテカルロシミュレーションを実行し、合格確率を計算します。
*
* @param answerMethod シミュレーション対象の解答方式(ランダム解答の精度)
* @param knownCorrectQuestionsOverall 事前に知っている正解数(試験全体に対する数)
* @param trials 試行回数
* @return シミュレーション結果を表す {@code SimulationResult} オブジェクト
* @throws IllegalArgumentException 不正な引数が指定された場合
*/
public SimulationResult simulateScenario(AnswerMethod answerMethod, int knownCorrectQuestionsOverall, int trials) {
if (knownCorrectQuestionsOverall < 0 || knownCorrectQuestionsOverall > TOTAL_EXAM_QUESTIONS) {
throw new IllegalArgumentException(
"事前に知っている正解数 (" + knownCorrectQuestionsOverall + ") は、0から総問題数 (" + TOTAL_EXAM_QUESTIONS + ") の範囲で指定してください。"
);
}
if (trials <= 0) {
throw new IllegalArgumentException("試行回数は1以上を指定してください。");
}
int successfulTrials = 0;
// 試験全体の「事前に知っている問題総数」を科目Aと科目Bに按分
int knownCorrectQuestionsA = (int) Math.round(knownCorrectQuestionsOverall * ((double) TOTAL_QUESTIONS_A / TOTAL_EXAM_QUESTIONS));
int knownCorrectQuestionsB = knownCorrectQuestionsOverall - knownCorrectQuestionsA;
// 科目ごとの上限チェック (念のため)
knownCorrectQuestionsA = Math.min(knownCorrectQuestionsA, TOTAL_QUESTIONS_A);
knownCorrectQuestionsB = Math.min(knownCorrectQuestionsB, TOTAL_QUESTIONS_B);
for (int i = 0; i < trials; i++) {
int currentScoreA = 0;
int currentScoreB = 0;
// 既に「事前に知っている問題」としてマークされた問題のインデックスを保持
Set<Integer> knownCorrectIndicesA = new HashSet<>();
Set<Integer> knownCorrectIndicesB = new HashSet<>();
// 1. 「事前に知っている問題」の処理(科目A)
List<Integer> questionIndicesA = new ArrayList<>();
for (int k = 0; k < TOTAL_QUESTIONS_A; k++) {
questionIndicesA.add(k);
}
Collections.shuffle(questionIndicesA, random); // 問題インデックスをシャッフル
for (int k = 0; k < knownCorrectQuestionsA; k++) {
int problemIndex = questionIndicesA.get(k); // シャッフルされたインデックス
currentScoreA += PROBLEMS.get(problemIndex).getScore(); // その問題の配点を加算
knownCorrectIndicesA.add(problemIndex); // 既に処理済みとしてマーク
}
// 2. 「事前に知っている問題」の処理(科目B)
List<Integer> questionIndicesB = new ArrayList<>();
// 科目Bの問題インデックスは、PROBLEMSリストの中での相対位置ではなく、科目B内での0からのインデックスで扱う
for (int k = 0; k < TOTAL_QUESTIONS_B; k++) {
questionIndicesB.add(k);
}
Collections.shuffle(questionIndicesB, random); // 問題インデックスをシャッフル
for (int k = 0; k < knownCorrectQuestionsB; k++) {
int problemIndexB_relative = questionIndicesB.get(k); // シャッフルされた科目B内でのインデックス
int problemIndexInOverallList = TOTAL_QUESTIONS_A + problemIndexB_relative; // 全体リストでのインデックス
currentScoreB += PROBLEMS.get(problemIndexInOverallList).getScore(); // その問題の配点を加算
knownCorrectIndicesB.add(problemIndexB_relative); // 科目B内での相対インデックスをマーク
}
// 3. 「残りの問題」の処理(ランダム解答、科目A)
for (int j = 0; j < TOTAL_QUESTIONS_A; j++) {
if (!knownCorrectIndicesA.contains(j)) { // 事前に知っている問題でなければランダムに解答
Problem problem = PROBLEMS.get(j);
if (answerMethod.isCorrect(random, problem)) {
currentScoreA += problem.getScore(); // 正解なら配点を加算
}
}
}
// 4. 「残りの問題」の処理(ランダム解答、科目B)
for (int j = 0; j < TOTAL_QUESTIONS_B; j++) {
if (!knownCorrectIndicesB.contains(j)) { // 事前に知っている問題でなければランダムに解答
Problem problem = PROBLEMS.get(TOTAL_QUESTIONS_A + j); // 全体リストから科目Bの問題を取得
if (answerMethod.isCorrect(random, problem)) {
currentScoreB += problem.getScore(); // 正解なら配点を加算
}
}
}
// 5. 合格条件の判定
boolean passedA = (currentScoreA >= REQUIRED_RAW_SCORE_A);
boolean passedB = (currentScoreB >= REQUIRED_RAW_SCORE_B);
if (passedA && passedB) {
successfulTrials++;
}
}
double successRate = (double) successfulTrials / trials * 100;
return new SimulationResult(knownCorrectQuestionsOverall, successRate);
}
/**
* シミュレーション結果を表示します。
*
* @param result 表示するシミュレーション結果
*/
public void displayResult(SimulationResult result) {
System.out.printf("・事前に%d問知っている場合: 合格率: %.2f%%\n",
result.getKnownCorrectQuestionsOverall(), result.getSuccessRate());
}
/**
* メインメソッド。基本情報技術者試験の各種シナリオでシミュレーションを実行します。
*/
public static void main(String[] args) {
FeSimulator simulator = new FeSimulator();
System.out.println("★基本情報技術者試験 突破確率計算:ランダム選択方式(モンテカルロシミュレーション)★");
System.out.println("---");
System.out.println("総問題数: " + TOTAL_EXAM_QUESTIONS + "問");
System.out.println("科目A 合格に必要な素点: " + REQUIRED_RAW_SCORE_A + "点 / " + TOTAL_QUESTIONS_A + "点満点 (100点換算60点)");
System.out.println("科目B 合格に必要な素点: " + REQUIRED_RAW_SCORE_B + "点 / " + TOTAL_QUESTIONS_B + "点満点 (100点換算60点)");
System.out.println("---");
System.out.println();
// 各解答方式のシミュレーション
for (AnswerMethod answerMethod : AnswerMethod.values()) {
System.out.println("■" + answerMethod.getName() + " 試行結果");
// knownCorrectQuestions は0問からTOTAL_EXAM_QUESTIONSまで繰り返す
for (int i = 0; i <= TOTAL_EXAM_QUESTIONS; i++) {
try {
SimulationResult result = simulator.simulateScenario(answerMethod, i, DEFAULT_TRIALS);
simulator.displayResult(result);
} catch (IllegalArgumentException e) {
System.err.println("エラー: " + e.getMessage());
}
}
System.out.println(); // 各解答方式の後に空行
}
System.out.println("---");
System.out.println("※本シミュレーターは、基本情報技術者試験のIRT(項目応答理論)採点を厳密に再現するものではなく、");
System.out.println(" 各問題の配点が上記で設定された値であると仮定した「素点計算」に基づいています。");
System.out.println(" 実際の試験結果と異なる場合があることをご留意ください。");
}
}