Efficient way to randomly select set bit

耗尽温柔 提交于 2019-12-12 02:30:00

问题


My current hobby project provides Monte-Carlo-Simulations for card games with French decks (52 cards, from 2 to Ace).

To simulate as fast as possible, I use to represent multiple cards as bitmasks in some spots. Here is some (simplified) code:

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);
    }
}

The structure which holds multiple cards as bitmask:

 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;
    }

}

I want to draw one or more cards at random from the second struct CardPool, but I cannot imagine how to do that without iterating single bits in the pool. Is there any known algorithm to perfom this? If not, do you have any idea of doing this fast?

Update: The structure is not intended to draw all cards from. It gets cloned frequently and cloning an array is no option. I really think of bitoperations for drawing one or multiple cards.

Update2: I wrote a class which holds the cards as List for comparison.

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();
    }
}

Comparing 1.000.000 clone operations of full decks: - struct: 17 ms - class: 73 ms

Admitted: The difference is not as much as I thought. But taken into account that I additionally give up the easy lookup of precalculated values, this makes a big difference. Of course, it would be faster to draw a random card with this class, but I would have to calculate an index for lookup then, what just transfers the problem to another spot.

I repeat my initial question: Is there a known algorithm for choosing a random set bit from an integer value or has someone an idea for getting this done faster than to iterate all bits?

The discussion about using a class with a List or an Array takes us nowhere, this is not my question and I am able to elaborate on my own if I would be better off using a class.

Update3, the lookup-code:

CODE DELETED: This might be misleading because it does not refer to passages which performance suffers from what is subject of the thread.


回答1:


Since a same card cannot be drawn twice in a row, you can place every card (in your case, the indices of Pool's set bits) in an array, shuffle it, and pop the cards one by one from any end of this array.

Here's a pseudo-code (because I don't know 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)

Edit

Here's an algorithm to get a random set bit once in a 64-bit integer, using a code from Bit Twiddling Hacks to get position of a bit with given rank (number of more significant set bits).

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

The commented lines make another version, with branches. If you want to compare the two versions, uncomment these lines and comment the lines following them.

In the original code, the last line is s = 65 - s, but since you use 1 << cardID for manipulations on card pools, and r is random anyway, s-- gives the correct value.

The only thing to watch out for is a zero value for v. But executing this code on an empty pool would be meaningless anyway.



来源:https://stackoverflow.com/questions/39317649/efficient-way-to-randomly-select-set-bit

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!