fc2ブログ
ヽ|∵|ゝ(Fantom) の 開発blog? ホーム »アルゴリズム
このページの記事一覧

【C#】最小公倍数を求める(ユークリッドの互除法)  


 前回の最大公約数の求め方のついで。

 ユークリッドの互除法が実装できてれば、最小公倍数を求めるのは簡単だ。

 前回の 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);

m = 144

 a > 0, b > 0 となっているので注意(特に負の剰余は言語によって結果が違うので注意)。

 Gcd() は前回の記事からコピペしてきて欲しい。

 これも Java からのそのまま移植なので、解説は以前の記事を見て欲しい。ユークリッド互除法最小公倍数の定義に関する説明も Wikipedia で良いだろう。数学的なものはこの辺りの例を見ればわかるだろう。

 定義をそのままコーディングしたようなものなので、他言語でも簡単に移植できると思う。

【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
ユークリッドの互除法 (Wikipedia)
最小公倍数 (Wikipedia)
最大公約数と最小公倍数







(関連記事)
【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法)
【Unity】【C#】画面解像度とアクペクト比(整数)を求める
【Java】最大公約数・最小公倍数を求める(ユークリッドの互除法)
【Java】3つ以上の最大公約数を求める(ユークリッドの互除法②)
【Java】拡張ユークリッドの互除法


関連記事
スポンサーサイト



category: C#

thread: プログラミング

janre: コンピュータ

tag: アルゴリズム  算術関数  約数 
tb: 0   cm: --

【C#】最大公約数を求める/分数の約分をする(ユークリッドの互除法)  


 ちょっとアプリを作っていて、画面解像度のアスペクト比(整数)を計算させようと思ったのだが、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)); //約分に利用

d = 120
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)); //約分に利用

d = 120
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}");

1920x1080 => 16:9

 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);

g3 = 12
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】拡張ユークリッドの互除法


関連記事

category: C#

thread: プログラミング

janre: コンピュータ

tag: アルゴリズム  算術関数  約数 
tb: 0   cm: --

【Unity】【C#】配列・リストのシャッフル  


 簡易音楽プレイヤーのデモを作っていて気づいたんだけど、C# にはシャッフル(Shuffle())の標準メソッドは無いのね。Java には Collections.shuffle() のメソッドがあるので、つい探してしまった…。あと要素のスワップ(Swap())も Java には標準であるんだよね(Collections.swap())。ちなみに私はプラグイン開発してるときは Java と C# を同時に書いているので、たまに混同する(笑)(実際に AndroidStudio とVisualSutdio の両方を開いている)。まぁ簡単に作れるものだけど、毎回書くのも面倒なのでとりあえずメモ的に残して置こうと思った。



(※) Unity 5.6.3p1 - 2018.1.5f1 / Windows10(x64) で確認



■配列のシャッフル

●配列のシャッフル(UnityEngine.Random / 拡張メソッド版)
using UnityEngine;
using Random = UnityEngine.Random;

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
T tmp = arr[i]; //swap
arr[i] = arr[j];
arr[j] = tmp;
}
}
}


//メインでは・・・
using System.Linq;

int[] arr = { 0, 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++)
{
arr.Shuffle();
Debug.Log(arr.Select(e => e.ToString()).Aggregate((s, a) => s + ", " + a));
}

4, 3, 1, 5, 2, 0
5, 4, 3, 2, 0, 1
0, 3, 5, 1, 2, 4
1, 5, 0, 2, 4, 3
0, 1, 2, 4, 3, 5

 使っているアルゴリズムは有名な「フィッシャー–イェーツのシャッフル」(英:Fisher–Yates shuffle)である。詳細は Wikipedia を見て欲しいが、簡単に言えばインデクス(i)を後ろから1つずつ前方へ移動しながら、0~i の範囲(終点は[1](と[0]))でランダムに要素をスワップするアルゴリズムである。オーダーは O(n) なので巨大な配列で毎フレーム更新とかではなければ(笑)、それほど負荷は無いだろう。音楽プレイヤーではどの様に使ってるかというと、曲順を1度シャッフルして、その順番に再生し、最後まで行ったらまたシャッフル…を繰り返しているだけである。なので、ほとんど負荷は無いと言って良い。そういう使い方の場合、何の問題も無いと考えて良いだろう(Unityの様な毎フレーム更新のアプリケーションはなるべくオーダーの小さいアルゴリズム[O(1)とかO(logn)とか]を採用する方が良い)。

 またついでなので要素のスワップも定義して置くと、以下のように簡潔に書ける。

●配列のシャッフル+要素のスワップ(UnityEngine.Random / 拡張メソッド版)
using UnityEngine;
using Random = UnityEngine.Random;

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
arr.Swap(i, j);
}
}

//要素のスワップ
public static void Swap<T>(this T[] arr, int i, int j)
{
T tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}

//メインは同じ(※略)

 なお、ここでは拡張メソッドを用いているが、通常のメソッドで書くと以下のようになる。

●配列のシャッフル(UnityEngine.Random / 通常メソッド版)
・・・(略)・・・
public class Extensions //※通常メソッドの場合、static でなくても可
{
//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
Swap(arr, i, j);
}
}

//要素のスワップ
public static void Swap<T>(T[] arr, int i, int j)
{
T tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}

//メインでは・・・
・・・(略)・・・
Extensions.Shuffle(arr); //※クラス名は任意の定義による
・・・(略)・・・

拡張メソッドは便利だが、増えすぎるとかえって邪魔になることもあるので、汎用性の高いものだけにすると良いだろう。

 あと、ここでは Unity用にランダム関数は UnityEngine.Random を使っているが、C#標準の System.Random を使いたい場合、以下のように書くこともできる。

●配列のシャッフル+要素のスワップ(System.Random / 拡張メソッド)
using UnityEngine;
using Random = System.Random; //ここでエイリアスにしておくと表記が楽

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
static Random rand = new Random(); //※毎回 new するならいっそ static で利用する

//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = rand.Next(i + 1); //[0]~[i]
arr.Swap(i, j);
}
}

//スワップは拡張メソッドと同じ(※略)
}

//メインは同じ(※略)

 System.Random の場合は毎回 new しなくてはならないので、いっそ static で使い回しした方が良いだろう。もちろん「Shuffle<T>(this T[] arr, Random rand)」のように引数にランダムジェネレータを与えるようにしても良いと思う(他の言語を含め、標準関数にはそういうものが多い)。

 が、まぁしかし、Unity を使っている分には「UnityEngine.Random」の方が簡単だし、円状とか球状とか便利なので迷わず使っても良いだろう。また数が少ない場合、UnityEngine.Random の方が良い感じに乱数が出るという話もある。こういった実験は私もよくやるのだが、情報を公開してくれていると非常に有り難い(笑)。なので私も複数の書き方やアルゴリズムを試したりしてるわけだ(といってもアルゴリズム系の記事は Java の方が多いが[※思いつきでやるのでたまたま])。以下に参考資料も載せておこう。

(参考)Unityの乱数が本当にランダムかどうか見てみた



■リストのシャッフル

●リストのシャッフル(UnityEngine.Random / 拡張メソッド版)
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this List<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
T tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
}
}


//メインでは・・・
using System.Linq;

List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++)
{
list.Shuffle();
Debug.Log(list.Select(e => e.ToString()).Aggregate((s, a) => s + ", " + a));
}

4, 3, 1, 5, 2, 0
5, 4, 3, 2, 0, 1
0, 3, 5, 1, 2, 4
1, 5, 0, 2, 4, 3
0, 1, 2, 4, 3, 5

 やっていることは配列のシャッフルと変わらないので、説明はいらないだろう。一瞬、IEnumerable で書くことも頭をよぎったが、そもそもシャッフルの対象となるのは静的配列かリストが最も多い。特に拡張メソッドで書くと入力候補もどんどん増加してしまうので、用途が限定されるならこれで十分と思った。まぁ、必要なら好きに改造してくれたまい(笑) [※たぶんコードが煩雑になる]。

 リストのスワップ定義も配列と同じようにやってみると、随分簡潔になる。

●リストのシャッフル+要素のスワップ(UnityEngine.Random / 拡張メソッド版)
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this List<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
list.Swap(i, j);
}
}

//要素のスワップ
public static void Swap<T>(this List<T> list, int i, int j)
{
T tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
}

//メインは同じ(※略)

 もちろん、配列のときと同じように通常のメソッドで書くには以下のようになる。

●リストのシャッフル(UnityEngine.Random / 通常メソッド版)
・・・(略)・・・
public class Extensions
{
//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(List<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
Swap(list, i, j);
}
}

//要素のスワップ
public static void Swap<T>(List<T> list, int i, int j)
{
T tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
}

//メインでは・・・
・・・(略)・・・
Extensions.Shuffle(list); //※クラス名は任意の定義による
・・・(略)・・・

 ついでに C#標準の System.Random を使う場合、配列のときと同じように以下のように書ける。

●配列のシャッフル+要素のスワップ(System.Random / 拡張メソッド)
using UnityEngine;
using Random = System.Random; //ここでエイリアスにしておくと表記が楽

public static class Extensions //拡張メソッド用 static クラス(名前は任意)
{
static Random rand = new Random(); //※毎回 new するならいっそ static で利用する

//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this List<T> list)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = rand.Next(i + 1); //[0]~[i]
arr.Swap(i, j);
}
}

//スワップは拡張メソッドと同じ(※略)
}

//メインは同じ(※略)

 これも配列のときと同じように「Shuffle<T>(this List<T> list, Random rand)」のように引数にランダムジェネレータを与えるようにしても良いと思う。だがまぁ、「UnityEngine.Random」の方を使った方が楽だろう。

 ちなみに配列版の方が2倍くらい速いので、動的に要素数を変更する必要がないのなら、配列を用いた方が良いだろう(※基本的に配列と List で全く同じアルゴリズムでやってみると、配列の方が2~3倍ほど処理が速い)。


 ゲームの場合、アイテムドロップに直接ランダム関数を用いることも多いと思うが、アイテムのインデクスの確率テーブルなどを作ってシャッフルすれば、全体としては確実に分布が行き渡るので、そういう使い方も良いかもね(例えばテーブルを {0,0,0,1,1,2} としてシャッフル→順番に取り出せば、確実に 3:2:1 の分布でアイテムが出る=直接ランダムの場合、回数が多くなるほど確率は正しくなるが、少ない回数では偏ることもあるため)。

 音楽プレイヤーのシャッフルなんかも直接ランダムの場合、曲数少ないと「A-B-A-A-B-C」(=3曲を6回ランダム再生)のように偏ることもあるが、シャッフルで {A,C,B}+{C,A,B} のようにすれば(=3曲を2回シャッフルで順に再生)偏りは減る(末尾と先頭が連続することはあるが、全体的にはバラバラ順に聞こえる)。プラグイン(ver.1.16以降)の簡易音楽プレイヤーでもそのまま実装しているので、興味があったらダウンロードしてビルドして試してみるのも良いだろう(※好きに改造して貰っても構わない)。


※他、様々な機能を使ったデモが同梱。Unity×Androidアプリ開発に!

※サンプルで使っているループ曲を含んだライブラリも配信中。全31曲!




(関連記事)
【Unity】AssetStore版 FantomPlugin のセットアップ
【C#】連想配列(Dictionary)を値 or キーでソート
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
【Java】配列要素の反転(reverse)


このコンテンツの一部には『ユニティちゃんライセンス』で許諾されたアセットが使用されています。
Portions of this work are licensed under Unity-Chan License


関連記事

category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityライブラリ  C#ライブラリ  アルゴリズム 
tb: 0   cm: --

【Java】数値の桁数を調べる(べき乗の桁数・べき乗のべき乗の桁を調べる)  


 数値の桁数を知りたいシーンはわりと良くあるもので、調べてみると「10で割っていく方法」や「文字列に変換する方法」「対数を使う方法」「配列にあらかじめ各桁の最大(9999のような)を入れておいて比較する方法」などあるようだが、実行速度は言語によってまちまちのようなので、簡単なアルゴリズムを色々知っておいても損はないだろうと試してみた。




■10で割っていく方法

 まずは一番オーソドックスな方法(?)でやってみよう。これは以前書いた「基数変換のアルゴリズム」と同じと言っても良い。基数変換では具体的な桁の値をリストで返しているが、それをカウントに変えるだけだ。故に基数変換の戻値のリストのサイズ(List.size())でも同じとなる。まずは基数変換のコードの一部を抜き出して少しばかり改造してみよう。

●10で割って桁数をカウントする
/**<h1>整数の桁数を求める</h1>
* <p>0 となるまで、10 で割り続ける方法。</p>
* @param value : 調べる値
* @return<b>int</b> : 桁数 (負の値は '-' を除いた桁数)
*/
public static final int length(long value) {
int cnt = 0;
do {
cnt++;
value /= 10;
} while (value != 0);
return cnt;
}


//メインでは...
int n = 1234; //4
//int n = -4321; //4
//int n = 0; //1
System.out.println(length(n));

4

 負の値のとき、マイナス記号 '-' を除外してカウントしているが、負の割り算は言語によって仕様が違うので、他の言語に移植するときは注意しよう(Ruby などは上手くいかない)。心配なら関数のはじめに絶対値(Math.abs())などで正の値にしておくと良い(Java では無くても良い)。調べる値が 0 のとき、1 桁としたいので、すぐにインクリメントしているが、while に書きなおしたいときは、はじめからカウントを 1 にしておけば良いだろう。上記の例は基数変換のコードをもとにしているが、以下のように while でシンプルに書くこともできる。

●上記の例を while に書きなおしたもの
/**<h1>整数の桁数を求める</h1>
* <p>0 となるまで、10 で割り続ける方法。</p>
* @param value : 調べる値
* @return<b>int</b> : 桁数 (負の値は '-' を除いた桁数)
*/
public static final int length(long value) {
int cnt = 1; //0 にも対応
while ((value /= 10) != 0) {
cnt++;
}
return cnt;
}


//メインでは...
int n = 1234; //4
//int n = -4321; //4
//int n = 0; //1
System.out.println(length(n));

4

 内容は全く同じで、実行速度も変わらないので、好みの書き方で選べば良い。




■文字列に変換する方法

 「10で割る方法」と同じようによく使われるのは、「文字列に変換する方法」だ。ただし、Java の場合は文字列変換は負荷が高いので、実行速度は遅くなる。しかし、Ruby をはじめ、PHP や Perl のようなスクリプト言語は文字列変換のスピードは非常に速いので、むしろ「10で割る方法」の方が遅いらしい。参考までに色々な言語でやっているページを載せておくが、私が実際に Ruby で試したところ、「loop do~break if~」の代わりに「while」に書きなおしたら、ここまでの差は出なかった(それでも文字列変換の方がわずかに速い)。他の言語から移植するときのためにも、この手のやり方には慣れておいた方が良いだろう。

(参考)
整数の桁数を求める方法

●文字列に変換し、長さを取得する
//int n = 1234;     //4
int n = -4321; //4
//int n = 0; //1

System.out.println(String.valueOf(Math.abs(n)).length());

4

 なお、値が正の数だけとわかっていれば、Math.abs() は必要ない。




■べき乗(xk)の桁数を調べる

 次に、普通に計算したらオーバーフローするような「べき乗の値の桁を調べる方法」をやってみよう。それは(常用)対数関数Math.log10())を使う方法だ。ちなみにこの方法で int や long の桁数を調べることもできるが、log() は負荷が高い関数なので、「10で割る方法」の方がよほど速い。なので、整数型では求められない値など、使い分けが必要になるだろう。

 まずは簡単に対数関数の使い方をおさらいしてみよう。といっても高校の数学の教科書そのままの内容だ(笑)。また対数関数指数関数は裏返しの関数なので、ついでにおさらいしておくとなお良い。以下の問題の解き方を丸暗記してしまえば、わりと簡単に理解できるハズだ。

【問】230は何桁の数字か。log102 = 0.3010 を用いて調べよ。

【解】
log102 = 0.3010 だから、2 = 100.3010  [指数への変換:logap = q ⇔ p = aq]
230 = (100.3010)30
  = 100.3010×30  [指数法則:(ap)q = ap×q]
  = 109.03
よって、109 < 230 < 1010
したがって、230 は 10桁の数字である。  [109 = 1,000,000,000 (頭 1 と 0 が 9 個で 10桁)]


 さて、上の解法を理解したら、いよいよ本題のべき乗(xk)の桁数を求めてみよう。例で使ってるのは底が10である常用対数の使い方であるが、それは言い換えればある数値 n を 10q に置き換える関数であり、桁を表していると言える。つまり、ここでは具体的な値ではなく、桁数を求めたいわけだから、10 の指数部分 q を求めれば良いわけである。

 上の例を用いれば桁数は 230 = 109.03 の 9.03 部分のことで、これは log102×30 であり、この値の小数部分を切り捨て、1 を足したものである(頭 1 と 0 が 9 個で 10桁)。この 2 を x、30乗を k と置けば、以下のようになっているとわかるだろう。

230 = 100.3010×30 = 10log102×30

(2 を x、30乗を k と置くと)
xk = 10log10x × k  … ① (←以降、この式を公式のように扱う)

(桁数は指数部分のことだから)
桁数 = floor(log10x × k) + 1

●べき乗(xk)の桁数を log10() により求める
/**<h1>x<sup>n</sup> の桁数を求める</h1>
* <p>log10() により求める。底が負のときは NaN となるので注意。</p>
* @param x : 底 (> 0)
* @param k : べき指数 (>= 0)
* @return<b>double</b> : 桁数
*/
public static final double powLength(final double x, final double k) {
return Math.floor(Math.log10(x) * k) + 1;
}


//メインでは...
int x = 2, k = 30; //10.0
//int x = 123456789, k = 99999; //809144.0
System.out.println(powLength(x, k));

10.0

 log() の引数は負の値だと NaN が返される注意しよう。必要ならエラーを投げるのも良い。引数の値によっては非常に大きな桁数になるので double のまま返しているが、値の範囲がわかっていれば整数型にキャストするのも良いだろう。Java の場合、long は 19桁の値、double は指数表現を含めて 309桁の値まで表せるが、べき乗はあっという間にオーバーフロー(Infinity)することがあるので、使う際にはあらかじめ確認しておいた方が良いだろう。




■べき乗のべき乗(xyz)の桁数を求める

 次に更にべき乗をかけた「べき乗のべき乗」の桁を調べてみよう。考え方は「べき乗(xk)の桁数を調べる」をそのままで、少しばかり拡張したものになる。その式を考えてみよう。

(xyz の指数部分 yz を log10 を使って変換すると [①を参照])
yz = 10log10y × z

(xyz の指数部分 yz を k と置き、同じように変換すると [①を参照])
xyz = xk = 10log10x × k

(k を元に戻すと)
xyz = xk = 10log10x × 10log10y × z

(桁数は指数部分のことだから)
桁数 = floor(log10x × 10log10y × z) + 1

●べき乗のべき乗(xyz)の桁数を log10() により求める
/**<h1>x<sup>y<sup>z</sup></sup> (べき乗のべき乗)の桁数を求める</h1>
* <p>log10() により求める。底が負のときは NaN となるので注意。</p>
* @param x : 底 (> 0)
* @param y : べき指数1 (>= 0)
* @param z : べき指数2 (>= 0)
* @return<b>double</b> : 桁数 (オーバーフロー: Infinity に注意)
*/
public static final double powpowLength(final double x, final double y, final double z) {
final double p = Math.log10(y) * z; //y^z
final double q = Math.log10(x); //x^p
return Math.floor(q * Math.pow(10, p)) + 1;
}


//メインでは...
int x = 2, y = 3, z = 5; //74.0
//int x = 5, y = 6, z = 7; //195667.0
System.out.println(powpowLength(x, y, z));

74.0




■2つのべき乗のべき乗(xyz, abc)の大きさを比較する

 ここでは2つのべき乗のべき乗(xyz, abc)の大小関係を比較することを考えてみよう。もちろん前述した「べき乗のべき乗(xyz)の桁数を求める」で比較しても構わないが、なぜわざわざ比較だけをピックアップしたかというと、引数によっては「べき乗のべき乗」では桁数が非常に大きくなりオーバーフロー(Infinity)しかねないので、対数のまま利用した方が良いからだ。もう一度「べき乗のべき乗(xyz)の桁数を求める」のコードを見てみよう。桁数が大きくなる原因に「Math.pow(10, p)」という部分がある。ここを変形してオーバーフローを防ぐ方法を考えてみよう。

(桁数を表す指数部分を抜き出すと)
yz = log10x × 10log10y × z

log10x = m と置き、今までと同じように[①を参照] 10q に変形すると、m = 10log10m [m1 = 10log10m × 1

(m を元に戻すと)
10log10m = 10log10(log10x) (= log10x)

(yzの式に代入して)
yz = 10log10(log10x) × 10log10y × z = 10log10(log10x) + log10y × z [指数法則:ap×aq = ap+q]

(xyzの式に戻すと)
xyz = 1010log10(log10x) + zlog10y

(つまり、一番上の指数だけを比較すれば大小関係がわかる)
log10(log10x) + zlog10y

●べき乗のべき乗(xyz)の比較をする
/**<h1>x<sup>y<sup>z</sup></sup> (べき乗のべき乗) の log10() を返す</h1>
* <p>この値で比較すれば、大小関係がわかる。底が負のときは NaN となるので注意。</p>
* @param x : 底 (> 0)
* @param y : べき指数1 (>= 0)
* @param z : べき指数2 (>= 0)
* @return<b>double</b> : 底を10<sup>10</sup>に合わせた log10() を返す
*/
public static final double powpowLog10(final double x, final double y, final double z) {
final double p = Math.log10(y) * z; //y^z
final double q = Math.log10(Math.log10(x)); //10^q = log10 x
return p + q;
}

/**<h1>x<sup>y<sup>z</sup></sup> (べき乗のべき乗) 同士の大小の比較</h1>
* <p>比較の関係値を返す(-1, 0, 1 のみ)。差分ではない。</p>
* @param x1 : 底 a (> 0)
* @param y1 : べき指数1 a (>= 0)
* @param z1 : べき指数2 a (>= 0)
* @param x2 : 底 b (> 0)
* @param y2 : べき指数1 b (>= 0)
* @param z2 : べき指数2 b (>= 0)
* @return<b>int</b> : -1 : a < b / 1 : a > b / 0 : a == b
*/
public static final int powpowCompare(final double x1, final double y1, final double z1,
final double x2, final double y2, final double z2) {

final double a = powpowLog10(x1, y1, z1);
final double b = powpowLog10(x2, y2, z2);
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}


//メインでは...
int x1 = 123456, y1 = 9876543, z1 = 24356891; //Infinity
int x2 = 333333, y2 = 5555555, z2 = 22222222; //Infinity
System.out.println(powpowLength(x1, y1, z1));
System.out.println(powpowLog10(x1, y1, z1));
System.out.println(powpowLength(x2, y2, z2));
System.out.println(powpowLog10(x2, y2, z2));
System.out.println(powpowCompare(x1, y1, z1, x2, y2, z2));

Infinity
1.7036683127845693E8
Infinity
1.4988283149816477E8
1

 この方法で行けば、309桁(Double.MAX_VALUE = 1.7976931348623157E308) までは比較できる

 また、比較だけなら、Math.log10() の代わりに Math.log() を使っても良いが、値が大きくなる可能性があるので、Math.log10() を使った方が無難かも知れない(実行速度も変わらない)。もちろん引数が負の値のときは NaN となるので注意しよう。戻値は比較の関係値を返す -1, 0, 1 のみにしているが、必要なら (a - b) の差分を返すように改造しても良いだろう(ただし、型に注意。比較関数は大抵 int になっているので)。

 ちなみに同じように「べき乗のべき乗のべき乗」も考えられるが、log() を3回以上入れ子にすると負の値となってしまうので、上手く行かないようだ。式としては以下のようになる。

wxyz = 101010log10(log10(log10w)) + log10(log10x) + zlog10y

(一番上の指数だけを抜き出せば)
log10(log10(log10w)) + log10(log10x) + zlog10y

 式は無理矢理なので、正しい書き方とかは勘弁して欲しい(笑)。級数(数列の和)っぽく書けばもっとカッコイイのかも知れないが、この書き方なら高校数学の範疇なので理解できるハズだ(無理矢理感は別として)。でもまぁ、log() でもこれ以上は無理そうなので、他の方法を研究してみるのも面白いだろう。




■任意精度を使う

 最後に任意精度実数を使って桁を調べてみよう。考え方としては「文字列に変換する方法」と同じと思って良い。具体的な値を返すので、桁が大きくなるほど重くなるが、検算したいときには役に立つこともあるだろう(といっても100万桁ぐらいが限界でそれ以上はフリーズしたようになる)。なお、BigInteger を用いても構わないが、桁を調べるだけなら、文字列に変換する(toString())する必要はないので、整数部分の桁を返す BigDecimal.precision() を使った方が速い

●任意精度実数で具体的な値と桁数を調べる
int x = 2, y = 3, z = 5;
int a = (int)Math.pow(y, z);
BigDecimal b = new BigDecimal(x).pow(a);
System.out.println(b.toString());
System.out.println(b.precision());
System.out.println(powpowLength(x, y, z));

14134776518227074636666380005943348126619871175004951664972849610340958208
74
74.0

 上の例はべき乗のべき乗(235)と同じものだ。ただし、BigDecimal.pow() の引数は int 型にしか対応していない(と言っても、long でできたとして重すぎてフリーズしたようになると思うが)。具体的な値を知りたければ文字列化(toString())すれば良いが、桁が大きいときは負荷が高いので、あらかじめ値の範囲を調べた上で使った方が良いだろう。オンラインジャッジでの問題などでは、任意精度ではほとんどの場合タイムアウトすると思うので、桁数だけなら、前述の「powLength()」(べき乗の桁数)や「powpowLength()」(べき乗のべき乗の桁数)を使った方が速い。





(関連記事)
【Java】n進数変換(基数変換)・桁の分割をする
【Java】べき乗計算を高速化する(繰り返し二乗法)
【Java】任意精度整数(多倍長整数)で演算する


関連記事

category: Java

thread: プログラミング

janre: コンピュータ

tag: 算術関数  べき乗  アルゴリズム 
tb: 0   cm: --

【Java】n進数変換(基数変換)・桁の分割をする  


 今回のテーマは「10進数 ←→ n進数」の相互変換(基数変換)のアルゴリズムなのだが、標準ライブラリにもあるので、確認用のためにも、まずはおさらいしてみよう。もちろん不要なら「基数変換のアルゴリズム」まで読み飛ばしても良い。




■標準ライブラリでの基数変換

●10進数→n進数変換(標準ライブラリ)
int n;
n = 46;
String bin = Integer.toString(n, 2); //10進数→2進数
//String bin = Integer.toBinaryString(n); //10進数→2進数(※同じ)
System.out.println(n + " -> " + bin + " (2)");

n = 51;
String tri = Integer.toString(n, 3); //10進数→3進数
System.out.println(n + " -> " + tri + " (3)");

n = 16;
String oct = Integer.toString(n, 8); //10進数→8進数
//String oct = Integer.toOctalString(n); //10進数→8進数(※同じ)
System.out.println(n + " -> " + oct + " (8)");

n = 30;
String hex = Integer.toString(n, 16); //10進数→16進数
//String hex = Integer.toHexString(n); //10進数→16進数(※同じ)
System.out.println(n + " -> " + hex + " (16)");

46 -> 101110 (2)
51 -> 1220 (3)
16 -> 20 (8)
30 -> 1e (16)

 標準ライブラリでは、例のように、

 Integer.toString(10進数, 基数)

で良い。long を変換したい場合は、Long.toString() になる。内容的には数値から文字列への変換となる。

 2進数、8進数、16進数に関しては以下の専用の関数もある、

 Integer.toBinaryString(10進数) 10進数→2進数
 Integer.toOctalString(10進数) 10進数→8進数
 Integer.toHexString(10進数) 10進数→16進数

 基数がわかっているなら、専用関数の方が実行速度は速い。

●n進数→10進数変換(標準ライブラリ)
int v;
String bin = "101110";
v = Integer.parseInt(bin, 2); //2進数→10進数
System.out.println(bin + " (2) -> " + v);

String tri = "1220";
v = Integer.parseInt(tri, 3); //3進数→10進数
System.out.println(tri + " (3) -> " + v);

String oct = "20";
v = Integer.parseInt(oct, 8); //8進数→10進数
System.out.println(oct + " (8) -> " + v);

String hex = "1e";
v = Integer.parseInt(hex, 16); //16進数→10進数
System.out.println(hex + " (16) -> " + v);

101110 (2) -> 46
1220 (3) -> 51
20 (8) -> 16
1e (16) -> 30

 標準ライブラリでは、例のように、

 Integer.parseInt(n進数文字列, 基数)

で良い。long に変換したい場合は、Long.parseLong() になる。内容的には文字列から数値への変換となる。

 1つだけ気をつけないといけないのは、parseInt() では負の値の表現となる文字列のパースはできない(エラーとなる)。その場合は parseUnsignedInt() を使えば負の値でパースできる。
 
String s = "11111111111111111111111111111111";  //2進数は31ビット目(最下位を0とするとき)が1のとき負の値となる
//int v = Integer.parseInt(s, 2); //エラー(NumberFormatException)
int v = Integer.parseUnsignedInt(s, 2); //-1




■コード上での n進数表記

 ちなみに、変換ではないのだが、ソースコードに直接書きたい場合は、以下のように表記できる。

●n進数表現
int b = 0b101110;    //2進数表現
System.out.println("b = " + b); //10進数

int o = 020; //8進数表現
System.out.println("o = " + o); //10進数

int h = 0x1e; //16進数表現
System.out.println("h = " + h); //10進数

b = 46
o = 16
h = 30



■基本的な基数変換のアルゴリズム

 では、ここからは本題の基数変換のアルゴリズムを考えてみよう。

●各桁の分解

 まずは、10進数の数はどのような構成でできているかを見てみよう。10進数の各桁を分解して考えると、


のように、

 各桁の数×基数k-1+・・・+各桁の数×基数1+各桁の数×基数0 (1~k 桁のとき)

となっていることがわかる。つまりは、10進数というのは「桁ごとの数字に基数[=10]を掛けたもので構成され、それらを加算したもの」でできている。桁を表すには最下位を「100(= 1)」とし、桁が上がるごとに、「×101(= 10)」「×102(= 100)」「×103(= 1000)」・・・としていけば良い。それらに各桁の数を掛けて合計すれば、1つの10進数の値となる。

 そしてこれは他の基数(n進数)でも同じで、例えば、2進数や3進数を分解してみると、



のようになる。

 これらのことをそのままコーディングすれば、「10進数 ←→ n進数」の相互変換は簡単にできることがわかる。


 
■10進数 → n進数 に変換する(基数で分解する)

 「n進数に変換」とは上記の例を見れば、「値を0になるまで基数で割り続け、余りを並べる」ことと同じである。それをそのまま作ってみよう。

●基数変換したリストを作る(各桁を分解)
import java.util.ArrayList;
import java.util.List;

/**<h1>基数変換したリストを作る(各桁を分解)</h1>
* <p>各桁の値は、下位から上位に向かって入る。</p>
* @param value : 元の数値 (10進数)
* @param radix : 変換する基数 (>= 2)
* @return<b>List<Integer></b> : 基数で割った余りのリスト
*/
public static final List<Integer> baseRemainder(int value, final int radix) {
final List<Integer> list = new ArrayList<Integer>();
int m;
do {
m = value % radix;
value /= radix;
list.add(m);
} while (value > 0);

return list;
}


//メインでは...
int n = 51;
List<Integer> list = baseRemainder(n, 3);
for (int k : list) {
System.out.print(k + " ");
}

0 2 2 1

 これは「10進数の 51 を、3進数の 1220 に変換した」ことになる。戻値のリストの要素は「下位→上位」へ向かって各桁の値が入っていることに注意しよう。慣れていないと何となく数字の各桁は左から右へ上位→下位と考えてしまいがちだが、配列などは下位→上位へインデクスは増えていくわけで、コンピュータにとってはむしろ自然な順である。任意精度整数(多倍長整数)や n進数、ビット列などを扱うような実装をしてみれば、わりと普通に使うものだったりする。頭のなかで逆向きに読めば良い(笑)。あとはこのリストを文字列にするなり、逆順に表示するなりすれば良いのである。しかし、n進数で計算などするなら、文字列に直さないでこのまま計算する方がもちろん速い

 そしてまた、基数を変換しないでそのまま使うなら、これは各桁の分解と同じことになる。例えば普通に基数を10進数にすると、

int n = 1234;
List<Integer> list = baseRemainder(n, 10);
for (int k : list) {
System.out.print(k + " ");
}

4 3 2 1

のように「下位→上位」に向かって、各桁が分解されていることがわかる。もし、どうしても桁を「上位→下位」の順位にしたければ、Collecctions.reverse() を使って反転する手もあるが、baseRemainder() の要素追加部分「list.add(m)」を「list.add(0, m)」のように修正し、常に先頭から挿入していけば良い。ただし、10進数に変換したい場合は桁を逆向きにパースしなくてはいけなくなるため、コードが冗長となり、変換を繰り返す際には実行速度の面でも劣る。「上位→下位」へ修正したものは別の関数にして使い分けた方が利口だろう。


 
■n進数 → 10進数 に変換する

 今度は「n進数を10進数に変換」してみよう。これは上記の例を見てもわかるように、これまでの桁の分解とは逆の操作になる。つまりは「各桁の値を基数のべき乗で掛けて、それらを合計する」ことで得られることがわかる。それをやってみよう。

import java.util.ArrayList;
import java.util.List;

/**<h1>各桁のリスト(n進数)を10進数に変換する</h1>
* <p>要素(各桁の値)は、下位から上位に向かって入っているものを使う。</p>
* @param list : 各桁のリスト
* @param radix : リストの基数
* @return<b>int</b> : 10進数
*/
public static final int digitsToInt(final List<Integer> list, final int radix) {
int sum = 0;
int x = 1;
final int len = list.size();
for (int i = 0; i < len; i++) {
sum += list.get(i) * x;
x *= radix;
}
return sum;
}


//メインでは...
int b = 0b101110; //2進数表現
List<Integer> list = baseRemainder(b, 2);

System.out.print("list = ");
for (int k : list) {
System.out.print(k + " ");
}
System.out.println();

int v = digitsToInt(list, 2);
System.out.println("v = " + v);

list = 0 1 1 1 0 1
v = 46

 この例では、10進数の 46 (=2進数表現:0b101110)を一旦「baseRemainder()」 で 2進数の桁に分解し、それを「digitsToInt()」で 10進数に戻す操作をしている。ただのサンプルなので無駄は大目に見て欲しい(笑)。

 リストの要素取得のループに for を用いているが、少し冗長と感じるようなら、以下のように、foreach(拡張for)に書き換えても良いだろう。

for (int v : list) {
sum += v * x;
x *= radix;
}

 実のところを言うと、オブジェクトの foreach は、その度に Iterator が生成されているのだが、このくらいのサンプルだと違いは出ないので、どちらでも構わない。しかし例えば、GC(ガベージコレクタ)が動きまくると困るようなものなら(リアルタイムなアクションゲームとか、延々とループするものとか)、オブジェクトの foreach を避けるのも1つの手かも知れない。


 単純な基数変換なら、標準ライブラリを使った方が確実だろう。ただし、標準ライブラリは基本的に数値←→文字列の変換となるため、変換を繰り返す場合は無駄が多く、実行速度は遅くなる。また、例えば「平衡三進法」や「n進数グレイコード」などを扱うなら、自分で実装した方が応用が効く。文字列に直さないで良いのなら、「baseRemainde()」や「digitsToInt()」を使って数値のままで処理した方がずっと速い。「桁の分解」として利用する分にも非常に幅広く使える。





(関連記事)
【Java】グレイコード変換をする
【Java】平衡三進数変換をする


関連記事

category: Java

thread: プログラミング

janre: コンピュータ

tag: 算術関数  アルゴリズム 
tb: 0   cm: --


プロフィール

Social

検索フォーム

全記事一覧

カテゴリ

ユーザータグ

最新記事

リンク

PR

▲ Pagetop