- 2020/11/15 【Unity】【C#】画面解像度とアクペクト比(整数)を求める
- 2020/11/14 【C#】最小公倍数を求める(ユークリッドの互除法)
- 2020/11/13 【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法)
- 2016/06/20 【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②)
- 2016/06/19 【Java】拡張ユークリッドの互除法
« prev next »
【Unity】【C#】画面解像度とアクペクト比(整数)を求める 
2020/11/15 Sun [edit]
前回までのユークリッド互除法の実装は画面解像度とアスペクト比を一気に調べたいがために、Java から移植したものだったりする(笑)。
せっかくなので「分数の約分」する関数を使って、画面解像度の一覧からアクペクト比を計算させてみよう。
(※) Unity 2019.4.14f1 / Windows10(x64) で確認
●画面解像度とアクペクト比の一覧を出力してみる
using UnityEngine;
var resolutions = Screen.resolutions;
foreach (var res in resolutions)
{
Debug.Log($"{res} {Reduce(res.width, res.height)}, w/h = {(float)res.width/res.height}"); //※Reduce は以前の記事を参照
}
800 x 600 @ 75Hz (4, 3), w/h = 1.333333
1024 x 768 @ 50Hz (4, 3), w/h = 1.333333
1024 x 768 @ 59Hz (4, 3), w/h = 1.333333
1024 x 768 @ 60Hz (4, 3), w/h = 1.333333
1024 x 768 @ 75Hz (4, 3), w/h = 1.333333
1152 x 864 @ 50Hz (4, 3), w/h = 1.333333
1152 x 864 @ 59Hz (4, 3), w/h = 1.333333
1152 x 864 @ 60Hz (4, 3), w/h = 1.333333
1152 x 864 @ 75Hz (4, 3), w/h = 1.333333
1280 x 600 @ 50Hz (32, 15), w/h = 2.133333
1280 x 600 @ 59Hz (32, 15), w/h = 2.133333
1280 x 600 @ 60Hz (32, 15), w/h = 2.133333
1280 x 720 @ 50Hz (16, 9), w/h = 1.777778
1280 x 720 @ 59Hz (16, 9), w/h = 1.777778
1280 x 720 @ 60Hz (16, 9), w/h = 1.777778
1280 x 768 @ 50Hz (5, 3), w/h = 1.666667
1280 x 768 @ 59Hz (5, 3), w/h = 1.666667
1280 x 768 @ 60Hz (5, 3), w/h = 1.666667
1280 x 800 @ 50Hz (8, 5), w/h = 1.6
1280 x 800 @ 59Hz (8, 5), w/h = 1.6
1280 x 800 @ 60Hz (8, 5), w/h = 1.6
1280 x 960 @ 50Hz (4, 3), w/h = 1.333333
1280 x 960 @ 59Hz (4, 3), w/h = 1.333333
1280 x 960 @ 60Hz (4, 3), w/h = 1.333333
1280 x 1024 @ 50Hz (5, 4), w/h = 1.25
1280 x 1024 @ 59Hz (5, 4), w/h = 1.25
1280 x 1024 @ 60Hz (5, 4), w/h = 1.25
1280 x 1024 @ 75Hz (5, 4), w/h = 1.25
1360 x 768 @ 50Hz (85, 48), w/h = 1.770833
1360 x 768 @ 59Hz (85, 48), w/h = 1.770833
1360 x 768 @ 60Hz (85, 48), w/h = 1.770833
1366 x 768 @ 50Hz (683, 384), w/h = 1.778646
1366 x 768 @ 59Hz (683, 384), w/h = 1.778646
1366 x 768 @ 60Hz (683, 384), w/h = 1.778646
1400 x 1050 @ 50Hz (4, 3), w/h = 1.333333
1400 x 1050 @ 59Hz (4, 3), w/h = 1.333333
1400 x 1050 @ 60Hz (4, 3), w/h = 1.333333
1440 x 900 @ 50Hz (8, 5), w/h = 1.6
1440 x 900 @ 59Hz (8, 5), w/h = 1.6
1440 x 900 @ 60Hz (8, 5), w/h = 1.6
1600 x 900 @ 50Hz (16, 9), w/h = 1.777778
1600 x 900 @ 59Hz (16, 9), w/h = 1.777778
1600 x 900 @ 60Hz (16, 9), w/h = 1.777778
1680 x 1050 @ 50Hz (8, 5), w/h = 1.6
1680 x 1050 @ 59Hz (8, 5), w/h = 1.6
1680 x 1050 @ 60Hz (8, 5), w/h = 1.6
1920 x 1080 @ 50Hz (16, 9), w/h = 1.777778
1920 x 1080 @ 59Hz (16, 9), w/h = 1.777778
1920 x 1080 @ 60Hz (16, 9), w/h = 1.777778
Reduce() は以前の記事のコードをコピペして欲しい。
出てくる解像度はエディタとランタイムで違うので注意。また、設定にも依る(ProjectSetting>Player>Resolution and Presentation>Supported Aspect Ratios)もちろんハードウェア(モニタ)にも依る。
あと、ここでは約分をしてるだけなので、例えば 16:10 → 8:5 になるので注意。
また、ほぼ 16:9 の 1360x768 (85:48), 1366x768 (683:384) みたいなものも Supported Aspect Ratios で 16:9 に含まれるようだ。この辺りは、width/height = 1.77 で判別した方が簡単だ。
ちなみに、私の環境では、Supported Aspect Ratios を 16:9 だけに設定したら、以下のようになった。
1280 x 720 @ 59Hz (16, 9), w/h = 1.777778
1280 x 720 @ 60Hz (16, 9), w/h = 1.777778
1360 x 768 @ 50Hz (85, 48), w/h = 1.770833
1360 x 768 @ 59Hz (85, 48), w/h = 1.770833
1360 x 768 @ 60Hz (85, 48), w/h = 1.770833
1366 x 768 @ 50Hz (683, 384), w/h = 1.778646
1366 x 768 @ 59Hz (683, 384), w/h = 1.778646
1366 x 768 @ 60Hz (683, 384), w/h = 1.778646
1600 x 900 @ 50Hz (16, 9), w/h = 1.777778
1600 x 900 @ 59Hz (16, 9), w/h = 1.777778
1600 x 900 @ 60Hz (16, 9), w/h = 1.777778
1920 x 1080 @ 50Hz (16, 9), w/h = 1.777778
1920 x 1080 @ 59Hz (16, 9), w/h = 1.777778
1920 x 1080 @ 60Hz (16, 9), w/h = 1.777778
アス比を改めて調べる必要性が出てきたのは、Unity2019.3 から起動時の画面解像度を入力設定のダイアログが無くなってしまったため。
・What's the resolution dialog replacement?
フォーラム見てると、Unityの中の人から「問題を起こしやすいため、削除した」みたいなコメントがあったりするが、まぁ、ゲームなら確かに固定解像度でも良いのかもね。しかし、最近では色々な3Dツールとしての使われ方も多くなった。
実際、私も VRM Live Viewer では録画(キャプチャ)するときは解像度を低く、ただ観て楽しむだけのときは解像度を高く、なんて使い分けもしてた。特に Twitter など SNS ではスペック制限あったりするので、あまり高解像度で撮っても意味がない。なので、自分で解像度変更を実装する必要性が出てきてしまった。
最終的には、オープンソースの UnityResolutionDialog を参考に実装しようと思っているが、設定によってどの解像度が範囲に含まれるか知っておきたいと考えた。そういう意味でも一目でアス比を見れるようにするは便利だ(どちらかというとデバッグ用だが、可変解像度なら整数比の方がユーザーにとって理解しやすい利点もある。実装自体は width/height した float 値でも十分)。
ゲームパッドは InputSystem で対応するのが簡単かもね。一応、ピンチ/スワイプ/長押し/ダブル・トリプルクリックなどの操作も InputSystem に対応させたので、いずれはこの辺りの移行も必要かも知れない。
しかし最近、Unity のアップグレードが入るたびに、実装を変えないといけない仕様は何とかして欲しいものである(泣)。
(関連記事)
【Unity】【C#】Quality (グラフィック品質) を文字列で取得/設定する
【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法)
【C#】最小公倍数を求める(ユークリッドの互除法)
【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②)
【Java】拡張ユークリッドの互除法
【C#】最小公倍数を求める(ユークリッドの互除法) 
2020/11/14 Sat [edit]
前回の最大公約数の求め方のついで。
ユークリッドの互除法が実装できてれば、最小公倍数を求めるのは簡単だ。
前回の Gcd (Greatest Common Divisor) を用いて、Lcm (Least Common Multiple) を書いてみよう。
●最小公倍数を求める(ユークリッドの互除法)
/// <summary>
/// 最小公倍数を求める(Least Common Multiple)
/// 2020/11/14 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-385.html
/// http://fantom1x.blog130.fc2.com/blog-entry-384.html
/// http://fantom1x.blog130.fc2.com/blog-entry-215.html#lcm
/// </summary>
/// <param name="a">数値1 (>0)</param>
/// <param name="b">数値2 (>0)</param>
/// <returns>最小公倍数</returns>
public static int Lcm(this int a, int b)
{
return a * b / Gcd(a, b); //※Gcd は前回の記事を参照
}
●メインコード例
using System;
int h = 16;
int v = 9;
int m = Lcm(h, v);
Console.WriteLine("m = " + m);
a > 0, b > 0 となっているので注意(特に負の剰余は言語によって結果が違うので注意)。
Gcd() は前回の記事からコピペしてきて欲しい。
これも Java からのそのまま移植なので、解説は以前の記事を見て欲しい。ユークリッド互除法や最小公倍数の定義に関する説明も Wikipedia で良いだろう。数学的なものはこの辺りの例を見ればわかるだろう。
定義をそのままコーディングしたようなものなので、他言語でも簡単に移植できると思う。
・【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
・ユークリッドの互除法 (Wikipedia)
・最小公倍数 (Wikipedia)
・最大公約数と最小公倍数
(関連記事)
【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法)
【Unity】【C#】画面解像度とアクペクト比(整数)を求める
【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②)
【Java】拡張ユークリッドの互除法
【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法) 
2020/11/13 Fri [edit]
ちょっとアプリを作っていて、画面解像度のアスペクト比(整数)を計算させようと思ったのだが、C# ではユークリッドの互除法のライブラリを作ってなかった…。
というわけで、以前に作った Java のコードをそのまま移植してみた。私はライブラリに関しては、割と古い書き方をするので、ほぼ Java → C# への移植はコピペに近い感じでできる。Java8 なら、LINQ 的なもできるけど、C# とは互換性あまり無いし、実行速度の問題もある。ライブラリは古い書き方で、メインコードは LINQ や Rx など新しい書き方で、なんて使い分けも実はオススメだ(笑)。
■最大公約数を求める(ユークリッドの互除法)[非再帰版]
ユークリッドの互除法の実装を調べると、非再帰と再帰でのやり方が出てくると思うが、一応両方書いておこう。普通に使うなら、非再帰版で良いと思うが、シンプルに書きたいときなどは再帰版でも良いかもね。
●最大公約数を求める(ユークリッドの互除法)[非再帰版]
/// <summary>
/// 最大公約数を求める(Greatest Common Divisor).
/// ユークリッドの互除法 (非再帰版).
/// 2020/11/12 Fantom (Java から移植)
/// http://fantom1x.blog130.fc2.com/blog-entry-384.html
/// http://fantom1x.blog130.fc2.com/blog-entry-215.html
/// </summary>
/// <param name="a">数値1 (>0)</param>
/// <param name="b">数値2 (>0)</param>
/// <returns>最大公約数 (なし=1 [互いに素])</returns>
public static int Gcd(this int a, int b)
{
//a > b にする(スワップ)
if (a < b)
{
int tmp = a;
a = b;
b = tmp;
}
//ユークリッドの互除法
int r = -1;
while (r != 0)
{
r = a % b;
a = b;
b = r;
}
return a; //b には r = 0 の値が入るため、a を返す
}
●メインコード例
using System;
int a = 1920;
int b = 1080;
int d = Gcd(a, b);
Console.WriteLine("d = " + d); //最大公約数
Console.WriteLine(a + "x" + b + " => " + (a / d) + ":" + (b / d)); //約分に利用
1920x1080 => 16:9
a > 0, b > 0 となっているので注意(特に負の剰余は言語によって結果が違うので注意)。
Java からのそのまま移植なので、解説は以前の記事を見て欲しい。ユークリッド互除法に関する説明も Wikipedia で良いだろう(アニメーションがとてもわかり易い)。数学的なものはこの辺りの例を見ればわかるだろう。
・【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
・ユークリッドの互除法 (Wikipedia)
・最大公約数と最小公倍数
■最大公約数を求める(ユークリッドの互除法)[再帰版]
●最大公約数を求める(ユークリッドの互除法)[再帰版]
/// <summary>
/// 最大公約数を求める(Greatest Common Divisor).
/// [ユークリッドの互除法] (再帰版).
/// 2020/11/12 Fantom (Java から移植)
/// http://fantom1x.blog130.fc2.com/blog-entry-384.html
/// http://fantom1x.blog130.fc2.com/blog-entry-215.html
/// </summary>
/// <param name="a">数値1 (>0) (a >= b)</param>
/// <param name="b">数値2 (>0)</param>
/// <returns>最大公約数 (なし=1 [互いに素])</returns>
public static int GcdRec(int a, int b)
{
return (b == 0) ? a : GcdRec(b, a % b);
}
●メインコード例
using System;
int a = 1920;
int b = 1080;
int d = (a >= b) ? GcdRec(a, b) : GcdRec(b, a); //a >= b にする
Console.WriteLine("d = " + d); //最大公約数
Console.WriteLine(a + "x" + b + " => " + (a / d) + ":" + (b / d)); //約分に利用
1920x1080 => 16:9
a > 0, b > 0 となっているので注意(特に負の剰余は言語によって結果が違うので注意)。
また、再帰版は、a >= b を関数内でチェックしてないので少し注意だ。
また、シンプルなので再帰的な書き方は好まれる傾向があるが、再帰というのは自身のメソッドを呼ぶたびにスタックを積むため、メモリ的には優しくない。特にスマホみたいなメモリが小さいものには要注意だ。Gcd ではそれほど深くはならないかもだが、フィボナッチ数などで大きな数になると、かなりの深さになることもある。もし使わなくても書けるなら、使わない方が良い(フィボナッチ数も動的計画法を使うと良い)。
■分数の約分をする
Gcd の使用例で約分しているが、それを簡単にまとめたものを作ってみよう。引数 a, b を分数の分子/分母に見立てただけのものである。これが分数の約分計算と同等になる。
●分数の約分をする
/// <summary>
/// a / b の約分をする
/// 2020/11/12 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-384.html
/// </summary>
/// <param name="a">分子 (>0)</param>
/// <param name="b">分母 (>0)</param>
/// <returns></returns>
public static (int, int) Reduce(this int a, int b)
{
int d = Gcd(a, b); //最大公約数
return (a / d, b / d);
}
●メインコード例
using System;
int width = 1920;
int height = 1080;
var aspect = Reduce(width, height);
Console.WriteLine($"{width}x{height} => {aspect.Item1}:{aspect.Item2}");
a > 0, b > 0 となっているので注意(特に負の剰余は言語によって結果が違うので注意)。
.NET 4.x の書き方となっているので、.NET 3.5 以前なら、
public static int[] Reduce(this int a, int b)
{
int d = Gcd(a, b); //最大公約数
return new int[]{ a / d, b / d };
}
にして、[0]分子/[1]分母 にすれば良いだろう。
これを使えば、アスペクト比(整数)の計算も簡単だ(笑)。
ちなみに、.NET 4.x の方の書き方の戻値に (int, int) を使っているが、これはタプルと言い、C#7.3 以降からは値型の ValueTuple がデフォルトとなり、メモリパフォーマンスも良くなったようだ(以前は参照型で、パフォーマンスも悪かったらしい)。また、ここでは .Item1, .Item2,... のアクセサを使っているが、名前を付けることもできる。
Unity の場合、2018.3 以降ではデフォルトで使えるようになっているので、int[] よりタプルの方が良いかも知れない(int[] は参照型なので、無駄にヒープを使うため)。詳しい内容は以下に参考資料を載せておくので、確認してみるのも良いだろう。
(参考)
・タプル記法 (ValueTuple)
・タプル型 (C# リファレンス)
・C# のメモリ管理
・Unity と C# のバージョン
■3つ以上の最大公約数を求める(ユークリッドの互除法②)
もう1つ、ついでに3つ以上の数の最大公約数を求めるユークリッドの互除法も作っておこう。テスト用のコードには2つの Gcd との計算比較も書いておくので、それをまとめたものということもわかるだろう。
●3つ以上の最大公約数を求める(ユークリッドの互除法②)
/// <summary>
/// 3つ以上の最大公約数を求める[gcd(a, b, c,...)].
/// ユークリッドの互除法の繰り返しで求める.
/// 2020/11/12 Fantom (Java から移植)
/// http://fantom1x.blog130.fc2.com/blog-entry-384.html
/// http://fantom1x.blog130.fc2.com/blog-entry-220.html
/// </summary>
/// <param name="param">数値1, 2, 3,... (a, b, c,...) (>0) [引数は3個以上]</param>
/// <returns>最大公約数(なし=1[互いに素])</returns>
public static int Gcd(params int[] param) {
int len = param.Length;
int g = Gcd(param[0], param[1]);
for (int i = 1; i < len - 1; i++)
{
g = Gcd(g, param[i + 1]);
}
return g;
}
●メインコード例
using System;
int a = 12;
int b = 36;
int c = 60;
int d = 96;
int g1 = Gcd(a, b);
int g2 = Gcd(g1, c);
int g3 = Gcd(g2, d);
Console.WriteLine("g3 = " + g3);
int g = Gcd(a, b, c, d);
Console.WriteLine("g = " + g);
Console.WriteLine(g == g3);
g = 12
true
a > 0, b > 0 となっているので注意(特に負の剰余は言語によって結果が違うので注意)。
これも Java コードそのままなので、解説は以前の記事を参照して欲しい。
・【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②)
・The case of more than two numbers (Wikipedia)
私は以前、アルゴリズム系の問題をよく Java で解いていたが、ちょっとしたとき、便利なアルゴリズムなんかもあったりするので、興味あったら勉強してみるのも良いかもね。アルゴリズム自体を完全理解しなくても(物によっては数学的に難しいものもある)、使い方だけ覚えておけば、色々なものに応用できることもある。アルゴリズム自体は特に言語に関係ないしね。1冊くらい何か読んでおくのはオススメ。
(関連記事)
【Unity】【C#】画面解像度とアクペクト比(整数)を求める
【C#】最小公倍数を求める(ユークリッドの互除法)
【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②)
【Java】拡張ユークリッドの互除法
- 関連記事
-
-
【C#】二分探索の実装(範囲インデクス指定実装)
-
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
-
【C#】ソート済み List を動的に作る拡張メソッド
-
【C#】倍数での Floor, Ceil, Round(一定間隔での切り捨て、切り上げ、四捨五入) [double 版]
-
【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法)
-
category: C#
thread: プログラミング
janre: コンピュータ
tag: アルゴリズム 算術関数 約数【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②) 
2016/06/20 Mon [edit]
「ユークリッドの互除法」や「拡張ユークリッドの互除法」は「自然数 a, b 2つの最大公約数を求める」ものであるが、では3つ以上の複数の最大公約数(a, b, c, d,...)はどうやって求めるのだろうか。実はこれも英語版の Wikipedia に書いてある。説明を翻訳しなくても「gcd(a, b, c) = gcd(gcd(a, b), c)」の式を見れば十分わかるだろう。簡単に言えば「a, b の最大公約数を求め、更にその答えと c の最大公約数を求める」ということになる。更に複数のパラメタがあるときは、それを繰り返す感じになる。つまりは入れ子にすれば良いだけである。
といっても、3つ4つ…と増やした場合は表記が面倒になるので、いっそのこと専用の関数を作ってしまおう。Java には可変引数(型...)があるので、それを使えば簡単に書ける。
●3つ以上の最大公約数を求める[ユークリッドの互除法②]
/**<h1>3つ以上の最大公約数を求める[gcd(a, b, c,...)]</h1>
* <p>ユークリッドの互除法の繰り返しで求める。</p>
* @param param : 数値1, 2, 3,... (a, b, c,...) (>0) [引数は3個以上]
* @return<b>int</b> : 最大公約数(なし=1[互いに素])
*/
public static final int gcd(final int... param) {
final int len = param.length;
int g = gcd(param[0], param[1]); //gcd(a, b) は以前作ったもの
for (int i = 1; i < len - 1; i++) {
g = gcd(g, param[i + 1]); //gcd(a, b) は以前作ったもの
}
return g;
}
//メインでは...
int a = 12;
int b = 36;
int c = 60;
int d = 96;
int g1 = gcd(a, b);
int g2 = gcd(g1, c);
int g3 = gcd(g2, d);
System.out.println("g3 = " + g3);
int g = gcd(a, b, c, d);
System.out.println("g = " + g);
System.out.println(g == g3);
g = 12
true
gcd(a, b) は以前作ったものをそのまま使って欲しい。入れ子はループで書いてあるが、いかにも再帰で書けそうな感じなので、試してみても良いかも知れない。ここでは後述する「3つ以上のパラメタに対応させた拡張ユークリッド互除法」と比較するためにもループで書いてみる。
また、引数が1つのときは不正となるので気をつけよう。必要があれば例外処理を入れても良いかも知れない(引数が1つ=param.length(len)が1のときはエラーとするなど)。gcd() を同名でオーバーロードした場合は引数2つの場合は gcd(a, b) になり、3つ以上の場合は gcd(...) の方にディスパッチされる。ちなみに可変引数はメソッド内では配列のように扱われる。紛らわしいと感じるなら別名で関数定義しても良いだろう。
それではもう1つ、同じように「拡張ユークリッドの互除法」を入れ子にして、複数のパラメタに対応させてみよう。
●3つ以上のパラメタに対応させた拡張ユークリッドの互除法
/**<h1>3つ以上のパラメタに対応させた拡張ユークリッド互除法[ax + by + cz +... = gcd(a, b, c,...)]</h1>
* <p>拡張ユークリッドの互除法の繰り返しで求める。</p>
* @param param : 数値 1, 2, 3,... (a, b, c,...) (>0) [引数は3個以上]
* @return<b>int[]</b> : {[0]:gcd, [1]:x, [2]:y, [3]z,...} 最大公約数(なし=1 [互いに素])と解 x, y, z...
*/
public static final int[] extgcd(final int... param) {
final int len = param.length;
final int[][] g = new int[len - 1][];
g[0] = extgcd(param[0], param[1]); //extgcd(a, b) は以前作ったもの
for (int i = 1; i < len - 1; i++) {
g[i] = extgcd(g[i - 1][0], param[i + 1]); //extgcd(a, b) は以前作ったもの
}
//解の係数を展開する
final int[] res = new int[len + 1];
res[len] = g[len - 2][2];
res[0] = g[len - 2][0]; //gcd
int k = g[len - 2][1];
for (int i = len - 3; i >= 0; i--) {
res[i + 2] = g[i][2] * k;
k *= g[i][1];
}
res[1] = k;
return res;
}
//メインでは...
int a = 12;
int b = 16;
int c = 18;
int d = 21;
int[] g1 = extgcd(a, b);
System.out.println("g1 = " + g1[0] + ", x = " + g1[1] + ", y = " + g1[2]);
int[] g2 = extgcd(g1[0], c);
System.out.println("g2 = " + g2[0] + ", x = " + g2[1] + ", y = " + g2[2]);
int[] g3 = extgcd(g2[0], d);
System.out.println("g3 = " + g3[0] + ", x = " + g3[1] + ", y = " + g3[2]);
int[] g = extgcd(a, b, c, d);
System.out.println("g = " + g[0] + ", w = " + g[1] + ", x = " + g[2] + ", y = " + g[3] + ", z = " + g[4]);
g2 = 2, x = -4, y = 1
g3 = 1, x = -10, y = 1
g = 1, w = -40, x = 40, y = -10, z = 1
extgcd(a, b) は以前作ったものをそのまま使って欲しい。これも gcd(...) と基本的には変わらないので、引数が1つのときはエラーとなるので注意しよう。例外処理を追加しても良い。
拡張ユークリッドの互除法を繰り返している部分は gcd(...) はほぼ同じと言っても良いが、解も入れ子になっているので、それらを展開するために配列に1回ごとの extgcd(a, b) の答えを記録し、最後に計算している。コードが少し難しく感じるかも知れないが、元々は以下のように、1つずつの式だと思えばそれほどでないことがわかるだろう。
g1 = extgcd(a, b) → g1 = 4, x = -1, y = 1 → 4 = 12 * -1 + 16 * 1
g2 = extgcd(g1, c) → g2 = 2, x = -4, y = 1 → 2 = 4 * -4 + 18 * 1
g3 = extgcd(g2, d) → g3 = 1, x = -10, y = 1 → 1 = 2 * -10 + 21 * 1
g = extgcd(extgcd(extgcd(a, b), c), d) のように入れ子になっているので、
1 = ((12 * -1 + 16 * 1) * -4 + 18 * 1) * -10 + 21 * 1 (式を代入する)
= 12 * -40 + 16 * 40 + 18 * -10 + 21 * 1 (展開する)
g = a * w + b * x + c * y + d * z (対応させる)
使う際に少しだけ気をつけないといけないのは、係数(a, b, c, d)の値の順番を入れ替えると解(w, x, y, z)は変わる点だ。というのは a, b で求めた解 w, x を用いて更に y を求め、更に z を求め…という感じで計算しているので、1つ前の答えによって後の答えが変わるからだ。もちろんどの式も検算すれば正しいことがわかる。「いちばん小さい値(全ての解の絶対値の和とか)を求めよ」などのときには、最小となるパターンを別途見つけなくてはならない。例えば、上の例の係数を順列で並び変えてみると、
a = 12, b = 16, c = 21, d = 18, g = 1, w = 5, x = -5, y = 1, z = 0
a = 12, b = 18, c = 16, d = 21, g = 1, w = 30, x = -30, y = 10, z = 1
a = 12, b = 18, c = 21, d = 16, g = 1, w = -15, x = 15, y = -5, z = 1
a = 12, b = 21, c = 16, d = 18, g = 1, w = -10, x = 5, y = 1, z = 0
a = 12, b = 21, c = 18, d = 16, g = 1, w = -10, x = 5, y = 0, z = 1
a = 16, b = 12, c = 18, d = 21, g = 1, w = 40, x = -40, y = -10, z = 1
a = 16, b = 12, c = 21, d = 18, g = 1, w = -5, x = 5, y = 1, z = 0
a = 16, b = 18, c = 12, d = 21, g = 1, w = 10, x = -10, y = 0, z = 1
a = 16, b = 18, c = 21, d = 12, g = 1, w = 10, x = -10, y = 1, z = 0
a = 16, b = 21, c = 12, d = 18, g = 1, w = 4, x = -3, y = 0, z = 0
a = 16, b = 21, c = 18, d = 12, g = 1, w = 4, x = -3, y = 0, z = 0
a = 18, b = 12, c = 16, d = 21, g = 1, w = -30, x = 30, y = 10, z = 1
a = 18, b = 12, c = 21, d = 16, g = 1, w = 15, x = -15, y = -5, z = 1
a = 18, b = 16, c = 12, d = 21, g = 1, w = -10, x = 10, y = 0, z = 1
a = 18, b = 16, c = 21, d = 12, g = 1, w = -10, x = 10, y = 1, z = 0
a = 18, b = 21, c = 12, d = 16, g = 1, w = 5, x = -5, y = 0, z = 1
a = 18, b = 21, c = 16, d = 12, g = 1, w = 5, x = -5, y = 1, z = 0
a = 21, b = 12, c = 16, d = 18, g = 1, w = 5, x = -10, y = 1, z = 0
a = 21, b = 12, c = 18, d = 16, g = 1, w = 5, x = -10, y = 0, z = 1
a = 21, b = 16, c = 12, d = 18, g = 1, w = -3, x = 4, y = 0, z = 0
a = 21, b = 16, c = 18, d = 12, g = 1, w = -3, x = 4, y = 0, z = 0
a = 21, b = 18, c = 12, d = 16, g = 1, w = -5, x = 5, y = 0, z = 1
a = 21, b = 18, c = 16, d = 12, g = 1, w = -5, x = 5, y = 1, z = 0
min = 7
a = 16, b = 21, c = 12, d = 18, g = 1, w = 4, x = -3, y = 0, z = 0
a = 16, b = 21, c = 18, d = 12, g = 1, w = 4, x = -3, y = 0, z = 0
a = 21, b = 16, c = 12, d = 18, g = 1, w = -3, x = 4, y = 0, z = 0
a = 21, b = 16, c = 18, d = 12, g = 1, w = -3, x = 4, y = 0, z = 0
のようになり、最小となる解の絶対値の総和は7となる(解に0を許す場合)。
再帰でも書いてもいいかも知れないが、特に拡張ユークリッド互除法の方はコードが複雑化し兼ねないので気をつけよう。再帰よりループの方があとから改造しやすい利点もあるしね。
(関連記事)
【Java】拡張ユークリッドの互除法
【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
【Java】約数を求める
【Java】素因数分解をする
【Java】拡張ユークリッドの互除法 
2016/06/19 Sun [edit]
「ユークリッドの互除法」は「自然数 a, b の最大公約数」を求めるものだったが、「拡張ユークリッド互除法」は「ax + by = gcd(a, b) となる a, b の最大公約数を求める」ものになる。簡単な方程式の整数解を求めるのにも使える。
ググッてみると C言語で書かれている関数で引数が extgcd(a, b, &x, &y) ['&'は参照渡しとなる] となっているものが多いが、Java はプリミティブ型で参照渡しができないので、ループ版(非再帰版)の方は英語版 Wikipedia に載っているように、引数を extgcd(a, b) にして作ってみよう。無理矢理参照渡しのようにするために、要素が1つの配列を用いる方法もあるが、それは再帰版でのサンプルを見て欲しい。ループ版も同じように書き換えることもできるが、3つ以上の係数に対応したいときなどには不便なので、用途によって使い分けても良いかも知れない。
(参考)
・ユークリッドの互除法/拡張ユークリッドの互除法
・拡張版ユークリッドの互除法
・拡張ユークリッドの互除法 (Extended Euclidean Algorithm)
・Extended Euclidean algorithm
●拡張ユークリッドの互除法(非再帰版)
/**<h1>拡張ユークリッド互除法</h1>
* <p>ax + by = gcd(a, b) となる a, b の最大公約数と解 x, y を求める。</p>
* @param a : 数値1(>0)
* @param b : 数値2(>0)
* @return<b>int[]</b> : {[0]:gcd, [1]:x, [2]:y}:最大公約数(なし=1 [互いに素])と解 x, y
*/
public static final int[] extgcd(int a, int b) {
int x0 = 1, x1 = 0;
int y0 = 0, y1 = 1;
while (b != 0) {
int q = a / b;
int r = a % b;
int x2 = x0 - q * x1;
int y2 = y0 - q * y1;
a = b; b = r;
x0 = x1; x1 = x2;
y0 = y1; y1 = y2;
}
return new int[]{a, x0, y0};
}
//メインでは...
int a = 12;
int b = 21;
int[] res = extgcd(a, b);
int g = res[0];
int x = res[1];
int y = res[2];
System.out.println("g = " + g + ", x = " + x + ", y = " + y);
System.out.println(a + " * " + x + " + " + b + " * " + y + " = " + g);
int sum = a * x + b * y;
System.out.println(sum == g);
12 * 2 + 21 * -1 = 3
true
出力の意味は a = 12, b = 21 のときに、12 * 2 + 21 * -1 = 3 [ax + by = gcd(a, b)] となる解 x = 2, y = -1 が存在することを表す。実はこのような解は常に存在するので、このことを利用して解く問題などもある。
また、最大公約数がないときは 1 となるが、このとき「互いに素」であることがわかる(お互いの共通の因子を持たない)。しばしばこの性質を利用する解法も見かけるので、覚えておいた方が良いだろう。
もう1つ、再帰でも書いてみよう。冒頭に書いた「無理矢理参照渡しのようにするために要素が1つの配列を用いる方法」の例でもある。表記が複雑になるので、少し不便だが、C言語などからの移植する際の手法の1つとして覚えておくと役に立つこともあるだろう。
●拡張ユークリッドの互除法(再帰版)
/**<h1>拡張ユークリッド互除法</h1>
* <p>ax + by = gcd(a, b) となる a, b の最大公約数と解 x, y を求める。</p>
* @param a : 数値1(>0)
* @param b : 数値2(>0)
* @param x : 解 x (参照渡し用 new int[1] で作る)
* @param y : 解 y (参照渡し用 new int[1] で作る)
* @return<b>int</b> : 最大公約数(なし=1 [互いに素])
*/
public static final int extgcd(final int a, final int b, final int[] x, final int[] y) {
int g = a;
if (b != 0) {
g = extgcd(b, a % b, y, x);
y[0] -= (a / b) * x[0];
} else {
x[0] = 1;
y[0] = 0;
}
return g;
}
//メインでは...
int a = 12;
int b = 21;
int[] x = new int[1]; //参照渡しにするため、要素1個で作る
int[] y = new int[1]; //参照渡しにするため、要素1個で作る
int g = extgcd(a, b, x, y);
System.out.println("g = " + g + ", x = " + x[0] + ", y = " + y[0]);
System.out.println(a + " * " + x[0] + " + " + b + " * " + y[0] + " = " + g);
int sum = a * x[0] + b * y[0];
System.out.println(sum == g);
12 * 2 + 21 * -1 = 3
true
ループ版と再帰版とで実行速度を比べてみると、差はほとんどないので、どちらを使っても良いだろう。
せっかくなので、上記の extgcd() を使って練習問題を解いてみよう。
●双六(すごろく) (参考) アリ本 P.108 [解答例は載ってない]
0のマス目がスタートで、1のマス目がゴールなのですが、サイコロの目には a, b, -a, -b の4つの整数しかありません。
そのため、a, b の値によってはゴールに辿り着けない場合があります。
┌─┬─┬─┬─┬─┬─┬─┬─┬─┐
…│-4│-3│-2│-1│ 0│ 1│ 2│ 3│ 4│…
└─┴─┴─┴─┴─┴─┴─┴─┴─┘
4つの目をそれぞれ何回出せばゴールに辿り着けますか?
複数解が存在する場合は、どれか1つを出力しなさい。解がなければ -1 を出力しなさい。
制約
・1 <= a, b <= 109
入力
a = 4, b = 11
●拡張ユークリッド互除法での解法
int a = 4; //入力
int b = 11; //入力
int[] res = extgcd(a, b);
int g = res[0];
int x = res[1];
int y = res[2];
if (g != 1) {
System.out.println(-1);
} else {
String ans = "" + ((x >= 0) ? x : 0);
ans += " " + ((y >= 0) ? y : 0);
ans += " " + ((x < 0) ? -x : 0);
ans += " " + ((y < 0) ? -y : 0);
System.out.println(ans); //答え
}
答えは左から a, b, -a, -b に対応させて、右へ 4(a)×3回、左へ 11(b)×1回 進めば、1 (gcd(a, b)) のマス目(ゴール)に辿り着けることを表している。ちなみに a = 11, b = 4 と入れ替えて試してみると、答えは「0 3 1 0」となり意味は同じとなる。では逆に辿り着けない例はどうなるだろうか。例えば gcd が2となる a, b を与えれば(両方偶数など)、1には辿り着けない。全探索で見つけるまでもなく一瞬で求められるのは非常に有り難い(笑)。
(関連記事)
【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②)
【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
【Java】約数を求める
【Java】素因数分解をする