# IOICamp 2023
# Final
Editorial
---
## Problem A
騷擾選手的卷積
----
簡報上的粗體字應該有提醒大家想想看:
- FWT 的位元是獨立的嗎?
- FWT 的計算順序重要嗎?
----
思考後可以發現,對,每個位元是獨立的。
如果可以求得每一種位元卷積的變換,就可以這樣實作:
```cpp=
const int mat[2][6][2][2] = {
{ // transform matrix
{ // AND
{1, 1},
{0, 1}
},
},
{ // inverse transform matrix
// ...
}
};
void transform(const int N, std::vector<int>& A, const std::vector<int>& X, int inv = 0) {
for (int d = 0, k = 1, u, v; k < N; ++d, k <<= 1) {
for (int i = 0; i < N; i += 2 * k) {
for (int j = 0; j < k; ++j) {
u = A[i + j];
v = A[i+j+k];
A[i + j] = mad(mul(u, mat[inv][X[d]][0][0]), mul(v, mat[inv][X[d]][0][1]));
A[i+j+k] = mad(mul(u, mat[inv][X[d]][1][0]), mul(v, mat[inv][X[d]][1][1]));
}
}
}
}
```
----
那要怎麼求轉換矩陣呢?
- XOR 好好用線性代數算
- N$\oplus$ 就只是 $\oplus$ 算完之後兩半 swap ,所以把轉換或逆轉換其中一個矩陣的 row 交換就好了。
- 也可以用 `if (x[i] > 2) swap(u, v);` 做。
----
為什麼我的 XOR 卷積不會過?
- 逆矩陣要記得除以行列式值。
----
官解:
```cpp=
#include <bits/stdc++.h>
const int MOD = 998244353;
const int INV2 = (MOD + 1) / 2;
const int NEG1 = MOD - 1;
const int NEGINV2 = MOD - INV2;
inline int mad(int u, int v) {
u += v - MOD;
u += MOD & (u >> 31);
return u;
}
inline int mul(int u, int v) {
return (int) ((int64_t) u * v % MOD);
}
const int mat[2][6][2][2] = {
{ // transform matrix
{ // AND
{1, 1},
{0, 1}
},
{ // OR
{1, 0},
{1, 1}
},
{ // XOR
{1, 1},
{1, NEG1}
},
{ // NAND
{1, 1},
{0, 1}
},
{ // NOR
{1, 0},
{1, 1}
},
{ // NXOR
{1, 1},
{1, NEG1}
},
},
{ // inverse transform matrix
{ // AND
{1, NEG1},
{0, 1}
},
{ // OR
{1, 0},
{NEG1, 1}
},
{ // XOR
{INV2, INV2},
{INV2, NEGINV2}
},
{ // NAND
{0, 1},
{1, NEG1}
},
{ // NOR
{NEG1, 1},
{1, 0}
},
{ // NXOR
{INV2, NEGINV2},
{INV2, INV2}
},
}
};
void transform(const int N, std::vector<int>& A, const std::vector<int>& X, int inv = 0) {
for (int d = 0, k = 1, u, v; k < N; ++d, k <<= 1) {
for (int i = 0; i < N; i += 2 * k) {
for (int j = 0; j < k; ++j) {
u = A[i + j];
v = A[i+j+k];
A[i + j] = mad(mul(u, mat[inv][X[d]][0][0]), mul(v, mat[inv][X[d]][0][1]));
A[i+j+k] = mad(mul(u, mat[inv][X[d]][1][0]), mul(v, mat[inv][X[d]][1][1]));
}
}
}
}
int main() {
std::cin.tie(0)->sync_with_stdio(0);
std::cin.exceptions(std::cin.failbit);
int N, K;
std::cin >> N >> K;
std::vector<int> A(N), B(N), X(K);
for (int& i: A) std::cin >> i;
for (int& i: B) std::cin >> i;
for (int& i: X) std::cin >> i;
transform(N, A, X);
transform(N, B, X);
for (int i = 0; i < N; ++i)
A[i] = mul(A[i], B[i]);
transform(N, A, X, 1);
for (int i = 0; i < N; ++i)
std::cout << A[i] << " \n"[i == N-1];
return 0;
}
```
----
附上天才 Tester `nathanlee726` 的實作:
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll mod = 998244353;
int n, k;
vector<int> a, b, bt;
int madd(int a, int b){return (a + b >= mod? a + b - mod: a + b);}
int msub(int a, int b){return (a < b? a + mod - b: a - b);}
int mmul(int a, int b){return (int) ((ll) a * b % mod);}
int mpow(int a, int b){
if(b == 0) return 1;
if(b == 1) return (a % mod);
int tem = mpow(a, b / 2);
if(b & 1) return (int) (((((ll)tem * tem) %mod) * a) % mod);
else return (int) (((ll)tem * tem) % mod);
}
void FWT(vector<int>& v, int inv){
for(int i = 0; i < k; i++){
int ty = bt[i] % 3;
int dv = 0;
if(ty == 2) dv = mpow(2, mod - 2);
for(int j = 0; j < n; j += (1 << (i + 1)))
for(int l = j; l < j + (1 << i); l++){
int x = v[l], y = v[l + (1 << i)];
if(ty == 0){
if(inv) v[l] = msub(x, y);
else v[l] = madd(x, y);
}
if(ty == 1){
if(inv) v[l + (1 << i)] = msub(y, x);
else v[l + (1 << i)] = madd(x, y);
}
if(ty == 2){
if(inv) v[l] = mmul(madd(x, y), dv), v[l + (1 << i)] = mmul(msub(x, y), dv);
else v[l] = madd(x, y), v[l + (1 << i)] = msub(x, y);
}
}
}
}
signed main(){
ios_base::sync_with_stdio(0); cin.tie(0);
cin >> n >> k;
for(int i = 0; i < n; i++){int x; cin >> x; a.push_back(x);}
for(int i = 0; i < n; i++){int x; cin >> x; b.push_back(x);}
for(int i = 0; i < k; i++){int x; cin >> x; bt.push_back(x);}
FWT(a, 0);
FWT(b, 0);
for(int i = 0; i < n; i++) a[i] = mmul(a[i], b[i]);
FWT(a, 1);
int bse = 0;
for(int i = 0; i < k; i++) bse |= ((bt[i] >= 3) << i);
for(int i = 0; i < n; i++) cout << a[i ^ bse] << " \n"[i == n - 1];
return 0;
}
```
---
## Problem B
讓我們先加上一個條件:
每次蛋餅都會選擇序列 A,那要怎麼做呢?
----
合法的結果長怎麼樣?
假設 $Q$ 是 $A$ 的差分序列,那
1. $\sum{\max(0, Q_i)} \le K$
2. $\sum{Q_i} = 0$
----
於是我們可以令 $dp_{i,j,k}$ 代表長度為 $i$,正的和為 $j$,總和為 $k$ 的序列有幾種,列出轉移式後發現可以用前綴和優化加速到均攤 $O(1)$ 轉移。
----
但是每次蛋餅可以選擇 $A,B$ 序列之一要怎麼辦?
----
其實我們可以想像蛋餅最一開使都選擇 $A$ 序列,最後的時候再決定要把 $A$ 序列上面的值搬動多少給 $B$ 序列。所以我們在計算 $dp_{i,j,k}$ 時,直接再替 $dp_{i,j,k}$ 乘上 $k+1$ 即可
---
## Problem C
三口羊之戰
----
給你 $N$ 個點和 $M$ 條線段,這 $M$ 條線段互不相交,求有幾對點之間的連線不經過任何線段。
----
枚舉每一個點 $A$
這個點可以看到多少個點?
----
考慮以它出發的極角掃描線
當掃描線碰到一個點 $B$ 時
我們想知道 $A,B$ 之間是否有經過任何一條線段
----
每一條線段都會在一段時間內和掃描線相交
$A,B$ 之間有線段 <=> 掃描線上離 $A$ 最近的線段,和 $\overline{AB}$ 相交
----
用 `std::set` 維護與掃描線相交的線段
交點到 $A$ 的距離順序
怎麼排序?
----
一種無腦的方法是直接硬爆射線交點
接下來我們講一種全整數的作法
----
對於每一條線段 $\overline{P_1P_2}$
調整兩個點順序使得 $\overrightarrow{AP_1} \times \overrightarrow{AP_2} > 0$
----
比較兩條線段時
假設它們的兩個點依序是 $P_1,P_2$ 和 $P_3,P_4$
調整兩條線段的順序使得
$\overrightarrow{AP_1} \times \overrightarrow{AP_3} > 0$
----
<img src="https://i.imgur.com/xyUzAlg.png" width="400" />
----
- 它們一定會相交。
- 它們聯集起來不會是全部,因為一個線段的角度範圍會小於 $180^\circ$。
- 射線 $\overrightarrow{AP_3}$ 一定會通過兩條線段。
----
只要判斷 $\overline{AP_3}$ 是否和 $\overline{P_1P_2}$ 相交
是的話代表第一條線段比較近
----
另一種無腦的作法
是 $A$ 到那四個點的射線肯定有其中之一和兩條線段相交
射線和線段相交檢查 is left as an exercise
----
$O(N^2 \log N)$
Fun fact: 把 set 換成 vector 不會比 set 慢太多(`set::insert` = `vector::lower_bound` + `vector::insert`、`set::erase` = `vector::lower_bound` + `vector::erase`),不過除非你是壓常超人,不然應該不會過這題。
Not fun fact: 我昨天晚上 11:30 回家路上遇到三口羊
---
## Problem D
----
簡易題敘:把一個長度 $N$ 的正整數序列切成 $k$ 段,令第 $i$ 段結尾是 $p_i$,則需滿足 $l_{p_i}\leq p_{i-1}\leq r_{p_i}$。令每段的花費為區間最大值 $\times$ 長度,最小化每段花費的總和。
----
分治的上課例題?
- $dp[i] = \min_{\color{red}{l_i\leq j\leq r_i}}\{dp[j]+\left(\max_{j<k\leq i}a_k\right)\times(i-j)\}$
- 上課是 $1\leq j\leq i-C$
----
基本上做法都寫在投影片上了(?
- 不同的是要改把區間 $[l_i, r_i]$ 打到線段樹上
- 再次強調一下實作細節
- 記得用 sparse table 得出區間 $\max$
- 這題的斜率優化是用 stack 做單調斜率優化
- 這題的斜率會一樣,所以單調斜率優化的時候要特判斜率一樣取截距小的
----
標程
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
#include <utility>
using namespace std;
typedef long long ll;
const ll INF = 1000000000000 + 1;
const int MAXN = 500005;
ll dp[MAXN];
int val[MAXN];
int mx[__lg(MAXN) + 1][MAXN];
vector<int> seg[MAXN << 2];
void add(int L, int R, int l, int r, int rt, int idx) {
if (L <= l && R >= r)
return seg[rt].push_back(idx);
int mid = (l + r) >> 1;
if (L <= mid) add(L, R, l, mid, rt << 1, idx);
if (R > mid) add(L, R, mid + 1, r, rt << 1 | 1, idx);
}
int get_mx(int l, int r) {
int lg = __lg(r - l + 1);
return max(mx[lg][l], mx[lg][r - (1 << lg) + 1]);
}
ll get_v(int tr, int idx) {
return dp[tr] + (ll)get_mx(tr + 1, idx) * (idx - tr);
}
void upd(int tr, int idx) {
dp[idx] = min(dp[idx], get_v(tr, idx));
}
void dfs(int l, int r, int rt) {
if (l == r) {
for (int idx : seg[rt])
upd(l, idx);
return;
}
int mid = (l + r) >> 1;
dfs(l, mid, rt << 1);
dfs(mid + 1, r, rt << 1 | 1);
val[r] = 0;
for (int i = r - 1; i >= l; --i)
val[i] = max(val[i + 1], mx[0][i + 1]);
for (int idx : seg[rt])
val[idx] = get_mx(r + 1, idx);
int nw;
vector<int> stk;
auto challenge = [](int a, int b, int c, auto f) {
auto [ma, ka] = f(a);
auto [mb, kb] = f(b);
auto [mc, kc] = f(c);
return (kb - ka) * (ma - mc) >= (kc - ka) * (ma - mb);
};
// m_x <= m_y
stk.clear();
nw = r;
auto f1 = [&](int i) {
return pair<ll, ll>(i, dp[i]);
};
for (int idx : seg[rt]) {
while (nw >= l && val[nw] <= val[idx]) {
while (int(stk.size()) >= 2 && challenge(stk[int(stk.size()) - 2], stk.back(), nw, f1))
stk.pop_back();
stk.push_back(nw);
--nw;
}
while (int(stk.size()) >= 2 && get_v(stk[int(stk.size()) - 2], idx) <= get_v(stk.back(), idx))
stk.pop_back();
if (!stk.empty()) upd(stk.back(), idx);
}
// m_x >= m_y
stk.clear();
reverse(seg[rt].begin(), seg[rt].end());
nw = l;
auto f2 = [&](int i) {
return pair<ll, ll>(val[i], dp[i] - (ll)i * val[i]);
};
for (int idx : seg[rt]) {
while (nw <= r && val[nw] >= val[idx]) {
// the slope may be the same in this case
if (!stk.empty() && val[stk.back()] == val[nw]) {
auto [m, k] = f2(stk.back());
auto [nm, nk] = f2(nw);
if (k >= nk) stk.pop_back();
else {
++nw;
continue;
}
}
while (int(stk.size()) >= 2 && challenge(stk[int(stk.size()) - 2], stk.back(), nw, f2))
stk.pop_back();
stk.push_back(nw);
++nw;
}
while (int(stk.size()) >= 2 && get_v(stk[int(stk.size()) - 2], idx) <= get_v(stk.back(), idx))
stk.pop_back();
if (!stk.empty()) upd(stk.back(), idx);
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
int n, L;
cin >> n, L = __lg(n);
for (int i = 1; i <= n; ++i)
cin >> mx[0][i], dp[i] = INF;
for (int i = 1; i <= n; ++i) {
int l, r;
cin >> l >> r;
add(l, r, 0, n, 1, i);
}
for (int i = 1; i <= L; ++i)
for (int j = 1; j + (1 << i) <= n + 1; ++j)
mx[i][j] = max(mx[i - 1][j], mx[i - 1][j + (1 << (i - 1))]);
dfs(0, n, 1);
cout << dp[n] << "\n";
}
```
----
$N$ 很小?
- 其實斜率優化那個部分可以直接動態凸包砸下去就收工了
- 有板子的話會好寫很多XD
---
## Problem E
----
$\dfrac{1}{p}+\dfrac{1}{q} = \dfrac{p+q}{pq}$
----
假設 $p\neq q$
$p+q$ 與 $pq$ 互質:
$p,q$ 皆為質數 $\implies$ $pq$ 的因數只有 $1,p,q,pq$
若 $p+q$ 為 $p$ 的倍數,則 $q$ 為 $p$ 的倍數(矛盾)
若 $p+q$ 為 $q$ 的倍數,則 $p$ 為 $q$ 的倍數(矛盾)
$\implies$ $p+q$ 與 $pq$ 互質
----
將 $\dfrac{a}{b}$ 約分至最簡分數 $\dfrac{a'}{b'}$
$\implies$ $p+q = a', pq = b'$
$\implies$ 想辦法求出 $p,q$
----
$\begin{cases}
p+q = a' \\
p-q = \sqrt{a'^2-4b'} \\
\end{cases}$
時間複雜度:$O(T\log{b})$
(求最大公因數)
----
走(一點點)自己的路:
* 記得約分
* $a'=b'=1 \implies p=q=2$
* $a'=2 \implies p=q=b'$
----
TLE 們:
* $O(T\sqrt{b})$(枚舉 $1$ 到 $\sqrt{b}$)
* $O(T\pi(b))$(枚舉質數)
* pollard rho(吃毒)
* Python(why ?_?)
---
## Problem F
滅台題(未定)
----
#### subtask 1
* 操作次數同於 $\texttt{AC}$ 的子序列數量
* $dp_{i, 0}$ 代表考慮前 $i$ 項的所有可能字串,$\texttt{AC}$ 的子序列總數量
* $dp_{i, 1}$ 代表 $\texttt{A}$ 的數量
* $dp_{i, 2}$ 代表有幾種可能的字串
* 轉移很好轉移
* 整體複雜度 $O(n)$
----
#### subtask 2
* 注意到當 $dp_i$ 轉移到 $dp_{i + 1}$ 的時候,轉移式同於一個 $3 \times 3$ 的矩陣
* 單點修改同於對一個轉移矩陣修改
* 開一棵線段樹支援單點修改
* 整體複雜度 $O(3^3(n + q\log n))$
----
#### 滿分解
* 想辦法在 dp 的過程維護出平方這種項
* 注意到在轉移的時候 $\texttt{AC}^2$ 這東西可以用 $\texttt{AC} \times \texttt{A}$ 和 $\texttt{A}^2$ 湊出來
* dp 需要維護更多東西!
----
* 以下都代表考慮前 $i$ 項的所有可能字串某個特定值的總和
* $dp_{i, 0}$ 代表 $\texttt{AC}^2$
* $dp_{i, 1}$ 代表 $\texttt{AC} \times \texttt{A}$
* $dp_{i, 2}$ 代表 $\texttt{A}^2$
* $dp_{i, 3}$ 代表 $\texttt{AC}$
* $dp_{i, 4}$ 代表 $\texttt{A}$
* $dp_{i, 5}$ 代表可能的字串數量
* 轉移可以用類似的想法推完
----
* 一些對於轉移有用的式子
* $\texttt{AC}^2 \to \texttt{AC}^2 + 2 \times \texttt{AC} \times \texttt{C} + \texttt{A}^2$
* $\texttt{AC} \times \texttt{A} \to \texttt{AC} \times \texttt{A} + (\texttt{A}^2 \ \text{or} \ \texttt{AC})$
* $\texttt{A}^2 \to \texttt{A}^2 + 2 \times \texttt{A} + \texttt{cnt}$
* $\texttt{AC} \to \texttt{AC} + \texttt{A}$
* $\texttt{A} = \texttt{A} + \texttt{cnt}$
----
* 轉移依然可以寫成一個 $6 \times 6$ 的矩陣
* 用類似 subtask 2 的想法用線段樹維護轉移式的矩陣
* 整體複雜度 $O(6^3(n + q \log n))$
---
## Problem G
----
構造一個長度為 $N$ 的序列,
使得其任意非空區間內的區間 XOR 都互不相同。
----
#### Subtask 1
* $N \leq 30$
* $a_i = 2^i$,好水
----
#### 觀察
* 簡化成前綴們的 XOR
* 令 $\displaystyle p_i = \bigoplus_{k = 1}^i a_k$
* $\displaystyle \bigoplus_{k = l}^{r} a_k = p_{l - 1} \oplus p_r$
* 找到 $\langle p_i\rangle_{i = 1}^N$ 使得 $p_x \oplus p_y$ 互不相同
----
#### Subtask 2
* Heuristic: 每次隨機抓一個合法的數加入序列
* 需要互不相同的集合:$\{p_x \oplus p_y\}$
* 新增之數不能在 $\{p_x \oplus p_y \oplus p_z\}$ 裡面
* 每次新增一數 $a_k$ 時,禁忌集合需要新增 $O(k^2)$ 項
* Overall: $O(N^3) \Rightarrow$ Subtask 2 Passed!
----
#### Alternative Solution
* 大大大隨機
* $\binom{400}{2} << 2^{30}$
----
#### Subtask 3
* 怎麼優化?照做!
* $5000^3 / 10^8 = 1250$ 秒
* 回頭看一下 code length 限制... 試試打表!
* 用 bitset 維護禁忌集合,每次取 mex 作為下一項
* Subtask 3 Passed!
---
## Problem H
----
一個極端區間是指兩端分別是兩個極值的區間,一個平衡排列是不存在任何極端區間使得長度不小於 3,給定 $N, K$ 求字典序第 $K$ 小長度為 $N$ 的平衡排列。
----
- 在算字典序之前應該要先知道這種序列的結構
----
- 在算字典序之前應該要先知道這種序列的結構
- 如果你是個快樂的打表人,你應該會發現恰有 $2^{N-1}$ 種排列
----
- 假設 $1$ 在最左邊,那只有唯一的排列 $1, N, 2, \ldots$
- 考慮固定 $1$ 的位置,每個其他的數字可以選擇在 $1$ 的左邊或右邊
- 固定之後排列就唯一了,因此個數是 $2^{N-1}$ 個
----
- 如果你是個懂觀察的打表人,你應該會發現恰有 ?? 種排列以 $M$ 開頭。
----
- 如果你是個懂觀察的打表人,你應該會發現恰有 $\binom{N-1}{M-1}$ 種排列以 $M$ 開頭。
- 組合對應、數學歸納法...
----
回顧前面,我們只要關心 $1$ 的左邊有誰即可,考慮一種從 $\{1, 2, \ldots, M - 1, M + 1, \ldots, N\}$ 選 $M-1$ 個數的方法。
不妨假設在 $\{1, 2, \ldots, M-1 \}$ 選到的是 $a_1 > a_2 > \ldots > a_k$, $\{M + 1, \ldots, N\}$ 中**沒有**選到的是 $b_1 < b_2 < \ldots < b_k$
那麼把這個選擇對應到排列
- $M, a_1, b_1, a_2, b_2, \ldots, a_k, b_k, 1, \ldots$, 若 $a_k \neq 1$
- $M, b_1, a_1, b_2, a_2, \ldots, b_k, a_k = 1, \ldots$, 若 $a_k = 1$
----
從上面的對應還可以知道 $p_1 = M$ 且 $p_2 > p_1$ 的排列數為 $\binom{N-2}{M-1}$, 否則為 $\binom{N-2}{M-2}$
- 也可以透過打表或通靈猜出這個結論(因為已知開頭為 $M$ 的個數形式所以其實是有可能直接猜到的)
----
直接逐項枚舉每一位應該要是多少,因為 $K \leq 10^{18}$,所以第一項一定不超過 $60$,這代表只要枚舉最多 $120$ 項就會跑到 $1$
- 實務上當 $N$ 夠大的話會更快因為 $\binom{N}{0} + \binom{N}{1} + \binom{N}{2} + \cdots$ 超快
- $N=10^6$ 底只會到 $3$, $N=1000$ 底只會到 $6$
----
除了前兩位以外,接下來每一位在枚舉的時候都會知道他應該要比前一樣大或小
e.x. $N=7$: $3, 5, x, ...$, 還剩下數字 $1, 2, 4, 6, 7$
$x=1$: $\binom{5-2}{1-1}=1$ 種 $(1, 7, 2, 6, 4)$
$x=2$: $\binom{5-2}{2-1}=3$ 種 $(2, 1, 7, 4, 6)$; $(2, 6, 1, 7, 4)$; $(2, 7, 1, 6, 4)$;
----
剩下的工作是快速計算組合數,但因為 $M$ 最多只會是 $60$,所以直接 $O(M)$ 計算即可。
複雜度沒有很重要,總之很快因為很多東西都比最差估計快很多。只要確保碰到 $1$ 就直接停下來把後面的數字一次性放進答案就好。
---
## Problem I
----
給定一個平面圖的平面鑲嵌,問他是幾-邊連通的(即刪掉幾條邊不連通)?
----
### Subtask 1:
- $n$ 很小,依據提示可以知道 $m$ 最多 $21$。直接枚舉所有 $2^m$ 種拔邊方式。
----
### Subtask 2:
$n \leq 60?$
- 考慮兩個點 $S$ 跟 $T$,要讓這兩個點不連通,發現答案就是 $S$ 到 $T$ 的 min cut,其中 capacity 都是 $1$。跑 $S$ 到 $T$ 的最大流就好了,注意邊是無向的。
----
- 因此只要枚舉所有 $S$ 跟 $T$,跑 $O(n^2)$ 次 dinic 就好了。
- 實際上這個問題叫做 global min cut,在帶權的情況下甚至存在 $O(n^3)$ 的算法,但通常都是抄板子,所以不預期有人這樣做。
- 另外,出題者出完才發現 IOIC 甚至沒教flow,所以這個 subtask 只是想讓剛好會 flow 的人有事情做,跟正解沒啥關係。
----
### Subtask 3:
答案只有 $0$ 跟 $1$。
- 你只要會判圖是不是連通的就好了,跟鑲嵌一點關係都沒有。
----
### Subtask 4:
答案還可以是 $2$。
- 如果圖沒有割邊(橋)答案就是 $2$,一次 dfs 解決,跟鑲嵌一點關係都沒有。
----
### Subtask 5:
答案還可以是$3$。
- ltf0501 曾經在台大 icpc 培訓班校內初選出過一般圖三連通分量。
- 神靈種 baluteshih(8BQube) 和唯一神 olmrgcsi(ckiseki) 都在賽中 AC 了,因此搞不好有人可以通靈出這題怎麼做。
----
- 其實 Radewoosh 也有發過線性四連通分量的論文。
- 但出題人當然不是希望你這樣做。出這個 subtask 是希望可以提示到答案總是很小,或是發掘跟上一頁提到的各種人物一樣的明日之星。正解請直接看下一個 subtask。
----
### Subtask 6:
本題實際上是一個拼裝車題。
- 首先從提示可以知道總是存在一個點的度數 $\leq 5$。只要把這個點的鄰邊都割掉就可以不連通,因此答案總是 $\leq 5$。
- 如何判 $0, 1, 2, 3, 4$?
----
- 再來是一個套路,考慮平板圖的對偶 (dual),可以發現割和對偶圖的環是一一對應的。
- 實際上這是一個常見的技巧,但多半出現在一些比較規律 (例如grid) 的圖,比較常是$S-T$最小割轉最短路。
----
- 答案 $=0$:原圖不連通。
- 答案 $=1$:對偶圖上有**自環**。
- 答案 $=2$:對偶圖上有**重邊**。
- 答案 $=3$:對偶圖上有**三元環**。
- 答案 $=4$:對偶圖上有**四元環**。
- 除此以外答案是 $5$。
----
- 怎麼建造對偶圖?只要知道怎麼找出每一個面就可以了。
- 將每一個邊都拆成兩個方向,並規定一個面是逆(順)時針繞一圈。可以發現對於每一條邊 $u \rightarrow v$ ,只要能找到從 $v$ 盡可能左(右)轉是哪一條邊,就可以知道在(這個面上)這條邊的下一條邊是哪一條。
- 怎麼找?把出邊都極角排序!
----
```c++
vector<int> nxt(2 * m);
for (int i = 0; i < n; i++) {
auto &vec = g[i];
sort(vec.begin(), vec.end(), [&](auto x, auto y) {
return polarCmp(p[x.first] - p[i], p[y.first] - p[i]);
});
int sz = vec.size();
for (int i = 0; i < sz; i++) {
int j = (i + 1) % sz;
nxt[vec[j].second ^ 1] = vec[i].second;
}
}
```
----
```c++
int V = 0;
vector<int> id(2 * m, -1);
for (int i = 0; i < 2 * m; i++) {
if (id[i] == -1) {
for (int u = i; id[u] == -1; u = nxt[u])
id[u] = V;
}
V++;
}
}
vector<vector<int>> dual(V);
for (int i = 0; i < 2 * m; i++) {
dual[id[i]].push_back(id[i ^ 1]);
}
```
----
- 如何找三/四元環?事實上,**一般圖**三/四元環**計數**都可以在 $O(m \sqrt{m})$ 做到,只要寫得出這個就可以 AC 了。
- 具體作法有很多種,不過本質都差不多。因為網路上都有資料,以下只解釋三元環:
----
- 考慮將無向邊依照 $pair(deg[u], u)$ 小到大定向,得到一個dag。注意到每一個點的出度都不超過 $\sqrt{m}$ ,不然那個點就會超過 $\sqrt{m}$ 條出邊,且他指到的點的出度也都超過,就炸了。代碼如下:
```c++
for (int x = 0; x < n; x++) {
for (auto y : dag[x])
vis[y] = 1;
for (auto y : dag[x])
for (auto z : dag[y])
ans += vis[z];
for (auto y : dag[x])
vis[y] = 0;
}
```
----
四元環大概長這樣,但不解釋:
```c++
for (int x = 0; x < n; x++) {
for (auto y : dag[x])
for (auto z : g[y])
if (z != x) {
ans += vis[z]++;
for (auto y : dag[x])
for (auto z : g[y])
if (z != x) {
vis[z]--;
}
```
----
- 原本這題想要再出一個 $10$ 分左右的 subtask,$n \leq 1e6$,因為平面圖的三/四元環**計數**可以在**線性**時間內做到。這可能是本題唯一的精髓。
- 實際上,根號作法的核心就是定向後得到的 dag 的出度上界。能不能做得更好?
----
- 對於平面圖,因為總是有一個點度數 $\leq 5$,只要不斷把度數 $\leq 5$ 的點拔掉,並且每次拔一個點時將這個點僅剩的鄰邊都定向為出邊,就得到一個出度 $\leq 5$ 的有向無環圖。這可以用一個 bfs 實作。
- 套前面的做法就可以線性。
----
- 也存在別的作法。可以發現三元環或是四元環經過定向以後根本沒有幾種可能,直接判所 case 也行。(四元環的有一個 case 會用到 std::bitset 或是 std::set)
----
總結本題用到的東西
- 看提示,發現最小度數 $\leq 5$。
- 割轉環。
- 極角排序(用來建對偶圖)。$O(n\log n)$
- 三/四元環。$O(n)$,所有 $o(n^2)$ 其他做法應該都會過。
----
- 預期定位是hard。
- 但實作難度遠比 Day 5 定位習題的題目低!!!!!!
----
後記:
- 實際上,這題的測資偏弱。
- 整個 package 裡面都是一堆手畫 $+$ 手動輸入,$\leq 60$ 個點的圖。wolfram mathworld 的 planar graph 這因為出題人不會隨機產生大的 4/5-邊連通平面圖。所以大測資基本上都是不複雜的圖,或是一些其他比賽出過的平面圖題的測資,也因此沒有任何一筆 $\geq 100$ 個點的圖的答案是 $4$ 或 $5$,如果有人會做請教教我。
----
- 不知道有沒有人盲猜答案 $=$ 最小度數。
- 實際上這大部分時候是對的,出題人為了造最小度數從 $1$ 到 $5$ 的反例煞費苦心。對於最小度數是 $5$ 但答案是 $4$,出題人能造出的最小的圖有 $35$ 個點和 $96$ 條邊的 case,你能造出更小的嗎?
----
- 這兩個圖的拼接:
![](https://i.imgur.com/rxlmHRk.png =300x)
![](https://i.imgur.com/792dTL9.png =300x)
---
## Problem J
----
長度為 $N$ 的 LIS 其實就是要從某個 $1$ 開始依序經過 $2,3,\ldots$ 一直走到 $N$。
學長程度就是這東西的最小距離。
----
對於一個固定的排列,令 $i$ 出現在 $a_i$。
學長程度即為 $1+dist(a_1,a_2)+dist(a_2,a_3)+\cdots+dist(a_{N-1},a_N)$。
這裡 $dist(x,y)$ 是從位置 $x$ 向右走走到位置 $y$ 的最小距離。
----
把所有 $dist(x,y)$ 展開會發現等於 $a_N-a_1+1+\sum_{i=1}^{N-1} [a_i>a_{i+1}]\cdot N$。
因此我們可以考慮對每個 $i=1,2,\ldots,N-1$,去算有多少種排列滿足 $a_i>a_{i+1}$。
$a_N-a_1+1$ 的部分也是可以對 $1$ 和 $N$ 分開討論。
----
注意到要分成四種情況討論:($i$ 是否已經被固定) $\times$ ($i+1$ 是否已經被固定)
細節留給讀者自行思考。
----
注意到過程會用到階乘,所以顯然要先預處理。
總時間複雜度:$O(N)$
---
## Problem K
----
### Subtask 1
每條邊必須都要走到 -> 歐拉路
* 圖要連通:原題已經保證
* 奇偶性:令 $d_i$ 為第 $i$ 個點的度數,則 $d_1 \equiv d_n \equiv 1 \mod 2$ 且 $d_i \equiv 0 \mod 2\ (\forall\ 2 \le i \le n - 1)$
----
### Subtask 2
把題目想成「圖上已經有一些邊,加一些邊使得整張圖有 $1$ 到 $n$ 的歐拉路徑」
方便思考:加一條 $(1, n)$ 的重要路
題目就變成「加一些邊使得整張圖有*歐拉迴路*」
----
* 圖要連通:已經連通
* 奇偶性:圖上每個點有 $d_i$,對於每條非重要路 $(u, v)$ 你可以 $d_u+=1, d_v+=1$ 或不做,目的是把 $d_i$ 變偶數
----
現在圖上只剩非重要路
考慮生成森林,對於每個連通塊,可以用樹邊來 greedy 的取邊
如果小孩的 $d_i$ 是奇數就用連小孩的邊把它變偶數
----
greedy 過程很單純,不用構造解的話可以推到簡單結論
> 對於每一個連通塊,$d_u$ 是奇數的 $u$ 的數量都必須是偶數
先令 $d_1 = d_n = 1$,再分別把重要路的兩端都 $d_u += 1$,在只含非重要路的子圖上判斷這件事
$O(N + M)$
----
### 假解們
這題測資沒有很強qq
* 直接忽視有固定起終點
* 一開始加的是 $(1, n)$ 非重要路而不是重要路
* 含 DSU 的一些怪
* 其他怪又怪
---
## Problem L
----
- 題目大意
- $H$ 根據一張給定的圖 $H$,構一個圖 $G$ 使得
- $V(G)=V(H)$
- $E(G) = \{(u, v, w_i ) \mid \forall u,v\ \exists\ i \text{ s.t. }$
$D(H, u, u_i) \leq d_{1,i} \land D(H, v, v_i) \leq d_{2,i}\}$
- 求 $G$ 上的單點源最短路
----
### Subtask 1
- 樹退化成鍊的話怎麼做?
- 它就是個經典線段樹優化建圖練習題
- 可以把講義的建模稍微修改一下
- 複雜度 $O((N + Q) \log{N}\log{(N+Q)})$
----
### Subtask 2
- 回到樹的問題,能不能做類似的事情?
- 線段樹是把序列分治結果存下來,那樹有類似的方式嗎?
- 重心樹就是一種支援對樹上某個"區間"(距離某點特定範圍內的點們)做事的分治結構
----
- 重心樹優化建圖!
- 利用重心樹上"區間"們的關係,構造一個邊較少,但還是可以維持任兩點之間距離的新圖 $G$
----
- 重心樹上記錄這個所蘊含的"區間"內的每個點到它的距離
- 對每個重心樹節點 $u$ 建一系列 $G$ 上的點 $v_{u,i}$ 讓我們之後連到這個點就可以跟所有距離 $u$ 前 $i$ 近的點連
- 把"區間內"每個點都接到它對應的 $v_{u,i}$ 上,然後把 $v_{u,i}$ 串在一起
- Kinda similar to 前綴和思想 (?)
----
- 每次查詢一樣沿著重心樹往上走,在每個節點都查距離少於某個值對應到的 $v_{u,i}$ 是誰
- 這些 $v_{u,i}$ 就跟線段樹優化建圖時線段樹節點的功用一樣
----
- 建樹 $O(N \log^2{N})$
- 查樹 $O(Q \log^2{N})$
- **最後總共有 $O((N + Q)\log{N})$ 條邊與 $O(N\log{N} + Q)$ 個點**
- 帶入 Dijkstra 複雜度 $O(E \log{V})$ 中
- 總時間複雜度為 $(N + Q)\log{N}\log{(N\log{N} + Q)}$
---
## Problem M
----
水題。
---
{"metaMigratedAt":"2023-06-17T19:54:30.236Z","metaMigratedFrom":"YAML","title":"IOICamp 2023 Day 5 Contest Editorial","breaks":"true","contributors":"[{\"id\":\"791336ad-9906-4cf9-ae4f-8e2b6f9b856e\",\"add\":9444,\"del\":2713},{\"id\":\"8f3294cf-0a65-4b41-8b4d-80b1f6d5208a\",\"add\":5296,\"del\":2109},{\"id\":\"23e4e31a-87e4-4969-98c7-ac3a9c14f5a9\",\"add\":1253,\"del\":723},{\"id\":\"d7cc2e0a-f5c6-493c-baaa-f0efce285d9f\",\"add\":1579,\"del\":403},{\"id\":\"d14c47f5-9d89-4c85-8d7d-f5e2bc08cb62\",\"add\":515,\"del\":335},{\"id\":\"04d32f9a-57cc-45cd-8549-086ea8ee6d8a\",\"add\":5921,\"del\":1577},{\"id\":\"02353542-5acb-4a66-abd0-128b1af24abb\",\"add\":1100,\"del\":454},{\"id\":\"d3e63fe1-dc3b-4f31-97c0-10807543b008\",\"add\":1223,\"del\":9},{\"id\":\"d6f2ea40-6a64-4129-92e9-ea26e7b35ac5\",\"add\":719,\"del\":40},{\"id\":\"ae4b766f-b425-4139-98b1-cd8c8bbb7fb9\",\"add\":5091,\"del\":2324},{\"id\":\"6b95215f-91a7-4eaf-bcfe-d43740108f96\",\"add\":654,\"del\":1692},{\"id\":\"acb6534a-1dbb-4c0e-a5b9-1410fbceb21b\",\"add\":377,\"del\":0}]"}