总数相等的随机数

时间:2013-10-19 11:04:48

标签: delphi delphi-xe2

我有4个号码

a,b,c,d : integers

我需要在2-7之间分配一个随机数,但所有四个数字的总数必须为22

我该怎么做?

3 个答案:

答案 0 :(得分:8)

首先,我要说清楚,如上所述,问题不能唯一地定义问题。您要求随机抽样,但不要指定所需的样本分布。

当您实际意味着均匀分布时,通常滥用数学术语来说 random 。所以我会假设这就是你的意思。具体而言,您希望所有可能不同的4个数字组具有相同的选择概率。实现这一目标的最简单,最有效的方法如下:

  • 列举所有可能的4个数字组。
  • 计算这些数字,比如N.
  • 要进行采样,请从0到N-1范围内的均匀分布中选择随机数。
  • 返回第i组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

只是为了把它推回家,下面是各种分布的经验概率质量函数的一些图:

enter image description here


好的,现在我修复了@ TLama的代码。它错误地使用了RandomRangedocumentation州:

  

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情节变为:

enter image description here


所有这一切的底线是,如果你关心分发,很难做到抽样。

答案 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)

一个有效的替代方案,不涉及构建所有可能样本的表格,如下:

  1. 在[2..7]范围内选择三个值。
  2. 将第四个值设置为等于前三个之和的22。
  3. 如果第四个值不在[2..7]范围内,请转到1。
  4. 返回四个值。
  5. 编码很简单,使用与我的第一个答案相同的结构:

    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;
    

    使用与我的第一个答案相同的测试工具的经验概率质量看起来像这样:

    enter image description here