近日看到这篇博客http://www.cnblogs.com/songsz1/archive/2012/10/31/2748098.html,写一个程序:生成26个非负随机数,要求其和是301。最近对Haskell很感兴趣,就试着写了一下,不动手则已,真写起来还是遇到点问题。在函数式编程里每个确定的输入一定会有确定的输出,而随机数就意味着相同的程序每次运行就输出不同的结果。
一开始完成的代码如下,不算import,实际就3行代码:
import System.Random-- haskell的算法大量采用递归,如果只有一个数,要求其和为sum,那么这个列表当然就是[sum]了 f 1 sum =[sum]-- 对于n个数时,就是随机选一个范围在0到sum之前的数m,然后再与(n-1)个随机数(要求其和为sum-m)合在一起即可 f n sum = m : (f (n-1) (sum-m)) where m = randomRIO (0,sum)
但编译出现问题,因为代码用到了IO,纯函数和带IO的函数放在一起有问题,想了许多办法也解决不了,无奈想到了stackoverflow,注册账号后把这个问题发到stackoverflow网站上,没想到高手真多,不出几个小时问题就搞定了,正确代码如下:
f 1 sum = return [sum] f n sum = do x <- randomRIO (0, sum) xs <- f (n -1) (sum - x) return (x : xs)
几个运行结果
[215,33,6,12,7,1,4,4,15,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[284,1,1,13,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
[297,3,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
更有高手finnw发现这种算法会出现大串的0,想出了下面的改进算法(原来与博客园上的算法一是一样的):
假设要得到4个随机数,和为100
1)取3个随机数,例如[72,33,43]
2)前后分别插入0,100,排序,得到 [0,33,43,72,100]
3)计算相邻2个数的差值 [33-0, 43-33, 72-43, 100-72]
结果就出来了 [33,10,29,28],分布比原来的算法好多了。
[0,17,1,19,35,1,4,11,29,2,12,1,6,6,47,1,18,25,1,11,1,2,2,20,2,27]
[0,1,14,11,7,16,4,5,40,14,4,2,14,16,31,2,3,5,1,18,26,17,1,28,0,21]
[15,2,1,28,2,15,6,18,0,19,12,3,7,17,33,23,41,4,2,2,10,0,2,3,32,4]
英文原文如下:
As others have pointed out, your algorithm will not give uniformly-distributed output.
An easy way to get uniform output is:
- Generate
n-1
random numbers in the range from0
tosum
(inclusive) - Insert
0
andsum
into the list of random numbers - Sort the resulting list
- Return the list of differences between consecutive values in the sorted list
Example:
- Suppose we want four integers with a sum of 100, we request three random values from the RNG and it gives us
[72,33,43]
- We insert
0
and100
and sort the list, giving[0,33,43,72,100]
- We compute the differences
[33-0, 43-33, 72-43, 100-72]
- The result would be
[33,10,29,28]
In Haskell:
import System.Random import Data.List randomsWithSum :: (Num n, Ord n, Random n) => Int -> n -> IO [n] randomsWithSum len sum = do b <- sequence $ take (len-1) $ repeat $ randomRIO (0,sum) let sb = sort (sum:b) in return $ zipWith (-) sb (0:sb)
For your example you would call this as randomsWithSum 26 (301::Int)