# 隨機
:::spoiler 目錄
[TOC]
:::
# 社團discord
本學期電研社分兩班,這邊上雜學,另一班上機器學習,如果想要上機器學習的話現在可以去隔壁(廁所旁邊那一間)
![qr.ioi.tw](https://hackmd.io/_uploads/Sk6NNyya6.png)
## 什麼是隨機
數學、機率和統計領域使用隨機性的正式定義,通常假設存在某種分布,通常是均勻分布(uniform distribution),也就是說,對於他值域內的所有數字,出現機率都是相等的。
以擲骰子來說,出現1, 2, 3, 4, 5, 6的機率都是$\frac{1}{6}$。
![](https://hackmd.io/_uploads/HJ8WKXP2T.png)
![](https://hackmd.io/_uploads/HyAeqQwnT.jpg =30%x)
## 如何產生隨機
在前面我們以骰子為例,但是仔細想想,骰子出現的數字**真的是隨機的嗎?**
實際上,骰子出現的數字受到擲出的力度、角度、空氣阻力、摩擦力影響,嚴格而言,這是可以被計算出來的,而且也能夠控制出現的數字。但我們還是叫他**隨機**因為變因非常多且難以計算。
> 偽隨機性:一個過程似乎是隨機的,但實際上並不是。
在電腦上我們也可以藉由實作很複雜的函數來生成偽隨機數
:::warning
如果自己寫的話,要注意運算後是否依然為uniform distribution,例如兩隨機數直接相加就不是
:::
### 梅森旋轉演算法
```python=
def _int32(x):
return int(0xFFFFFFFF & x)
class MT19937:
def __init__(self, seed):
self.mt = [0] * 624
self.mt[0] = seed
self.mti = 0
for i in range(1, 624):
self.mt[i] = _int32(1812433253 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)
def extract_number(self):
if self.mti == 0:
self.twist()
y = self.mt[self.mti]
y = y ^ y >> 11
y = y ^ y << 7 & 2636928640
y = y ^ y << 15 & 4022730752
y = y ^ y >> 18
self.mti = (self.mti + 1) % 624
return _int32(y)
def twist(self):
for i in range(0, 624):
y = _int32((self.mt[i] & 0x80000000) + (self.mt[(i + 1) % 624] & 0x7fffffff))
self.mt[i] = (y >> 1) ^ self.mt[(i + 397) % 624]
if y % 2 != 0:
self.mt[i] = self.mt[i] ^ 0x9908b0df
```
### seed
作為函數的初始參數,通常是用電腦的時間
```cpp=
// 記得 #include<chrono>
chrono::steady_clock::now().time_since_epoch().count()
```
## C++的隨機
### std::rand()
```cpp=
srand(chrono::steady_clock::now().time_since_epoch().count());//seed,這一行不打的話預設為1
cout<<rand()<<endl;
```
有很多缺點
* 在windows系統上值域只有到32767
* 缺陷很多
### mt19937
```cpp=
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
cout<<rnd()<<endl;
```
如果要限制值域的話可以自己取模或者
```cpp=
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
uniform_int_distribution<int> dis(0, 1e9);
//uniform_real_distribution<double> dis(0, 1e9);
cout<<dis(rnd)<<endl;
```
也有其他種分布可以用,[這裡](https://cplusplus.com/reference/random/)有很多。
### shuffle
```cpp=
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
vector<int> v={1,2,3,4,5};
shuffle(v.begin(),v.end(),rnd);
```
## 唬爛
有時候可以應用題目的特性去得到一個好的算法,例如有些題目會跟你說測資是隨機生的,有一些題目測資品質很差(例如能競校內賽),或者沒有全對也有分數(例如能競區賽)
### [D. Problem with Random Tests](https://codeforces.com/problemset/problem/1743/D)
給你一個隨機生成的01字串$s$,求從$s$中選出兩個可重疊的子字串$s_1$、$s_2$的最大OR。
首先,不失一般性假設$|s_1|\geq |s_2|$,$s_1$一定可以選原本的字串去掉前綴0。
證明:把兩個01字串作OR運算,把比較長的那個字串變長,運算結果一定會變大。
接著,我們會希望$s_1$的第1個0變成1,那麼$s_2$會有以下條件:
1. 第一位是1
2. 長度等於 $s_1$的第一個0到末端的長度
只要枚舉符合條件的子字串就好了,這樣的子字串有幾個呢?
($s_1$第一個0前面1的數量)個!
會發現這個數字通常很小,因為測資是隨機生的,所以有一長串1的機率很小。
### 數的分解
來自2023能競校內賽、2010 ISSC 第6次模擬賽
給你一個數字$N$,由小到大輸出盡量少的質數,使得這些質數的和等於$N$,並在滿足前項條件下使這些質數的乘積最大。
* $2\leq N\leq 10^6$
* 多筆測資但沒跟你講範圍
首先試試看一些小測資,會發現似乎最多只要3個質數就可以湊出所有數字。(實際上目前沒有任何反例,詳見[哥德巴赫猜想](https://zh.wikipedia.org/zh-tw/%E5%93%A5%E5%BE%B7%E5%B7%B4%E8%B5%AB%E7%8C%9C%E6%83%B3))
藉由通靈,可以分成以下case:
1. N是質數
2. N是偶數,N=質數+質數
3. N是奇數,N=2+質數
4. N是奇數,N=質數+質數+質數
對於1.和3.可以利用埃式篩預處理達到$\text{O}(1)$查詢(之後可能會教),
對於2.和4.我們可以想像這些質數大機率會很接近(為了讓乘積最大),那麼我們不妨假設一個唬爛常數$hulan$,如果要把$N$切成三個質數,就暴力枚舉$\frac{N}{3}$的前後$hulan$個質數中的其中兩個質數$a,b$,再選$N-a-b$也是質數且乘積最大的那一組。
單筆複雜度$\text{O}(hulan^2)$
但還有一個問題是,我不知道有幾筆測資啊?
沒關係,就相信他不會太多。
賽後去看了測資,發現他只有2組測資,一組只有5筆==
這時可以引用GrandTiger說的一句話:
> 要相信 $\text{O}(很快)$
## 題目
### [區間絕對眾數](https://neoj.sprout.tw/problem/794/)
### 成雙成對
給一個長度為$N$的陣列$A$,回答下列詢問$Q$次:
給$L$與$R$,問$A_L, A_{L+1}, A_{L+2} ,\dots, A_{R}$內的每個數字是否都出現偶數次?
* $N,Q\leq 10^6$
* $A_i\leq 10^9$
看到出現偶數次就要想到,任兩個相同的數字XOR起來一定會是$0$。
那麼就可以想到一個很簡單的做法:如果$A_L$到$A_R$的區間XOR是$0$,就輸出YES,反之則輸出NO,可以搭配前綴和在$\text{O}(1)$時間內回答。
但,考慮以下的case:
$\{ 1,2,3\}$的XOR為$01_{(2)}\oplus 10_{(2)}\oplus 11_{(2)} = 0$,但並不符合題目要求。
這時可以將每個數字隨機映射到某個很大的數字,因為我們只在乎同個數字出現的次數,而非數字本身。可以相信這麼做的話出現上面反例的機率很小。
另外一個取代XOR的方法是,紀錄每個數字目前出現奇數次還是偶數次,若出現奇數次就把他加上負號,詢問時檢查區間合是否等於0。例如:
$\{7, 9, -9, -7\}$
### [c120: 張皓晴的笑話 AI](https://judge.tcirc.tw/ShowProblem?problemid=c120)
### [D. Cut and Stick](https://codeforces.com/problemset/problem/1514/D)
### [想不到題目標題QQ](https://neoj.sprout.tw/problem/793/)