この問題は、ほんの数行のコードで解決できるもののように見えました。
残念ながら、実際に書き始めてから、思ったほど簡単ではないことに気付きました。
私が必要とするのは、Xの乱数のセットで、それぞれがAとBの間であり、それらはすべてXになります。私が直面している問題の正確な変数はさらに単純なようです。 1と1(注:これらは有理(浮動小数点)数です)、合計すると1になります。
私の最初の「数行のコード、簡単なはず」のアプローチは、-1と1の間の4つの数値をランダム化し(これは非常に簡単です)、最後の1つを1-(sum of previous numbers)
にすることでした。最後の数値が1より大きいか-1より小さい可能性があるため、これはすぐに間違っていることが判明しました。
この問題に取り組む最善の方法は何でしょうか?
PS。参考までに:C#を使用していますが、重要ではないと思います。私は実際、頭の中での問題に対して十分な解決策を作成するのに苦労しています。
私はまた、問題の現在の解決策を提供したいと思っていましたただし、それは非常に不完全であり、初期の問題に対する迅速な修正として作成されたことを忘れないでください!
X=SUM(previousNumbers)
これは、このアルゴリズムが-1と1の間の5つの数値を生成し、それらの合計が1であるという意味で機能します。ただし、欠点は、生成された数値の1つが1(または-1)であり、非常にランダム。
前に述べたように、この質問には実際には答えがありません。数値に課せられた制限により、ランダム性はよくても疑わしいものになります。
ただし、次のような数値のリストを返すプロシージャを考え出すことができます。
最初の2つの数値をランダムに-0.8と-0.7として選択したとします。ここでの要件は、合計が2.5で、すべて[-1,1]の範囲にある3つの「乱数」を考え出すことです。この問題は、開始時の問題と非常によく似ており、寸法のみが変更されています。ただし、[-1,1]の範囲の乱数を取得すると、解が得られなくなる可能性があります。範囲を制限して、ソリューションがまだ存在することを確認できます。最後の2つの数値の合計は、範囲[-2,2]になります。つまり、合計で2.5に到達できるようにするには、[0.5,1]の範囲の数値を選択する必要があります。
上記のセクションでは、プロセスの1つのステップについて説明しました。
一般的には、残りの数値の範囲を必要な合計に適用することにより、次の数値の範囲を決定します。疑似コード:
function randomNumbers (number, range, sum) {
restRange = range * (number - 1)
myRange = intersection ([sum - restRange.upper, sum - restRange.lower], range)
myNumber = random(myRange)
rest = randomNumbers (number-1, range, sum - myNumber)
return [myNumber, rest]
}
上記のケースでは
randomNumbers (3, [-1,1], 2.5)
restRange = [-1,1] * (3-1) = [-2,2]
myRange = intersection ([2.5-2,2.5-(-2)], [-1,1]) = intersection ([0.5,4.5],[-1,1]) = [0.5,1]
Javaでの簡単な実装:
public class TestRandomNumberList
{
@Test
public void test()
{
double[] numbers = new double[5];
randomNumbers(numbers, 0, -1, 1, 1);
assertEquals(sum(numbers), 1.0, 0.00001);
for (double d : numbers)
{
assertTrue(d >= -1 );
assertTrue(d <= 1);
}
}
private void randomNumbers(double[] numbers, int index, double lowerBound, double upperBound, double sum)
{
int next = index + 1;
if (next == numbers.length)
{
numbers[index] = sum;
}
else
{
int rest = numbers.length - next;
double restLowerBound = lowerBound * rest;
double restUpperBound = upperBound * rest;
double myLowerBound = Math.max(lowerBound, sum - restUpperBound);
double myUpperBound = Math.min(upperBound, sum - restLowerBound);
numbers[index] = random(myLowerBound, myUpperBound);
randomNumbers(numbers, next, myLowerBound, myUpperBound, sum - numbers[index]);
}
}
private double random(double myLowerBound, double myUpperBound)
{
double random = Math.random();
return myLowerBound + random * (myUpperBound - myLowerBound);
}
private double sum(double[] numbers)
{
double result = 0;
for (double num : numbers)
{
result += num;
}
return result;
}
}
あなたがいくつ知っている限り、簡単です。
小さな丸め誤差が発生する可能性がありますが、これが問題になるとは思いません。
数Nがランダムであると想定されている場合は、最初にそれを選択します。
申し訳ありませんが、数値がAとBの間にある必要性を逃しました。アルゴリズムはまったく同じです。 N個の乱数を選択し、A、B、実際の合計、および必要な合計に基づいてそれらをスケーリングします。残りは実装の詳細として残します。
結合分布が余次元1の制約を満たしている間、各変数が区間に均一に分布する可能性があります。たとえば、(x、y、z)を選択して単位球に均一に分布させると、各座標は間隔[-1,1]に均一に分布します。座標は単に独立していません。
独立性をあきらめたとしても、[-1,1]に均一に分布する5つの数値の定数の合計が1になることは不可能です。これは、独立変数だけでなく、確率変数の期待値が線形であるためです。 [-1,1]にそれぞれ均一に分布する5つの確率変数がある場合、それらの合計は平均値0になるため、定数1にすることはできません。
他のいくつかの回答は、最初の数を[-1,1]で統一するように選択し、最後の数を修正することを提案しています。これは通常、数値間の対称性を放棄します。 5番目の数値は[-1,1]にある可能性がありますが、均一な分布ではない可能性があるため、制約なしで生成され、合計を適合させるために使用された数値を判別できる場合があります。
代わりに、条件付き分布を使用する場合があります。問題は対称であるため、条件付き分布は対称です。いくつかのイプシロン> 0を選択し、[-1,1]から独立して5回サンプリングし、合計が1から離れたイプシロンを超えている場合は5-タプルを拒否するとします。これにより、ユニット内でいくつかの共同分布が得られます5-制約超平面x0 + x1 + x2 + x3 + x4 = 1の近くに集中している立方体。次に、イプシロンを0にすると、分布が制限されます。平面上の点を取得する確率は低すぎるため(数学的には0ですが、たとえばランダムな32ビットの浮動小数点に対しては正)、拒否サンプリングを直接使用することはできないため、この分布を生成する別の方法が必要です。
これは、[0,1]で{xi/2 + 1/2}ユニフォームを合計3で生成するか、[0,1/3]で{xi/6 + 1/6}ユニフォームを合計1で生成するのと同じです。したがって、 合計が1になる5つの正の数値を生成し、サンプルを拒否します(これらのいずれかが1/3より大きい場合は繰り返します(その後、これらに6を掛けて1を減算すると、[-1 、1]を1に合計します。1に合計する5つの正数を生成するには、[0,1]で4つの乱数を生成し、それらを並べ替え(y0、y1、y2、y3)、0と1を前面と背面、(0、y0、y1、y2、y3,1)、そして差(y0、y1-y0、y2-y1、y3-y2,1-y3)をとります(別の方法は5を作成することです) -log(uniform)を使用して指数分布の確率変数を作成し、その合計で正規化します。)
受け入れの確率が低い可能性があるため、高次元での拒否サンプリングに注意する必要があり、その後、何度も繰り返す必要があります。 1に合計する5つの数値のうち、どれも少なくとも1/3でない確率は、包含/除外で見つけることができます:1-5(2/3)^ 4 + 10(1/3)^ 4 = 11/81> 1/8。したがって、1/3を超えない数値を見つける前にこのメソッドで生成する必要がある5タプルの平均数は8未満なので、5タプルを生成するのに平均して40未満の一様乱数が必要です。合計1は条件付き分布から抽出されます。問題のパラメーターが変化する場合は、拒否のサンプリングを回避する、より複雑な方法を好むかもしれません。
この質問は古いかもしれませんが、Maarten Winkels Java code:
public static double[] GenerateRandomNumbers(uint values, double minimum, double maximum, double sum, Random generator = null)
{
if (values == 0)
throw new InvalidOperationException($"Cannot create list of zero numbers.");
if (minimum * values > sum)
throw new InvalidOperationException($"The minimum value ({minimum}) is too high.");
if (maximum * values < sum)
throw new InvalidOperationException($"The maximum value ({maximum}) is too low.");
if (minimum > maximum)
throw new InvalidOperationException($"The maximum value ({maximum}) is lower than the minimum value ({minimum}).");
if (generator == null)
generator = new Random();
var numberList = new double[values];
for (var index = 0; index < values - 1; index++)
{
var rest = numberList.Length - (index + 1);
var restMinimum = minimum * rest;
var restMaximum = maximum * rest;
minimum = Math.Max(minimum, sum - restMaximum);
maximum = Math.Min(maximum, sum - restMinimum);
var newRandomValue = generator.NextDouble(minimum, maximum);
numberList[index] = newRandomValue;
sum -= newRandomValue;
}
numberList[values - 1] = sum;
return numberList;
}
最小値と最大値の間のランダムなdouble値を生成するコード:
public static double NextDouble(this Random generator, double minimum, double maximum)
{
if (minimum > maximum)
throw new InvalidOperationException($"The maximum value ({maximum}) is lower than the minimum value ({minimum}).");
return generator.NextDouble() * (maximum - minimum) + minimum;
}
そして、これがどのように使用されるかです:
var min = -1.0;
var max = 1.0;
var sum = 0.0;
uint values = 100000;
var seed = 123;
var generator = new Random(seed);
var randomNumbers = Extensions.GenerateRandomNumbers(values, min, max, sum, generator);
Debug.WriteLine($"Distinct Values: {randomNumbers.Distinct().Count()}");
Debug.WriteLine($"Min: {randomNumbers.Min()}");
Debug.WriteLine($"Max: {randomNumbers.Max()}");
Debug.WriteLine($"Average: {randomNumbers.Average()}");
Debug.WriteLine($"Median: {randomNumbers.Median()}");
Debug.WriteLine($"Sum: {randomNumbers.Sum()}");
Debug.WriteLine("\nFirst 10 values:");
randomNumbers.Take(10).ToList().ForEach(v => Debug.WriteLine(v));
出力:
Distinct Values: 99800
Min: -0,999962684698385
Max: 1
Average: 0
Median: 0,00128587102577371
Sum: 0
First 10 values:
0,969113830462617
0,815630646336652
0,487091036274606
0,623283306892628
0,477558290342595
-0,903369966849391
-0,965998261219821
-0,701281160908416
-0,610592191857562
0,26017893536956
Winkelsのコードで直面した問題の1つは、それが再帰的な方法であり、大きなリスト(30000を超える数値)の場合、プログラムがStackOverflow例外をスローすることでした。これが、反復関数として記述した理由です。また、不要な操作を削除して削除しようとしました。乱数ジェネレーターのシードのサポートといくつかのエラー処理も追加しました。
まだ改善の余地があります。私はこれがどれほどランダムであるかについてはあまり詳しく調べていません。そのため、科学的なことを行う必要がある場合は、さらに調査してください。
範囲内の任意の5つの乱数から開始できますが、次に、どちらが負でどれが正であるかに注意を払う必要があります。
ケース(A):5つの数値はすべて正です。
ここでは、合計で割ることができます。各数値Nは正であるため、合計は個々の数値より大きいことが保証されます。その後、あなたは持っています
0 < N < 1
個々の数値Nに対して、正規化された合計が1になります。
ケース(B):ポジティブとネガティブ。
最初にネガを正規化して、それらの合計が
-1 < (Sum of normalized negatives) < 0
この時点で、各負の数がまだ範囲内にあることを確認します
-1 < N_negative < 0.
個々の負の数ごとにN_negative。
次に、ポジティブを正規化して、
1 - (Sum of normalized positives) = (Sum of normalized negatives)
ここで、範囲外の1つ以上の正の数値を取得する際に問題が発生した可能性があります。それが起こるかどうか確認してください。そうでない場合は、完了です。その場合は、正の数値を再正規化して、最大の個別値が1になるようにします。正規化された正の合計は、最初の正の正規化の合計よりも小さくなりますが、再正規化された合計は1よりも大きくなります。したがって、ネガティブを再正規化できるため、
1 - (Sum of *re*normalized positives) = (Sum of *re*normalized negatives)
これで、ケースBは確実に完了しました。
ケース(C):5つの数値はすべて負です。
これには解決策がありません。
この問題を解決する1つのオプションは、検索ツリーを構築することです。目標状態は、現在のパスに沿ったすべてのノードの合計がXに等しくなります。各ノードは範囲間で異なる値であるため、各レベルのノード数はabs(A)+ abs(B)になります。すべてのパスを探索し、すべての可能な解決策を入手してください。次に、ソリューションのリストからランダムに1つを選択します。
Nとabs(A)+ abs(B)が大きい場合、サーチスペースが大きくなりすぎて計算できなくなります。ルールを作成して、検索スペースを制限できます。ここでの単純なルールは、パスに同じ値が含まれているが順序が異なる場合、それらは同じと見なされるということです。これにより、検索ツリー上の多くのパスが削除されます。検索スペースを制限するためにさらに多くのことを考えることができますが、その演習はあなたにお任せします。
注:このソリューションはやり過ぎですが、楽しい練習です。
問題を単純化することから始めます。 [A、B]からN個の数値がある場合、それらは合計してN * AとN * Bの間の値になります。各数値からAを減算し、ターゲットXからN * Aを減算するだけです。
したがって、一般性を失うことなく、合計がX 'になる[0、B']の間のN個の数値を探すことができます。繰り返しますが、これはX 'をB'で除算することで簡略化できます。
結果として生じる問題は、[0,1]でNの数値を探し、合計するとX "=(X-A)/(B-A)になります。
これらの乱数の分布は何ですか?合計がXになるという要件は、それらを独立した分布にすることはできないことを意味します。すべてのN個の数値に対して同じ分布が必要であると想定します。それ以外の場合、それは自明です。すべての分布が同じであることがわかっている場合、それらの平均はX "/ Nでなければならないことを知っています。これは必ずしも1/2である必要はないため、[0,1]で非対称の可能性のある分布を考え出す必要があります。p(x)== p(1-x)なので、対称分布は平均0,5になります。ただし、X "/ Nが0または1でない場合は、すべての数値が可能です。
この時点で、(0,1)の平均を可能にするパラメーター化された分布を自由に選択できます。指数分布は正常に機能するはずです。
最後に、この分布から1つの数値A(0)を引き出したら、次の数値の再帰を計算して新しい分布を計算する必要があります。 X "-A(0)まで。そしてもちろん、最後の数については、選択肢が残りません。