• 随机函数变换示例


    随机函数变换相关技巧

    作者:Grey

    原文地址:

    博客园:随机函数变换相关技巧

    CSDN:随机函数变换相关技巧

    说明#

    本示例中的语言使用的是 Java ,其他语言也有类似的 API。

    问题一#

    Java 中 Math.random() 函数是等概率返回区间 [0,1) 中的任意一个小数,即 x < 1 情况下,[0,x) 中的数出现的的概率是 x,如果我们要将 x < 1 情况下,[0,x) 中的数出现的的概率调整成x2,应该如何做?

    主要思路

    由于[0,x)的概率是x,那么调用两次 Math.random(),如果较大的那个值也要在[0,x)区间内,那么两次调用都必须在[0,x)区间内(因为任意一次在[x,1)都会导致返回值不在[0,x)上),即概率是x2

    代码如下

    // [0,1)中任何一个值x出现的概率是x的平方
    public static double randToPow2() {
        return Math.max(Math.random(), Math.random());
    }
    

    可以通过如下测试代码来验证问题1的解法:

    public class Code_RandToPow2 {
      public static double randToPow2() {
        return Math.max(Math.random(), Math.random());
      }
      public static void main(String[] args) {
        int count = 0;
        int testTimes = 30000000;
        double x = 0.17;
        for (int i = 0; i < testTimes; i++) {
          if (randToPow2() < x) {
            count++;
          }
        }
        // 以下两个数值应该大小接近一致
        System.out.println((double) count / (double) testTimes);
        System.out.println(Math.pow(x, 2));
      }
    }
    

    打印的结果如下

    0.028884233333333332
    0.028900000000000006
    

    接近目标要求。

    问题二#

    假设我们有一个随机函数 f(),这个函数可以等概率返回 [1,5] 中的一个数,如何利用 f() 函数而不引入其他随机函数,得到一个等概率返回 [1,7] 中任意一个数的函数 g()

    主要思路

    由于目标是求 [1,7] 等概率返回一个,如果我们能加工得到一个x()函数,这个函数是等概率返回 [0,6] 范围内的任意一个数,那么目标函数 g() 只需要调用这个 x() 函数再加上 1,就可以等概率返回 [1,7] 中任意一个数,即 g() 函数要求

    // 等概率返回 [1,7] 中任意一个数
    public static int g() {
        // x() 函数是等概率返回 [0,6] 中的任意一个数
        return x() + 1;
    }
    

    接下来问题就变成了 x() 这个函数的实现,要 [0,6] 等概率返回一个数,思路如下

    先得到一个 0 和 1 等概率返回的随机函数 m()

    可以通过题目中给的 f() 函数来加工得到 m()

    f()[1,5] 等概率返回的一个数,也就是说,调用 f() 会等概率返回 1,2,3,4,5 中的一个数,

    当调用 f() ,得到 1 或者 2 的时候,返回 0;

    当调用 f() ,得到 4 或者 5 的时候,返回 1;

    当调用 f() , 得到 3 的时候,弃而不用,再次尝试调用 f()

    代码如下

        // 利用[1,5]随机函数f() 生产等概率返回 0 和 1 的函数 m()
        public static int m() {
            int ans = 0;
            do {
                ans = f();
            } while (ans == 3);
            return ans < 3 ? 0 : 1;
        }
    

    这样就得到了 0 和 1 等概率返回的随机函数m()

    由于 6 的二进制表示是 110, 所以区间 [0,6] 的所有整数只需要 3 个二进制位就可以表示,

    调用三次m()函数,可以等概率得到[0,7]范围内任意一个数(因为区间[0,7]用二进制表示就是 000~111),即 [0,1,2,3,4,5,6,7] 等概率返回一个,

    在得到 7 的时候,重试上述过程,只有结果在 [0,6] 才返回,这样就加工出了 x() 函数,即等概率返回 [0,6]

        // 等概率返回0~6
        public static int x() {
            int ans = 0;
            do {
                // 三个二进制位 M N Y
                ans = (m() << 2) // 得到 M
                + (m() << 1) // 得到 N
                + m(); // 得到 Y
            } while (ans == 7);
            return ans;
        }
    

    最后,目标函数 f() 通过如下方式

        // 等概率返回1~7
        public static int g() {
            return x() + 1;
        }
    

    完整代码和测试方法如下

    
    public class Code_Rand5ToRand7 {
    
        // 此函数只能用,不能修改
        // 等概率返回1~5
        public static int f() {
            return (int) (Math.random() * 5) + 1;
        }
    
        public static int m() {
            int ans = 0;
            do {
                ans = f();
            } while (ans == 3);
            return ans < 3 ? 0 : 1;
        }
    
        public static int x() {
            int ans = 0;
            do {
                ans = (m() << 2) + (m() << 1) + m();
            } while (ans == 7);
            return ans;
        }
    
        // 等概率返回1~7
        public static int g() {
            return x() + 1;
        }
    
        public static void main(String[] args) {
            final int testTimes = 100000000;
            int[] count = new int[8];
            for (int i = 0; i < testTimes; i++) {
                count[g()]++;
            }
            for (int i = 0; i < count.length; i++) {
                System.out.println(i + " 出现的次数是:" + count[i]);
            }
        }
    }
    

    测试 100000000 次, 每次记录随机生成的数字,得到每个数字出现的次数

    输出(参考)

    0 出现的次数是:0
    1 出现的次数是:14286835
    2 出现的次数是:14287329
    3 出现的次数是:14287207
    4 出现的次数是:14286037
    5 出现的次数是:14285675
    6 出现的次数是:14282222
    7 出现的次数是:14284695
    

    可以看到 1 到 7 这些数字出现的次数都差不多,接近等概率,满足题目要求。

    和问题二一样,本题也有同样思路的解法

      public static int g2() {
        return (f() + f() + f() + f() + f() + f() + f()) % 7 + 1;
      }
    

    调用 7 次 f() 之和,得到的结果 % 7以后,一定是等概率返回 0~6 中任意一个整数的,再加1,就变成等概率返回 1~7 任意一个。

    问题三#

    见:LeetCode 470. Implement Rand10() Using Rand7()

    和问题二思路一致,核心都是要先实现一个等概率返回 0 和 1 的随机函数 m()。,然后看目标函数区间需要几个二进制位,来决定调用几次 m() 函数,不赘述,完整代码见

    class Solution extends SolBase {
        public int rand10() {
            return rand(10);
        }
        // 等概率返回任意一个`[1,N]`区间的整数
        public int rand(int N) {
            // bit 表示 N 需要几个二进制位来表示
            int bit = 1;
            int base = 2;
            while (base <= N) {
                base = 2 << bit;
                bit++;
            }
            int v = build(bit);
            while (v < 1 || v > N) {
                v = build(bit);
            }
            return v;
        }
    
        private int build(int bit) {
            int v = 0;
            for (int i = 0; i < bit; i++) {
                v += (m() << i);
            }
            return v;
        }
    
        // 核心:生成 0 和 1 等概率返回的随机函数
        public int m() {
            int i = rand7();
            while (i == 7) {
                i = rand7();
            }
            return (i == 1 || i == 2 || i == 3) ? 0 : 1;
        }
    }
    

    注:这里实现了一个更为通用的rand(int N)函数,这个函数表示等概率返回任意一个[1,N]区间的整数。

    此外,本题还有另外一种解法,代码如下

      public int rand10() {
        return (rand7() + rand7() + rand7() + rand7() + rand7() + rand7() + rand7() + rand7() + rand7()
            + rand7()) % 10 + 1;
      }
    

    原理也很简单,由 10 个 rand7() 之和得到的值 % 10 以后,一定是等概率返回[0,9]中任何一个数,再加 1,就是等概率返回[1,10]

    问题四#

    有一个函数 f(),不等概率(但是是固定概率)返回 0 和 1 ,如何只通过 f() 函数,得到等概率返回 0 和 1 的随机函数 g()

    主要思路

    调用两次 f() 函数,可以得到如下情况

    情况1:0 0
    情况2:1 1
    情况3:0 1
    情况4:1 0
    

    当两次都是 0,或者两次都是 1 的时候,即情况 1 和 情况 2,舍弃;虽然 0 和 1 的概率不一样,但是

    0 1
    1 0
    

    即情况 3 和 情况 4,概率一定一样

    所以得到 0 1就返回 0 ,得到 1 0就返回 1,即 g() 函数要求

    完整代码和测试代码如下

    public class Code_0003_EqualProbabilityRandom {
    
        // 不等概率函数,
        // 内部内容不可见
        public static int f() {
            return Math.random() < 0.8 ? 0 : 1;
        }
    
        // 等概率返回0和1
        public static int g() {
            int first;
            do {
                first = f(); // 0 1
            } while (first == f());
            return first;
        }
    
    
        public static void main(String[] args) {
            final int testTimes = 10000000;
            // count[0] 统计0出现的次数
            // count[1] 统计1出现的次数
            int[] count = new int[2];
            for (int i = 0; i < testTimes; i++) {
                count[g()]++;
            }
            System.out.println("0 出现的次数:" + count[0]);
            System.out.println("1 出现的次数:" + count[1]);
        }
    }
    

    调用 10000000 次,记录 1 和 0 出现的次数,

    输出结果为(参考)

    0 出现的次数:4999698
    1 出现的次数:5000302
    

    可以看到 1 和 0 实际出现的次数差不多,近似等概率,满足题目要求。

    问题五#

    如何用一个生成区间 a~b 的随机函数加工并生成 c~d 的随机函数。

    核心思路和之前问题的思路一样,都是构造等概率生成 0 和 1 的rand01()函数,然后通过 rand01()函数加工出随机生成 c~d 的函数。

    假设生成区间 a~b 的随机函数是f(a,b)

    如果 a~b 的区间元素个数是偶数,那么将区间分两半,如果得到的是前面一半的数,则返回 0 ,如果得到后面一半的数,则返回 1;

    如果 a~b 的区间元素个数是奇数,那么把将区间分两半,如果得到的是前面一半的数,则返回 0 ,如果得到后面一半的数,则返回 1,得到中点位置的数,则继续重试;

    这样一来,就加工出一个等概率随机生成 0 或 1 的函数 rand01(),然后判断 1 ~ d - c + 1 这个区间需要几个二进制,即上述rand(int N)方法,就可以得到随机生成 c ~ d 的函数。

    完整代码如下

        // 底层依赖一个等概率返回a~b的随机函数f,
        // 如何加工出等概率返回c~d的随机函数
        public static int g(int a, int b, int c, int d) {
            int range = d - c; // 0~range c~d -> 0 ~ d-c
            int num = 1;
    
            while ((1 << num) - 1 < range) { // 求0~range需要几个2进制位
                num++;
            }
            int ans; // 最终的累加和,首先+0位上是1还是0,1位上是1还是0,2位上是1还是0...
            do {
                ans = 0;
                for (int i = 0; i < num; i++) {
                    ans += (rand01(a, b) << i);
                }
            } while (ans > range);
            return ans + c;
        }
    
        // 底层依赖一个等概率返回a~b的随机函数f,
        // 如何加工出等概率返回0和1的随机函数
        public static int rand01(int a, int b) {
            // a = 4, b = 8
            // size = 5
            int size = b - a + 1;
            // odd = true
            // 是否为奇数
            boolean odd = size % 2 != 0;
            // mid = 2
            int mid = size / 2;
            int ans;
            do {
                // 如果是奇数,那么中点位置弃用,< 中点 位置 和 > 中点位置的数字返回概率一样,一个定为0,一个定为1
                // 如果是偶数,那么这里的中点是上中点,< 上中点 位置 和 >= 上中点位置的数字出现的概率一样,一个定为0,一个定为1即可
                ans = f(a, b) - a;
            } while (odd && ans == mid);
            // 来到这步的时候,
            // 如果是偶数,ans可能会等于mid
            // 如果是奇数,ans必不等于mid
            return ans < mid ? 0 : 1;
        }
    
        // 构造一个等概率返回a~b的随机函数
        public static int f(int a, int b) {
            // Math.random() -> [0, 1)
            // [a, b] -> a + [0,b-a] -> a + (int)(Math.random() * (b-a+1))
            return a + (int) (Math.random() * (b - a + 1));
        }
    

    更多#

    算法和数据结构笔记

  • 相关阅读:
    C# 自定义List
    2013年武汉某校848数据结构真题
    从零开始:开发直播商城APP的技术指南
    Numpy 的简单使用(一)
    ICCV 2023 | 当尺度感知调制遇上Transformer,会碰撞出怎样的火花?
    iWatch框架设计
    基于Java毕业设计中药分类管理系统源码+系统+mysql+lw文档+部署软件
    探索云世界的无限可能
    寻找领域不变量:从生成模型到因果表征
    【week307-amazon】leetcode6154. 感染二叉树需要的总时间
  • 原文地址:https://www.cnblogs.com/greyzeng/p/16618329.html