我有4个号码
a,b,c,d : integers
我需要在2-7之间分配一个随机数,但所有四个数字的总数必须为22
我该怎么做?
答案 0 :(得分:8)
首先,我要说清楚,如上所述,问题不能唯一地定义问题。您要求随机抽样,但不要指定所需的样本分布。
当您实际意味着均匀分布时,通常滥用数学术语来说 random 。所以我会假设这就是你的意思。具体而言,您希望所有可能不同的4个数字组具有相同的选择概率。实现这一目标的最简单,最有效的方法如下:
可能的不同集合列表很小。在我的头脑中,我猜大约有50名候选人。
生成候选人名单非常简单。只需从2到7运行三个嵌套for循环。这将为您提供前三个数字的组合。将它们相加,从22减去并检查最终数字是否在范围内。
由于您似乎希望看到代码,因此这是一个简单的演示:
{$APPTYPE CONSOLE}
uses
System.Math,
Generics.Collections;
type
TValue = record
a, b, c, d: Integer;
procedure Write;
end;
procedure TValue.Write;
begin
Writeln(a, ' ', b, ' ', c, ' ', d);
end;
var
Combinations: TArray<TValue>;
procedure InitialiseCombinations;
var
a, b, c, d: Integer;
Value: TValue;
List: TList<TValue>;
begin
List := TList<TValue>.Create;
try
for a := 2 to 7 do
for b := 2 to 7 do
for c := 2 to 7 do
begin
d := 22 - a - b - c;
if InRange(d, 2, 7) then
begin
Value.a := a;
Value.b := b;
Value.c := c;
Value.d := d;
List.Add(Value);
end;
end;
Combinations := List.ToArray;
finally
List.Free;
end;
end;
function GetSample: TValue;
begin
Result := Combinations[Random(Length(Combinations))];
end;
var
i: Integer;
begin
Randomize;
InitialiseCombinations;
for i := 1 to 25 do
GetSample.Write;
Readln;
end.
从检查中可以清楚地看出,该算法均匀地从可用值中进行采样。
但是其他提出的算法呢?我们可以通过重复采样并计算每个可能的样本产生的次数来执行粗略的启发式测试。这是:
{$APPTYPE CONSOLE}
uses
System.SysUtils,
System.Math,
Generics.Collections;
type
TValue = record
a, b, c, d: Integer;
procedure Write;
class operator Equal(const lhs, rhs: TValue): Boolean;
end;
procedure TValue.Write;
begin
Writeln(a, ' ', b, ' ', c, ' ', d);
end;
class operator TValue.Equal(const lhs, rhs: TValue): Boolean;
begin
Result := (lhs.a=rhs.a) and (lhs.b=rhs.b) and (lhs.c=rhs.c) and (lhs.d=rhs.d);
end;
var
Combinations: TArray<TValue>;
procedure InitialiseCombinations;
var
a, b, c, d: Integer;
Value: TValue;
List: TList<TValue>;
begin
List := TList<TValue>.Create;
try
for a := 2 to 7 do
for b := 2 to 7 do
for c := 2 to 7 do
begin
d := 22 - a - b - c;
if InRange(d, 2, 7) then
begin
Value.a := a;
Value.b := b;
Value.c := c;
Value.d := d;
List.Add(Value);
end;
end;
Combinations := List.ToArray;
finally
List.Free;
end;
end;
function GetSampleHeffernan: TValue;
begin
Result := Combinations[Random(Length(Combinations))];
end;
function GetSampleVanDien: TValue;
const
TOTAL = 22;
VALUE_COUNT = 4;
MIN_VALUE = 2;
MAX_VALUE = 7;
var
Values: array[0..VALUE_COUNT-1] of Integer;
Shortage: Integer;
Candidates: TList<Integer>;
ValueIndex: Integer;
CandidateIndex: Integer;
begin
Assert(VALUE_COUNT * MAX_VALUE >= TOTAL, 'Total can never be reached!');
Assert(VALUE_COUNT * MIN_VALUE <= TOTAL, 'Total is always exceeded!');
Randomize;
Candidates := TList<Integer>.Create;
try
for ValueIndex := 0 to VALUE_COUNT-1 do
begin
Values[ValueIndex] := MIN_VALUE;
Candidates.Add(ValueIndex);
end;
Shortage := TOTAL - VALUE_COUNT * MIN_VALUE;
while Shortage > 0 do
begin
CandidateIndex := Random(Candidates.Count);
ValueIndex := Candidates[CandidateIndex];
Values[ValueIndex] := Values[ValueIndex] + 1;
if Values[ValueIndex] = MAX_VALUE then
Candidates.Remove(CandidateIndex);
Shortage := Shortage - 1;
end;
finally
Candidates.Free;
end;
Result.a := Values[0];
Result.b := Values[1];
Result.c := Values[2];
Result.d := Values[3];
end;
function GetSampleLama: TValue;
type
TRandomValues = array[1..4] of Integer;
var
IntSum: Integer;
Values: TRandomValues;
begin
// initialize a helper variable for calculating sum of the generated numbers
IntSum := 0;
// in the first step just generate a number in the range of 2 to 7 and store
// it to the first integer element
Values[1] := RandomRange(2, 7);
// and increment the sum value
IntSum := IntSum + Values[1];
// as the next step we need to generate number, but here we need also say in
// which range by the following rules to ensure we ever reach 22 (consider, if
// the 1st number was e.g. 3, then you can't generate the second number smaller
// than 5 because then even if the next two numbers would be max, you would get
// e.g. only 3 + 4 + 7 + 7 = 21, so just use this rule:
// Values[1] Values[2]
// 2 6..7
// 3 5..7
// 4 4..7
// 5 3..7
// 6..7 2..7
Values[2] := RandomRange(Max(2, 8 - Values[1]), 7);
// and increment the sum value
IntSum := IntSum + Values[2];
// if the third step we need to generate a value in the range of 15 to 20 since
// the fourth number can be still in the range of 2 to 7 which means that the sum
// after this step must be from 22-7 to 22-2 which is 15 to 20, so let's generate
// a number which will fit into this sum
Values[3] := RandomRange(Max(2, Min(7, 15 - IntSum)), Max(2, Min(7, 20 - IntSum)));
// and for the last number let's just take 22 and subtract the sum of all previous
// numbers
Values[4] := 22 - (IntSum + Values[3]);
Result.a := Values[1];
Result.b := Values[2];
Result.c := Values[3];
Result.d := Values[4];
end;
function IndexOf(const Value: TValue): Integer;
begin
for Result := 0 to high(Combinations) do
if Combinations[Result] = Value then
exit;
raise EAssertionFailed.Create('Invalid value');
end;
procedure CheckCounts(const Name: string; const GetSample: TFunc<TValue>);
const
N = 1000000;
var
i: Integer;
Counts: TArray<Integer>;
Range: Integer;
begin
SetLength(Counts, Length(Combinations));
for i := 1 to N do
inc(Counts[IndexOf(GetSample)]);
Range := MaxIntValue(Counts) - MinIntValue(Counts);
Writeln(Name);
Writeln(StringOfChar('-', Length(Name)));
Writeln(Format('Range = %d, N = %d', [Range, N]));
Writeln;
end;
begin
Randomize;
InitialiseCombinations;
CheckCounts('Heffernan', GetSampleHeffernan);
//CheckCounts('Van Dien', GetSampleVanDien);
CheckCounts('Lama', GetSampleLama);
Readln;
end.
来自一个特定运行的输出是:
Heffernan --------- Range = 620, N = 1000000 Lama ---- Range = 200192, N = 1000000
Van Dien变体目前被注释掉,因为它会产生无效值。
好的,我调试并修复了Van Dien变体。测试和结果现在看起来像这样:
{$APPTYPE CONSOLE}
uses
System.SysUtils,
System.Math,
Generics.Collections;
type
TValue = record
a, b, c, d: Integer;
procedure Write;
class operator Equal(const lhs, rhs: TValue): Boolean;
end;
procedure TValue.Write;
begin
Writeln(a, ' ', b, ' ', c, ' ', d);
end;
class operator TValue.Equal(const lhs, rhs: TValue): Boolean;
begin
Result := (lhs.a=rhs.a) and (lhs.b=rhs.b) and (lhs.c=rhs.c) and (lhs.d=rhs.d);
end;
var
Combinations: TArray<TValue>;
procedure InitialiseCombinations;
var
a, b, c, d: Integer;
Value: TValue;
List: TList<TValue>;
begin
List := TList<TValue>.Create;
try
for a := 2 to 7 do
for b := 2 to 7 do
for c := 2 to 7 do
begin
d := 22 - a - b - c;
if InRange(d, 2, 7) then
begin
Value.a := a;
Value.b := b;
Value.c := c;
Value.d := d;
List.Add(Value);
end;
end;
Combinations := List.ToArray;
finally
List.Free;
end;
end;
function GetSampleHeffernan: TValue;
begin
Result := Combinations[Random(Length(Combinations))];
end;
function GetSampleVanDien: TValue;
const
TOTAL = 22;
VALUE_COUNT = 4;
MIN_VALUE = 2;
MAX_VALUE = 7;
var
Values: array[0..VALUE_COUNT-1] of Integer;
Shortage: Integer;
Candidates: TList<Integer>;
ValueIndex: Integer;
CandidateIndex: Integer;
begin
Assert(VALUE_COUNT * MAX_VALUE >= TOTAL, 'Total can never be reached!');
Assert(VALUE_COUNT * MIN_VALUE <= TOTAL, 'Total is always exceeded!');
Candidates := TList<Integer>.Create;
try
for ValueIndex := 0 to VALUE_COUNT-1 do
begin
Values[ValueIndex] := MIN_VALUE;
Candidates.Add(ValueIndex);
end;
Shortage := TOTAL - VALUE_COUNT * MIN_VALUE;
while Shortage > 0 do
begin
CandidateIndex := Random(Candidates.Count);
ValueIndex := Candidates[CandidateIndex];
inc(Values[ValueIndex]);
if Values[ValueIndex] = MAX_VALUE then
Candidates.Delete(CandidateIndex);
dec(Shortage);
end;
finally
Candidates.Free;
end;
Result.a := Values[0];
Result.b := Values[1];
Result.c := Values[2];
Result.d := Values[3];
end;
function GetSampleLama: TValue;
type
TRandomValues = array[1..4] of Integer;
var
IntSum: Integer;
Values: TRandomValues;
begin
// initialize a helper variable for calculating sum of the generated numbers
IntSum := 0;
// in the first step just generate a number in the range of 2 to 7 and store
// it to the first integer element
Values[1] := RandomRange(2, 7);
// and increment the sum value
IntSum := IntSum + Values[1];
// as the next step we need to generate number, but here we need also say in
// which range by the following rules to ensure we ever reach 22 (consider, if
// the 1st number was e.g. 3, then you can't generate the second number smaller
// than 5 because then even if the next two numbers would be max, you would get
// e.g. only 3 + 4 + 7 + 7 = 21, so just use this rule:
// Values[1] Values[2]
// 2 6..7
// 3 5..7
// 4 4..7
// 5 3..7
// 6..7 2..7
Values[2] := RandomRange(Max(2, 8 - Values[1]), 7);
// and increment the sum value
IntSum := IntSum + Values[2];
// if the third step we need to generate a value in the range of 15 to 20 since
// the fourth number can be still in the range of 2 to 7 which means that the sum
// after this step must be from 22-7 to 22-2 which is 15 to 20, so let's generate
// a number which will fit into this sum
Values[3] := RandomRange(Max(2, Min(7, 15 - IntSum)), Max(2, Min(7, 20 - IntSum)));
// and for the last number let's just take 22 and subtract the sum of all previous
// numbers
Values[4] := 22 - (IntSum + Values[3]);
Result.a := Values[1];
Result.b := Values[2];
Result.c := Values[3];
Result.d := Values[4];
end;
function IndexOf(const Value: TValue): Integer;
begin
for Result := 0 to high(Combinations) do
if Combinations[Result] = Value then
exit;
raise EAssertionFailed.Create('Invalid value');
end;
procedure CheckCounts(const Name: string; const GetSample: TFunc<TValue>);
const
N = 1000000;
var
i: Integer;
Counts: TArray<Integer>;
Range: Integer;
begin
SetLength(Counts, Length(Combinations));
for i := 1 to N do
inc(Counts[IndexOf(GetSample)]);
Range := MaxIntValue(Counts) - MinIntValue(Counts);
Writeln(Name);
Writeln(StringOfChar('-', Length(Name)));
Writeln(Format('Range = %d, N = %d', [Range, N]));
Writeln;
end;
begin
Randomize;
InitialiseCombinations;
CheckCounts('Heffernan', GetSampleHeffernan);
CheckCounts('Van Dien', GetSampleVanDien);
CheckCounts('Lama', GetSampleLama);
Readln;
end.
Heffernan --------- Range = 599, N = 1000000 Van Dien -------- Range = 19443, N = 1000000 Lama ---- Range = 199739, N = 1000000
只是为了把它推回家,下面是各种分布的经验概率质量函数的一些图:
好的,现在我修复了@ TLama的代码。它错误地使用了RandomRange
。 documentation州:
RandomRange从AFrom和ATo之间的范围(不包括)中返回一个随机整数。
关键是范围被定义为闭合开放区间。返回的值在[AFrom..ATo]范围内,或用不等号表示,AFrom&lt; = Value&lt; A要。
但@ TLama的代码是在假设间隔在两端都关闭的情况下编写的。因此,可以通过向每个RandomRange
调用的第二个参数添加1来轻松修复代码。当我们这样做时,输出看起来像这样:
Heffernan --------- Range = 587, N = 1000000 Van Dien -------- Range = 19425, N = 1000000 Lama ---- Range = 79320, N = 1000000
经验PMF情节变为:
所有这一切的底线是,如果你关心分发,很难做到抽样。
答案 1 :(得分:2)
警告:@DavidHeffernan证明此解决方案不均匀。
22是一个相当低的数字,所以以下应该可以很好地运作:
procedure ShowValues;
const
TOTAL = 22;
VALUE_COUNT = 4;
MIN_VALUE = 2;
MAX_VALUE = 7;
var
Values: array[0..VALUE_COUNT-1] of Integer;
Shortage: Integer;
Candidates: TList<Integer>;
ValueIndex: Integer;
CandidateIndex: Integer;
begin
Assert(VALUE_COUNT * MAX_VALUE >= TOTAL, 'Total can never be reached!');
Assert(VALUE_COUNT * MIN_VALUE <= TOTAL, 'Total is always exceeded!');
Candidates := TList<Integer>.Create;
try
for ValueIndex := 0 to VALUE_COUNT-1 do
begin
Values[ValueIndex] := MIN_VALUE;
Candidates.Add(ValueIndex);
end;
Shortage := TOTAL - VALUE_COUNT * MIN_VALUE;
while Shortage > 0 do
begin
CandidateIndex := Random(Candidates.Count);
ValueIndex := Candidates[CandidateIndex];
inc(Values[ValueIndex]);
if Values[ValueIndex] = MAX_VALUE then
Candidates.Delete(CandidateIndex);
dec(Shortage);
end;
finally
Candidates.Free;
end;
ShowMessage(IntToStr(Values[0]) + ' ' + IntToStr(Values[1]) +
' ' + IntToStr(Values[2]) + ' ' + IntToStr(Values[3]));
end;
所有四个数字都被初始化为最小值。然后,虽然我们没有达到总数,但我们会随机选择一个可能仍然增加的数字并将其增加一个。
答案 2 :(得分:2)
一个有效的替代方案,不涉及构建所有可能样本的表格,如下:
编码很简单,使用与我的第一个答案相同的结构:
function GetSample: TValue;
begin
repeat
Result.a := RandomRange(2, 8);
Result.b := RandomRange(2, 8);
Result.c := RandomRange(2, 8);
Result.d := 22 - Result.a - Result.b- Result.c;
until InRange(Result.d, 2, 7);
end;
使用与我的第一个答案相同的测试工具的经验概率质量看起来像这样: