【问题标题】:c, obtaining a special random numberc、获取一个特殊的随机数
【发布时间】:2018-01-25 16:29:38
【问题描述】:

我有一个算法问题需要加速:)

我需要一个 32 位随机数,精确的 10 位设置为 1。但同时,像 101(12 月 5 日)和 11(12 月 3 日)这样的模式被视为非法。

现在 MCU 是 8051(8 位),我在 Keil uVision 中测试了所有这些。我的第一次尝试完成,给出了解决方案

0x48891249
1001000100010010001001001001001   // correct, 10 bits 1, no 101 or 11

问题是它在 97 秒或 1165570706 个 CPU 周期内完成,这太荒谬了!!!

这是我的代码

// returns 1 if number is not good. ie. contains at leats one 101 bit sequence
bool checkFive(unsigned long num)
{
    unsigned char tmp;

    do {

            tmp = (unsigned char)num;

        if(
            (tmp & 7) == 5 
        || (tmp & 3) == 3
        ) // illegal pattern 11 or 101
                return true; // found 

            num >>= 1;
    }while(num);

    return false;
}

void main(void) {


    unsigned long v,num; // count the number of bits set in v
    unsigned long c; // c accumulates the total bits set in v

    do {
            num = (unsigned long)rand() << 16 | rand(); 
            v = num;

            // count all 1 bits, Kernigen style
            for (c = 0; v; c++)
                    v &= v - 1; // clear the least significant bit set

    }while(c != 10 || checkFive(num));

  while(1);
}

一个聪明的头脑的大问题:) 可以做得更快吗?看来我的做法很幼稚。

提前谢谢你,

哇,我印象深刻,谢谢大家的建议。但是,在接受之前,这些天我需要对其进行测试。

现在使用第一个选项(查找)它只是不现实,将完全炸毁整个 8051 微控制器的 4K RAM :) 如下图所示,我测试了代码块中的所有组合,但有远远超过 300,直到 5000 索引才完成......

我用来测试的代码

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>

//#define bool  bit
//#define   true    1
//#define false 0



// returns 1 if number is not good. ie. contains at leats one 101 bit sequence
bool checkFive(uint32_t num)
{
    uint8_t tmp;

    do {

            tmp = (unsigned char)num;

        if(
            (tmp & 7) == 5
        || (tmp & 3) == 3
        ) // illegal pattern 11 or 101
                return true; // found

            num >>= 1;
    }while(num);

    return false;
}

void main(void) {


    uint32_t v,num; // count the number of bits set in v
    uint32_t c, count=0; // c accumulates the total bits set in v

    //printf("Program started \n");

    num = 0;

    printf("Program started \n");

    for(num=0; num <= 0xFFFFFFFF; num++)
    {


        //do {



                //num = (uint32_t)rand() << 16 | rand();
                v = num;

                // count all 1 bits, Kernigen style
                for (c = 0; v; c++)
                        v &= v - 1; // clear the least significant bit set

        //}while(c != 10 || checkFive(num));

                if(c != 10 || checkFive(num))
                    continue;

                count++;

        printf("%d: %04X\n", count, num);
    }

    printf("Complete \n");
  while(1);
}

也许我可以重新提出问题:

我需要一个号码:

  • 精确(已知)数量的 1 位,在我的示例中为 10
  • 没有 11 或 101 个模式
  • 剩余的零可以是任意的

所以不知何故,只洗牌里面的 1 位。

或者,取一个 0x00000000 并在随机位置添加 1 位中的 10 个,但非法模式除外。

【问题讨论】:

  • CLOCKS_PER_SEC 宏不可靠(它已过时)。在现代计算机上,97 秒最多接近 3000 亿次循环
  • 只是 randomly shuffle 一个由 10 个 1 和 22 个 0 组成的数组。
  • @EugeneSh。但是要排除的模式呢?
  • 我想知道这样的数字到底有多少。很容易彻底检查。我预计不会那么多,所以也许只是枚举它们并随机选择?
  • 这个算法是颠倒的。由于规则非常严格,因此最好使用规则生成而不是生成 int 并查看其是否正常

标签: c random


【解决方案1】:

解决方案

给定一个例程r(n),它返回一个从 0(含)到 n(不含)且分布均匀的随机整数,问题中描述的值可以通过调用 P(10, 4) 以均匀分布生成,其中P 是:

static uint32_t P(int a, int b)
{
    if (a == 0 && b == 0)
        return 0;
    else
        return r(a+b) < a ? P(a-1, b) << 3 | 1 : P(a, b-1) << 1;
}

所需的随机数生成器可以是:

static int r(int a)
{
    int q;

    do
        q = rand() / ((RAND_MAX+1u)/a);
    while (a <= q);

    return q;
}

(除以(RAND_MAX+1u)/ado-while 循环的目的是将rand 的范围修剪为a 的偶数倍,从而消除由于非多范围引起的偏差。)

P中的递归可以转化为迭代,此处省略,无需说明算法。)

讨论

如果数字不能包含连续位11101,则最接近的两个1 位可以相隔三位,如1001。在 32 位中拟合十个 1 位然后需要至少 28 位,如在 1001001001001001001001001001 中。因此,为了满足没有11101 并且恰好有10 个1 位的约束,该值必须是1001001001001001001001001001,并在某些位置插入四个0 位(可能包括开头或结束)。

选择这样的值相当于将001的10个实例和0的4个实例按某种顺序放置。1有14个!订购 14 种物品的方式,但 10 种中的任何一种!重新排列 10 个 001 实例的方法是相同的,并且是 4 个中的任何一个! 0 实例彼此重新排列的方式是相同的,因此不同选择的数量是 14! / 10! /4!,也称为从14中选择10的组合数。这是1,001。

要执行这种均匀分布的选择,我们可以使用递归算法:

  • 选择概率分布等于选项在可能排序中的比例的第一个选项。
  • 递归地选择剩余的选项。

当订购一个对象的 a 实例和第二个对象的 b 实例时,a/(a+ b) 的潜在排序将从第一个对象开始,b/(a+b) 将从第二个对象开始。因此,P 例程的设计是:

  • 如果没有要整理的对象,则返回空位字符串。
  • 在 [0, a+b) 中选择一个随机整数。如果小于a(概率a/(a+b)),插入位字符串001,然后递归为001a-1个实例和0b个实例选择一个顺序。
  • 否则,插入位字符串0,然后递归选择001a 个实例和0b-1 个实例的顺序。

(由于一旦a 为零,则只生成0 实例,P 中的if (a == 0 &amp;&amp; b == 0) 可以更改为if (a == 0)。我将其保留为前一种形式,因为它显示了一般形式在涉及其他字符串的情况下的解决方案。)

奖金

这是一个列出所有值的程序(尽管不是按升序排列)。

#include <stdint.h>
#include <stdio.h>

static void P(uint32_t x, int a, int b)
{
    if (a == 0 && b == 0)
        printf("0x%x\n", x);
    else
    {
        if (0 < a) P(x << 3 | 1, a-1, b);
        if (0 < b) P(x << 1, a, b-1);
    }
}

int main(void)
{
    P(0, 10, 4);
}

脚注

1 这个公式意味着我们最终得到一个以001…而不是1…开头的字符串,但是结果值,解释为二进制,是等价的,即使有@987654362的实例@ 在它前面插入。所以10个001和4个0的字符串与插入1001001001001001001001001001的4个0的字符串一一对应。

【讨论】:

  • 所以我建议建立一个查找表并随机选择值。鉴于您的回答,它应该是一个小问题。
  • @EugeneSh.: 一开始我弄错了;约束只需要 28 位来表示 1,而不是 31 位,因此不仅有 11 种解决方案。查找表仍然可行,但会很大。
  • 11_chose_4 是 330。比可行的多……好吧,虽然 8051 环境可能会受到很大限制
  • @EugeneSh.: 330 用于选择而不替换。在这种情况下,可能会有重复。
  • 我对组合数学很生疏,抱歉。我猜这点很重要:) 正如我所说,表格甚至可以完全离线生成。
【解决方案2】:

在有限数量的解决方案中满足您的标准的一种方法是利用这样一个事实,即在比特群中不能有超过四组000s。这也意味着值中可以有一组0000。知道了这一点,您可以在位 27-31 中使用单个 1 为您的值播种,然后继续添加随机位,检查添加的每个位是否满足您的 35 约束。

在向您的值添加随机位并满足您的约束时,总会有一些组合导致解决方案永远无法满足所有约束。为了防止出现这些情况,只需保持迭代计数并在迭代超过该值时重置/重新启动值生成。在这里,如果要找到一个解决方案,它将在不到 100 次迭代中找到。并且通常在 1-8 次尝试中被发现。对于您生成的每个值,平均而言,您的迭代次数不超过800,这将远远少于"97 Seconds or 1165570706 CPU cycles"(我没有计算周期,但返回几乎是瞬时的)

有很多方法可以解决这个问题,这只是在合理的时间内起作用的一种方法:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>

#define BPOP   10
#define NBITS  32
#define LIMIT 100

/** rand_int for use with shuffle */
static int rand_int (int n)
{
    int limit = RAND_MAX - RAND_MAX % n, rnd;

    rnd = rand();
    for (; rnd >= limit; )
        rnd = rand();

    return rnd % n;
}

int main (void) {

    int pop = 0;
    unsigned v = 0, n = NBITS;
    size_t its = 1;

    srand (time (NULL));

    /* one of first 5 bits must be set */
    v |= 1u << (NBITS - 1 - rand_int (sizeof v + 1));
    pop++;      /* increment pop count */

    while (pop < BPOP) {        /* loop until pop count 10 */
        if (++its >= LIMIT) {   /* check iterations */
#ifdef DEBUG
            fprintf (stderr, "failed solution.\n");
#endif
            pop = its = 1;  /* reset for next iteration */
            v = 0;
            v |= 1u << (NBITS - 1 - rand_int (sizeof v + 1));
        }

        unsigned shift = rand_int (NBITS);  /* get random shift */

        if (v & (1u << shift))   /* if bit already set */
            continue;
        /* protect against 5 (101) */
        if ((shift + 2) < NBITS && v & (1u << (shift + 2)))
            continue;
        if ((int)(shift - 2) >= 0 && v & (1u << (shift - 2)))
            continue;
        /* protect against 3 (11) */
        if ((shift + 1) < NBITS && v & (1u << (shift + 1)))
            continue;
        if ((int)(shift - 1) >= 0 && v & (1u << (shift - 1)))
            continue;

        v |= 1u << shift;   /* add bit at shift */
        pop++;              /* increment pop count */
    }
    printf ("\nv : 0x%08x\n", v);   /* output value */

    while (n--) {   /* output binary confirmation */
        if (n+1 < NBITS && (n+1) % 4 == 0)
            putchar ('-');
        putchar ((v >> n & 1) ? '1' : '0');
    }
    putchar ('\n');
#ifdef DEBUG
    printf ("\nits: %zu\n", its);
#endif

    return 0;
}

(注意:如果您打算在一个循环中生成多个随机解决方案,您可能需要一个更好的随机源,例如 getrandom() 或从 /dev/urandom 读取 - 特别是如果您正在调用在你的 shell 中循环执行)

我还包含了一个DEBUG 定义,您可以通过将-DDEBUG 选项添加到编译器字符串来启用它,以查看失败解决方案的数量和最终的迭代次数。

使用/输出示例

连续 8 次运行的结果:

$ ./bin/randbits

v : 0x49124889
0100-1001-0001-0010-0100-1000-1000-1001

v : 0x49124492
0100-1001-0001-0010-0100-0100-1001-0010

v : 0x48492449
0100-1000-0100-1001-0010-0100-0100-1001

v : 0x91249092
1001-0001-0010-0100-1001-0000-1001-0010

v : 0x92488921
1001-0010-0100-1000-1000-1001-0010-0001

v : 0x89092489
1000-1001-0000-1001-0010-0100-1000-1001

v : 0x82491249
1000-0010-0100-1001-0001-0010-0100-1001

v : 0x92448922
1001-0010-0100-0100-1000-1001-0010-0010

【讨论】:

  • 1000 1000 1000 1000 1001 0010 0100 1001 会给出四组三个零...我做错了什么还是你错过了什么?
  • 不,这行得通,我的计数少了一个 :) 修复。
  • @David C. Rankin 谢谢,看起来很不错,我会测试一下。
【解决方案3】:

正如 Eric 在他的回答中提到的,由于每个 1 但必须至少由两个 0 位分隔,因此您基本上从 28 位模式 1001001001001001001001001001 开始。然后将剩余的四个0 位放入此位模式中,并且有 11 个不同的位置可以插入每个零。

这可以通过首先选择一个从 1 到 11 的随机数来确定放置位的位置。然后你将目标位上方的所有位左移 1。再重复 3 次,你就有了你的价值。

这可以按如下方式完成:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>

void binprint(uint32_t n)
{
    int i;
    for (i=0;i<32;i++) {
        if ( n & (1u << (31 - i))) {
            putchar('1');
        } else {
            putchar('0');
        }
    }
}

// inserts a 0 bit into val after pos "1" bits are found
uint32_t insert(uint32_t val, int pos)
{
    int cnt = 0;
    uint32_t mask = 1u << 31;
    uint32_t upper, lower;

    while (cnt < pos) {
        if (val & mask) {              // look for a set bit and count if you find one
            cnt++;
        }
        mask >>= 1;
    }

    if (mask == (1u << 31)) {
        return val;                    // insert at the start: no change
    } else if (mask == 0) {
        return val << 1;               // insert at the end: shift the whole thing by 1
    } else {
        mask = (mask << 1) - 1;        // mask has all bits below the target set
        lower = val & mask;            // extract the lower portion
        upper = val & (~mask);         // extract the upper portion
        return (upper << 1) | lower;   // recombine with the upper portion shifted 1 bit
    }
}

int main()
{
    int i;
    uint32_t val = 01111111111;        // hey look, a good use of octal!

    srand(time(NULL));
    for (i=0;i<4;i++) {
        int p = rand() % 11;
        printf("p=%d\n", p);
        val = insert(val, p);
    }

    binprint(val);
    printf("\n");
    return 0;
}

两次运行的示例输出:

p=3
p=10
p=9
p=0
01001001000100100100100100100010

...

p=3
p=9
p=3
p=1
10001001000010010010010010010001

运行时间可以忽略不计。

【讨论】:

  • 这不会在可能的值上产生均匀分布,因为它可能以多种方式创建相同的值(并且不同值的方式数量不同)。例如,只有一种方法可以创建在插​​槽 1 中插入所有 0 位的模式(所有四个随机数都是 1),但有多种方法可以创建在插​​槽 1 中插入一个 0 其余在插槽 2 中的模式(第一个随机数number 为 1,其余为 2;second 为 2,rest 为 1;etc.).
  • 谢谢,我也去测试一下
【解决方案4】:

由于您不想要查找表,因此方法如下:

基本上,您将这个数字设置为 0 和 1 的 28 位,您需要在其中插入 4x 0:

0b1001001001001001001001001001

因此您可以使用以下算法:

int special_rng_nolookup(void)
{
    int secret = 0b1001001001001001001001001001;
    int low_secret;
    int high_secret;
    unsigned int i = 28; // len of secret
    unsigned int rng;
    int mask = 0xffff // equivalent to all bits set in integer

    while (i < 32)
    {
        rng = __asm__ volatile(.    // Pseudo code
                    "rdrand"
                 );
        rng %= (i + 1);  // will generate a number between 0 and 28 where you will add a 0. Then between 0 and 29, 30, 31 for the 3 next loop.
        low_secret = secret & (mask >> (i - rng)); // locate where you will add your 0 and save the lower part of your number.
        high_secret = (secret ^ low_secret) << (!(!rng)); // remove the lower part to your int and shift to insert a 0 between the higher part and the lower part. edit : if rng was 0 you want to add it at the very beginning (left part) so no shift.
        secret = high_secret | low_secret; // put them together.
        ++i;
    }
    return secret;
}

【讨论】:

  • int table[1001] 恭喜,你刚刚让 OP 的 8051 爆炸了。你知道8051是什么吗?
  • 不,我没有,但 OP 已经意识到 8051 的内存限制。您很快就会批评看起来很愚蠢的答案(而且它确实考虑了 RAM 限制),但我的第二个呢?回答 ?另请注意,该问题被标记为“c”而不是“嵌入”。我删除了第一个答案,所以也删除了你的反对票;)
  • 您仍在使用 32 位算术,尽管 OP 已明确声明他们使用 8051。或者更确切地说,您认为您使用的是 32 位算术,但 int 在 OP 上是 16 位系统。我还认为 x86 内联汇编器将很难在 8051 上运行。8051 不是 PC。它是有史以来最慢、最可怕的 CPU 之一。
  • 因为他们不知道自己在做什么。
  • @Lundin:如今,大多数 8051 是 1-2 TCY 用于 80% 的指令。即便如此,由于仅使用 16 位拇指和/或缺少强大的指令,ARM Cortex M0(32 位)将被淘汰。此外,由于多种原因,无法更改 MCU。
猜你喜欢
  • 1970-01-01
  • 2017-10-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-06
  • 2011-03-05
  • 1970-01-01
  • 2015-03-22
相关资源
最近更新 更多