c# - 随机选择设置位的有效方法

标签 c# algorithm random montecarlo bitmask

我当前的爱好项目为带有法国牌组的纸牌游戏提供蒙特卡洛模拟(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: 我编写了一个类,将卡片作为列表进行比较。

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 毫秒 - 类:73 毫秒

承认:差异没有我想象的那么大。 但考虑到我还放弃了预先计算值的简单查找,这会产生很大的差异。 当然,用这个类随机抽一张牌会更快,但是我必须计算一个查找索引,这只是将问题转移到另一个地方。

我重复我最初的问题:是否有一种已知的算法可以从整数值中选择随机设置位,或者是否有人有比迭代所有位更快地完成此操作的想法?

关于使用带有列表或数组的类的讨论没有任何意义,这不是我的问题,我可以自己详细说明是否使用类会更好。

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)

编辑

这里有一个算法,使用 Bit Twiddling Hacks 中的代码在 64 位整数中获取一次随机设置位。获取给定等级的位的位置(更有效的设置位的数量)。

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 的零值。但是在空池上执行此代码无论如何都是没有意义的。

关于c# - 随机选择设置位的有效方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39317649/

相关文章:

perl - Perl 的 rand 参数可以有多大?

c# - 在 asp.net 上的 sql server 中插入空值,并提供必填字段验证器

c# - httphandler 无法正常工作

algorithm - 极平面中的线段方向

java - 合并 K 个排序数组

java - 为什么在 java.util.Random 中没有接受绑定(bind)的 nextDouble()、nextFloat() 和 nextLong()

c# - 高级伪随机位生成

c# - 允许红外设备发送信号来控制 PC 的显示器

c# - 通信安全 : Fiddler intercepts my talks. 如何保护我的应用程序?

algorithm - 选择策略的动态规划