嘘つきは何人か?——約数と自己言及の数学
Xで@Nazoi_doriさんの次のポストを見かけました。
面白そうだったので,考えて見ました。
(実は,勤務校の4月の始業式で全校生徒に紹介しました。。。)
問題
160人います。n番の人(
「嘘つきの人数は
の倍数だ。」 n
正直者は本当のことを言い、嘘つきは嘘をつきます。嘘つきは何人でしょうか?
考え方
嘘つきの人数を
-
n番が正直者
「\iff はL の倍数」が真n \iff n \mid L -
n番が嘘つき
「\iff はL の倍数」が偽n \iff n \nmid L
正直者の人数は、1〜160の中で
あるいは
Julia で全探索
# L の候補を全探索する
solutions = Int[]
for L in 0:160
# 1〜160 の中で L を割り切る数の個数(正直者の数)
honest_count = count(n -> L % n == 0, 1:160)
liar_count = 160 - honest_count
if liar_count == L
push!(solutions, L)
end
end
println("解: ", solutions)
解: [0, 152]
解は L = 0 と L = 152 の2つです。
解の詳細
# 解の詳細を表示する
for L in solutions
divisors_in_range = filter(n -> L % n == 0, 1:160)
println("=== 嘘つきの人数: L = $L ===")
println(" 正直者の数: ", 160 - L, " 人")
println(" 正直者(Lの約数): ", divisors_in_range)
println()
end
=== 嘘つきの人数: L = 0 ===
正直者の数: 160 人
正直者(Lの約数): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92,
93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109,
110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139,
140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,
155, 156, 157, 158, 159, 160]
=== 嘘つきの人数: L = 152 ===
正直者の数: 8 人
正直者(Lの約数): [1, 2, 4, 8, 19, 38, 76, 152]
-
L = 0:嘘つきが0人 → 全員正直者 → 全員「0は
の倍数」と言っている(0はすべての整数の倍数)→ 矛盾なし ✓n -
L = 152:嘘つきが152人。
の約数は152 = 2^3 \times 19 の8個 → 正直者8人、嘘つき152人 → 一致 ✓\{1, 2, 4, 8, 19, 38, 76, 152\}
なぜ 152 が答えになるのか
満たすべき条件を整理します:
「
1〜160 内の数の約数の個数の最大値
using Primes
X = []
for x in 1:160
append!(X, divisors(x) |> length)
end
@show union!(X)
union!(X) = Any[1, 2, 3, 4, 6, 5, 8, 9, 10, 12, 7, 16, 15]
約数の個数として登場するのは一部の値のみで、最大値は 16(
X = 160 .- [7, 8, 9, 12, 15, 16]
[divisors(x) for x in X]
6-element Vector{Vector{Int64}}:
[1, 3, 9, 17, 51, 153]
[1, 2, 4, 8, 19, 38, 76, 152]
[1, 151]
[1, 2, 4, 37, 74, 148]
[1, 5, 29, 145]
[1, 2, 3, 4, 6, 8, 9, 12, 16, 18, 24, 36, 48, 72, 144]
|
|
約数の個数 | 一致? | ||
|---|---|---|---|---|
| 7 | 153 | 1, 3, 9, 17, 51, 153 | 6 | 6 ≠ 7 ✗ |
| 8 | 152 | 1, 2, 4, 8, 19, 38, 76, 152 | 8 | 8 = 8 ✓ |
| 9 | 151 | 1, 151 | 2 | 2 ≠ 9 ✗ |
| 12 | 148 | 1, 2, 4, 37, 74, 148 | 6 | 6 ≠ 12 ✗ |
| 15 | 145 | 1, 5, 29, 145 | 4 | 4 ≠ 15 ✗ |
| 16 | 144 | 1, 2, 3, 4, 6, 8, 9, 12, 16, 18, 24, 36, 48, 72, 144 | 15 | 15 ≠ 16 ✗ |
152 だけが条件を満たします。
グラフによる視覚化
2つの関数を考えます:
-
(対角線:「仮定した嘘つきの数」)f(L) = L -
(「g(L) = 160 - d(L) を仮定したときに計算される嘘つきの数」)L
解 = 2つの曲線の交点
using Plots, PlotsGRBackendFontJaEmoji
Ls = 0:160
# f(L) = L(対角線)
f = collect(Ls)
# g(L) = 160 - d(L)(Lを仮定したときの嘘つきの数)
g = [160 - count(n -> L % n == 0, 1:160) for L in Ls]
# 解(交点)
sols = [L for L in Ls if g[L+1] == L]
p = plot(Ls, f,
label = "f(L) = L (仮定した嘘つきの数)",
color = :steelblue,
linewidth = 2,
linestyle = :dash,
xlabel = "L(嘘つきの人数の仮定値)",
ylabel = "人数",
title = "嘘つき問題:2つの関数の交点が解",
size = (700, 500),
legend = :bottomright
)
plot!(p, Ls, g,
label = "g(L) = 160 - d(L) (計算される嘘つきの数)",
color = :crimson,
linewidth = 2
)
# 交点をプロット
scatter!(p, sols, sols,
label = "解: L = $(sols)",
color = :green,
markersize = 8,
markershape = :circle
)
# 解にアノテーション
for s in sols
annotate!(p, s + 3, s + 6, text("L = $s", 9, :green))
end
p

L = 140〜160 の拡大図:
# L = 140〜160 の拡大グラフ
Ls2 = 140:160
f2 = collect(Ls2)
g2 = [160 - count(n -> L % n == 0, 1:160) for L in Ls2]
sols2 = [L for L in Ls2 if g2[L-139] == L]
p2 = plot(Ls2, f2,
label = "f(L) = L (仮定した嘘つきの数)",
color = :steelblue,
linewidth = 2,
linestyle = :dash,
xlabel = "L(嘘つきの人数の仮定値)",
ylabel = "人数",
title = "拡大図:L = 140〜160",
size = (700, 500),
legend = :bottomright,
xticks = 140:2:160
)
plot!(p2, Ls2, g2,
label = "g(L) = 160 - d(L) (計算される嘘つきの数)",
color = :crimson,
linewidth = 2
)
scatter!(p2, sols2, sols2,
label = "解: L = $(sols2)",
color = :green,
markersize = 8,
markershape = :circle
)
for s in sols2
annotate!(p2, s + 0.3, s + 1.5, text("L = $s", 9, :green))
end
p2

拡大すると
一般化:総人数を x に変えたとき
を満たす
# x = 1〜1000 について,嘘つきの人数 L の解を全探索
results = Dict{Int, Vector{Int}}()
for x in 1:1000
solutions = Int[]
for L in 0:x
honest_count = count(n -> L % n == 0, 1:x)
liar_count = x - honest_count
if liar_count == L
push!(solutions, L)
end
end
results[x] = solutions
end
# 結果を表示
println("x(総人数) 解 L")
println("-" ^ 40)
for x in 1:1000
sols = results[x]
println("x = $(lpad(x,4)) : $(sols)")
end
x(総人数) 解 L
----------------------------------------
x = 1 : [0]
x = 2 : [0, 1]
x = 3 : [0]
x = 4 : [0, 2]
x = 5 : [0, 3]
x = 6 : [0]
x = 7 : [0, 4, 5]
x = 8 : [0]
x = 9 : [0, 7]
x = 10 : [0, 6]
...
x = 160 : [0, 152]
...
x = 1000 : [0, 984, 988]
解の個数の分布
# 解の個数別に集計
using Printf
println("解の個数 該当する x の一覧")
println("-" ^ 60)
for k in sort(unique(length(v) for v in values(results)))
xs = sort([x for (x, v) in results if length(v) == k])
println("解が $(k) 個 : x = $(xs)")
end
println()
println("解が 0 のケース(解なし):")
no_sol = sort([x for (x, v) in results if isempty(v)])
println(no_sol)
解の個数 該当する x の一覧
------------------------------------------------------------
解が 1 個 : x = [1, 3, 6, 8, 11, 16, 17, 20, 22, 23, ...]
解が 2 個 : x = [2, 4, 5, 9, 10, 13, 14, 15, 24, 28, ...]
解が 3 個 : x = [7, 12, 18, 19, 21, 25, 26, 31, 39, 43, ...]
解が 4 個 : x = [38, 50, 69, 81, 86, 138, 162, 172, 181, ...]
解が 5 個 : x = [122, 159, 318, 339, 362, 718, 750]
解が 0 のケース(解なし):
Int64[]
解なし(
散布図
# x と解 L の関係をグラフで可視化
using Plots, PlotsGRBackendFontJaEmoji
xs_all = Int[]
ls_all = Int[]
for x in 1:1000
for L in results[x]
push!(xs_all, x)
push!(ls_all, L)
end
end
scatter(xs_all, ls_all,
xlabel = "x(総人数)",
ylabel = "解 L(嘘つきの人数)",
title = "人数 x を変化させたときの解 L(x = 1〜1000)",
label = "解 L",
color = :steelblue,
markersize = 3,
markerstrokewidth = 0,
size = (900, 500),
legend = :topleft
)
# x = 160 の解を強調
x160_sols = results[160]
scatter!(fill(160, length(x160_sols)), x160_sols,
label = "x = 160 の解 $(x160_sols)",
color = :red,
markersize = 8,
markershape = :star5
)

解
x = 50 の詳細例
# x = 50 の詳細確認
x = 50
solutions_50 = Int[]
for L in 0:x
honest_count = count(n -> L % n == 0, 1:x)
liar_count = x - honest_count
if liar_count == L
push!(solutions_50, L)
end
end
println("=== x = $x のとき ===")
println("解: ", solutions_50)
println()
for L in solutions_50
divs = filter(n -> L % n == 0, 1:x)
println("--- L = $L ---")
println(" 正直者の数: $(x - L) 人")
println(" 約数(1〜$(x) 内): $(divs) → $(length(divs)) 個")
println(" $(x) - $(L) = $(x - L) vs 約数の個数 = $(length(divs)) → $(length(divs) == x - L ? "一致 ✓" : "不一致 ✗")")
println()
end
=== x = 50 のとき ===
解: [0, 42, 44, 46]
--- L = 0 ---
正直者の数: 50 人
約数(1〜50 内): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50] → 50 個
50 - 0 = 50 vs 約数の個数 = 50 → 一致 ✓
--- L = 42 ---
正直者の数: 8 人
約数(1〜50 内): [1, 2, 3, 6, 7, 14, 21, 42] → 8 個
50 - 42 = 8 vs 約数の個数 = 8 → 一致 ✓
--- L = 44 ---
正直者の数: 6 人
約数(1〜50 内): [1, 2, 4, 11, 22, 44] → 6 個
50 - 44 = 6 vs 約数の個数 = 6 → 一致 ✓
--- L = 46 ---
正直者の数: 4 人
約数(1〜50 内): [1, 2, 23, 46] → 4 個
50 - 46 = 4 vs 約数の個数 = 4 → 一致 ✓
x = 50 のまとめ
総人数が 50 人の場合を詳しく見てみましょう。
満たすべき条件:
| 解 |
素因数分解 | 約数(1〜50 内) | 約数の個数 | 確認 | |
|---|---|---|---|---|---|
| 0 | — | 1〜50 すべて | 50 | 50 | ✓ |
| 42 | 1, 2, 3, 6, 7, 14, 21, 42 | 8 | 8 | ✓ | |
| 44 | 1, 2, 4, 11, 22, 44 | 6 | 6 | ✓ | |
| 46 | 1, 2, 23, 46 | 4 | 4 | ✓ |
x = 50 では解が 4つ存在し、x = 160 よりも豊かな構造をもちます。
x > 1000 での解の最大個数
と書けます(
篩で
# x > 1000 で解の個数の最大値を更新する x を探す
# 条件: L + d(L) = x (L=0 は常に解なので別扱い)
MAX_L = 2_000_000 # L の探索上限
# 篩で約数の個数を一括計算 O(N log N)
println("約数の個数を篩で計算中 (MAX_L = $MAX_L)...")
d = zeros(Int32, MAX_L)
for k in 1:MAX_L
for m in k:k:MAX_L
d[m] += 1
end
end
println("完了")
# L + d(L) = x となる L を x ごとに集約(x > 1000 のみ)
sol_dict = Dict{Int, Vector{Int}}()
for L in 1:MAX_L
s = L + Int(d[L])
if s > 1000
push!(get!(sol_dict, s, Int[]), L)
end
end
# x > 1000 で解の個数が最大値を更新するものを出力
let current_max = 5
println("\nx > 1000 で解の個数が最大値を更新するもの(初期最大値 = $current_max)")
println("-" ^ 70)
for x in sort(collect(keys(sol_dict)))
ls = sol_dict[x]
n_sols = 1 + length(ls) # +1 は L = 0 の分
if n_sols > current_max
current_max = n_sols
println("x = $(lpad(x,8)) : 解の個数 = $n_sols, 解 L = $(sort([0; ls]))")
end
end
println("\n最終的な最大解の個数: $current_max")
end
約数の個数を篩で計算中 (MAX_L = 2000000)...
完了
x > 1000 で解の個数が最大値を更新するもの(初期最大値 = 5)
----------------------------------------------------------------------
x = 2766 : 解の個数 = 6, 解 L = [0, 2736, 2750, 2752, 2758, 2762]
x = 64686 : 解の個数 = 7, 解 L = [0, 64638, 64656, 64670, 64674, 64678, 64682]
x = 1972296 : 解の個数 = 8, 解 L = [0, 1972152, 1972200, 1972224, 1972260, 1972264, 1972280, 1972284]
最終的な最大解の個数: 8
まとめ
| 項目 | 内容 |
|---|---|
| 元の問題( |
|
| 条件の式 |
|
|
|
0 はすべての整数の倍数なので |
|
|
なし |
|
|
|
|
|
8個( |
Discussion