一、前言
之前在做leetcode的时候,遇到一些随机打乱的题目,笔者发现要把这种类型的题目录到oj服务器中是有一些问题的。因为oj的判别机制是以输入和输出判断的,即将用例输入后,与程序输出相比较,如果不同则判错,但是对于这种随机打乱题目以及对返回结果如果是一组数或者字符,不限制顺序时,oj的(我称之为)“静态”判别机制则无法适应该类题目。但是虽然无法作为题目,但是笔者通过做这些打乱题发现其实可以使用这种随机打乱算法去生成测试用例,对就是我们在oj上的测试用例。
二、随机算法
这里指的随机算法特指打乱算法,也就是洗牌算法,对数组中的元素顺序进行打乱。
笔者了解到的有两种方式
1. 暴力方法
这里的暴力法,其实使用一个数据结构waiting先存放数组中所有的元素,然后进行n次循环,(这里的n既是循环次数,也是原数组的长度),每次从waiting中使用随机函数rand选取一个元素放入到新数组的第i位,i表示当前循环的次数 - 1,然后从waiting中移除该元素。
这样,每个元素选取的概率即为
P
(
i
)
=
{
(
n
−
1
n
∗
n
−
2
n
−
1
∗
.
.
.
∗
n
−
i
n
−
i
+
1
)
∗
1
n
−
i
=
1
n
,
i
>
0
1
n
,
i
=
0
P(i) = \left\{ \begin{aligned} (\frac{n-1}{n} *\frac{n-2}{n-1}*...*\frac{n - i}{n-i+1})*\frac{1}{n-i}=\frac{1}{n}, i > 0 \\ \frac{1}{n}, i = 0 \end{aligned} \right.
P(i)=⎩⎪⎪⎨⎪⎪⎧(nn−1∗n−1n−2∗...∗n−i+1n−i)∗n−i1=n1,i>0n1,i=0
所以对于数组中任意一个元素,选取的几率都是一样大的。
2.Fisher-Yates洗牌算法
会看暴力算法,其实就是随机选取元素然后依此放入数组中,而洗牌算法的核心就是下标 i ,i 表示当前的循环次数和被交换的数的位置,也是当前洗牌选取的元素的起点。
这句话听起来比较抽象举个例子,比如i = 0时, 数组长度为n,那么随机函数就从i 到 n-1 中得到一个作为下标,假设为j,i和j对应的元素交换,然后 i 自增为1,随机的范围就变成了 从 1 到 n-1,依此类推,每交换一次,i 就向后移动一次,最终实现洗牌打乱。
洗牌算法的打乱部分
nums为一个vector容器
/** Returns a random shuffling of the array. */
vector<int> shuffle() {
// Fisher-Yates 洗牌算法
// 思想简述:
// 将数组分为已经打乱和待打乱的两部分
for (int i = 0; i < nums.size(); ++i)
{
// 得到随机下标
int j = i + rand() % (nums.size() - i);
// 交换
swap(nums[i], nums[j]);
}
return nums;
}
三、创建样例(应用实例)
这里选取了leetcode的1117题,从英文中重建数字
有兴趣的小伙伴可以去了解一下
大家在选择使用洗牌算法生成测试用例的时候,一定要知道改题目的测试用例具有很强的随机性,与算法的吻合性
该题的测试用例是一串字符串,通过调换顺序,最终可以实现变成由0~9的英文单词组成的字符串,所以我们的测试用例除了随机打乱外,还要可以实现最终变成一串数字,最终数字会以升序排序(规定顺序就是为了方便oj的检测机制)
用例的算法思路
由于样例一定是会变成数字,那么我们为什么不直接使用数字呢?
可以用下图表示
洗牌的对象变成string数组,其实并不是,而是char数组!或者说是string字符串!
运行结果
代码
#include<iostream>
#include<vector>
#include<string>
#include<algorithm>
#include<iterator>
using namespace std;
// 生成需要的测试输入
class Solution {
public:
Solution(vector<char>& nums) {
this->nums = nums;
this->original.resize(nums.size());
copy(nums.begin(), nums.end(), original.begin());
}
/** Resets the array to its original configuration and return it. */
vector<char> reset() {
// copy 复制,一个泛型函数
copy(original.begin(), original.end(), nums.begin());
return nums;
}
/** Returns a random shuffling of the array. */
vector<char> shuffle() {
// Fisher-Yates 洗牌算法
// 思想简述:
// 将数组分为已经打乱和待打乱的两部分
for (int i = 0; i < nums.size(); ++i)
{
// 得到随机下标
int j = i + rand() % (nums.size() - i);
// 交换
swap(nums[i], nums[j]);
}
return nums;
}
private:
vector<char> nums;
// 初始数组
vector<char> original;
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* obj = new Solution(nums);
* vector<int> param_1 = obj->reset();
* vector<int> param_2 = obj->shuffle();
*/
const string numsWord[10] = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
vector<char> input_num_to_char(string nums)
{
vector<char> ans;
for (char num : nums)
{
string numword = numsWord[num - '0'];
for (char ch : numword)
{
ans.push_back(ch);
}
}
return ans;
}
int main(int argc, char** argv)
{
string input;
cin >> input;
vector<char> inputCase = input_num_to_char(input);
Solution* sol = new Solution(inputCase);
vector<char> result = sol->shuffle();
for (int i = 0; i < result.size(); ++i)
{
cout << result[i];
}
return 0;
}