LCG是线性同余生成器,通过公式a*x+c模m迭代产生伪随机序列,其随机性完全由参数a、c、m和种子x0决定,序列可复现故称“伪随机”。

什么是LCG,它为什么能“伪随机”
LCG(Linear Congruential Generator)不是真正的随机,而是用 a * x + c 模 m 这个固定公式反复迭代,输出看起来没规律的整数序列。它的“随机性”完全依赖三个参数:a(乘数)、c(增量)、m(模数),以及初始种子 x0。只要这四个数确定,整个序列就完全确定——所以叫“伪”随机。
实际使用中,m 通常取 2 的幂(比如 UINT32_MAX + 1U),这样模运算可被编译器优化为位截断;a 和 c 则有成熟经验值,乱选容易导致周期极短、分布偏斜甚至全零循环。
怎么写一个可用的LCG类(C++11及以上)
直接手写结构体比用全局函数更安全,避免状态污染。关键点是封装状态、隐藏参数选择逻辑,并提供 next() 和 next_double() 两种常用接口:
-
m推荐用static constexpr uint32_t m = 0x100000000ULL;(即 2³²),配合uint32_t存储状态,自然溢出等效于模运算 -
a和c用经典组合:例如a = 1664525U,c = 1013904223U(Numerical Recipes 推荐),或更优的a = 214013U,c = 2531011U(MSVC 旧版 rand 实现) - 构造函数必须接受种子,且不默认用
time(0)——那会破坏可复现性;测试或游戏存档时需要确定性行为
struct LCG {
uint32_t state;
static constexpr uint32_t a = 1664525U;
static constexpr uint32_t c = 1013904223U;
LCG(uint32_t seed) : state(seed) {}
uint32_t next() {
state = a * state + c;
return state;
}
double next_double() {
return next() / static_cast<double>(0x100000000ULL);
}
};常见错误:溢出、周期崩坏、浮点精度陷阱
最容易翻车的是没注意整数溢出语义。C++ 中 uint32_t 乘加溢出是明确定义的(回绕),但若误用 int 或 long,在不同平台可能触发未定义行为。另外两个坑:
立即学习“C++免费学习笔记(深入)”;
- 用
rand() % N模除法生成 [0, N) 区间整数——LCG 输出均匀,但模除会导致低位偏差,尤其当N不是 2 的幂时。正确做法是:next() * N / m(整数运算)或用next_double() * N转换后取整 - 把
next_double()写成next() / 0x100000000——字面量0x100000000是int,溢出后变成 0,结果永远是nan。必须写成0x100000000ULL或显式类型转换 - 以为周期就是
m——实际完整周期需满足:c != 0且gcd(c, m) == 1,同时a-1被所有m的质因数整除。参数选错,周期可能只有几十
什么时候不该用LCG
LCG 速度快、内存小,但统计质量差。以下场景应换库:
- 密码学用途(如生成密钥、token)——LCG 可被前几个输出逆推全部状态,
std::random_device或 OpenSSL 才合规 - 蒙特卡洛模拟要求高维均匀性——LCG 在超立方体上呈现明显网格结构,
std::mt19937更稳 - 多线程环境共享同一个 LCG 实例——没有原子操作保护,
next()非线程安全。要么加锁,要么每个线程独立实例,别试图“优化”成全局单例
真正需要它的场合其实很窄:嵌入式资源受限、游戏逻辑帧内确定性随机、教学演示。写的时候,别为了“自己造轮子”而绕过 <random>——除非你清楚自己在放弃什么。


















