# Mathematics(37 題)
## Josephus Queries [problem](https://cses.fi/problemset/task/2164)
```markdown
題目:
- 給定多組查詢,每組查詢給你 n 和 k。
- 問:在一個大小為 n 的圓圈裡進行 Josephus 問題,每次從當前人開始數第 2 個人並淘汰,問最後第 k
個人是誰(1-indexed)。
- 這題是 k 給定,問第 k 個人會坐在哪個位置。
```
``` markdown
解法 :
### 1. Josephus 的遞迴解法(kill step = 2):
- 用經典的 Josephus 遞迴方式推導:
- 若 k ≤ ⌈n/2⌉,表示第 k 人還活著,位置會被平移到偶數位:
- 若 2k > n,需對 n 取模(環狀),結果為 2k % n。
- 否則,結果就是 2k。
- 若 k > ⌈n/2⌉,表示第 k 人在淘汰之後的子問題中,重編號求解:
- 遞迴轉為求 f(n/2, k - ⌈n/2⌉)。
- 根據 n 為奇或偶,平移量不同:
- 若 n 為奇,結果為 2×f + 1。
- 若 n 為偶,結果為 2×f − 1。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
LL f(LL n,LL k)
{
if(n==1) return 1;
if(k<=(n+1)/2)
{
if(2*k>n) return (2*k)%n;
else return 2*k;
}
LL temp=f(n/2,k-(n+1)/2);
if(n%2==1) return 2*temp+1;
return 2*temp-1;
}
int main(){
fastio;
int T;
cin >> T;
while (T--) {
int n, k;
cin >> n >> k;
cout<< f(n,k) <<"\n";
}
}
```
## Exponentiation [problem](https://cses.fi/problemset/task/1095)
```markdown
題目:
- 給定多組查詢,每次輸入兩個整數 n 和 k。
- 請輸出 n^k mod 10^9+7 的值。
- 數據範圍允許 k 非常大,因此不能使用暴力乘法。
```
``` markdown
解法 :
### 1. 快速冪(Binary Exponentiation):
- 利用指數的二進位展開,讓每次乘法都只在必要時做:
- f(n, k) = f(n, k/2)^2 若 k 為偶數。
- 若 k 為奇數,則再乘上 n。
- 每一步都取 mod 可保證數值不爆炸。
- 時間複雜度為 O(log k),非常高效。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
LL f(LL n,LL k)
{
if(k == 1)
return n;
if(k == 0)
return 1;
LL ret = f(n, k >> 1)%MOD;
ret = (ret * ret) % MOD;
if(k & 1){
ret *= n;
ret %= MOD;
} return ret;
}
int main(){
fastio;
int T;
cin >> T;
while (T--) {
int n, k;
cin >> n >> k;
cout<< f(n,k) <<"\n";
}
}
```
## Exponentiation II [problem](https://cses.fi/problemset/task/1712)
```markdown
題目:
- 給定多組查詢,每組包含三個整數 a、b、c。
- 要你計算 (a^(b^c)) mod 10^9+7。
- 因為 b^c 可能非常大,不能直接算出再取模,必須分層處理。
```
``` markdown
解法 :
### 1. 使用模反冪的性質簡化問題:
- 根據歐拉定理(若 a 與 m 互質,則 a^φ(m) ≡ 1 mod m):
- 可將 exponent 降模為 φ(MOD) = 10^9+6。
- 所以 b^c % (MOD−1) 可作為 a 的最終次方。
- 最終為:a^(b^c % (MOD−1)) % MOD。
---
### 2. 巢狀快速冪處理:
- 先用快速冪計算 b^c mod (MOD−1),再算 a^result mod MOD。
- 遞迴實作中 `t` 控制當前模數是 MOD(t=0)還是 MOD−1(t=1),防止取錯模。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
LL solve(LL n, LL k, LL t)
{
if(k == 1)
return n;
if(k == 0)
return 1;
LL ret = solve(n, k >> 1, t) % (MOD-t);
ret = (ret * ret) % (MOD - t);
if(k & 1){
ret *= n;
ret %= (MOD - t);
} return ret;
}
int main(){
fastio;
int T;
cin >> T;
while (T--) {
int a, b, c;
cin >> a >> b >> c;
cout<< solve(a, solve(b, c, 1), 0) <<"\n";
}
}
```
## Counting Divisors [problem](https://cses.fi/problemset/task/1713)
```markdown
題目:
- 給定 T 筆查詢,每筆輸入一個正整數 n。
- 對每個 n,輸出它的**正因數個數**。
- 例如:12 有因數 {1, 2, 3, 4, 6, 12},總共 6 個。
```
``` markdown
解法 :
### 1. 利用質因數分解求因數個數:
- 如果 n 的質因數分解為:n = p1^e1 × p2^e2 × ... × pk^ek,
- 則它的正整數因數個數為:(e1+1) × (e2+1) × ... × (ek+1)。
- 程式中做法:
- 先對 2 和 3 特別處理(效率最佳化)。
- 接著以 6 為間隔測試質數(i, i+2)來進行 trial division。
- 若 n 最後大於 1,代表它本身是個大質數,也需要加入。
- 最後對所有質因數的次方加一並相乘,即為答案。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
int main(){
fastio;
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
map<int, int> m;
while((n % 2) == 0){
m[2]++;
n >>= 1;
} while((n % 3) == 0){
m[3]++;
n /= 3;
} for(int i = 5; i * i <= n; i += 6){
while((n % i) == 0){
m[i]++;
n /= i;
} while((n % (i+2)) == 0){ // 別忘了 i+2
m[i+2]++;
n /= (i+2);
}
}
if (n > 1) m[n]++;
LL ans = 1;
for(auto e: m){
ans *= e.s + 1;
}
cout << ans << '\n';
}
}
```
## Common Divisors [problem](https://cses.fi/problemset/task/1081)
```markdown
題目:
- 給定 n 個整數,從中選出兩個數,問這些數中**最大的共同因數(gcd ≥ 1)**可能是幾?
- 換句話說:求所有數對中,最大可能的 gcd。
```
``` markdown
解法 :
### 1. 頻率表 + 反向枚舉因數:
- 用 freq[x] 記錄每個數字出現次數。
- 從 d = 1e6 開始向下枚舉每個可能的 gcd 值 d。
- 枚舉所有 d 的倍數 m,統計 freq[m] 出現幾次。
- 若某個 d 作為因數時,出現的數量 ≥ 2,表示存在至少一對數的 gcd 為 d。
- 最先找到的 d 就是最大的合法解,因為從大到小枚舉。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
int main(){
fastio;
int T = 1;
while (T--) {
int n;
cin >> n;
vector<int> freq(1e6 + 5, 0);
for(int i=0; i<n; ++i) {
int x;
cin >> x;
freq[x]++;
}
for(int d=(1e6); d>=1; --d) {
int cnt = 0;
for(int m=d; m<=1e6; m+=d) {
cnt += freq[m];
if(cnt >= 2) break;
}
if(cnt >= 2) {
cout << d << endl;
return 0;
}
}
}
}
```
## Sum of Divisors [problem](https://cses.fi/problemset/task/1082)
```markdown
題目:
- 給定一個正整數 n,請計算:
- σ(n) = ∑_{i=1}^{n} (n // i) × i。
- 最後答案對 10^9+7 取模。
- 注意:n 非常大,最多到 10^12,不能直接暴力枚舉每個 i。
```
``` markdown
解法 :
### 1. 數學推導 + 分塊技巧(Divisor Summation Trick):
- 對於所有的 i ∈ [1, n],n // i 的值其實只會改變 √n 次。
- 令 q = n // i 固定,則能找到一段連續的區間 [l, r],使得 n//l = n//r = q。
- 可以對 q 同值的整段 i 批次計算:∑_{i=l}^{r} i = (l + r)(r − l + 1)/2。
- 每段的貢獻為:q × ∑_{i=l}^{r} i。
---
### 2. 實作細節:
- 使用快速乘法與模逆元:
- (l + r)(r − l + 1)/2 mod MOD 使用 inv2 = 500000004(因為 2 的模逆元)。
- 記得每一項都對 MOD 取模,避免 overflow。
- 時間複雜度:O(√n)
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
int main(){
fastio;
LL n;
cin >> n;
LL ans = 0;
for (LL l=1; l<=n; ) {
LL k = n/l;
LL r = n/k;
const LL inv2 = 500000004; // 因為 (2*inv2) ≡ 1 (mod 1e9+7)
LL cnt = (r - l + 1) % MOD;
LL sum_lr = (l + r) % MOD;
LL sum_d = cnt * sum_lr % MOD * inv2 % MOD;
ans = (ans + k % MOD * sum_d % MOD) % MOD;
l = r+1;
}
cout << ans << '\n';
}
```
## Divisor Analysis [problem](https://cses.fi/problemset/task/2182)
```markdown
題目:
- 給定 n 個質因數分解的結果,每個為 (p, a),代表整數 N = p₁^a₁ × p₂^a₂ × ... × pₙ^aₙ。
- 請輸出以下三個值(mod 10^9+7):
1. N 的因數總數。
2. N 的所有因數之和。
3. N 的所有因數之積。
```
``` markdown
解法 :
### 1. 因數總數:
- 利用公式:N 的因數總數為 (a₁+1) × (a₂+1) × ... × (aₙ+1)。
---
### 2. 因數總和:
- 每個質因數 pᵢ 貢獻的總和為等比數列:
- Sᵢ = 1 + pᵢ + pᵢ² + ... + pᵢ^aᵢ = (pᵢ^(aᵢ+1) − 1)/(pᵢ − 1)。
- 取 mod 需要使用乘法逆元來處理除法:
- inv(p−1) ≡ pow(p−1, MOD−2)。
---
### 3. 因數積(關鍵):
- 因數個數為 D,則因數積為 N^(D/2)。
- 但要注意 MOD 為質數,指數模運算需用 MOD−1:
- 使用費馬小定理:a^b mod M,若 b 很大,要對 b 取 mod M−1。
- 處理奇偶:
- 若 D 為奇數,將某個 (aᵢ+1)/2,其他正常,確保整體指數為整數。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
LL pow(LL n, LL k){
if(k == 1LL){
return n;
}
if(k == 0LL){
return 1;
}
LL ret = pow(n, k >> 1) % (MOD);
ret *= ret % MOD;
ret %= MOD;
if(k & 1){
ret *= n % MOD;
ret %= MOD;
}return ret;
}
int main(){
fastio;
LL n;
cin >> n;
vector<pair<LL, LL>> prime(n);
for(auto &x: prime){
cin >> x.f >> x.s;
}
LL ans = 1LL;
for(auto x: prime){
ans *= x.s + 1;
ans %= MOD;
} cout << ans;
ans = 1LL;
for(auto x: prime){
LL GP = pow(x.f, x.s + 1) % MOD;
LL inv = pow(x.f - 1, MOD - 2) % MOD;
ans = ans * (((GP-1) * inv) % MOD) % MOD;
} cout << ' ' << ans;
ans = 1LL;
LL num1=1;
LL flag=0;
for(int i=0;i<n;i++)
{
if(prime[i].s%2==1 && flag==0)
{
num1*=((prime[i].s+1)/2);
num1%=MOD - 1;
flag=1;
}
else
{
num1*=(prime[i].s+1) % (MOD - 1);
num1%= (MOD - 1);
}
}
if(flag==0)
{
for(int i=0;i<n;i++)
{
prime[i].s /= 2;
}
}
LL number=1;
for(int i=0;i<n;i++)
{
number *= pow(prime[i].f, prime[i].s);
number %= MOD;
}
ans = pow(number,num1);
cout << ' ' << ans;
}
```
## Prime Multiples [problem](https://cses.fi/problemset/task/2185)
```markdown
題目:
- 給定一個整數 n 和 k 個質數 a₁, a₂, ..., aₖ。
- 問從 1 到 n 中,有多少整數是這些質數的倍數?
- 即:有幾個整數 i ∈ [1, n] 滿足 i mod aⱼ = 0 for some j。
```
``` markdown
解法 :
### 1. 容斥原理 + 枚舉子集:
- 題目要求的是 union:|A₁ ∪ A₂ ∪ ... ∪ Aₖ|。
- 使用容斥原理:
- 每個非空子集 S 對應的貢獻為:
- (+1)^|S| × floor(n / lcm(S))。
- 所以對所有子集 mask,計算其 lcm,並將貢獻加總:
- 若子集大小為奇數 → 加;
- 若子集大小為偶數 → 減。
---
### 2. 處理 overflow:
- lcm(a₁, a₂, ..., aⱼ) 容易爆 long long。
- 因此在乘法前判斷:`prod > n / a[i]` 則提早跳出(避免爆炸)。
- 此技巧保證運算過程安全。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
ll n;
int k;
cin >> n >> k;
vector<ll> a(k);
for (int i = 0; i < k; ++i) cin >> a[i];
ll ans = 0;
// 枚舉非空子集
for (int mask = 1; mask < (1 << k); ++mask) {
ll prod = 1;
bool overflow = false;
for (int i = 0; i < k; ++i) {
if ((mask >> i) & 1) {
if (prod > n / a[i]) {
overflow = true;
break;
}
prod *= a[i];
}
}
if (overflow) continue;
int bits = __builtin_popcount(mask);
if (bits % 2 == 1)
ans += n / prod;
else
ans -= n / prod;
}
cout << ans << '\n';
}
```
## Counting Coprime Pairs [problem](https://cses.fi/problemset/task/2417)
```markdown
題目:
- 給定一個長度為 n 的整數陣列,問有多少對數對 (i, j) 滿足:
- 1 ≤ i < j ≤ n,且 gcd(a[i], a[j]) = 1。
- 也就是要計算總共有哪些互質的數對。
```
``` markdown
解法 :
### 1. 容斥 + 穆比烏斯函數(Mobius function):
- 如果我們能算出 gcd(a[i], a[j]) = d 的數對有幾對,就能透過容斥來求出 gcd = 1 的情況。
- 令 sum[d] 表示陣列中是 d 的倍數的數量。
- 則 gcd(x, y) 為 d 的數對總數為 C(sum[d], 2)。
---
### 2. 穆比烏斯反演計算互質對數:
- 使用 Mobius function μ(d),可得:
- 互質對數 = ∑_{d=1}^{max} μ(d) × C(sum[d], 2)
- 其中 C(sum[d], 2) = sum[d] × (sum[d]−1)/2
---
### 3. 實作步驟:
- 使用線性篩預處理 μ 值。
- cnt[x]:記錄每個值 x 出現次數。
- sum[d]:統計所有是 d 的倍數的數有幾個。
- 枚舉每個 d,若 μ(d) ≠ 0,累加 μ(d) × C(sum[d], 2)
- 最後得到所有互質對數量。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX = 1e6 + 5;
int n, a[MAX], cnt[MAX], mu[MAX];
ll sum[MAX];
void getMobius() {
vector<int> prime;
vector<bool> isPrime(MAX, true);
mu[1] = 1;
for (int i = 2; i < MAX; ++i) {
if (isPrime[i]) {
prime.push_back(i);
mu[i] = -1;
}
for (int p : prime) {
if (i * p >= MAX) break;
isPrime[i * p] = false;
if (i % p == 0) {
mu[i * p] = 0;
break;
} else {
mu[i * p] = mu[i] * mu[p];
}
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
vector<int> nums(n);
for (int i = 0; i < n; ++i) {
cin >> nums[i];
cnt[nums[i]]++;
}
getMobius();
for (int d = 1; d < MAX; ++d)
for (int m = d; m < MAX; m += d)
sum[d] += cnt[m];
ll total = 0;
for (int d = 1; d < MAX; ++d) {
if (mu[d] == 0) continue;
ll pairs = sum[d] * (sum[d] - 1) / 2;
total += mu[d] * pairs;
}
cout << total << '\n';
}
```
## Next Prime [problem](https://cses.fi/problemset/task/3396)
```markdown
題目:
- 給定 t 筆查詢,每筆為一個整數 n。
- 對每筆查詢,輸出比 n 大的最小質數。
- 注意 n 的數值可以非常大(最大為 2^63−1),需要一個快速且準確的質數判定方法。
```
``` markdown
解法 :
### 1. 使用 Miller-Rabin 質數測試(確定版):
- 對於 64-bit 整數,可以使用特定 base 的 Miller-Rabin 測試組合,達到**確定性質數判定**。
- 測試流程:
- 把 n−1 寫成 d × 2^r。
- 驗證 base^d ≡ 1 或 base^{d×2^i} ≡ −1(mod n)是否成立。
- 若都不成立,n 為合數。
---
### 2. 快速冪(模乘時使用 __int128):
- 因為質數 n 可能接近 2^63−1,乘法需避免 overflow。
- 使用 `(__int128)` 來保證乘法過程中不會超界。
---
### 3. 搜尋邏輯:
- 從 n+1 開始暴力遞增,直到找到第一個 isPrime(n) 為 true 的數,即為答案。
- 由於 Miller-Rabin 判定極快,整體效能可接受。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 快速冪 (a^b mod m)
ll qpow(ll a, ll b, ll m) {
ll res = 1 % m;
a %= m;
while (b > 0) {
if (b & 1) res = (ll)((__int128)res * a % m);
a = (ll)((__int128)a * a % m);
b >>= 1;
}
return res;
}
// Miller-Rabin 單次測試
bool check(ll n, ll base) {
if (n % base == 0) return false;
ll d = n - 1;
int r = 0;
while ((d & 1) == 0) d >>= 1, r++;
ll x = qpow(base, d, n);
if (x == 1 || x == n - 1) return true;
for (int i = 1; i < r; ++i) {
x = (ll)((__int128)x * x % n);
if (x == n - 1) return true;
}
return false;
}
// 確定版質數測試(64-bit 安全)
bool isPrime(ll n) {
if (n < 2) return false;
if (n == 2 || n == 3) return true;
if (n % 2 == 0) return false;
static const ll bases[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
for (ll base : bases) {
if (base >= n) break;
if (!check(n, base)) return false;
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
ll n;
cin >> n;
++n;
while (!isPrime(n)) ++n;
cout << n << '\n';
}
}
```
## Binomial Coefficients [problem](https://cses.fi/problemset/task/1079)
```markdown
題目:
- 給定多組查詢,每次給你整數 a 和 b,要求你計算:
- C(a, b) = a! / (b! × (a − b)!) mod 10^9+7
```
``` markdown
解法 :
### 1. 組合數公式:
- C(a, b) = a! / (b!(a−b)!)。
- 由於 MOD 是質數,可以使用**費馬小定理**處理除法:
- x / y ≡ x × y^(MOD−2) (mod MOD)
---
### 2. 實作細節:
- 預先計算 0 到 10^6 的階乘表 `table[i] = i! mod MOD`
- 每次查詢時:
- 先取 table[a]。
- 再乘上 table[b] 的乘法逆元。
- 再乘上 table[a−b] 的乘法逆元。
- 時間複雜度:
- 前處理:O(N)
- 每次查詢:O(log MOD)(快速冪取 inverse)
---
### 3. 注意事項:
- 當 b = 0 或 b = a 時,C(a, b) = 1,自然地這套公式仍然適用。
- 使用快速冪 pow(base, MOD−2) 處理模逆元,確保效能與正確性。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
LL pow(LL n, LL k){
if(k == 1LL){
return n;
}
if(k == 0LL){
return 1;
}
LL ret = pow(n, k >> 1) % (MOD);
ret *= ret % MOD;
ret %= MOD;
if(k & 1){
ret *= n % MOD;
ret %= MOD;
}return ret;
}
int main(){
fastio;
int t;
cin >> t;
vector<LL> table(1e6 + 10, 1LL);
for(LL i = 1LL; i <= 1e6; ++i){
table[i] = (table[i - 1LL] * i) % MOD;
}
while(t--){
LL a, b;
cin >> a >> b;
LL inv1 = pow(table[b], MOD - 2LL);
LL inv2 = pow(table[a - b], MOD - 2LL);
cout << ((table[a] * inv1 % MOD)* inv2 % MOD) << '\n';
}
}
```
## Creating Strings II [problem](https://cses.fi/problemset/task/1079)
```markdown
題目:
- 給定一個小寫字母組成的字串 s。
- 請你計算有多少種不同的排列方式(字典順序不限)。
- 最後答案對 10^9+7 取模。
```
``` markdown
解法 :
### 1. 排列組合公式:
- 假設字串長度為 n,且每個字母出現次數為 freq₁, freq₂, ..., freqₖ。
- 那麼總排列數為:
- n! / (freq₁! × freq₂! × ... × freqₖ!)
- 因為涉及除法,需要使用**模逆元**處理:
- x / y ≡ x × y^(MOD−2) (mod MOD)
---
### 2. 實作細節:
- 預先計算階乘表 table[i] = i! mod MOD。
- 統計每個字母出現次數 vocab[i]。
- 計算:
- 分子為 table[n]
- 每個 freq[i]! 取模後再取 inverse,相乘進去。
- 時間複雜度:
- O(N + 26logMOD),適用於長度最多 1e6 的情況。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
LL pow(LL n, LL k){
if(k == 1LL){
return n;
}
if(k == 0LL){
return 1;
}
LL ret = pow(n, k >> 1) % (MOD);
ret *= ret % MOD;
ret %= MOD;
if(k & 1){
ret *= n % MOD;
ret %= MOD;
}return ret;
}
int main(){
fastio;
vector<LL> table(1e6 + 10, 1LL);
for(LL i = 1LL; i <= 1e6; ++i){
table[i] = (table[i - 1LL] * i) % MOD;
}
string input;
cin >> input;
vector<LL> vocab(26, 0LL);
for(int i = 0; input[i]; ++i){
vocab[input[i] - 'a']++;
}
LL ans = table[input.length()];
for(int i = 0; i < 26; ++i){
if(vocab[i] == 0)
continue;
LL inv = pow(table[vocab[i]], MOD - 2);
ans = (ans * inv) % MOD;
}
cout << ans << '\n';
}
```
## Distributing Apples [problem](https://cses.fi/problemset/task/1716)
```markdown
題目:
- 有 m 顆蘋果要分給 n 個小孩,每個小孩可以拿 0 顆以上。
- 問有幾種不同的分法(不同分配順序視為不同分法)。
- 最後答案對 10^9+7 取模。
```
``` markdown
解法 :
### 1. 轉換為組合數問題:
- 問題等價於:
- 把 m 顆蘋果放入 n 個桶子中,每桶可為 0。
- 這是經典「**整數劃分 + 重複組合**」問題。
- 對應公式為:
- C(n + m − 1, m)
- 選 m 個蘋果要插入 n−1 個隔板(可重複)
---
### 2. 組合數計算:
- 使用組合公式:
- C(a, b) = a! / (b! × (a − b)!)
- 預先算出階乘表 table[i]。
- 使用快速冪處理除法模逆:
- y^(-1) ≡ y^(MOD−2) mod MOD
- 最終答案為:
- table[n+m−1] × inv(table[m]) × inv(table[n−1])
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
bool cmp(pair<pLL, LL> a, pair<pLL, LL> b){
if(a.f.f == b.f.f){
return a.f.s > b.f.s;
} return a.f.f < b.f.f;
}
bool cmp(pLL a, pLL b){
return (a.f == b.f) ? a.s > b.s : a.f < b.f;
}
LL pow(LL n, LL k){
if(k == 1LL){
return n;
}
if(k == 0LL){
return 1;
}
LL ret = pow(n, k >> 1) % (MOD);
ret *= ret % MOD;
ret %= MOD;
if(k & 1){
ret *= n % MOD;
ret %= MOD;
}return ret;
}
int main(){
fastio;
vector<LL> table(2e6 + 10, 1LL);
for(LL i = 1LL; i <= 2e6; ++i){
table[i] = (table[i - 1LL] * i) % MOD;
}
LL n, m;
cin >> n >> m;
LL ans = table[n + m - 1];
cout << ((ans * pow(table[m], MOD - 2) % MOD) * pow(table[n - 1], MOD - 2) % MOD)<< '\n';
}
```
## Christmas Party [problem](https://cses.fi/problemset/task/1717)
```markdown
題目:
- 有 n 個人參加聖誕派對,每個人可以選擇:
- 要和某個人互相交換禮物(兩人一組),或
- 不參與交換。
- 每人最多參加一組,問總共有幾種不同的分組方式(mod 10^9+7)。
```
``` markdown
解法 :
### 1. 將問題轉換為「錯排(Derangement)」類型:
- 這是一個經典的「**容斥 + 配對**」動態規劃問題。
- 定義 D[i] 為 i 個人能產生的有效配對方案數。
- 遞推關係:
- D[1] = 0(1 個人無法成對)
- D[2] = 1(只有 1 種互換)
- D[n] = (n−1) × (D[n−1] + D[n−2])
- 選一人 A,有兩種情況:
1. A 和某人配對:剩下 n−2 人問題轉為 D[n−2]
2. A 不配對:剩下 n−1 人問題轉為 D[n−1]
---
### 2. 實作細節:
- 使用動態規劃陣列 D[] 記錄答案。
- 每次都對 MOD 取模避免 overflow。
- 時間與空間複雜度:O(n)
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
LL pow(LL n, LL k){
if(k == 1LL){
return n;
}
if(k == 0LL){
return 1;
}
LL ret = pow(n, k >> 1) % (MOD);
ret *= ret % MOD;
ret %= MOD;
if(k & 1){
ret *= n % MOD;
ret %= MOD;
}return ret;
}
int main(){
fastio;
int n;
cin >> n;
vector<long long> D(n + 1);
D[1] = 0;
D[2] = 1;
for(int i=3;i<=n;i++) {
D[i] = (i-1) * (D[i-1] + D[i-2]) % MOD;
}
cout << D[n] << '\n';
}
```
## Permutation Order [problem](https://cses.fi/problemset/task/3397)
```markdown
題目:
- 有兩種查詢操作,共 t 筆:
1. 給你 n 和 k,請你輸出在所有 n! 個排列中字典序第 k 小的排列。
2. 給你一個 n 的排列,請你輸出它在所有排列中的字典序排名(從 1 開始)。
```
``` markdown
解法 :
### 1. 查詢類型 1:第 k 小的排列
- 這是**康托展開反推(Cantor Unranking)**的問題。
- 對於長度 n 的排列:
- 每一段有 (n−1)! 種開頭選法。
- 找到 k 屬於哪個區段後,遞迴解剩下的部分。
- 時間複雜度:O(n^2),使用 table[] 來紀錄剩餘可選數字。
---
### 2. 查詢類型 2:給定排列求排名
- 這是**康托展開(Cantor Ranking)**的問題。
- 對於每個位置,計算目前未用數中有多少比當前值小的。
- 計算這些小值能形成多少個排列數,加總即為排名。
- 最後記得要加 1,因為題目要求從 1 開始算排名。
---
### 3. 技術細節與實作
- 需用一個 table[] 或 bit[] 來記錄哪些數字已經被選過。
- 預處理 factorial:最大值約到 20! 以內。
- 注意康托展開與反展開都需避免重複計數與 index 錯誤。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
LL pow(LL n, LL k){
if(k == 1LL){
return n;
}
if(k == 0LL){
return 1;
}
LL ret = pow(n, k >> 1) % (MOD);
ret *= ret % MOD;
ret %= MOD;
if(k & 1){
ret *= n % MOD;
ret %= MOD;
}return ret;
}
LL findKth(LL n, vector<LL> perm){
LL ret = 0, factorial = 1;
for(int i = 1; i<n; ++i){
factorial *= i;
}
vector<LL> table(n + 1, 1);
for(int i = 0; i < n - 1; ++i){
int cnt = 0;
for(int j = 1; j <= perm[i]; ++j){
cnt += table[j];
} table[perm[i]] = 0;
ret += (cnt - 1) * factorial;
factorial /= (n - i - 1);
} return ret;
}
vector<LL> getKth(LL n, LL k){
if(n == 1){
return vector<LL>(1, k);
}
LL section = 1;
for(LL i = 1; i < n; ++i){
section *= i;
}
vector<LL> ret;
for(LL i = 1; i <= n; ++i){
if(k <= section * (i)){
ret = getKth(n - 1, k - (section * (i-1)));
ret.push_back(i);
break;
}
} return ret;
}
int main(){
fastio;
int t;
cin >> t;
while(t--){
int cmd;
cin >> cmd;
if(cmd == 1) {
LL n, k;
cin >> n >> k;
vector<LL> perm = getKth(n, k);
reverse(perm.begin(), perm.end());
vector<LL> table(n + 1, 1);
for(auto &e: perm){
for(int i = 1, cnt = 0; i <= n; ++i){
if(cnt + table[i] == e){
e = i;
table[i] = 0;
break;
} cnt+= table[i];
}
}
for(auto e: perm){
cout << e << ' ';
} cout << '\n';
} else {
int n;
cin >> n;
vector<LL> perm(n);
for(auto &e: perm)
cin >> e;
cout << findKth(n, perm) + 1 << '\n';
}
}
}
```
## Permutation Rounds [problem](https://cses.fi/problemset/task/3398)
```markdown
題目:
- 給定一個長度為 N 的排列 perm[1..N],定義一場遊戲如下:
- 每次從一個尚未被拜訪的點開始,沿著 perm 的映射跳,直到回到起點,形成一個 cycle。
- 問你:**所有 cycle 長度的最小公倍數(LCM)是多少?**
- 輸出對 10^9+7 取模的結果。
```
``` markdown
解法 :
### 1. 尋找 cycle 長度:
- 每個元素剛好出現在一個 cycle 中。
- 使用 DFS 或 while-loop 掃描 permutation,記錄每個 cycle 的長度 len。
- 對所有 len 做 LCM。
---
### 2. 如何高效地取模 LCM?
- 直接算 LCM 會爆 long long,因此將每個長度分解質因數:
- LCM(a, b, ...) = 乘上所有質因數的最大次方。
- 對每個長度做質因數分解,並用 `map<int, int>` 存每個質因數的最高次方。
---
### 3. 實作細節:
- 使用埃氏篩先求出質數表(primes[])。
- 對每個 cycle 的長度進行分解質因數,更新每個質數的最大指數。
- 最後重建答案:
- ans = ∏ (p^e) for each (p, e) in map。
- 用快速乘法處理模 10^9+7。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxN = 2e5+1;
const ll MOD = (ll) 1e9+7;
int N, perm[maxN];
bool vis[maxN], is_prime[maxN];
vector<int> primes;
map<int,int> ans;
void init_primes(){
for(int i = 2; i < maxN; i++) is_prime[i] = true;
for(int i = 2; i < maxN; i++){
if(is_prime[i]){
primes.push_back(i);
for(int j = 2 * i; j < maxN; j += i)
is_prime[j] = false;
}
}
}
int dfs(int u){
vis[u] = true;
if(vis[perm[u]]) return 1;
return dfs(perm[u]) + 1;
}
void prime_factor(int x){
for(int i = 0; i < (int) primes.size() && primes[i] <= x; i++){
int p = primes[i];
int num_divisions = 0;
while(x % p == 0){
x /= p;
num_divisions++;
}
if(num_divisions > 0)
ans[p] = max(ans[p], num_divisions);
}
}
int main(){
init_primes();
scanf("%d", &N);
for(int i = 1; i <= N; i++)
scanf("%d", &perm[i]);
for(int u = 1; u <= N; u++){
if(!vis[u]){
int len = dfs(u);
prime_factor(len);
}
}
ll prod = 1;
for(auto const& [prime, power] : ans)
for(int i = 0; i < power; i++)
prod = (prod * prime) % MOD;
printf("%lld\n", prod);
}
```
## Bracket Sequences I [problem](https://cses.fi/problemset/task/2064)
```markdown
題目:
- 給定一個整數 n,問有多少個長度為 n 的合法括號序列。
- 括號序列只能包含 `()`,且必須合法(左括號數量等於右括號,且任意前綴中左括號不少於右括號)。
- 輸出對 10^9+7 取模的結果。
```
``` markdown
解法 :
### 1. 卡特蘭數公式(Catalan Number):
- 合法括號序列數量對應第 k 個卡特蘭數(k = n/2):
- Cₖ = C(2k, k) / (k + 1)
- 即:Cₖ = (2k)! / (k!(k+1)!)
- 若 n 為奇數,直接輸出 0。
---
### 2. 快速計算組合數:
- 預先計算 fact[i] = i! mod MOD,以及其反元素 invfact[i]。
- 使用快速冪計算模反元素:
- a^(-1) ≡ a^(MOD - 2) mod MOD(MOD 為質數時)
---
### 3. 實作技巧:
- `fact[]` 與 `invfact[]` 須開到 2e6+5,避免 overflow。
- 注意不要在模數下做除法,應使用乘上逆元。
- 最終答案為:
- catalan = C(2k, k) × inv(k + 1) mod MOD
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX = 2e6 + 5; // 要開到 2 * 1e6
const ll MOD = 1e9 + 7;
ll fact[MAX], invfact[MAX];
// 快速冪
ll power(ll a, ll b) {
ll res = 1;
while (b) {
if (b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
// 組合數
ll C(int n, int k) {
if (k < 0 || k > n) return 0;
return fact[n] * invfact[k] % MOD * invfact[n - k] % MOD;
}
// 初始化階乘與反元素
void init() {
fact[0] = invfact[0] = 1;
for (int i = 1; i < MAX; i++)
fact[i] = fact[i - 1] * i % MOD;
invfact[MAX - 1] = power(fact[MAX - 1], MOD - 2);
for (int i = MAX - 2; i >= 1; i--)
invfact[i] = invfact[i + 1] * (i + 1) % MOD;
}
int main() {
int n;
cin >> n;
if (n % 2 == 1) {
cout << 0 << '\n';
return 0;
}
init();
int k = n / 2;
ll catalan = C(2 * k, k) * power(k + 1, MOD - 2) % MOD;
cout << catalan << '\n';
}
```
## Bracket Sequences II [problem](https://cses.fi/problemset/task/2187)
```markdown
題目:
- 給定一個整數 n(括號總長度)和一段長度為 k 的合法括號前綴字串 prefix。
- 請問:在補上剩下的 n − k 個括號後,有多少種方法可以形成一個**完整合法**的括號序列?
- 所有結果對 10^9+7 取模。
```
``` markdown
解法 :
### 1. 前綴合法性檢查:
- 若 prefix 本身不合法(右括號比左括號多),直接輸出 0。
- 合法前綴定義:任意前綴中左括號數 ≥ 右括號數。
---
### 2. 剩餘括號補法計算:
- 計算剩餘括號數 rem = n − k。
- 假設目前 balance = open − close(還欠 balance 個 `)` 才平衡)。
- 想要形成合法序列,需保證:
- 剩下的括號能補足這個 balance,並形成完整對。
- 所以:(rem + balance) 必須為偶數,且 rem ≥ balance。
---
### 3. 將問題轉為 Catalan-like:
- 設 a 為要補的左括號個數,b 為要補的右括號個數:
- a = (rem + balance) / 2
- b = rem − a
- 最後用標準公式計算合法括號數:
- 答案 = C(a + b, a) − C(a + b, a + 1)
- C(a + b, a): 所有的左右配對可能
- C(a + b, a + 1): 不合法的括號(會出現過多右括號)
- 這是經典的「Catalan 數列在有限前綴條件下的應用」
---
### 4. 計算組合數技巧:
- 使用預處理 factorial 和 inverse factorial 表
- 所有除法都改為乘上逆元(MOD 為質數)
- C(n, k) = fact[n] × invfact[k] × invfact[n − k] mod MOD
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAX = 2e6 + 5; // 要夠大支援最大 n = 1e6
ll fact[MAX], invfact[MAX];
ll power(ll a, ll b) {
ll res = 1;
while (b) {
if (b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
void init() {
fact[0] = invfact[0] = 1;
for (int i = 1; i < MAX; i++)
fact[i] = fact[i - 1] * i % MOD;
invfact[MAX - 1] = power(fact[MAX - 1], MOD - 2);
for (int i = MAX - 2; i >= 1; i--)
invfact[i] = invfact[i + 1] * (i + 1) % MOD;
}
ll C(int n, int k) {
if (k < 0 || k > n) return 0;
return fact[n] * invfact[k] % MOD * invfact[n - k] % MOD;
}
int main() {
init();
int n;
string prefix;
cin >> n >> prefix;
int k = prefix.length();
int open = 0, close = 0;
for (char c : prefix) {
if (c == '(') open++;
else close++;
if (close > open) {
cout << 0 << '\n';
return 0; // 不合法:右括號比左括號還多
}
}
int rem = n - k;
int balance = open - close;
// 如果剩下不是偶數個,無法湊成合法對
if ((rem + balance) % 2 != 0 || rem < balance) {
cout << 0 << '\n';
return 0;
}
int a = (rem + balance) / 2; // 要補的左括號
int b = rem - a; // 要補的右括號
// 使用 Catalan-like 公式:C(a + b, a) - C(a + b, a + 1)
ll ans = (C(a + b, a) - C(a + b, a + 1) + MOD) % MOD;
cout << ans << '\n';
}
```
## Counting Necklaces [problem](https://cses.fi/problemset/task/2209)
```markdown
題目:
- 給定整數 n(珠子的數量)和 m(每顆珠子可選的顏色數),
- 問有多少種不同的「項鍊塗色方式」?
- 項鍊是環狀結構,若透過旋轉可以互相對應,則視為相同方案。
- 結果對 10^9+7 取模。
```
``` markdown
解法 :
### 1. Burnside 引理(Burnside’s Lemma):
- 我們要算的是:在 n 個位置中,有多少種 m 顏色的上色方法,對所有旋轉情況下不重複計數。
- 根據 Burnside 引理:
- ans = (1/n) × ∑_{r=0}^{n−1} m^gcd(n, r)
- r 表示旋轉 r 位
- gcd(n, r) 表示固定點數(不變的位置)
- m^gcd(n, r):固定後每個 cycle 可以隨意染色
---
### 2. 快速冪與模逆元:
- 因為每次都需要計算 m^gcd(n, r),使用快速冪加速。
- 最後的除法 `(sum / n) mod MOD`,改為乘上 n 的乘法逆元:
- 使用 `mod_pow(n, MOD − 2)` 來計算。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
// 快速冪 m^k % MOD
ll mod_pow(ll m, ll k) {
ll res = 1;
while (k) {
if (k & 1) res = res * m % MOD;
m = m * m % MOD;
k >>= 1;
}
return res;
}
// m^gcd(n, r) 對所有 r=0 到 n-1 做和,再除以 n
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll n, m;
cin >> n >> m;
ll sum = 0;
for (ll r = 0; r < n; ++r) {
ll g = __gcd(n, r);
sum = (sum + mod_pow(m, g)) % MOD;
}
// 除以 n,相當於乘上 n 的乘法反元素
ll inv_n = mod_pow(n, MOD - 2);
ll ans = sum * inv_n % MOD;
cout << ans << '\n';
}
```
## Counting Grids [problem](https://cses.fi/problemset/task/2210)
```markdown
題目:
- 給定一個 n×n 的網格,每格可填入 0 或 1(共 2 種顏色),
- 兩個填色方案若經過旋轉(0°, 90°, 180°, 270°)後一致,則視為**同一種**填法。
- 問有多少種不同的填色方式,mod 10^9+7。
```
``` markdown
解法 :
### 1. Burnside 引理(Burnside’s Lemma):
- 要計算在所有對稱操作下的不重複配置數量,可使用 Burnside 引理:
- ans = (1/|G|) × ∑_{g∈G} Fix(g)
- G:所有操作(本題為旋轉 0°, 90°, 180°, 270°)
- Fix(g):在操作 g 下不變的塗色方案數
---
### 2. 旋轉類型的固定點數:
- **0° (恆等變換)**:全部固定,2^(n²)
- **180°**:每對對稱格子需一致 ⇒ 2^((n² + 1)/2)
- **90° & 270°**:每 4 個格子形成 cycle ⇒ 2^((n² + 3)/4)
- 最後總和為:
- total = (term0 + 2×term90_270 + term180) / 4
---
### 3. 實作技巧:
- 使用快速冪計算模下的大次方。
- 除以 4 使用乘法逆元:`modpow(4, MOD−2)`。
- 注意使用 `(n² + 1)/2` 和 `(n² + 3)/4` 是為了確保整數除法正確。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;
// 快速冪
ll modpow(ll base, ll exp) {
ll res = 1;
while (exp) {
if (exp & 1) res = res * base % MOD;
base = base * base % MOD;
exp >>= 1;
}
return res;
}
int main() {
ll n;
cin >> n;
ll n2 = n * n;
ll term0 = modpow(2, n2);
ll term180 = modpow(2, (n2 + 1) / 2);
ll term90_270 = modpow(2, (n2 + 3) / 4);
ll total = (term0 + 2 * term90_270 + term180) % MOD;
total = total * modpow(4, MOD - 2) % MOD; // 除以 4
cout << total << "\n";
return 0;
}
```
## Fibonacci Numbers [problem](https://cses.fi/problemset/task/1722)
```markdown
題目:
- 給定一個整數 k,請你輸出 Fibonacci(k) 的值(mod 10^9+7)。
- Fibonacci 定義如下:
- F(0) = 0, F(1) = 1
- F(k) = F(k−1) + F(k−2) for k ≥ 2
- 限制:0 ≤ k ≤ 10^18,需要高效計算。
```
``` markdown
解法 :
### 1. 快速矩陣冪(Matrix Exponentiation):
- 對於 Fibonacci 數列,可以用轉移矩陣表示為:
- [F(k) ] = [1 1]^k × [F(1)]
- [F(k−1)] [1 0] [F(0)]
- 因此,我們只需要將矩陣 A = [[1,1],[1,0]] 快速冪到 k−1 次方,即可得到 F(k)。
---
### 2. 優化細節:
- 使用 2×2 矩陣乘法實作。
- 快速冪時間複雜度為 O(log k),可應對高達 10^18 的 k。
- 注意要模 MOD = 10^9+7,避免 overflow。
---
### 3. 特殊情況:
- 若 k = 0,直接輸出 0。
- 若 k = 1 或 2,F(k) = 1,可直接輸出。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1e9+7;
struct Mat {
ll m[2][2] = {{0,0},{0,0}};
Mat operator*(const Mat &b) const {
Mat r;
for(int i=0;i<2;i++)
for(int j=0;j<2;j++)
for(int k=0;k<2;k++)
r.m[i][j] = (r.m[i][j] + m[i][k]*b.m[k][j]%MOD)%MOD;
return r;
}
};
Mat qpow(Mat a, ll k) {
Mat r; // 單位矩陣
r.m[0][0]=r.m[1][1]=1;
while(k) {
if(k&1) r = r * a;
a = a * a;
k >>= 1;
}
return r;
}
int main() {
ll k;
cin >> k;
if(k == 0){
cout << 0 << endl;
return 0;
}
if(k==1 || k==2) {
cout << 1 << endl;
return 0;
}
Mat A;
A.m[0][0]=A.m[0][1]=A.m[1][0]=1;
Mat res = qpow(A, k-2);
ll ans = (res.m[0][0] + res.m[0][1]) % MOD;
cout << ans << endl;
}
```
## Throwing Dice [problem](https://cses.fi/problemset/task/1096)
```markdown
題目:
- 有一顆六面骰子(面值為 1~6),你每次丟出來的點數會累加。
- 問你總共有幾種方式讓點數總和正好為 n。
- 結果對 10^9+7 取模。
```
``` markdown
解法 :
### 1. 定義與遞推關係:
- 定義 dp[n] 為總和為 n 的合法組合數。
- 轉移式:
- dp[n] = dp[n−1] + dp[n−2] + ... + dp[n−6]
- 初始條件:
- dp[0] = 1(什麼都不丟就是一種方式)
- dp[1] = 1, dp[2] = 2, dp[3] = 4, ..., dp[6] = 32(可直接列舉)
---
### 2. 矩陣快速冪優化:
- 因為轉移式是線性遞推,最多依賴前六項,因此可以使用**矩陣快速冪**優化成 O(log n)。
- 狀態設為長度為 6 的向量 dp[n], dp[n−1], ..., dp[n−5]。
- 使用 6×6 的轉移矩陣 T,使得:
- T^k × base = dp[n],其中 base 是 dp[6], dp[5], ..., dp[1]
---
### 3. 實作細節:
- 若 n ≤ 6,直接輸出對應初始值。
- 否則:
- 對轉移矩陣 T 做 T^(n−6)
- 再與 base 向量做乘法,取第 0 項即為 dp[n]
- 所有乘法與加法都取模,確保不爆 long long。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int MOD = 1e9 + 7;
const int K = 6;
using Matrix = vector<vector<ll>>;
Matrix mul(Matrix& a, Matrix& b) {
Matrix res(K, vector<ll>(K));
for (int i = 0; i < K; ++i)
for (int j = 0; j < K; ++j)
for (int k = 0; k < K; ++k)
res[i][j] = (res[i][j] + a[i][k] * b[k][j]) % MOD;
return res;
}
Matrix power(Matrix base, ll exp) {
Matrix res(K, vector<ll>(K));
for (int i = 0; i < K; ++i)
res[i][i] = 1;
while (exp) {
if (exp & 1)
res = mul(res, base);
base = mul(base, base);
exp >>= 1;
}
return res;
}
int main() {
ll n;
cin >> n;
if (n <= 6) {
// 手動 dp 初始化
vector<ll> base = {1, 2, 4, 8, 16, 32};
cout << base[n - 1] << '\n';
return 0;
}
// base dp[1~6]
vector<ll> base = {1, 2, 4, 8, 16, 32};
// 建立轉移矩陣 T
Matrix T(K, vector<ll>(K));
T[0] = {1,1,1,1,1,1};
for (int i = 1; i < K; ++i)
T[i][i-1] = 1;
Matrix Tn = power(T, n - 6);
ll result = 0;
for (int i = 0; i < K; ++i)
result = (result + Tn[0][i] * base[5 - i]) % MOD;
cout << result << '\n';
}
```
## Graph Paths I [problem](https://cses.fi/problemset/task/1723)
```markdown
題目:
- 給定一張 n 個點、m 條有向邊的圖,以及一個整數 k。
- 問從點 1 出發,到點 n,走恰好 k 步(邊)的不同路徑總數。
```
``` markdown
解法 :
### 1. 觀察:
- 要從 1 到 n 的路徑,且長度正好是 k。
- 這可以轉化成一個典型的**矩陣乘法遞推問題**。
- 若 A 為鄰接矩陣,則 A^k[i][j] 表示從 i 到 j 恰好走 k 步的路徑數。
---
### 2. 矩陣快速冪:
- 使用 n×n 矩陣 A 表示圖的鄰接關係。
- A[i][j] 為從 i 到 j 有幾條邊(可重邊)。
- 要計算 A^k 的結果,使用**快速矩陣冪**:
- 時間複雜度:O(n^3 × log k)
- 最後輸出 A^k[1][n] 即為答案。
---
### 3. 技術細節:
- 自定義結構 Mat 支援乘法運算與初始化。
- 初始化 r 為單位矩陣,用於快速冪起始值。
- 注意所有運算都要取模 MOD = 10^9+7。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9+7;
const int N = 110;
int n;
struct Mat {
ll m[N][N];
Mat() { memset(m, 0, sizeof m); }
Mat operator*(const Mat &b) const {
Mat r;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
r.m[i][j] = (r.m[i][j] + m[i][k]*b.m[k][j]%MOD)%MOD;
return r;
}
};
Mat qpow(Mat a, ll k) {
Mat r;
for(int i=1;i<=n;i++) r.m[i][i]=1;
while(k) {
if(k&1) r = r*a;
a = a*a;
k >>=1;
}
return r;
}
int main() {
int m;
ll k;
cin >> n >> m >> k;
Mat A;
while(m--) {
int u,v;
cin >> u >> v;
A.m[u][v]++;
}
A = qpow(A, k);
cout << A.m[1][n] << endl;
}
```
## Graph Paths II [problem](https://cses.fi/problemset/task/1724)
```markdown
題目:
- 給定一張有向圖(n 個點,m 條邊),每條邊有非負權重。
- 問:從節點 1 出發到節點 n,恰好經過 k 條邊的**最短路徑長度**是多少?
- 若無法在恰好 k 步內抵達,輸出 -1。
```
``` markdown
解法 :
### 1. 觀察與建模:
- 我們要找的是:從點 1 到點 n,經過**恰好 k 條邊**的最短路徑。
- 傳統 Dijkstra / Bellman-Ford 不適用於「剛好 k 步」的限制。
- 解法是:**最短路徑矩陣 + min-plus 矩陣乘法 + 快速冪**。
---
### 2. 定義與操作:
- 定義鄰接矩陣 A,其中 A[i][j] 為從 i 到 j 的邊權(若存在,否則為 ∞)。
- 那麼:
- A^1[i][j] 表示:從 i 到 j 經過 1 條邊的最短路。
- A^2[i][j] 表示:從 i 到 j 經過 2 條邊的最短路。
- ...
- A^k[i][j] 表示:從 i 到 j 經過 k 條邊的最短路。
- 所以,我們只需計算 A^k,然後輸出 A^k[1][n]。
---
### 3. 快速矩陣冪(min,+ 乘法):
- 與一般矩陣乘法不同,我們這邊使用:
- r[i][j] = min over all k of (a[i][k] + b[k][j])
- 稱為 **min-plus 矩陣乘法**。
- 搭配快速冪計算 A^k,在 O(n³ log k) 時間內完成。
---
### 4. 特別注意:
- 初始矩陣除了有邊的地方,其餘皆設為 INF(不能為 0!)
- 計算過程需避免溢位:使用 64-bit 整數。
- 最後答案若為 INF,表示無解,輸出 −1。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9+7;
const int N = 110;
const long long INF = 1LL << 60;
int n;
struct Mat {
ll m[N][N];
Mat() {
for(int i=0;i<N;i++) for(int j=0;j<N;j++)
m[i][j] = INF;
}
Mat operator*(const Mat &b) const {
Mat r;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
r.m[i][j] = min(r.m[i][j], m[i][k] + b.m[k][j]);
return r;
}
};
Mat qpow(Mat a, ll k) {
Mat r = a; k--;
while(k) {
if(k&1) r = r*a;
a = a*a;
k >>=1;
}
return r;
}
int main() {
int m;
ll k;
cin >> n >> m >> k;
Mat A;
while(m--) {
ll u, v, c;
cin >> u >> v >> c;
A.m[u][v] = min(A.m[u][v], c);
}
A = qpow(A, k);
cout << (A.m[1][n] == INF ? -1 : A.m[1][n]) << endl;
}
```
## System of Linear Equations [problem](https://cses.fi/problemset/task/3154)
```markdown
題目:
- 給定一個 n × m 的線性方程組 Ax = b(模質數 MOD)。
- 每個係數與常數項都在 0 到 MOD-1 之間,MOD 是質數(這裡為 1e9+7)。
- 需要求出一組解,如果無解則輸出 -1。
- 如果有多組解,則輸出其中任意一組(自由變數可取 0)。
```
``` markdown
解法 :
### 1. Gaussian Elimination 模數化:
- 將矩陣擴充成 n × (m+1)(最後一欄為常數項 b)。
- 從左到右依序選擇主元列,透過 `modinv` 計算模反元素,使主元歸一化。
- 使用模數下的加減乘消去其他列的該列元素。
---
### 2. 檢查無解條件:
- 如果某一列前 m 個係數全為 0,但最後一欄不為 0,則表示 0 = 非零,無解。
---
### 3. 解的構造:
- `where[col]` 記錄每個變數是否有主元所在的行。
- 對於有主元的變數,解為該行最後一欄值;自由變數取 0 即可。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = 1'000'000'007;
int modinv(int a, int m = MOD) {
int b = m, x = 1, y = 0;
while (b) {
int q = a / b;
tie(a, b) = make_pair(b, a % b);
tie(x, y) = make_pair(y, x - q * y);
}
return (x % m + m) % m;
}
void fastio() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
int main() {
fastio();
int n, m;
cin >> n >> m;
vector<vector<int>> A(n, vector<int>(m + 1)); // augmented matrix: A | b
for (int i = 0; i < n; ++i)
for (int j = 0; j <= m; ++j)
cin >> A[i][j];
vector<int> where(m, -1);
for (int col = 0, row = 0; col < m && row < n; ++col) {
int sel = row;
for (int i = row; i < n; ++i)
if (A[i][col] != 0)
sel = i;
if (A[sel][col] == 0) continue;
swap(A[sel], A[row]);
where[col] = row;
int inv = modinv(A[row][col]);
for (int j = 0; j <= m; ++j)
A[row][j] = (1LL * A[row][j] * inv) % MOD;
for (int i = 0; i < n; ++i) {
if (i != row && A[i][col]) {
int coef = A[i][col];
for (int j = 0; j <= m; ++j) {
A[i][j] = (A[i][j] - 1LL * coef * A[row][j] % MOD + MOD) % MOD;
}
}
}
++row;
}
// Check for inconsistency
for (int i = 0; i < n; ++i) {
bool all_zero = true;
for (int j = 0; j < m; ++j)
if (A[i][j]) all_zero = false;
if (all_zero && A[i][m]) {
cout << -1 << '\n'; // No solution
return 0;
}
}
vector<int> ans(m);
for (int j = 0; j < m; ++j) {
if (where[j] != -1)
ans[j] = A[where[j]][m];
else
ans[j] = 0; // free variable
}
for (int i = 0; i < m; ++i)
cout << ans[i] << " \n"[i == m - 1];
return 0;
}
```
## Sum of Four Squares [problem](https://cses.fi/problemset/task/3355)
```markdown
題目:
- 給定一個整數 n(多組測資)。
- 需要找到四個非負整數 a, b, c, d,使得
a² + b² + c² + d² = n。
- 如果存在多組解,輸出任意一組即可。
- 由於 n 的範圍相對可控,可以直接枚舉。
```
``` markdown
解法 :
### 1. 理論背景:
- 由拉格朗日四平方和定理可知,任意非負整數皆可表示為四個平方數之和。
- 因此必定有解,只需要找到其中一組即可。
---
### 2. 枚舉策略:
- 枚舉 a 從 0 到 sqrt(n)。
- 枚舉 b 從 a 到 sqrt(n - a²)(b ≥ a 可減少重複)。
- 枚舉 c 從 b 到 sqrt(n - a² - b²)(c ≥ b 同理)。
- 剩下的值 rem = n - (a² + b² + c²),判斷是否為完全平方數:
- d = sqrt(rem),如果 d² == rem 則找到解。
---
### 3. 複雜度分析:
- 外層 a 枚舉到 sqrt(n),b 枚舉到 sqrt(n),c 枚舉到 sqrt(n),最壞情況接
- 近O(n^(3/2))。
- 由於有提早終止(找到解後立即輸出並結束該測資),實際運行會快得多,適合中等
- 範圍的 n。
---
### 4. 實作細節:
- 為避免重複計算平方根,使用 sqrt(rem) 並檢查平方是否相等。
- b 從 a 開始,c 從 b 開始,確保輸出非降序(符合部分題目要求或方便檢查)。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
bool done = false;
for (int a = 0; a*a <= n && !done; ++a) {
for (int b = a; a*a+b*b <= n && !done; ++b) {
for (int c = b; a*a+b*b+c*c <= n && !done; ++c) {
int rem = n - (a*a+b*b+c*c);
int d = sqrt(rem);
if (d*d == rem) {
cout << a << " " << b << " " << c << " " << d << '\n';
done = true;
break;
}
}
}
}
}
}
```
## Triangle Number Sums [problem](https://cses.fi/problemset/task/3406)
```markdown
題目:
- 定義三角形數為 tri(k) = k(k+1)/2,其中 k 為正整數。
- 對於每個輸入的 n,求最少需要幾個三角形數的和,才能表示成 n。
- 題目保證輸入範圍適合在 long long 計算內。
- 根據數論結果,任何正整數都可以用不超過 3 個三角形數表示(高斯三角形數定理)。
```
``` markdown
解法 :
### 1. 判斷 1 個三角形數:
- 若 n 本身是三角形數,答案為 1。
- 判斷方法:檢查 1 + 8n 是否為完全平方數,且 sqrt(1+8n) - 1 可被 2 整除。
---
### 2. 判斷 2 個三角形數:
- 設 l 從 1 開始,r 從最大可能的三角形數下標 max_k(n) 開始
- (tri(max_k) ≤ n)。
- 雙指針法:
- 若 tri(l) + tri(r) == n,答案為 2。
- 若和 < n,l++。
- 若和 > n,r--。
- 時間複雜度 O(k_max),其中 k_max ≈ sqrt(2n)。
---
### 3. 判斷 3 個三角形數:
- 若不屬於上述兩種情況,根據高斯定理可直接輸出 3。
- 因為定理保證任何正整數皆為 3 個三角形數之和。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 判斷是否為三角形數
bool is_tri(ll n) {
ll d = 1 + 8 * n;
ll sq = sqrtl(d);
return sq * sq == d && (sq - 1) % 2 == 0;
}
// 找到 <= n 的最大三角形數的下標 k
ll max_k(ll n) {
return (ll)((-1 + sqrtl(1 + 8.0 * n)) / 2);
}
// 計算三角形數
inline ll tri(ll k) {
return k * (k + 1) / 2;
}
int solve(ll n) {
if (is_tri(n)) return 1;
ll l = 1, r = max_k(n);
while (l <= r) {
ll sum = tri(l) + tri(r);
if (sum == n) return 2;
if (sum < n) ++l;
else --r;
}
return 3;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
int T;
cin >> T;
while (T--) {
ll n;
cin >> n;
cout << solve(n) << '\n';
}
}
```
## Dice Probability [problem](https://cses.fi/problemset/task/1725)
```markdown
題目:
- 擲一顆公平的六面骰子 n 次,每次點數為 1 到 6。
- 給定區間 [a, b],求最後總和落在該區間內的機率。
- 輸出結果需保留 6 位小數。
```
``` markdown
解法 :
### 1. 狀態定義:
- dp[i][sum] = 擲 i 次骰子後,總和恰為 sum 的機率。
- 初始條件:dp[0][0] = 1(0 次擲骰,總和為 0 的機率為 1)。
---
### 2. 狀態轉移:
- 對於第 i 次擲骰:
- dp[i][sum] = Σ_{face=1}^{6} dp[i-1][sum - face] / 6
- 意思是:最後一次骰出 face,前 i-1 次的總和為 sum - face,每種點數的機率
相等
---
### 3. 邊界條件:
- sum 需介於 i(全部擲 1)到 6i(全部擲 6)之間,其他狀況機率為 0。
- 在計算 dp[i][sum] 時,若 sum-face < 0 則跳過。
---
### 4. 最終答案:
- 將 dp[n][sum] 在 sum ∈ [a, b] 範圍內的機率加總,即為答案。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define f first
#define s second
#define INF LL(1e18)
#define MOD LL((1e9) + 7)
typedef long long LL;
typedef pair<LL, LL> pLL;
typedef pair<LL, int> pli;
bool cmp(pair<pLL, LL> a, pair<pLL, LL> b){
if(a.f.f == b.f.f){
return a.f.s > b.f.s;
} return a.f.f < b.f.f;
}
bool cmp(pLL a, pLL b){
return (a.f == b.f) ? a.s > b.s : a.f < b.f;
}
LL pow(LL n, LL k){
if(k == 1LL){
return n;
}
if(k == 0LL){
return 1;
}
LL ret = pow(n, k >> 1) % (MOD);
ret *= ret % MOD;
ret %= MOD;
if(k & 1){
ret *= n % MOD;
ret %= MOD;
}return ret;
}
int main() {
int n, a, b;
cin >> n >> a >> b;
vector<vector<long double>> dp(n+1, vector<long double>(6*n+1, 0));
dp[0][0] = 1.0;
for (int i = 1; i <= n; ++i) {
for (int sum = i; sum <= 6*i; ++sum) {
for (int face = 1; face <= 6; ++face) {
if (sum-face >= 0)
dp[i][sum] += dp[i-1][sum-face] / 6.0;
}
}
}
long double prob = 0;
for (int sum = a; sum <= b; ++sum)
prob += dp[n][sum];
cout << fixed << setprecision(6) << prob << endl;
}
```
## Moving Robots
## Candy Lottery
## Inversion Probability [problem](https://cses.fi/problemset/task/1728)
```markdown
題目:
- 給定一個長度為 n 的陣列 r,r[i] 表示第 i 個元素的隨機值會均勻分布在
[1, r[i]]。
- 定義 inversion(逆序對)為:i < j 且 a[i] > a[j]。
- 問所有可能情況下 inversion 的期望值(機率和)。
- 由於數值範圍可能導致精度誤差,本題在 C++ 使用 long double 仍可能 WA,需使用
高精度計算。
```
``` markdown
解法 :
### 1. 逆序對的期望公式:
- 對每一對 (j, i),其中 j < i,計算:
P(a[j] > a[i]) = Σ{x=1}^{min(r[j],r[i])} P(a[i] = x) × P(a[j] > x)
- a[i] 均勻分布在 [1, r[i]],P(a[i] = x) = 1 / r[i]。
- P(a[j] > x) = (r[j] - x) / r[j],若 r[j] > x,否則為 0。
---
### 2. 化簡計算:
- 設 m = min(r[j], r[i])。
- Σ_{x=1}^m (r[j] - x) = m × r[j] - m(m+1)/2。
- 機率和為:
(a × m - m(m+1)/2) / (a × b),其中 a = r[j], b = r[i]。
---
### 3. 精度處理:
- 本題輸出需要 6 位小數,但中間計算涉及大量分數相加,累積誤差極大。
- 若用 C++ double 或 long double,某些邊界測資會錯(精度不足)。
- Python 內建 `decimal.Decimal` 支援高精度浮點計算,可以安全通過所有測資
```
``` python
from decimal import Decimal, getcontext
getcontext().prec = 30
n = int(input())
r = [0] + list(map(int, input().split()))
ans = Decimal(0)
for i in range(1, n+1):
for j in range(1, i):
a = r[j]
b = r[i]
m = min(a, b)
s = Decimal(a*m - m*(m+1)//2)
ans += s / Decimal(a) / Decimal(b)
ans = ans.quantize(Decimal('0.000001'))
print(ans)
```
## Stick Game [problem](https://cses.fi/problemset/task/1729)
```markdown
題目:
- 有一堆長度為 n 的木棍,可以從中取走 p 單位長度(p 在集合 P 中)。
- 兩位玩家輪流取木棍,取走長度必須是 P 中的某一個數值。
- 無法取走合法長度的玩家輸掉遊戲。
- 要輸出對於每個初始長度 i(1 ≤ i ≤ n),先手玩家是贏家 (W) 還是輸家 (L)。
```
``` markdown
解法 :
### 1. 遊戲定義:
- dp[i] = true 表示當木棍長度為 i 時,先手必勝(Winning position)。
- dp[i] = false 表示當木棍長度為 i 時,先手必敗(Losing position)。
---
### 2. 狀態轉移:
- 對於每個長度 i,檢查是否存在一個可取的長度 p ∈ P:
- 若 i ≥ p 且 dp[i - p] == false(取走 p 後讓對手處於必敗狀態),
- 則 dp[i] = true。
- 否則 dp[i] = false。
- 這是經典的「取石子遊戲」DP,屬於巴什型博弈。
---
### 3. 初始條件:
- dp[0] = false(沒有木棍長度時,無法取,必敗)。
---
### 4. 輸出:
- 依序輸出 i = 1 到 n 的狀態,贏家輸出 'W',輸家輸出 'L'。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<int> P(k);
for (int i = 0; i < k; i++) cin >> P[i];
vector<bool> dp(n + 1, false); // dp[i] = true if winning
for (int i = 1; i <= n; i++) {
for (int p : P) {
if (i >= p && !dp[i - p]) {
dp[i] = true;
break;
}
}
}
for (int i = 1; i <= n; i++)
cout << (dp[i] ? 'W' : 'L');
cout << '\n';
}
```
## Nim Game I [problem](https://cses.fi/problemset/task/1730)
```markdown
題目:
- 有 n 堆石子,第 i 堆有 x_i 顆。
- 兩位玩家輪流操作,每次可以從某一堆取走任意正數顆石子。
- 無法操作的玩家輸掉遊戲。
- 給出多組測資,判斷先手玩家 ("first") 或後手玩家 ("second") 獲勝。
```
``` markdown
解法 :
### 1. 理論背景(Nim 遊戲定理):
- 對所有堆的石子數做 XOR,得到 nim-sum。
- 若 nim-sum ≠ 0,先手必勝;若 nim-sum = 0,後手必勝。
---
### 2. 為什麼成立:
- nim-sum = 0 為必敗局(P-position):無論先手怎麼取,都會是 nim-sum ≠ 0
- 的局面給對手。
- nim-sum ≠ 0 為必勝局(N-position):先手可透過操作讓局面為 nim-sum = 0
---
### 3. 演算法步驟:
1. 初始化 xor_sum = 0。
2. 對每個堆的石子數 x_i,xor_sum ^= x_i。
3. 若 xor_sum ≠ 0 → "first",否則 → "second"。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n; cin >> n;
int xor_sum = 0;
for (int i = 0, x; i < n; ++i) {
cin >> x;
xor_sum ^= x;
}
cout << (xor_sum ? "first\n" : "second\n");
}
}
```
## Nim Game II [problem](https://cses.fi/problemset/task/1098)
```markdown
題目:
- 有 n 堆石子,第 i 堆有 x_i 顆。
- 兩位玩家輪流操作,每次可以從某一堆取走 1、2 或 3 顆石子。
- 無法操作的玩家輸掉遊戲。
- 多組測資,需判斷先手 ("first") 或後手 ("second") 獲勝。
```
``` markdown
解法 :
### 1. Sprague–Grundy 理論:
- 每一堆的局面可抽象成「取走 1、2、3 顆石子」的子遊戲。
- Grundy 值定義:
- G(0) = 0(無石子,必敗)。
- G(1) = mex{ G(0) } = 1。
- G(2) = mex{ G(1), G(0) } = 2。
- G(3) = mex{ G(2), G(1), G(0) } = 3。
- G(4) = mex{ G(3), G(2), G(1) } = 0。
- 可以發現規律:G(x) = x % 4。
---
### 2. 判勝條件:
- 計算所有堆的 Grundy 值的 XOR(nim-sum)。
- 若 nim-sum = 0 → 後手勝。
- 若 nim-sum ≠ 0 → 先手勝。
---
### 3. 演算法步驟:
1. 初始化 xor_sum = 0。
2. 對每堆石子數 x,計算 g = x % 4。
3. xor_sum ^= g。
4. 輸出勝者。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
// Grundy(x) = x % 4, 因為拿法是 {1, 2, 3}
int get_sg(int x) {
return x % 4;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
int xor_sum = 0;
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
xor_sum ^= get_sg(x);
}
cout << (xor_sum == 0 ? "second\n" : "first\n");
}
}
```
## Stair Game [problem](https://cses.fi/problemset/task/1099)
```markdown
題目:
- 有 N 個台階(從 0 編號到 N-1),每個台階上有 p 顆球。
- 兩位玩家輪流操作,每次可以選擇某個台階,將該台階上的球向下移到更低的台階
(可以一次移動任意數量的球)。
- 無法移動的玩家輸掉遊戲。
- 多組測資,需判斷先手 ("first") 或後手 ("second") 獲勝。
```
``` markdown
解法 :
### 1. Sprague–Grundy 分析:
- 對於編號為 i 的台階,其 Grundy 值為:
- G(i) = i,如果允許一次移動任意數量的球往更低台階。
- 但這題的移動限制與台階位置奇偶性有關:
- 當台階編號為偶數時,該堆球的 Grundy 值為 0(因為只能移到更低的偶數層
- ,等價於無影響)。
- 當台階編號為奇數時,該堆球的 Grundy 值為球數 p(因為等價於 Nim 堆)。
---
### 2. 化簡成 Nim:
- 僅對奇數編號的台階,將該層的球數視為一堆 Nim 堆石子。
- 對所有奇數編號台階的 p 做 XOR(nim-sum)。
---
### 3. 判勝條件:
- 若 nim-sum = 0 → 後手勝。
- 若 nim-sum ≠ 0 → 先手勝。
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
int T, N, p, xor_sum;
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d", &N);
xor_sum = 0;
for (int i = 0; i < N; ++i) {
scanf("%d", &p);
// 若某樓梯上球數為奇數,則其對應 Grundy 值為 i
if (i % 2 == 1) {
xor_sum ^= p;
}
}
// XOR 結果決定勝負:0 -> second wins, non-zero -> first wins
printf(xor_sum ? "first\n" : "second\n");
}
}
```
## Grundy's Game [problem](https://cses.fi/problemset/task/2207)
```markdown
題目:
- 有一堆 N 顆石子。
- 每次可以將這一堆石子分成兩堆,且兩堆石子數量必須不同(禁止平均分)。
- 無法操作的玩家輸掉遊戲。
- 多組測資,需判斷先手 ("first") 或後手 ("second") 獲勝。
```
``` markdown
解法 :
### 1. 遊戲規則轉換:
- 每個局面只有一堆石子,能夠進行的唯一操作是「分堆」。
- 分堆後產生兩個子局面,後續遊戲在兩個子局面上獨立進行
-(這是 Sprague–Grundy 遊戲的典型結構)。
- 規則限制:兩堆數量必須不同,等於禁止將偶數堆均分。
---
### 2. Grundy 值計算:
- G(n) = mex{ G(a) ⊕ G(b) },其中 a + b = n 且 a ≠ b。
- 此遊戲已知只有有限個必敗局(Grundy 值為 0 的局面)。
- lose[] 陣列即是事先計算好的必敗局 N 值列表。
- 若 N 在 lose[] 中 → Grundy 值為 0 → 後手勝。
- 否則 → Grundy 值非零 → 先手勝。
---
### 3. 為什麼可以預先列舉:
- Grundy’s Game 的必敗局並非規律簡單的等差序列,但範圍內可以透過離線
- DP 預先計算出來。
- 題目中 lose[] 包含所有 N ≤ 1222 的必敗局,且實際測資保證 N 落在這個範圍
```
``` cpp
#include <bits/stdc++.h>
using namespace std;
const int maxN = 1e6+1;
/**
* A036685
* Retrivied from https://oeis.org/A036685
*/
int T, lose[42] = { 0, 1, 2, 4, 7, 10, 20, 23, 26, 50, 53, 270, 273, 276, 282, 285, 288, 316, 334, 337, 340, 346, 359, 362, 365, 386, 389, 392, 566, 630, 633, 636, 639, 673, 676, 682, 685, 923, 926, 929, 932, 1222 };
bool b[maxN];
void init(){
for(int x : lose)
b[x] = true;
}
int main(){
init();
scanf("%d", &T);
for(int t = 0, N; t < T; t++){
scanf("%d", &N);
printf("%s\n", b[N] ? "second" : "first");
}
}
```
## Another Game