随机 select 设置位的有效方法
Efficient way to randomly select set bit
我目前的爱好项目为纸牌游戏提供蒙特卡洛模拟 French 套牌(52 张纸牌,从 2 到 Ace)。
为了尽可能快地模拟,我在某些地方将多张卡片表示为位掩码。这是一些(简化的)代码:
public struct Card
{
public enum CardColor : byte { Diamonds = 0, Hearts = 1, Spades = 2, Clubs = 3 }
public enum CardValue : byte { Two = 0, Three = 1, Four = 2, Five = 3, Six = 4, Seven = 5, Eight = 6, Nine = 7, Ten = 8, Jack = 9, Queen = 10, King = 11, Ace = 12 }
public CardColor Color { get; private set; }
public CardValue Value { get; private set; }
// ID provides a unique value for each card, ranging from 0 to 51, from 2Diamonds to AceClubs
public byte ID { get { return (byte)(((byte)this.Value * 4) + (byte)this.Color); } }
// --- Constructors ---
public Card(CardColor color, CardValue value)
{
this.Color = color;
this.Value = value;
}
public Card(byte id)
{
this.Color = (CardColor)(id % 4);
this.Value = (CardValue)((id - (byte)this.Color) / 4);
}
}
包含多张卡片作为位掩码的结构:
public struct CardPool
{
private const ulong FULL_POOL = 4503599627370495;
internal ulong Pool { get; private set; } // Holds all cards as set bit at Card.ID position
public int Count()
{
ulong i = this.Pool;
i = i - ((i >> 1) & 0x5555555555555555);
i = (i & 0x3333333333333333) + ((i >> 2) & 0x3333333333333333);
return (int)((((i + (i >> 4)) & 0xF0F0F0F0F0F0F0F) * 0x101010101010101) >> 56);
}
public CardPool Clone()
{
return new CardPool() { Pool = this.Pool };
}
public void Add(Card card)
{
Add(card.ID);
}
public void Add(byte cardID)
{
this.Pool = this.Pool | ((ulong)1 << cardID);
}
public void Remove(Card card)
{
Remove(card.ID);
}
public void Remove(byte cardID)
{
this.Pool = this.Pool & ~((ulong)1 << cardID);
}
public bool Contains(Card card)
{
ulong mask = ((ulong)1 << card.ID);
return (this.Pool & mask) == mask;
}
// --- Constructor ---
public CardPool(bool filled)
{
if (filled)
this.Pool = FULL_POOL;
else
this.Pool = 0;
}
}
我想从第二个结构 CardPool 中随机抽取一张或多张卡片,但我无法想象如果不迭代池中的单个位如何做到这一点。
是否有任何已知的算法来执行此操作?如果没有,您有什么想法可以快速做到这一点吗?
更新:
该结构并非旨在从中抽取所有卡片。它经常被克隆,克隆阵列是没有选择的。我真的想到了抽一张或多张牌的位运算
更新2:
我写了一个 class 把卡片作为 List 来比较。
public class CardPoolClass
{
private List<Card> Cards;
public void Add(Card card)
{
this.Cards.Add(card);
}
public CardPoolClass Clone()
{
return new CardPoolClass(this.Cards);
}
public CardPoolClass()
{
this.Cards = new List<Card> { };
}
public CardPoolClass(List<Card> cards)
{
this.Cards = cards.ToList();
}
}
比较完整套牌的 1.000.000 次克隆操作:
- 结构:17 毫秒
- class:73 毫秒
承认:差别没有我想的那么大。
但考虑到我还放弃了预先计算值的简单查找,这有很大的不同。
当然,用这个class随机抽一张牌会更快,但我必须计算一个索引来查找,什么只是把问题转移到另一个地方。
我重复我最初的问题:是否有已知的算法可以从整数值中选择一个随机设置位,或者有人有想法比迭代所有更快地完成这项工作位?
关于将 class 与列表或数组一起使用的讨论让我们一无所获,这不是我的问题,我可以自己详细说明是否最好使用 class.
Update3,查找代码:
代码已删除:这可能会产生误导,因为它没有提到性能受到线程主题影响的段落。
因为同一张牌不能连续抽两次,你可以把每张牌(在你的例子中,Pool
的设置位的索引)放在一个数组中,洗牌,然后弹出从这个数组的任何一端一张一张地卡片。
这是伪代码(因为我不懂C#)。
declare cards as an array of indices
for each bit in Pool
push its index into cards
shuffle cards
when a card needs to be drawn
pop an index from cards
look up the card with Card(byte id)
编辑
这是一种在 64 位整数中获取随机设置位的算法,使用来自 Bit Twiddling Hacks 的代码来获取具有给定等级(更重要的设置位的数量)的位的位置。
ulong v = this.Pool;
// ulong a = (v & ~0UL/3) + ((v >> 1) & ~0UL/3);
ulong a = v - ((v >> 1) & ~0UL/3);
// ulong b = (a & ~0UL/5) + ((a >> 2) & ~0UL/5);
ulong b = (a & ~0UL/5) + ((a >> 2) & ~0UL/5);
// ulong c = (b & ~0UL/0x11) + ((b >> 4) & ~0UL/0x11);
ulong c = (b + (b >> 4)) & ~0UL/0x11;
// ulong d = (c & ~0UL/0x101) + ((c >> 8) & ~0UL/0x101);
ulong d = (c + (c >> 8)) & ~0UL/0x101;
ulong t = (d >> 32) + (d >> 48);
int bitCount = ((c * (~0UL / 0xff)) >> 56);
ulong r = Randomizer.Next(1, bitCount+1);
ulong s = 64;
// if (r > t) {s -= 32; r -= t;}
s -= ((t - r) & 256) >> 3; r -= (t & ((t - r) >> 8));
t = (d >> (s - 16)) & 0xff;
// if (r > t) {s -= 16; r -= t;}
s -= ((t - r) & 256) >> 4; r -= (t & ((t - r) >> 8));
t = (c >> (s - 8)) & 0xf;
// if (r > t) {s -= 8; r -= t;}
s -= ((t - r) & 256) >> 5; r -= (t & ((t - r) >> 8));
t = (b >> (s - 4)) & 0x7;
// if (r > t) {s -= 4; r -= t;}
s -= ((t - r) & 256) >> 6; r -= (t & ((t - r) >> 8));
t = (a >> (s - 2)) & 0x3;
// if (r > t) {s -= 2; r -= t;}
s -= ((t - r) & 256) >> 7; r -= (t & ((t - r) >> 8));
t = (v >> (s - 1)) & 0x1;
// if (r > t) s--;
s -= ((t - r) & 256) >> 8;
s--; // s is now the position of a random set bit in v
注释行制作另一个版本,带有分支。如果您想比较这两个版本,请取消注释这些行并注释它们后面的行。
原代码最后一行是s = 65 - s
,但是由于你用1 << cardID
来操作卡池,而且r
是随机的,所以s--
给出正确的值。
唯一需要注意的是 v
的零值。但是无论如何在空池上执行这段代码是没有意义的。
我目前的爱好项目为纸牌游戏提供蒙特卡洛模拟 French 套牌(52 张纸牌,从 2 到 Ace)。
为了尽可能快地模拟,我在某些地方将多张卡片表示为位掩码。这是一些(简化的)代码:
public struct Card
{
public enum CardColor : byte { Diamonds = 0, Hearts = 1, Spades = 2, Clubs = 3 }
public enum CardValue : byte { Two = 0, Three = 1, Four = 2, Five = 3, Six = 4, Seven = 5, Eight = 6, Nine = 7, Ten = 8, Jack = 9, Queen = 10, King = 11, Ace = 12 }
public CardColor Color { get; private set; }
public CardValue Value { get; private set; }
// ID provides a unique value for each card, ranging from 0 to 51, from 2Diamonds to AceClubs
public byte ID { get { return (byte)(((byte)this.Value * 4) + (byte)this.Color); } }
// --- Constructors ---
public Card(CardColor color, CardValue value)
{
this.Color = color;
this.Value = value;
}
public Card(byte id)
{
this.Color = (CardColor)(id % 4);
this.Value = (CardValue)((id - (byte)this.Color) / 4);
}
}
包含多张卡片作为位掩码的结构:
public struct CardPool
{
private const ulong FULL_POOL = 4503599627370495;
internal ulong Pool { get; private set; } // Holds all cards as set bit at Card.ID position
public int Count()
{
ulong i = this.Pool;
i = i - ((i >> 1) & 0x5555555555555555);
i = (i & 0x3333333333333333) + ((i >> 2) & 0x3333333333333333);
return (int)((((i + (i >> 4)) & 0xF0F0F0F0F0F0F0F) * 0x101010101010101) >> 56);
}
public CardPool Clone()
{
return new CardPool() { Pool = this.Pool };
}
public void Add(Card card)
{
Add(card.ID);
}
public void Add(byte cardID)
{
this.Pool = this.Pool | ((ulong)1 << cardID);
}
public void Remove(Card card)
{
Remove(card.ID);
}
public void Remove(byte cardID)
{
this.Pool = this.Pool & ~((ulong)1 << cardID);
}
public bool Contains(Card card)
{
ulong mask = ((ulong)1 << card.ID);
return (this.Pool & mask) == mask;
}
// --- Constructor ---
public CardPool(bool filled)
{
if (filled)
this.Pool = FULL_POOL;
else
this.Pool = 0;
}
}
我想从第二个结构 CardPool 中随机抽取一张或多张卡片,但我无法想象如果不迭代池中的单个位如何做到这一点。 是否有任何已知的算法来执行此操作?如果没有,您有什么想法可以快速做到这一点吗?
更新: 该结构并非旨在从中抽取所有卡片。它经常被克隆,克隆阵列是没有选择的。我真的想到了抽一张或多张牌的位运算
更新2: 我写了一个 class 把卡片作为 List 来比较。
public class CardPoolClass
{
private List<Card> Cards;
public void Add(Card card)
{
this.Cards.Add(card);
}
public CardPoolClass Clone()
{
return new CardPoolClass(this.Cards);
}
public CardPoolClass()
{
this.Cards = new List<Card> { };
}
public CardPoolClass(List<Card> cards)
{
this.Cards = cards.ToList();
}
}
比较完整套牌的 1.000.000 次克隆操作: - 结构:17 毫秒 - class:73 毫秒
承认:差别没有我想的那么大。 但考虑到我还放弃了预先计算值的简单查找,这有很大的不同。 当然,用这个class随机抽一张牌会更快,但我必须计算一个索引来查找,什么只是把问题转移到另一个地方。
我重复我最初的问题:是否有已知的算法可以从整数值中选择一个随机设置位,或者有人有想法比迭代所有更快地完成这项工作位?
关于将 class 与列表或数组一起使用的讨论让我们一无所获,这不是我的问题,我可以自己详细说明是否最好使用 class.
Update3,查找代码:
代码已删除:这可能会产生误导,因为它没有提到性能受到线程主题影响的段落。
因为同一张牌不能连续抽两次,你可以把每张牌(在你的例子中,Pool
的设置位的索引)放在一个数组中,洗牌,然后弹出从这个数组的任何一端一张一张地卡片。
这是伪代码(因为我不懂C#)。
declare cards as an array of indices
for each bit in Pool
push its index into cards
shuffle cards
when a card needs to be drawn
pop an index from cards
look up the card with Card(byte id)
编辑
这是一种在 64 位整数中获取随机设置位的算法,使用来自 Bit Twiddling Hacks 的代码来获取具有给定等级(更重要的设置位的数量)的位的位置。
ulong v = this.Pool;
// ulong a = (v & ~0UL/3) + ((v >> 1) & ~0UL/3);
ulong a = v - ((v >> 1) & ~0UL/3);
// ulong b = (a & ~0UL/5) + ((a >> 2) & ~0UL/5);
ulong b = (a & ~0UL/5) + ((a >> 2) & ~0UL/5);
// ulong c = (b & ~0UL/0x11) + ((b >> 4) & ~0UL/0x11);
ulong c = (b + (b >> 4)) & ~0UL/0x11;
// ulong d = (c & ~0UL/0x101) + ((c >> 8) & ~0UL/0x101);
ulong d = (c + (c >> 8)) & ~0UL/0x101;
ulong t = (d >> 32) + (d >> 48);
int bitCount = ((c * (~0UL / 0xff)) >> 56);
ulong r = Randomizer.Next(1, bitCount+1);
ulong s = 64;
// if (r > t) {s -= 32; r -= t;}
s -= ((t - r) & 256) >> 3; r -= (t & ((t - r) >> 8));
t = (d >> (s - 16)) & 0xff;
// if (r > t) {s -= 16; r -= t;}
s -= ((t - r) & 256) >> 4; r -= (t & ((t - r) >> 8));
t = (c >> (s - 8)) & 0xf;
// if (r > t) {s -= 8; r -= t;}
s -= ((t - r) & 256) >> 5; r -= (t & ((t - r) >> 8));
t = (b >> (s - 4)) & 0x7;
// if (r > t) {s -= 4; r -= t;}
s -= ((t - r) & 256) >> 6; r -= (t & ((t - r) >> 8));
t = (a >> (s - 2)) & 0x3;
// if (r > t) {s -= 2; r -= t;}
s -= ((t - r) & 256) >> 7; r -= (t & ((t - r) >> 8));
t = (v >> (s - 1)) & 0x1;
// if (r > t) s--;
s -= ((t - r) & 256) >> 8;
s--; // s is now the position of a random set bit in v
注释行制作另一个版本,带有分支。如果您想比较这两个版本,请取消注释这些行并注释它们后面的行。
原代码最后一行是s = 65 - s
,但是由于你用1 << cardID
来操作卡池,而且r
是随机的,所以s--
给出正确的值。
唯一需要注意的是 v
的零值。但是无论如何在空池上执行这段代码是没有意义的。