我有4个号码

a,b,c,d : integers

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

我怎样才能做到这一点?

最佳答案

首先,我要明确指出的是,该问题并不能唯一地定义问题。您要求随机抽样,但未指定所需的样本分布。

当您实际上指的是均匀分布时,说随机性是对数学术语的普遍滥用。所以我要假设你就是这个意思。具体来说,您希望所有可能的4个数字的不同集合都具有相等的选择概率。实现此目的的最简单,最有效的方法如下:

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

  • 可能的不同集合的列表很小。从我的头顶上,我猜大概有50个候选人。

    生成候选人列表非常简单。只需运行两个嵌套的for循环(从2到7)即可。这为您提供了前三个数字的组合。将它们加起来,减去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
    

    The Van Dien variant is commented out at the moment since it produces invalid values.


    OK, I debugged and fixed the Van Dien variant. The test and results now look like this:

    {$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.
    

    赫弗南
    ---------
    范围= 599,N = 1000000

    范迪恩
    --------
    范围= 19443,N = 1000000

    喇嘛
    -
    范围= 199739,N = 1000000


    只是为了说明一下,这是各种分布的经验概率质量函数的一些图:



    好,现在我修复了@TLama的代码。它使用了 RandomRange 错误。 documentation指出:



    关键是该范围被定义为一个封闭的开放时间间隔。返回的值在[AFrom..ATo)范围内,或用不等号AFrom
    但是@TLama的代码是在间隔两端都封闭的前提下编写的。因此,可以通过将每个调用RandomRange的第二个参数加1来轻松地固定代码。当我们这样做时,输出如下所示:

    赫弗南
    ---------
    范围= 587,N = 1000000

    范迪恩
    --------
    范围= 19425,N = 1000000

    喇嘛
    -
    范围= 79320,N = 1000000

    经验PMF图变为:



    最重要的是,如果您关心分布,则很难正确地进行采样。

    关于delphi - 总数相等的随机数,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/19465347/

    10-11 03:46