From cbca99e20c3940782bf12bdc88ec5a7cd1653acf Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sat, 6 Dec 2025 10:30:10 +0900 Subject: [PATCH] Math: Basic Number Theory Sherlock and GCD --- .../Easy}/Constructing_a_Number.ipynb | 0 .../HuckerRank/Easy/Sherlock_and_GCD.ipynb | 756 ++++++++++++++++++ 2 files changed, 756 insertions(+) rename Mathematics/Number Theory/{ => HuckerRank/Easy}/Constructing_a_Number.ipynb (100%) create mode 100644 Mathematics/Number Theory/HuckerRank/Easy/Sherlock_and_GCD.ipynb diff --git a/Mathematics/Number Theory/Constructing_a_Number.ipynb b/Mathematics/Number Theory/HuckerRank/Easy/Constructing_a_Number.ipynb similarity index 100% rename from Mathematics/Number Theory/Constructing_a_Number.ipynb rename to Mathematics/Number Theory/HuckerRank/Easy/Constructing_a_Number.ipynb diff --git a/Mathematics/Number Theory/HuckerRank/Easy/Sherlock_and_GCD.ipynb b/Mathematics/Number Theory/HuckerRank/Easy/Sherlock_and_GCD.ipynb new file mode 100644 index 00000000..d3b9beec --- /dev/null +++ b/Mathematics/Number Theory/HuckerRank/Easy/Sherlock_and_GCD.ipynb @@ -0,0 +1,756 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2dd5686f", + "metadata": {}, + "source": [ + "## 1. 問題の整理と数学的な本質\n", + "\n", + "条件を整理すると、求める部分集合 (S) は:\n", + "\n", + "1. (S) は空でない\n", + "2. 「すべての要素を割り切る整数 (d > 1) が存在しない」\n", + " → 数学的には **(\\gcd(S) = 1)**\n", + "3. (S) 内に同じ値の要素が存在しない\n", + " → (S) の値は **全部異なる**\n", + "\n", + "### 重複に関する考察\n", + "\n", + "* 元の配列には重複があってよい\n", + "* ただし「部分集合 (S) の中で同じ値を 2 回使ってはダメ」という制約\n", + "\n", + "> しかし GCD の観点では、\n", + "> **重複しても GCD は変わらない** というのがポイントです。\n", + "\n", + "例:\n", + "\n", + "* (\\gcd(6, 10) = 2)\n", + "* (\\gcd(6, 10, 6) = \\gcd(2, 6) = 2)(6 を重ねても GCD は 2 のまま)\n", + "\n", + "つまり:\n", + "\n", + "* 「ある(重複を含む)部分集合で GCD が 1」なら\n", + " → その中の **重複を 1 個ずつにした集合** でも GCD は 1 のまま\n", + " → 条件 3(全部異なる)を満たす部分集合に変換できる\n", + "\n", + "### 「配列全体の GCD」で十分な理由\n", + "\n", + "* 配列全体の GCD を (g) とする\n", + "* もし (g > 1) なら:\n", + "\n", + " * すべての要素が (g) の倍数\n", + " * 任意の部分集合を取ってきても、その GCD も (g) の倍数\n", + " → **GCD が 1 になる部分集合は存在しない**\n", + "* もし (g = 1) なら:\n", + "\n", + " * 「配列の全要素の GCD = 1」なので、\n", + " * 重複を 1 個ずつにした集合でも GCD は 1\n", + " * 各値を 1 回だけ使うようにインデックスを選べば\n", + " → 条件 1,2,3 をすべて満たす (S) が作れる\n", + "\n", + "よって\n", + "\n", + "> **「配列全体の GCD が 1 なら YES、それ以外なら NO」**\n", + "\n", + "で判定できます。\n", + "\n", + "---\n", + "\n", + "## 2. アルゴリズム比較表\n", + "\n", + "| アプローチ | 概要 | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | CPython的に妥当か | 備考 |\n", + "| --------------- | ------------------------ | ------------------------- | ------ | ----------- | --- | ------------ | ------------------ |\n", + "| 方法A: 全体GCD法(採用) | 配列全体の GCD を求めて 1 かどうかで判定 | (O(n))(各要素との gcd はほぼ定数時間) | (O(1)) | 低 | ★★★ | 非常に良い | `math.gcd` に丸投げで高速 |\n", + "| 方法B: 部分集合列挙 | すべての部分集合の GCD をチェック | (O(2^n)) | (O(n)) | 高 | ★☆☆ | 不適 | 制約的に完全に非現実的 |\n", + "| 方法C: ユニーク値GCD | 重複を set で削除してから GCD | (O(n)) | (O(n)) | 中 | ★★☆ | 良 | 正しいが A より余計なメモリ |\n", + "\n", + "**採用:** 方法A(全体 GCD)\n", + "\n", + "* 理由: 最速・最省メモリ・コード最短\n", + "* 数学的にも「全体 GCD が 1 ⇔ 条件を満たす部分集合が存在」と言える\n", + "\n", + "---\n", + "\n", + "## 3. Python特有の最適化ポイント\n", + "\n", + "* `math.gcd` をローカル変数に束縛して attribute lookup を削減\n", + "* GCD の初期値を 0 にして、1 回目だけ特別扱いしない(`gcd(0, x) = |x|`)\n", + "* 配列をコピー・ソート・`set` 化せず、**そのまま 1 パスで GCD を縮約**\n", + "* 値が負でも安全なように `abs(x)` を取っておくと堅牢(問題上は正数想定だが)\n", + "\n", + "---\n", + "\n", + "## 4. 実装パターン\n", + "\n", + "* `solve_production`\n", + "\n", + " * 業務開発向け(入力検証付き・例外を投げる想定)\n", + "* `solve_competitive`\n", + "\n", + " * 競技プログラミング向け(検証なし・性能優先)\n", + "* HackerRank が呼び出すのは `solve(a)` で、\n", + " 中身では `solve_competitive` を利用します。\n", + "\n", + "---\n", + "\n", + "## 5. HackerRank 用 完成コード\n", + "\n", + "※ 問題文末尾のテンプレートに合わせて実装しています。\n", + "※ `input()` / `print()` は使わず、`solve` 関数だけで判定します。\n", + "\n", + "```python\n", + "#!/bin/python3\n", + "\n", + "import math\n", + "import os\n", + "import random\n", + "import re\n", + "import sys\n", + "from typing import List, Any\n", + "\n", + "#\n", + "# Complete the 'solve' function below.\n", + "#\n", + "# The function is expected to return a STRING.\n", + "# The function accepts INTEGER_ARRAY a as parameter.\n", + "#\n", + "\n", + "\n", + "def solve_production(a: List[int]) -> str:\n", + " \"\"\"\n", + " 業務開発向け実装(入力検証あり)\n", + " - 条件を満たす部分集合 S が存在すれば \"YES\"\n", + " - 存在しなければ \"NO\" を返す\n", + " \"\"\"\n", + " _validate_input(a)\n", + "\n", + " # この問題の本質は「配列全体の GCD が 1 かどうか」\n", + " has_subset = _has_valid_subset_gcd_based(a)\n", + " return \"YES\" if has_subset else \"NO\"\n", + "\n", + "\n", + "def solve_competitive(a: List[int]) -> str:\n", + " \"\"\"\n", + " 競技プログラミング向け実装\n", + " エラーハンドリングを省略し、性能最優先で実装。\n", + "\n", + " Time Complexity: O(n)\n", + " Space Complexity: O(1)\n", + " \"\"\"\n", + " # 高速化のため型チェック等は省略\n", + " has_subset = _has_valid_subset_gcd_based(a)\n", + " return \"YES\" if has_subset else \"NO\"\n", + "\n", + "\n", + "def _validate_input(data: Any) -> None:\n", + " \"\"\"\n", + " 業務開発向けの簡易バリデーション。\n", + " HackerRank 本番では solve_competitive のみを使うので、\n", + " ここで例外が投げられることはありません。\n", + " \"\"\"\n", + " if not isinstance(data, list):\n", + " raise TypeError(\"Input must be a list of integers\")\n", + "\n", + " if not data:\n", + " # 問題制約的には N >= 1 を想定\n", + " raise ValueError(\"Input list cannot be empty\")\n", + "\n", + " if len(data) > 10**6:\n", + " # 任意の上限(業務システム向けの安全弁)\n", + " raise ValueError(\"Input size exceeds limit\")\n", + "\n", + " if not all(isinstance(x, int) for x in data):\n", + " raise TypeError(\"All elements must be integers\")\n", + "\n", + "\n", + "def _has_valid_subset_gcd_based(a: List[int]) -> bool:\n", + " \"\"\"\n", + " 問題固有のメインアルゴリズム。\n", + " - 条件を満たす部分集合 S が存在するかどうかを bool で返す。\n", + "\n", + " ロジック:\n", + " - 配列全体の GCD を g とする\n", + " - g == 1 なら YES(条件を満たす S が必ず存在)\n", + " - g != 1 なら NO(どの部分集合を取っても GCD は 1 にならない)\n", + " \"\"\"\n", + " g: int = 0\n", + " gcd_func = math.gcd\n", + "\n", + " for x in a:\n", + " # 念のため絶対値を取る(負数が来ても安全に)\n", + " g = gcd_func(g, abs(x))\n", + " if g == 1:\n", + " # これ以上見ても GCD は 1 のままなので早期終了\n", + " return True\n", + "\n", + " return g == 1\n", + "\n", + "\n", + "def solve(a: List[int]) -> str:\n", + " \"\"\"\n", + " HackerRank から呼び出されるエントリポイント。\n", + " 実際には競技プログラミング向けの solve_competitive を利用。\n", + " \"\"\"\n", + " return solve_competitive(a)\n", + "\n", + "\n", + "if __name__ == '__main__':\n", + " fptr = open(os.environ['OUTPUT_PATH'], 'w')\n", + "\n", + " t = int(input().strip())\n", + "\n", + " for t_itr in range(t):\n", + " a_count = int(input().strip())\n", + "\n", + " a = list(map(int, input().rstrip().split()))\n", + "\n", + " result = solve(a)\n", + "\n", + " fptr.write(result + '\\n')\n", + "\n", + " fptr.close()\n", + "```\n", + "\n", + "---\n", + "\n", + "この実装なら:\n", + "\n", + "* サンプル入力\n", + "\n", + " ```\n", + " 3\n", + " 3\n", + " 1 2 3\n", + " 2\n", + " 2 4\n", + " 3\n", + " 5 5 5\n", + " ```\n", + "\n", + "* 出力\n", + "\n", + " ```\n", + " YES\n", + " NO\n", + " NO\n", + " ```\n", + "\n", + "となり、問題の要件をすべて満たします。" + ] + }, + { + "cell_type": "markdown", + "id": "599d38dd", + "metadata": {}, + "source": [ + "# Sherlock and GCD - 配列全体のGCDだけで判定する高速アルゴリズム\n", + "\n", + "## 目次\n", + "\n", + "* [概要](#overview)\n", + "* [アルゴリズム要点(TL;DR)](#tldr)\n", + "* [図解](#figures)\n", + "* [正しさのスケッチ](#correctness)\n", + "* [計算量](#complexity)\n", + "* [Python 実装](#impl)\n", + "* [CPython最適化ポイント](#cpython)\n", + "* [エッジケースと検証観点](#edgecases)\n", + "* [FAQ](#faq)\n", + "\n", + "---\n", + "\n", + "

概要

\n", + "\n", + "HackerRank 問題 **Sherlock and GCD** では、与えられた配列 `a` について次の条件を満たす部分集合 `S` が存在するかを判定します。\n", + "\n", + "1. `S` は空でない\n", + "2. `S` のすべての要素を割り切る **1より大きい整数 `d` が存在しない**\n", + "\n", + " * 数学的には `gcd(S) = 1` と等価\n", + "3. `S` 内に同じ値は 2 回以上出現しない(要素はすべて異なる)\n", + "\n", + "テストケースが複数あり、配列長も大きくなり得るため、\n", + "**全部分集合を列挙するような解法は使えません**。\n", + "そこで、**配列全体の GCD を 1 回だけ計算する**シンプルかつ本質的な方法を使います。\n", + "\n", + "---\n", + "\n", + "

アルゴリズム要点(TL;DR)

\n", + "\n", + "* 戦略\n", + "\n", + " * 配列 `a` 全体の GCD を `g` として計算する\n", + " * 結論はただこれだけ:\n", + "\n", + " * `g == 1` → 条件を満たす部分集合 `S` が存在する → `YES` / `True`\n", + " * `g > 1` → どんな部分集合でも GCD は 1 にならない → `NO` / `False`\n", + "\n", + "* なぜそれで十分か(直感)\n", + "\n", + " * 配列全体の GCD が 1 ということは、\n", + " **その配列の要素を何個か組み合わせると GCD が 1 になる** という意味\n", + " * 「配列全体」そのものも立派な部分集合なので、\n", + " 少なくともその集合で GCD 1 が達成されている\n", + " * 重複を消しても GCD は変わらないので、「全部異なる」という条件も満たせる\n", + "\n", + "* データ構造\n", + "\n", + " * 必要なのは `List[int]` のみ(追加配列や木・グラフは不要)\n", + "\n", + "* 計算量(1 テストケースあたり)\n", + "\n", + " * Time: `O(n)`(配列長 `n` に対して 1 パス)\n", + " * Space: `O(1)`(累積 GCD だけ)\n", + "\n", + "---\n", + "\n", + "

図解

\n", + "\n", + "### フローチャート(全体GCDによる判定)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " Start[Start] --> Init[Init g as 0]\n", + " Init --> LoopCheck{More elements}\n", + " LoopCheck -- Yes --> Take[Read next value x]\n", + " Take --> Update[Update g with gcd g x]\n", + " Update --> Early{g equals 1}\n", + " Early -- Yes --> RetYes[Return YES]\n", + " Early -- No --> LoopCheck\n", + " LoopCheck -- No --> FinalCheck{g equals 1}\n", + " FinalCheck -- Yes --> RetYes2[Return YES]\n", + " FinalCheck -- No --> RetNo[Return NO]\n", + "```\n", + "\n", + "**説明(日本語)**\n", + "\n", + "* GCD を 0 から累積し、各要素を取り込むたびに更新します。\n", + "* 途中で `g == 1` になったら、それ以降 GCD は 1 のままなので即 `YES` を返せます。\n", + "* 最後まで 1 にならなければ、どの部分集合でも GCD は 1 にならないため `NO` です。\n", + "\n", + "---\n", + "\n", + "### データフロー図(入力から出力まで)\n", + "\n", + "```mermaid\n", + "graph LR\n", + " subgraph Precheck\n", + " A[Input array] --> B[Read values]\n", + " end\n", + " subgraph Core\n", + " B --> C[Iterate elements]\n", + " C --> D[Compute gcd cumulatively]\n", + " D --> E[Check gcd equals 1]\n", + " end\n", + " E --> F[Output YES or NO]\n", + "```\n", + "\n", + "**説明(日本語)**\n", + "\n", + "* 入力配列をそのまま 1 回走査して GCD を累積するだけのシンプルな構造です。\n", + "* 追加の配列や補助データ構造は不要で、出力は `YES/NO` のブーリアン判定です。\n", + "\n", + "---\n", + "\n", + "

正しさのスケッチ

\n", + "\n", + "ここでは、この「全体 GCD だけ見る」アルゴリズムがなぜ正しいかを、\n", + "数学的な性質に基づいて確認します。\n", + "\n", + "### 1. 条件 2 と GCD の関係\n", + "\n", + "* 整数列 `S` の GCD を `g = gcd(S)` とすると:\n", + "\n", + " * `g` は「`S` のすべての要素を割り切る整数のうち最大のもの」\n", + " * よって、1 より大きい整数 `d` が `S` のすべての要素を割り切る ⇔ `g >= 2`\n", + " * 逆に「1 より大きい共通の割り切れる整数が存在しない」 ⇔ `g = 1`\n", + "\n", + "* つまり、問題の条件 2 は\n", + "\n", + "> 「`S` の GCD が 1 であること」\n", + "\n", + "と完全に等価です。\n", + "\n", + "### 2. 全体 GCD が 1 なら十分(十分性)\n", + "\n", + "配列 `A = [a1, a2, ..., an]` とし、`g = gcd(A)` とします。\n", + "\n", + "* 仮定:`g = 1`\n", + "\n", + "* このとき、**配列全体の集合 `{a1, ..., an}` 自体が GCD 1 の部分集合 `S`** です。\n", + "\n", + " * これは `S` が空でないことも満たします。\n", + "\n", + "* さらに、GCD を左から順に計算していくと:\n", + "\n", + " ```text\n", + " g1 = gcd(a1)\n", + " g2 = gcd(a1, a2)\n", + " ...\n", + " gn = gcd(a1, a2, ..., an) = 1\n", + " ```\n", + "\n", + " となります。`gn = 1` になっているので、どこかの `k` で\n", + " `gk = gcd(a1, ..., ak) = 1` が初めて成立します。\n", + "\n", + "* このときの prefix 集合 `S_prefix = {a1, ..., ak}` も GCD 1 の部分集合です(ただし、`k = n` の場合もあります)。\n", + "\n", + "* ここで、もし `S_prefix` の中に同じ値が複数回出現していても、\n", + " **重複を 1 回ずつに削っていっても GCD は変わりません**。\n", + "\n", + " * 例として `{6, 10, 15, 6}` の GCD は `{6, 10, 15}` の GCD と同じです。\n", + "\n", + "* よって、重複を除いた集合 `S`(各値 1 回ずつだけ使う)でも GCD は 1 のままです。\n", + "\n", + "* 結果として:\n", + "\n", + "> 全体 GCD が 1 であれば、\n", + "> 条件 1(非空)・条件 2(GCD 1)・条件 3(全部異なる)を満たす部分集合 `S` が必ず存在する。\n", + "\n", + "### 3. 全体 GCD が 1 でないなら不可能(必要性)\n", + "\n", + "次に、逆方向を確認します。\n", + "\n", + "* 仮定:配列全体 `A` の GCD が `g > 1`\n", + "\n", + " * つまり、すべての `a_i` が `g` の倍数です。\n", + "* 任意の部分集合 `S` を取っても、要素はすべて `g` の倍数のままなので、\n", + "\n", + " * `g` は `S` のすべての要素を割り切ります。\n", + " * したがって `gcd(S)` は `g` の約数であり、`gcd(S) >= 2` が必ず成り立ちます。\n", + "* よって、**どんな部分集合を取ってきても GCD が 1 になることはありません**。\n", + "* したがって、\n", + "\n", + "> 全体 GCD が 1 でない場合、条件 2 を満たす部分集合 `S` は存在しません。\n", + "\n", + "### 4. 条件 3(重複禁止)の扱い\n", + "\n", + "条件 3 は「`S` 内で同じ値が 2 回以上登場してはならない」というものです。\n", + "\n", + "* 重要な事実:**GCD は同じ値を何回足しても変わりません。**\n", + "\n", + " * 例:`gcd(6, 10) = 2` と `gcd(6, 10, 6) = 2` は同じ\n", + "* もし重複を含む集合 `S_full` で GCD が 1 になっているなら、\n", + "\n", + " * そこから重複を削っていっても GCD は 1 のままです。\n", + "* よって、「重複を許した世界で GCD 1 の部分集合が存在」するなら、\n", + "\n", + " * 「重複を削った世界(条件 3 を満たす世界)でも GCD 1 の部分集合が存在」します。\n", + "* これにより、**存在判定としては全体 GCD のみ見れば十分**であることが分かります。\n", + "\n", + "### 5. 終了性\n", + "\n", + "* 配列長を `n` とすると、アルゴリズムは `n` ステップ(最大)で終了します。\n", + "* 各ステップで行う処理は:\n", + "\n", + " * 1 回の `gcd` 計算(`math.gcd`)\n", + " * 1 回の `abs`(任意)\n", + "* 途中で `g == 1` になれば即座に終了します。\n", + "* よって、**必ず有限ステップで終了します**。\n", + "\n", + "---\n", + "\n", + "

計算量

\n", + "\n", + "1 テストケースあたりの計算量:\n", + "\n", + "* 時間計算量: **O(n)**\n", + "\n", + " * 配列を 1 回走査するだけ(`n` は配列長)\n", + "\n", + "* 空間計算量: **O(1)**\n", + "\n", + " * 累積 GCD `g` とループ変数のみ(入力配列以外の追加配列なし)\n", + "\n", + "表にまとめると:\n", + "\n", + "| 観点 | 計算量 | メモ |\n", + "| ----- | --------- | ----------------------- |\n", + "| Time | O(n) | 1 パスの線形時間 |\n", + "| Space | O(1) | in-place 判定、追加データ構造ほぼ不要 |\n", + "| データ構造 | List[int] | Python の通常のリストのみ利用 |\n", + "\n", + "---\n", + "\n", + "

Python 実装

\n", + "\n", + "ここでは、LeetCode 風のクラス形式インターフェースで実装します。\n", + "\n", + "* メソッドシグネチャ例:\n", + "\n", + " ```python\n", + " class Solution:\n", + " def hasValidSubset(self, nums: List[int]) -> bool:\n", + " ...\n", + " ```\n", + "\n", + "* 返り値:\n", + "\n", + " * 条件を満たす部分集合 `S` が存在する → `True`\n", + " * 存在しない → `False`\n", + "\n", + "```python\n", + "from __future__ import annotations\n", + "\n", + "import math\n", + "from typing import List\n", + "\n", + "\n", + "class Solution:\n", + " \"\"\"\n", + " Sherlock and GCD 判定ロジック\n", + "\n", + " 与えられた整数配列 nums について、以下を満たす部分集合 S が\n", + " 存在するかどうかを判定する:\n", + "\n", + " 1. S は空でない\n", + " 2. S の全要素を割り切る 1 より大きい整数 d が存在しない\n", + " (すなわち gcd(S) == 1)\n", + " 3. S 内で同じ値が 2 回以上登場しない\n", + "\n", + " 実装上は「配列全体の gcd が 1 かどうか」を見るだけで十分。\n", + " \"\"\"\n", + "\n", + " def hasValidSubset(self, nums: List[int]) -> bool:\n", + " \"\"\"\n", + " 条件を満たす部分集合が存在すれば True、存在しなければ False を返す。\n", + "\n", + " Time: O(n)\n", + " Space: O(1)\n", + " \"\"\"\n", + " # 累積 GCD。0 から始めることで、最初の要素を自然に取り込める。\n", + " g: int = 0\n", + "\n", + " # ローカル変数に束縛して、ループ内での属性アクセスコストを削減。\n", + " gcd_func = math.gcd\n", + "\n", + " for x in nums:\n", + " # 負数が紛れ込んでも安定するよう、絶対値をとっておく。\n", + " # 問題設定上、正の整数のみであればそのままでも構わないが、\n", + " # こうしておくとロバストネスが増す。\n", + " g = gcd_func(g, abs(x))\n", + "\n", + " # GCD が 1 になった時点で、以降何を見ても GCD は 1 のままなので、\n", + " # ここで早期リターンしてよい。\n", + " if g == 1:\n", + " return True\n", + "\n", + " # 最後まで GCD が 1 にならなかった場合、\n", + " # 条件を満たす部分集合は存在しない。\n", + " return g == 1\n", + "```\n", + "\n", + "---\n", + "\n", + "

CPython最適化ポイント

\n", + "\n", + "この問題はそもそも O(n) で非常に軽いですが、CPython 3.11+ 的なチューニングポイントを挙げておきます。\n", + "\n", + "* `math.gcd` のローカル変数化\n", + "\n", + " ```python\n", + " gcd_func = math.gcd\n", + " ...\n", + " g = gcd_func(g, abs(x))\n", + " ```\n", + "\n", + " * ループ内部で `math.gcd` を毎回属性参照するより、\n", + " ローカル変数に束縛したほうが名前解決が速くなります。\n", + " * 小さな差ですが、ループ回数が多いと効いてきます。\n", + "\n", + "* 不要なコンテナ・コピーの排除\n", + "\n", + " * `set(nums)` や `sorted(nums)` を作らない\n", + " * 部分集合を実際に生成しない\n", + " * スライス(`nums[:]`)等も不要\n", + " * これにより、メモリアロケーションと GC コストを抑えます。\n", + "\n", + "* 早期終了で平均時間を短縮\n", + "\n", + " * 多くの実用的な入力では、最初の数要素で GCD がすぐ 1 になることが多いです。\n", + " * `if g == 1: return True` により、平均的な計算時間を大きく削減できます。\n", + "\n", + "* 純粋関数スタイル\n", + "\n", + " * `hasValidSubset` は引数のリストを書き換えない純粋関数的実装です。\n", + " * これにより、他処理との組み合わせやテストがやりやすくなります。\n", + "\n", + "---\n", + "\n", + "

エッジケースと検証観点

\n", + "\n", + "テスト設計の観点から、特に確認しておきたいケースを列挙します。\n", + "\n", + "1. **単一要素**\n", + "\n", + " * `[1]`\n", + "\n", + " * `gcd(1) = 1` → `True`(`S = {1}`)\n", + " * `[5]`\n", + "\n", + " * `gcd(5) = 5` → `False`(どの部分集合も GCD が 5)\n", + "\n", + "2. **すべて同じ値**\n", + "\n", + " * `[5, 5, 5]`\n", + "\n", + " * 全体 GCD = 5 → `False`\n", + " * 問題文サンプルの「5 5 5」ケースと一致。\n", + "\n", + "3. **すべてが同じ素数の倍数**\n", + "\n", + " * `[2, 4]`\n", + "\n", + " * GCD = 2 → `False`\n", + " * `[6, 10, 14]`\n", + "\n", + " * GCD = 2 → `False`\n", + " * どの部分集合でも GCD は 1 にならない。\n", + "\n", + "4. **全体 GCD は 1 だが、真部分集合では 1 にならない例**\n", + "\n", + " * `[6, 10, 15]`\n", + "\n", + " * 単体: 6, 10, 15 → GCD はそれぞれ 6, 10, 15\n", + " * ペア: gcd(6,10)=2, gcd(6,15)=3, gcd(10,15)=5\n", + " * 全体: gcd(6,10,15)=1\n", + " * **GCD が 1 になるのは全体集合のみ**だが、それでも条件を満たす部分集合は存在する\n", + " → アルゴリズムの判定(全体 GCD == 1 → True)は正しい。\n", + "\n", + "5. **重複を含むが GCD が 1**\n", + "\n", + " * `[2, 3, 3]`\n", + "\n", + " * `gcd(2, 3, 3) = 1` → `True`\n", + " * 条件 3 を満たす部分集合としては `S = {2, 3}` などが取れる。\n", + "\n", + "6. **0 や負数を含む場合(ロバストネスチェック)**\n", + "\n", + " 実際の問題では非負整数のみが想定されていることが多いですが、実装の堅牢性を確認:\n", + "\n", + " * `[0, 1]` → `gcd(0, 1) = 1` → `True`\n", + " * `[-2, 4]` → `gcd(2, 4) = 2` → `False`(abs を取った振る舞い)\n", + "\n", + "7. **最大サイズの入力**\n", + "\n", + " * 制約上許される最大の `n` と値を与えても、\n", + " 本アルゴリズムは O(n)・定数メモリのため、TLE や MLE の心配はほぼありません。\n", + "\n", + "---\n", + "\n", + "

FAQ

\n", + "\n", + "**Q1. なぜ部分集合を実際に列挙しなくてよいのですか?**\n", + "\n", + "* A. GCD の性質上、\n", + "\n", + " * 全体 GCD が 1 のとき:\n", + "\n", + " * 少なくとも「配列全体」という部分集合で GCD 1 が達成されています。\n", + " * さらに、その過程のどこかの prefix でも GCD 1 になる場合が多いです(ならない場合もありますが、配列全体が使えます)。\n", + " * 全体 GCD が 1 でないとき:\n", + "\n", + " * どんな部分集合を取っても、その GCD は全体 GCD の約数であり、1 にはなりません。\n", + "\n", + " という「存在判定」として十分な条件が成り立つため、\n", + " **部分集合を列挙する必要がなくなります**。\n", + "\n", + "---\n", + "\n", + "**Q2. 条件 3(重複禁止)はどう反映されているのですか?**\n", + "\n", + "* A. GCD は同じ数を何回足しても変わらないため、\n", + "\n", + " * もし重複を含む集合で GCD = 1 が達成されているなら、\n", + " * その重複を 1 つずつ消しても GCD は 1 のままです。\n", + "\n", + "* したがって、「重複を許した世界で GCD 1 の部分集合が存在」するなら、\n", + " 「重複を禁止した世界でも GCD 1 の部分集合が存在」します。\n", + " → **全体 GCD のみで判定して問題ありません。**\n", + "---\n", + "\n", + "**Q3. 全体 GCD が 1 なら、必ず「一部だけ取り出した集合」でも GCD 1 になりますか?**\n", + "\n", + "* **A. いいえ、それは保証できません。**\n", + "\n", + " 具体例として、配列が `[6, 10, 15]` の場合を見てみます。\n", + "\n", + " 1. 全体の GCD\n", + "\n", + " * `gcd(6, 10, 15) = 1`\n", + " → **「全部を使った集合 `{6, 10, 15}` では GCD が 1 になる**\n", + "\n", + " 2. 真部分集合(全部は使わないやつ)の GCD\n", + "\n", + " * 1 個だけ取る場合\n", + "\n", + " * `{6}` → GCD = 6\n", + " * `{10}` → GCD = 10\n", + " * `{15}` → GCD = 15\n", + " * 2 個だけ取る場合\n", + "\n", + " * `{6, 10}` → GCD = 2\n", + " * `{6, 15}` → GCD = 3\n", + " * `{10, 15}` → GCD = 5\n", + "\n", + " → この例では、**どの真部分集合でも GCD は 1 になっていません**。\n", + " **GCD が 1 になるのは「全部を使った集合」だけ**です。\n", + "\n", + "### それでも「YES」と判定してよい理由\n", + "\n", + "* 問題の条件は「**部分集合 `S` が存在するか**」であって、\n", + "\n", + " * 「真部分集合でないといけない」とは書いていません。\n", + "* 「配列全体を使った集合」も、ちゃんと **条件を満たす部分集合 `S` の 1 つ**です。\n", + "\n", + "つまり:\n", + "\n", + "* 全体 GCD が 1 なら\n", + "\n", + " * 少なくとも「配列全体」という集合で GCD が 1 になる\n", + " * それだけでも **「条件を満たす `S` が存在する」ので答えは YES**\n", + "\n", + "ということです。\n", + "\n", + "---\n", + "\n", + "**Q4. 配列に 1 が含まれていたらどうなりますか?**\n", + "\n", + "* A. 即座に答えは `YES` です。\n", + "\n", + " * `{1}` という単一要素の部分集合を取れば、\n", + "\n", + " * GCD(1) = 1 → 条件 2 を満たし\n", + " * 要素も 1 つだけなので重複もありません(条件 3 も満たす)\n", + "\n", + "* 実装上も、最初に 1 を取り込んだ瞬間に累積 GCD が 1 になり、即 `True` を返します。\n", + "\n", + "---\n", + "\n", + "**Q5. ゼロを含んでいる場合は安全ですか?**\n", + "\n", + "* A. `gcd(0, x) = |x|` と定義されており、実装もこの性質に従っています。\n", + "\n", + " * 初期値を `g = 0` にすることで、最初の要素を自然に取り込めます。\n", + " * もし全要素が 0 なら GCD = 0 のままなので、1 にはならず `False` になります。\n", + " * 0 と 1 が混ざっていれば GCD は 1 になります。\n", + "\n", + "* 問題の仕様が正の整数のみだとしても、この実装はゼロに対しても安定に動作します。\n", + "\n", + "---\n", + "\n", + "この README に沿って実装・検証を進めれば、\n", + "「GCD による存在判定」という観点から Sherlock and GCD をしっかり理解し、\n", + "類似問題にも応用できるはずです。" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}