owned this note
owned this note
Published
Linked with GitHub
---
tags: VNOJ, THT, DP, Difference Array, Combinatorics, Inclusion Exclusion, SPyofgame
title: 🌱 Tin học trẻ 2021 - Vòng Khu vực - Bảng C - Hoán vị không bất động
author: Editorial-Slayers Team
license: Public domain
---
<style>
.markdown-body { max-width: 2048px; }
</style>
$\Huge \text{🌱 Tin học trẻ 2021 - Vòng Khu vực - Bảng C - Hoán vị không bất động}$
-----
###### 🔗 Link: [https://oj.vnoi.info/problem/tht21_kvc_permu](https://oj.vnoi.info/problem/tht21_kvc_permu)
###### 📌 Tags: `DP`, `Difference Array`, `Combinatorics`, `Inclusion Exclusion`
###### 👤 Writer: @SPyofgame
###### 👥 Contributor: @deptrai2k7, @matchamachiato , @FireGhost , [@Editorial Slayers Team](https://hackmd.io/@Editorial-Slayers/)
###### 📋 Content:
[TOC]
-----
## Phát biểu lại đề bài
Cho $2$ số nguyên $n$ và $q$
Cho $q$ đoạn $[l_i, r_i]$
Xét tập $S$ gồm các điểm $1, 2, \dots n$ mà thuộc vào ít nhất $1$ trong $q$ đoạn
Gọi điểm cố định là điểm ở vị trí $p$ cũng có giá trị $p$ của một hoán vị đang xét
Đếm số hoán vị bậc $n$ sao cho không tồn tại điểm cố định trong tập $S$
Không mất tính tổng quát, đưa các điểm trong tập $S$ thành các điểm $1, 2, \dots, k$ với $k = |S|$
Bài toán trở thành đếm số hoán vị bậc $n$ có $k$ điểm đầu không cố định
Để tính $k$ ta có thể dùng [different array](https://codeforces.com/blog/entry/78762#:~:text=A%20difference%20array%20can%20be,an%20O(N)%20computation.) hoặc các CTDL khác.
-----
### Code
> **Time:** $O(n + q)$
> **Space:** $O(n)$
> **Algo:** Implementation, Difference Array
> [color=lightgreen]
:::success
:::spoiler SPyofgame Code
```cpp=
cin >> n;
memset(d, 0, sizeof(d[0]) * (n + 1)); /// diffrent array
int q;
cin >> q;
while (q-->0) /// for each required range
{
int l, r;
cin >> l >> r;
++d[l], --d[r + 1]; /// mark all range [l, r] as true
}
k = 0;
for (int i = 1; i <= n; ++i) /// for each position
{
d[i] += d[i - 1];
if (d[i] > 0) ++k; /// this position is in marked range
}
cin >> n;
```
:::
-----
## Hướng dẫn subtask 1-2
Ý tưởng đơn giản là thử từng hoán vị, và kiểm tra xem có tồn tại điểm cố định trong $k$ phần tử đầu hay không
Để thử từng hoán vị thì có thể sài `C++std::next_permutation`
-----
### Code
> **Time:** $O(n!)$
> **Space:** $O(n)$
> **Algo:** Brute-forces, Implementation, Math, Difference Array
> [color=lightgreen]
:::success
:::spoiler SPyofgame Code
```cpp=
#include <algorithm>
#include <iostream>
#include <cstring>
using namespace std;
const int MOD = 1e9 + 7;
const int LIM = 1e5 + 15;
int fact[LIM];
int p[LIM];
int solve(int n, int k)
{
/// Initial Permutation
for (int x = 1; x <= n; ++x) p[x] = x;
int res = 0;
do
{
/// Check if the first k point have fixed point
for (int x = 1; x <= k; ++x)
if (p[x] == x) /// Found a fixed point
goto skip;
++res;
skip: {}
}
while (next_permutation(p + 1, p + n + 1)); /// For each permutation
return res;
}
int d[LIM];
int main()
{
ios::sync_with_stdio(NULL);
cin.tie(NULL);
int n, q;
cin >> n >> q;
memset(d, 0, sizeof(d));
while (q-->0)
{
int l, r;
cin >> l >> r;
++d[l], --d[r + 1];
}
int k = 0;
for (int i = 1; i <= n; ++i)
{
d[i] += d[i - 1];
if (d[i] > 0) ++k;
}
fact[0] = fact[1] = 1;
for (int x = 2; x <= n; ++x)
fact[x] = (1LL * x * fact[x - 1]) % MOD;
cout << solve(n, k);
return 0;
}
```
:::
-----
### Bonus
> Bạn có thể tối ưu để hàm chạy trong $O(k!)$ không ?
-----
## Hướng dẫn subtask 3-4
Đặt hàm quy hoạch động $f[n][k]$ là số hoán vị bậc $n$ không tồn tại điểm cố định trong $k$ phần tử đầu
Trường hợp $n = 0$, thì không có hoán vị nào, nên $f[0][k] = 0$
Trường hợp $k = 0$, thì cần đếm số hoán vị bậc $n$, nên $f[n][0] = n!$
Ngược lại, ta có $f[n][k] = f[n][k - 1] - f[n - 1][k - 1]$ là
- $f[n][k - 1]$ là số cách chọn giá trị tại vị trí $k$
- $f[n - 1][k - 1]$ là số cách chọn giá trị tại vị trí $k$ mà có thêm một điểm cố định
Kết quả bài toán là $f[n][k]$.
-----
### Code
> **Time:** $O(n \times k)$
> **Space:** $O(n^2)$
> **Algo:** DP, Combinatorics, Difference Array
> [color=lightgreen]
:::success
:::spoiler SPyofgame Code
```cpp=
#include <iostream>
#include <cstring>
using namespace std;
const int MOD = 1e9 + 7;
const int LIM = 5050;
const int LIM_2 = 1e5 + 15;
int fact[LIM_2];
int f[LIM][LIM];
int solve(int n, int k)
{
if (n <= 0) return 0;
if (k <= 0) return fact[n];
int &res = f[n][k];
if (res != -1) return res;
res = solve(n, k - 1) - solve(n - 1, k - 1);
if (res < 0) res += MOD;
return res;
}
int d[LIM_2];
int main()
{
// file("Test");
ios::sync_with_stdio(NULL);
cin.tie(NULL);
int n, q;
cin >> n >> q;
memset(d, 0, sizeof(d));
while (q-->0)
{
int l, r;
cin >> l >> r;
++d[l], --d[r + 1];
}
int k = 0;
for (int i = 1; i <= n; ++i)
{
d[i] += d[i - 1];
if (d[i] > 0) ++k;
}
fact[0] = fact[1] = 1;
for (int x = 2; x <= n; ++x)
fact[x] = (1LL * x * fact[x - 1]) % MOD;
memset(f, -1, sizeof(f));
cout << solve(n, k);
return 0;
}
```
:::
-----
### Bonus
> Bạn có thể làm trong $O(n \times k)$ thời gian nhưng chỉ với $O(n)$ bộ nhớ không ?
-----
## Hướng dẫn subtask 5
Gọi $G[x]$ là số hoán vị bậc $n$ có ít nhất $x$ điểm cố định trong $k$ vị trí đầu tiên
Ta có $G[x] = C_k^x \times (n - x)!$
- Với $C_k^x$ là số cách chọn một hoán vị có $x$ điểm cố định trong $k$ phần tử đầu
- Và $(n - x)!$ là số cách chọn $n - x$ phần tử còn lại chưa được chọn (có thể có điểm cố định)
Gọi $F[k]$ là số hoán vị bậc $n$ không có bất cứ điểm cố định nào xuất hiện tại $k$ vị trí đầu tiên
- Dễ thấy được $F[k] = G[0] - G[1] + G[2] - G[3] + G[4] - ... \pm G[k]$
- Một cách tổng quát $F[k] = \underset{0 \leq i \leq k}{\Large \Sigma} \normalsize G[i] \times (-1)^i$
Kết quả bài toán là $F[k]$.
-----
Có $\large \binom{k}{x} \normalsize = \Large \frac{k!}{x! (k - x)!} \normalsize = k! \times x!^{-1} \times (k-x)!^{-1}$
Vì $M = 10^9 + 7$ là một số nguyên tố nên $x^{-1} \equiv x^{M-2} \pmod M$
Ngoài ra ta cũng có thể tính $x^{-1}$ dựa trên $p^{-1}$ với $p \equiv M \pmod x$
Ta có thể tiền xử lí các giá trị $n!$ và $n!^{-1}$ để tính $C_k^x$ trong $O(\log n)$ hoặc $O(1)$
-----
### Code
> **Time:** $O(n)$
> **Space:** $O(n)$
> **Algo:** DP, Difference Array, Combinatorics, Inclusion Exclusion
> [color=lightgreen]
:::success
:::spoiler SPyofgame Code
```cpp=
#pragma GCC target ("avx2")
#pragma GCC optimization ("O3")
#pragma GCC optimization ("unroll-loops")
#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
const int LIM = 1e5 + 15;
const int MOD = 1e9 + 7;
/// Combinatorics Precalculation
/// Complexity: Linear in time and space
int fact[LIM]; /// n! % MOD
int invs[LIM]; /// n^(-1) % MOD
int tcaf[LIM]; /// n!^(-1) % MOD
/// Precalculate first n number
void precal(int n)
{
fact[0] = fact[1] = 1;
invs[0] = invs[1] = 1;
tcaf[0] = tcaf[1] = 1;
for (int x = 2; x <= n; ++x)
{
/// n! = n * (n - 1)!
/// n^(-1) = floor(MOD / n) * (MOD % x)^(-1)
/// n!^(-1) = n^(-1) * (n-1)!^(-1)
fact[x] = (1LL * x * fact[x - 1]) % MOD;
invs[x] = MOD - 1LL * (MOD / x) * invs[MOD % x] % MOD;
tcaf[x] = (1LL * invs[x] * tcaf[x - 1]) % MOD;
}
}
/// Combinatorics Query
/// Complexity: Linear in time and constant space
/// Query for nCk = n! / k! / (n-k)!
int nck(int n, int k)
{
/// nCk = n! * k!^(-1) * (n-k)!^(-1)
return 1LL * fact[n] * tcaf[k] % MOD * tcaf[n - k] % MOD;
}
/// Get the needed input
/// Complexity: Linear in time and space
int d[LIM];
void input(int &n, int &k)
{
cin >> n;
memset(d, 0, sizeof(d[0]) * (n + 1)); /// diffrent array
int q;
cin >> q;
while (q-->0) /// for each required range
{
int l, r;
cin >> l >> r;
++d[l], --d[r + 1]; /// mark all range [l, r] as true
}
k = 0;
for (int i = 1; i <= n; ++i) /// for each position
{
d[i] += d[i - 1];
if (d[i] > 0) ++k; /// this position is in marked range
}
}
/// Number of permutation of n elements with non of the first k positions is fixed
/// Complexity: Linear in time and constant space
ll solve(int n, int k)
{
/// G[x] = Number of permutation of n elements with atleast (x) fixed point appear in first k position
/// G[x] = A * B
/// A = kCx = number of way to select (x) fixed point in first k position
/// B = (n-x)! = number of way to select the rest (n - x) elements (can also be fixed point)
///
/// F[x] = Number of permutation of n elements with non of the first x positions is fixed
/// Inclusion-Exclusion Formula:
/// F[x] = G[0] - G[1] + G[2] - G[3] + G[4] - ... ± G[x]
///
ll res = 0;
for (int x = 0; x <= k; ++x)
{
int v = 1LL * nck(k, x) * fact[n - x] % MOD;
res += (x & 1) ? -v : +v;
}
/// Take modulo
res %= MOD;
if (res < 0) res += MOD;
return res;
}
/// Problem solvin in linear time and space
int main()
{
// file("Test");
ios::sync_with_stdio(NULL);
cin.tie(NULL);
int n, k;
input(n, k);
precal(n);
cout << solve(n, k);
return 0;
}
```
:::
-----
### Bonus
> Với modulo nguyên tố đủ nhỏ thì bạn có có thể giải với $0 \leq k \leq n \leq 10^{12}$ không?
> Với modulo nguyên tố đủ nhỏ thì bạn có có thể giải với $0 \leq k \leq n \leq 10^{18}$ không?
> Bạn có thể chứng minh công thức $F[k] = G[0] - G[1] + G[2] - G[3] + G[4] - ... \pm G[k]$ hay không?
> Bạn có thể giải với $Q$ truy vấn $(n, k)$ trong bao lâu?