rand () đưa ra cùng một số lần nữa cho một phạm vi nhỏ


9

Tôi đang cố gắng tạo ra một trò chơi trong đó tôi có lưới 20x20 và tôi hiển thị một người chơi (P), mục tiêu (T) và ba kẻ thù (X). Tất cả đều có tọa độ X và Y được chỉ định sử dụng rand(). Vấn đề là nếu tôi cố gắng kiếm được nhiều điểm hơn trong trò chơi (nạp thêm năng lượng, v.v.) thì chúng trùng với một hoặc nhiều điểm khác vì phạm vi nhỏ (bao gồm 1 đến 20).

Đây là các biến của tôi và cách tôi gán giá trị cho chúng: ( COORDlà một structchỉ với X và Y)

const int gridSize = 20;
COORD player;
COORD target;
COORD enemy1;
COORD enemy2;
COORD enemy3;

//generate player
srand ( time ( NULL ) );
spawn(&player);
//generate target
spawn(&target);
//generate enemies
spawn(&enemy1);
spawn(&enemy2);
spawn(&enemy3);

void spawn(COORD *point)
{
    //allot X and Y coordinate to a point
    point->X = randNum();
    point->Y = randNum();
}

int randNum()
{
    //generate a random number between 1 and gridSize
    return (rand() % gridSize) + 1;
}

Tôi muốn thêm nhiều thứ vào trò chơi nhưng xác suất trùng lặp tăng lên khi tôi làm điều đó. Có cách nào để khắc phục điều này?


8
rand () là một RNG tồi tệ
ratchet freak

3
rand()là một RNG đáng tiếc, và dù sao với phạm vi nhỏ như vậy, bạn không cần phải mong đợi sự va chạm, chúng gần như được đảm bảo.
Ded repeatator

1
Mặc dù đúng rand()là một RNG tệ hại, nhưng nó có lẽ phù hợp với một trò chơi một người chơi và chất lượng RNG không phải là vấn đề ở đây.
Gort Robot

13
Nói về chất lượng rand()dường như không liên quan ở đây. Không có mật mã liên quan, và bất kỳ RNG nào cũng có thể sẽ va chạm trong một bản đồ nhỏ như vậy.
Tom Cornebize 20/07/2015

2
Những gì bạn đang thấy được gọi là Vấn đề sinh nhật. Nếu các số ngẫu nhiên của bạn đang được chuyển đổi thành một phạm vi nhỏ hơn phạm vi tự nhiên của PRNG thì xác suất nhận được hai trường hợp của cùng một số cao hơn nhiều so với bạn nghĩ. Cách đây một thời gian, tôi đã viết một bài giới thiệu về chủ đề này trên Stackoverflow tại đây.
Mối quan

Câu trả lời:


40

Mặc dù người dùng phàn nàn rand()và đề xuất RNG tốt hơn về chất lượng của các số ngẫu nhiên, họ cũng đang thiếu bức tranh lớn hơn. Không thể tránh được sự trùng lặp trong các dòng số ngẫu nhiên, chúng là một thực tế của cuộc sống. Đây là bài học của vấn đề sinh nhật .

Trên lưới 20 * 20 = 400 vị trí sinh sản có thể, một điểm sinh sản trùng lặp sẽ được dự kiến ​​(xác suất 50%) ngay cả khi chỉ sinh ra 24 thực thể. Với 50 thực thể (vẫn chỉ 12,5% toàn bộ lưới), xác suất trùng lặp là hơn 95%. Bạn phải đối phó với va chạm.

Đôi khi bạn có thể vẽ tất cả các mẫu cùng một lúc, sau đó bạn có thể sử dụng thuật toán xáo trộn để vẽ ncác mục riêng biệt được bảo đảm. Bạn chỉ cần tạo danh sách tất cả các khả năng. Nếu danh sách đầy đủ các khả năng quá lớn để lưu trữ, bạn có thể tạo từng vị trí sinh sản một lần như bây giờ (chỉ với RNG tốt hơn) và chỉ cần tạo lại khi xảy ra va chạm. Mặc dù có một số va chạm có khả năng xảy ra, nhiều va chạm liên tiếp là không thể theo cấp số nhân ngay cả khi phần lớn lưới điện được đưa vào.


Tôi đã nghĩ đến việc hồi sinh trong trường hợp va chạm nhưng nếu tôi có nhiều vật phẩm hơn, như tôi dự định sẽ có, thì việc tìm kiếm một vụ va chạm sẽ trở nên phức tạp. Tôi cũng sẽ phải chỉnh sửa các kiểm tra trong trường hợp một điểm được thêm hoặc xóa khỏi trò chơi. Tôi khá thiếu kinh nghiệm nên nếu có cách giải quyết vấn đề này, tôi không thể nhìn thấy nó.
Rabeez Riaz

7
Nếu bạn có một bàn cờ 20x20, trái ngược với mặt phẳng XY 20x20 liên tục (thực), thì cái bạn có là một bảng tra cứu 400 ô để kiểm tra va chạm. Đây là TRIVIAL.
John R. Strohm

@RabeezRiaz Nếu bạn có bản đồ lớn hơn, bạn sẽ có một số cấu trúc dữ liệu dựa trên lưới (một lưới bao gồm một số khu vực của các ô và mọi mục bên trong ô đó được lưu trữ trong một danh sách). Nếu bản đồ của bạn lớn hơn, bạn sẽ triển khai cây chỉnh lưu.
rwong 20/07/2015

2
@RabeezRiaz: nếu việc tra cứu quá phức tạp, hãy sử dụng đề xuất đầu tiên của anh ấy: tạo danh sách tất cả 400 vị trí bắt đầu có thể, xáo trộn chúng sao cho chúng theo thứ tự ngẫu nhiên (tra cứu thuật toán), sau đó bắt đầu sử dụng các vị trí từ phía trước khi bạn cần để tạo nội dung (theo dõi số lượng bạn đã sử dụng). Không va chạm.
RemcoGerlich 20/07/2015

2
@RabeezRiaz Không cần xáo trộn toàn bộ danh sách, nếu bạn chỉ cần một số lượng nhỏ các giá trị ngẫu nhiên, chỉ cần xáo trộn phần bạn cần (như trong, lấy một giá trị ngẫu nhiên từ danh sách 1..400, xóa nó và lặp lại cho đến khi bạn có đủ yếu tố). Trong thực tế, đó là cách mà một thuật toán xáo trộn hoạt động.
Dorus

3

Nếu bạn luôn muốn tránh chơi một thực thể mới ở một vị trí đã được phân bổ cho một thứ khác, bạn có thể thay đổi xung quanh quy trình của mình một chút. Điều này sẽ đảm bảo các vị trí độc đáo, nhưng nó đòi hỏi nhiều chi phí hơn. Dưới đây là các bước:

  1. Thiết lập bộ sưu tập các tham chiếu đến tất cả các vị trí có thể có trên bản đồ (đối với bản đồ 20x20, đây sẽ là 400 vị trí)
  2. Chọn một vị trí ngẫu nhiên từ bộ sưu tập 400 (rand () này sẽ hoạt động tốt cho việc này)
  3. Loại bỏ khả năng này khỏi bộ sưu tập vị trí có thể (vì vậy hiện có 399 khả năng)
  4. Lặp lại cho đến khi tất cả các thực thể có một vị trí được chỉ định

Vì vậy, miễn là bạn xóa vị trí khỏi bộ bạn đang chọn, sẽ không có cơ hội thứ hai nào nhận được cùng một vị trí (trừ khi bạn chọn các vị trí từ nhiều hơn một luồng).

Một thế giới tương tự như thế này sẽ là rút một lá bài từ bộ bài. Hiện tại, bạn đang xáo trộn bộ bài, rút ​​một lá bài và đánh dấu nó xuống, đặt lá bài đã rút lại vào bộ bài, xáo trộn lại và vẽ lại. Cách tiếp cận trên bỏ qua việc đưa thẻ trở lại vào bộ bài.


1

Xác nhận rand() % nlà ít hơn lý tưởng

Làm rand() % ncó một phân phối không đồng đều. Bạn sẽ nhận được số lượng giá trị nhất định không tương xứng vì số lượng giá trị không phải là bội số của 20

Tiếp theo, rand()thường là một trình tạo đồng quy tuyến tính (có nhiều công cụ khác , chỉ có điều này là khả năng được triển khai nhất - và với ít hơn các tham số lý tưởng (có nhiều cách để chọn tham số)). Vấn đề lớn nhất với điều này là thường các bit thấp trong đó (các bit bạn nhận được với % 20biểu thức kiểu) không phải là ngẫu nhiên. Tôi nhớ lại một rand()từ năm trước mà các bit thấp nhất xen kẽ từ 1đến 0với mỗi cuộc gọi đến rand()- đó là không phải là rất ngẫu nhiên.

Từ trang rand (3) man:

Các phiên bản của rand () và srand () trong Thư viện Linux C sử dụng giống nhau
trình tạo số ngẫu nhiên là ngẫu nhiên () và srandom (), vì vậy thứ tự thấp hơn
các bit nên ngẫu nhiên như các bit bậc cao hơn. Tuy nhiên, về già
triển khai rand () và trên các triển khai hiện tại khác nhau
các hệ thống, các bit bậc thấp ít ngẫu nhiên hơn nhiều so với các bit cao hơn
bit thứ tự. Không sử dụng chức năng này trong các ứng dụng dự định là
xách tay khi cần sự ngẫu nhiên tốt.

Điều này có thể đã bị rớt xuống lịch sử, nhưng hoàn toàn có khả năng bạn vẫn có một triển khai rand () kém ở đâu đó trong ngăn xếp. Trong trường hợp đó, nó vẫn còn khá áp dụng.

Điều cần làm là thực sự sử dụng một thư viện số ngẫu nhiên tốt (cung cấp các số ngẫu nhiên tốt) và sau đó yêu cầu các số ngẫu nhiên trong phạm vi bạn muốn.

Một ví dụ về bit số ngẫu nhiên tốt (từ 13:00 trong video được liên kết)

#include <iostream>
#include <random>
int main() {
    std::mt19937 mt(1729); // yes, this is a fixed seed
    std::uniform_int_distribution<int> dist(0, 99);
    for (int i = 0; i < 10000; i++) {
        std::cout << dist(mt) << " ";
    }
    std::cout << std::endl;
}

So sánh điều này với:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    srand(time(NULL));
    for (int i = 0; i < 10000; i++) {
        printf("%d ", rand() % 100);
    }
    printf("\n");
}

Chạy cả hai chương trình này và so sánh tần suất các số nhất định xuất hiện (hoặc không xuất hiện) trong đầu ra đó.

Video liên quan: rand () được coi là có hại

Một số khía cạnh lịch sử của rand () gây ra lỗi trong Nethack mà người ta nên xem và xem xét trong các triển khai của chính mình:

  • Vấn đề NNGack RNG

    Rand () là một hàm rất cơ bản để tạo số ngẫu nhiên của Nethack. Cách Nethack sử dụng nó là lỗi hoặc có thể lập luận rằng lrand48 () tạo ra các số giả ngẫu nhiên tào lao. (Tuy nhiên, lrand48 () là một hàm thư viện sử dụng phương thức PRNG được xác định và bất kỳ chương trình nào sử dụng nó đều phải tính đến các điểm yếu của phương thức đó.)

    Lỗi là Nethack dựa (đôi khi chỉ như trường hợp trong rn (2)) trên các bit thấp hơn của kết quả từ lrand48 (). Bởi vì điều này, RNG trong toàn bộ trò chơi hoạt động xấu. Điều này đặc biệt đáng chú ý trước khi hành động của người dùng giới thiệu tính ngẫu nhiên hơn nữa, tức là trong việc tạo nhân vật và tạo cấp độ đầu tiên.

Mặc dù ở trên là từ năm 2003, nhưng vẫn nên ghi nhớ vì có thể không phải tất cả các hệ thống chạy trò chơi dự định của bạn sẽ là một hệ thống Linux cập nhật với chức năng rand () tốt.

Nếu bạn chỉ làm điều này cho chính mình, bạn có thể kiểm tra trình tạo số ngẫu nhiên của bạn tốt như thế nào bằng cách viết một số mã và kiểm tra đầu ra với ent .


Về tính chất của số ngẫu nhiên

Có những cách hiểu khác về 'ngẫu nhiên' không chính xác ngẫu nhiên. Trong một luồng dữ liệu ngẫu nhiên, hoàn toàn có thể nhận được cùng một số hai lần. Nếu bạn lật một đồng xu (ngẫu nhiên), hoàn toàn có thể có được hai đầu liên tiếp. Hoặc ném xúc xắc hai lần và nhận được cùng một số hai lần liên tiếp. Hoặc quay một bánh xe roulette và nhận được cùng một số hai lần ở đó.

Sự phân bố số

Khi phát danh sách bài hát, mọi người mong đợi 'ngẫu nhiên' có nghĩa là cùng một bài hát hoặc nghệ sĩ sẽ không được phát lần thứ hai liên tiếp. Có một danh sách phát phát The Beatles hai lần liên tiếp được coi là "không ngẫu nhiên" (mặc dù đó ngẫu nhiên). Nhận thức cho một danh sách chơi gồm bốn bài hát đã chơi tổng cộng tám lần:

1 3 2 4 1 2 4 3

là 'ngẫu nhiên' hơn:

1 3 3 2 1 4 4 2

Thêm về điều này cho 'xáo trộn' các bài hát: Làm thế nào để xáo trộn các bài hát?

Trên các giá trị lặp lại

Nếu bạn không muốn lặp lại các giá trị, có một cách tiếp cận khác cần được xem xét. Tạo tất cả các giá trị có thể, và xáo trộn chúng.

Nếu bạn đang gọi rand()(hoặc bất kỳ trình tạo số ngẫu nhiên nào khác), bạn đang gọi nó với sự thay thế. Bạn luôn có thể nhận được cùng một số hai lần. Một tùy chọn là tung ra các giá trị nhiều lần cho đến khi bạn chọn một giá trị đáp ứng yêu cầu của bạn. Tôi sẽ chỉ ra rằng điều này có thời gian chạy không xác định và có thể bạn có thể thấy mình trong tình huống có một vòng lặp vô hạn trừ khi bạn bắt đầu thực hiện theo dõi ngược phức tạp hơn.

Liệt kê và chọn

Một tùy chọn khác là tạo danh sách tất cả các trạng thái hợp lệ có thể và sau đó chọn một yếu tố ngẫu nhiên từ danh sách đó. Tìm tất cả các điểm trống (đáp ứng một số quy tắc) trong phòng và sau đó chọn một điểm ngẫu nhiên từ danh sách đó. Và sau đó làm đi làm lại nhiều lần cho đến khi bạn hoàn thành.

Xáo trộn

Cách tiếp cận khác là xáo trộn như thể đó là một cỗ bài. Bắt đầu với tất cả các điểm trống trong phòng và sau đó bắt đầu chỉ định chúng bằng cách xử lý các điểm trống, từng điểm một, cho từng quy tắc / quy trình yêu cầu một điểm trống. Bạn đã hoàn thành khi bạn hết thẻ hoặc mọi thứ ngừng yêu cầu chúng.


3
Next, rand() is typically a linear congruential generatorĐiều này không đúng trên nhiều nền tảng. Từ trang man rand (3) của linux: "Các phiên bản của rand () và srand () trong Thư viện Linux C sử dụng cùng một trình tạo số ngẫu nhiên như ngẫu nhiên (3) và srandom (3), do đó, các bit bậc thấp hơn nên ngẫu nhiên như các bit bậc cao hơn. " Ngoài ra, như @delnan chỉ ra, chất lượng của PRNG không phải là vấn đề thực sự ở đây.
Charles E. Grant

4
Tôi đang hạ thấp điều này bởi vì nó không giải quyết được vấn đề thực tế.
dùng253751

@immibis Sau đó, không có câu trả lời khác "giải quyết" vấn đề thực tế và nên được hạ cấp. Tôi nghĩ rằng câu hỏi không phải là "sửa mã của tôi", đó là "tại sao tôi lại nhận được các số ngẫu nhiên trùng lặp?" Đối với câu hỏi thứ hai, tôi tin rằng câu hỏi đã được trả lời.
Neil

4
Ngay cả với giá trị nhỏ nhất là RAND_MAX32767, sự khác biệt là 1638 cách có thể để có được một số so với 1639 cho những người khác. Có vẻ như không thể tạo ra nhiều khác biệt thực tế cho OP.
Martin Smith

@Neil "Sửa mã của tôi" không phải là một câu hỏi.
Các cuộc đua nhẹ nhàng trong quỹ đạo

0

Giải pháp đơn giản nhất cho vấn đề này đã được trích dẫn trong các câu trả lời trước: đó là tạo một danh sách các giá trị ngẫu nhiên bên cạnh mỗi một trong số 400 ô của bạn, và sau đó, để sắp xếp danh sách ngẫu nhiên này. Danh sách các ô của bạn sẽ được sắp xếp thành danh sách ngẫu nhiên và theo cách này, sẽ được xáo trộn.

Phương pháp này có ưu điểm là hoàn toàn tránh sự chồng chéo của các ô được chọn ngẫu nhiên.

Nhược điểm là bạn phải tính toán một giá trị ngẫu nhiên trên một danh sách riêng cho từng ô của bạn. Vì vậy, bạn không nên làm điều đó trong khi trò chơi đã bắt đầu.

Đây là một ví dụ về cách bạn có thể làm điều đó:

#include <algorithm>
#include <iostream>
#include <vector>

#define NUMBER_OF_SPAWNS 20
#define WIDTH 20
#define HEIGHT 20

typedef struct _COORD
{
  int x;
  int y;
  _COORD() : x(0), y(0) {}
  _COORD(int xp, int yp) : x(xp), y(yp) {}
} COORD;

typedef struct _spawnCOORD
{
  float rndValue;
  COORD*coord;
  _spawnCOORD() : rndValue(0.) {}
} spawnCOORD;

struct byRndValue {
  bool operator()(spawnCOORD const &a, spawnCOORD const &b) {
    return a.rndValue < b.rndValue;
  }
};

int main(int argc, char** argv)
{
  COORD map[WIDTH][HEIGHT];
  std::vector<spawnCOORD>       rndSpawns(WIDTH * HEIGHT);

  for (int x = 0; x < WIDTH; ++x)
    for (int y = 0; y < HEIGHT; ++y)
      {
        map[x][y].x = x;
        map[x][y].y = y;
        rndSpawns[x + y * WIDTH].coord = &(map[x][y]);
        rndSpawns[x + y * WIDTH].rndValue = rand();
      }

  std::sort(rndSpawns.begin(), rndSpawns.end(), byRndValue());

  for (int i = 0; i < NUMBER_OF_SPAWNS; ++i)
    std::cout << "Case selected for spawn : " << rndSpawns[i].coord->x << "x"
              << rndSpawns[i].coord->y << " (rnd=" << rndSpawns[i].rndValue << ")\n";
  return 0;
}

Kết quả:

root@debian6:/home/eh/testa# ./exe 
Case selected for spawn : 11x15 (rnd=6.93951e+06)
Case selected for spawn : 14x1 (rnd=7.68493e+06)
Case selected for spawn : 8x12 (rnd=8.93699e+06)
Case selected for spawn : 18x13 (rnd=1.16148e+07)
Case selected for spawn : 1x0 (rnd=3.50052e+07)
Case selected for spawn : 2x17 (rnd=4.29992e+07)
Case selected for spawn : 9x14 (rnd=7.60658e+07)
Case selected for spawn : 3x11 (rnd=8.43539e+07)
Case selected for spawn : 12x7 (rnd=8.77554e+07)
Case selected for spawn : 19x0 (rnd=1.05576e+08)
Case selected for spawn : 19x14 (rnd=1.10613e+08)
Case selected for spawn : 8x2 (rnd=1.11538e+08)
Case selected for spawn : 7x2 (rnd=1.12806e+08)
Case selected for spawn : 19x15 (rnd=1.14724e+08)
Case selected for spawn : 8x9 (rnd=1.16088e+08)
Case selected for spawn : 2x19 (rnd=1.35497e+08)
Case selected for spawn : 2x16 (rnd=1.37807e+08)
Case selected for spawn : 2x8 (rnd=1.49798e+08)
Case selected for spawn : 7x16 (rnd=1.50123e+08)
Case selected for spawn : 8x11 (rnd=1.55325e+08)

Chỉ cần thay đổi NUMBER_OFinksAWNS để có được nhiều ô ngẫu nhiên, điều này sẽ không thay đổi thời gian tính toán cần thiết cho tác vụ.


"Và sau đó, để sắp xếp tất cả chúng" - Tôi tin rằng bạn có nghĩa là "xáo trộn"

Tôi đã hoàn thành lời giải thích của tôi một chút. Nó sẽ rõ ràng hơn bây giờ.
KwentRell
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.