专栏名称: 吴师兄学算法
和程序员小吴一起从初学者的角度学习算法,以动画的形式呈现解题的思路。每周四篇原创文章,期待你的鉴赏!
目录
相关文章推荐
绝对现场  ·  名医到院区 | ... ·  2 小时前  
共同体Community  ·  深圳市第三儿童医院,开业时间定了! ·  14 小时前  
共同体Community  ·  深圳市第三儿童医院,开业时间定了! ·  14 小时前  
闽南日报  ·  延时门诊!漳州市医院最新通知 ·  昨天  
连州点点网  ·  期待您到场领奖!关于召开“2025年连州市春 ... ·  3 天前  
51好读  ›  专栏  ›  吴师兄学算法

解锁蒙特卡洛算法:随机数背后的智慧

吴师兄学算法  · 公众号  ·  · 2024-08-10 16:32

正文

大家好,我是吴师兄。

提前批开始啦! 早点练习,准备好秋招吧。

今天分享的是 蒙特卡洛专题 ,拿下它!!!

蒙特卡洛模拟算法

蒙特卡洛模拟(Monte Carlo Simulation)是一种利用随机采样和统计分析来解决复杂问题的数值计算方法。

它得名于摩纳哥的蒙特卡洛赌场,因为这种方法 依赖于大量的随机试验 ,类似于赌博中的随机性。

已知一个单位圆的半径是1,算出圆的面积就是 π

一个经典的蒙特卡洛模拟例子是估算圆周率 π

  1. 在一个单位正方形内随机投放大量点(随机抽样)。
  2. 计算落在单位圆内的点的比例。由于圆的面积为 πr² (半径为 0.5 ,面积为 π/4 ),正方形的面积为 1 ,所以圆内点数与总点数之比接近于 π/4
  3. 通过大量点的统计结果估算 π 的值。
import random

def estimate_pi(num_samples):
    inside_circle = 0

    for _ in range(num_samples):
        x = random.uniform(01)
        y = random.uniform(01)
        if x**2 + y**2 <= 1:
            inside_circle += 1

    return (inside_circle / num_samples) * 4

num_samples = 1000000
pi_estimate = estimate_pi(num_samples)
print(f"Estimated Pi: {pi_estimate}")

基于这个理论基础来练习一道算法题。

题目描述与示例

题目描述

m 个学生排成一排,学生编号分别是 1 m m 3 的整倍数。

老师随机抽签决定将所有学生分成 n 3 人的小组, m=3*n 为了便于同组学生交流,老师决定将小组成员安排到一起,也就是同组成员彼此相连,同组任意两个成员之间无其它组的成员。

因此老师决定调整队伍,老师每次可以调整任何一名学生到队伍的任意位置,计为调整了一次,请计算最少调整多少次可以达到目标。

注意:对于小组之间没有顺序要求,同组学生之间没有顺序要求

输入描述

两行字符串,空格分隔表示不同的学生编号。

第一行是学生目前排队情况,第二行是随机抽签分组情况,从左开始每 3 个元素为一组 n 为学生的数量, m 的范围为 [3,900] m 一定为 3 的整数倍。第一行和第二行的元素个数一定相同。

输出描述

老师调整学生达到同组彼此相连的最小次数

备注

同组相连: 同组任意两个成员之间无其它组的成员 ,比如有两个小组 [4 5 6] [1 2 3] ,以下结果都满足要求

1 2 3 4 5 6
1 3 2 4 5 6
2 3 1 5 6 4
5 6 4 1 2 3

以下结果不满足要求

1 2 4 3 5 6`,`4`与`5`之间存在其它组的成员`3

示例一

输入

7 9 8 5 6 4 2 1 3
7 8 9 4 2 1 3 5 6

输出

1

说明

学生目前排队情况: 7 9 8 5 6 4 2 1 3

学生分组情况: [7 8 9] [4 2 1] [3 5 6]

3 调整到 4 之前,队列调整为 7 9 8 5 6 3 4 2 1

那么三个小组成员均彼此相连 [7 9 8] [5 6 3] [4 2 1]

输出: 1

示例二

输入

1 4 7 2 5 8 3 6 9
1 2 3 4 5 6 7 8 9

输出

4

说明

第一次移动:1 4 2 5 8 3 6 7 9

第二次移动:1 4 2 5 3 6 7 8 9

第三次移动:1 2 5 3 4 6 7 8 9

第四次移动:1 2 3 4 5 6 7 8 9

解题思路

本题直接考虑贪心算法来解决会比较麻烦,没办法直接从局部最优推到全局最优。

数组预处理

设未排好队的数组为 nums1 ,分组结果为 nums2

同学人数 m = len(nums1) = len(nums2) ,组数 n = m // 3

为了方便表示分组,我们可以把在 nums2 中每隔 3 个属于同一组的同学,用 组号 进行标记。

以示例一为例,我们希望

7 9 8 5 6 4 2 1 3
7 8 9 4 2 1 3 5 6

能够用

0 0 0 2 2 1 1 1 2
0 0 0 1 1 1 2 2 2

进行重新标记。

那么我们只需要让原 nums1 中应该在同一组号中的元素,通过调整顺序而全部相邻即可。

我们可以使用哈希表 dic 进行从 nums2 中的元素到组号 i 的映射。

对于组号 i 而言,在 nums2 中的属于同一个组中的元素索引应该为 i*3+j ,其中 j 的取值是 0 1 2

进而构建出 nums1 中元素用组号标记的数组 nums

nums1 = list(map(int, input().split()))
nums2 = list(map(int, input().split()))

m = len(nums1)
n = m // 3
dic = dict()
for i in range(n):
    for j in range(3):
        dic[nums2[i*3+j]] = i

nums = [dic[num] for num in nums1]

检查某个组是否已经排好队

在后面的模拟过程中,我们就可以通过构建一个函数 check_group(nums, idx) ,来判断第 idx 个组是否已经是排好队的。

对于第 idx 个组而言,如果 nums[idx*3] nums[idx*3+1] nums[idx*3+2] 相等,则意味着第 idx 个组已经的排好队的了。

def check_group(nums, idx):
    return nums[idx*3] == nums[idx*3+1] == nums[idx*3+2]

检查所有组是否已经排好队

结合 check_group(nums, idx) 函数,我们可以构建函数 check_all(nums, n) 来检查是否所有的组已经排好队。

def check_all(nums, n):
    return all(check_group(nums, idx) for idx in range(n))

蒙特卡洛模拟框架

考虑蒙特卡洛模拟,我们对原数组 nums 的排队过程进行 T 次的随机模拟。

每一次模拟都从初始状态开始,挑选出最少的能够使得原数组 nums 排好队的那一次调整次数作为答案。

故构建函数 mc_simulation(nums, n) 的代码框架为

def mc_simulation(nums, n):
    # 蒙特卡洛模拟的总的次数,该数值可以自行进行调整
    T = 10000
    # 初始化最多的调整次数,该数值可以自行进行调整
    ans = 10

    # T次蒙特卡洛模拟
    for _ in range(T):
        pass
    return ans

单次蒙特卡洛模拟

整体思路

在得到预处理的 nums 之后,我们需要对 nums_new = nums[:] ,即 nums 的一份拷贝进行单次蒙特卡洛模拟。

模拟 单次调整 位置的过程

  1. nums_new 中挑选一个下标 i i 范围是 randint(0, m-1)
  2. 获得 num = nums_new[i] ,删除这个元素 num
  3. nums_new 中挑选一个下标 j j 的范围的 randint(0, m-2)
  4. 把删除的元素 num 插入到 nums_new 中下标 j 的位置

单次蒙特卡洛模拟过程需要进行多次的调整位置。

单次蒙特卡洛模拟会计算得到一个对应的调整次数,对多次蒙特卡洛模拟计算得到的调整次数 取最小值 ,就是得到的全局的最少调整次数,将其作为答案输出。

编号挑选优化

当我们进行下标 i j 的挑选的时候,注意到如果 i j 当前已经位于同一组中,即存在 i // 3 == j // 3 成立,那么本次调整是没有意义的。

因为这样的调整只会无端增加调整次数,但并不能带来实质上的效果。

我们要尽量避免这种情况的出现,因此可以直接挑选两个不同编号的组,再从从这两个组中分别随机挑选出一个编号。其代码为

group_i = randint(0, n-1)
i = group_i * 3 + randint(02)

group_j = randint(0, n-1)
while(group_j == group_i):
    group_j = randint(0, n-1)

j = group_j * 3 + randint(02)

组号区间优化

另外,如果位于边界的组(最左边的和最右边的)已经是分好组的结果,那么不应该选择这些组。譬如

0 0 0 3 2 2 3 2 3 1 1 1

那么边界处的 0 0 0 1 1 1 中的元素不应该被挑选,而应该挑选位于中间位置,尚未分好组的那些元素。

故我们可以设置两个变量 start_idx end_idx ,来表示挑选的组 group_i group_j 的区间。

初始化 start_idx, end_idx = 0, n-1 为尚未排好队的组的区间。

每一次随机调整之前,我们都需要更新 start_idx, end_idx 的值。即

def update_group_interval(nums, start_group, end_group):
    i, j = start_group, end_group
    while check_group(nums, i):
        i += 1
    while check_group(nums, j):
        j -= 1
    return i, j

而随机挑选 group_i group_j ,进而挑选出具体元素下标 i j 的代码也随之发生调整。

需要把 randint(0, n-1) 修改为 randint(start_group, end_group)

group_i = randint(start_idx, end_idx)
i = group_i * 3 + randint(02)

group_j = randint(start_idx, end_idx)
while(group_j == group_i):
    group_j = randint(start_idx, end_idx)

j = group_j * 3 + randint(02)

同样地,检查所有组是否已经排好队的函数 check_all(nums, n) 也可以相应地进行调整,只考虑组号落在区间 [start_idx, end_idx] 内的那些组。

def check_all(nums, start_idx, end_idx):
    return all(check_group(nums, idx) for idx in range(start_idx, end_idx+1))

单次蒙特卡洛模拟的代码

综合以上讨论,我们构建出单次蒙特卡洛模拟的过程如下

  1. 若干初始化
    1. 初始化拷贝 nums 的副本 nums_new
    2. 初始化本次模拟的调整次数 time = 0






请到「今天看啥」查看全文