---
title: IOICamp 2023 Day 2 Contest Editorial
tags: editorial, competitive-programming
---
# IOIC 2023 Day 2 Editorial
## Problem 201
### 估計:證明 $n > 30$ 時無解
假設有解:
令 $b_i = a_1 \, \& \, a_2 \, \& \, \cdots \, a_i$。
1. 根據定義,$b_1, b_2, \ldots, b_{31}$ 必須兩兩不同。
2. 因為 $b_{i + 1} = b_i \, \& \, a_{i + 1}$,$b_{i + 1}$ 是 $b_i$ 的 submask。
綜上兩點,$b_{i + 1}$ 是 $b_i$ 的 proper submask。這使得
```
__builtin_popcount(b[1]) > __builtin_popcount(b[2]) > ... > __builtin_popcount(b[31])
```
因為 $0 \leq b_1, b_2, \ldots, b_{31} < 2^{30}$,`__builtin_popcount`的值域是$[0, 30]$
$\Rightarrow \,$ `__builtin_popcount(b[i])`$= 31 - i$
$\Rightarrow \, b_1 = a_1 = 111111111111111111111111111111_2$
$\Rightarrow \, a_1 \, \& \, a_2 = a_2 \quad \rightarrow\leftarrow$
### 構造:證明 $n \leq 30$ 時總是有解
$a_i = 2^{30} - 1 - 2^{i}$ 是個合法構造。
## Problem 202
### Subtask 1
注意到這就是裸的李超線段樹,只不過值域很大,所以要記得先將所有會代入的 $x$ 值離散化。
### Subtask 2
題目叫做 Double 線段樹,所以可以考慮看看同時使用兩棵線段樹。
注意到李超線段樹可以支援 undo,因此可以考慮再套時間線段樹,實作細節就留給讀者思考。
:::spoiler Sample Code
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll
#define maxn 200005
struct event{
int a, b;
int val(int x){
return a * x + b;
}
};
vector<pair<int, event> > undo[4 * maxn];
int np, rx[maxn], que[maxn], lin, tin;
vector<pair<int, int> > qu, tim;
vector<event> li;
struct Li_Seg_Tree{
vector<event> seg;
Li_Seg_Tree(): seg(4 * maxn) {}
void init(int n){
for(int i = 0; i < 4 * n; i++) seg[i].a = seg[i].b = 0;
}
void insert(int p, int l,int r, event e){
int mi = (l + r) / 2;
if(e.val(rx[mi]) > seg[p].val(rx[mi])){
undo[np].push_back({p, seg[p]});
swap(e,seg[p]);
}
if(l == r) return;
if(e.a < seg[p].a) insert(p * 2, l, mi, e);
else insert(p * 2 + 1, mi + 1, r, e);
}
int query(int p, int l, int r, int x){
int re = seg[p].val(rx[x]);
if(l == r) return re;
int mi = (l + r) / 2;
if(x > mi) return max(re, query(p * 2 + 1, mi + 1, r, x));
else return max(re, query(p * 2, l, mi, x));
}
}litree;
struct Time_Seg_Tree{
vector<event> seg[4 * maxn];
void insert(int p, int l, int r, event e, int ql, int qr){
if(l > qr || r < ql) return;
if(l >= ql && r <= qr){
seg[p].push_back(e);
return;
}
int mi = (l + r) / 2;
insert(p * 2, l, mi, e, ql, qr);
insert(p * 2 + 1, mi + 1, r, e, ql, qr);
return;
}
void travel(int p, int l, int r){
if(l == r){
if(que[l] >= 0){
np = p;
for(auto e: seg[p]) litree.insert(1, 0, lin - 1, e);
int ans = litree.query(1, 0, lin - 1, que[l]);
if(ans) cout << ans << '\n';
else cout << "double is good at problem setting\n";
reverse(undo[p].begin(), undo[p].end());
for(auto pi: undo[p]) litree.seg[pi.first] = pi.second;
}
return;
}
int mi = (l + r) / 2;
np = p;
for(auto e: seg[p]) litree.insert(1, 0, lin - 1, e);
travel(p * 2, l, mi);
travel(p * 2 + 1, mi + 1, r);
reverse(undo[p].begin(), undo[p].end());
for(auto pi: undo[p]) litree.seg[pi.first] = pi.second;
return;
}
}titree;
signed main(){
ios_base::sync_with_stdio(0);cin.tie(0);
int q;
cin >> q;
int cnt = 0;
while(q--){
int ty;
cin >> ty;
if(ty == 0){
int x;
cin >> x;
qu.push_back({cnt++, x});
}
else if(ty == 1){
int a, b;
cin >> a >> b;
tim.push_back({cnt++, 1e9});
event te;
te.a = a, te.b = b;
li.push_back(te);
}
else{
int id;
cin >> id;
tim[id].second = cnt++;
}
}
for(auto &pi: tim) if(pi.second > 1e8) pi.second = cnt;
vector<int> v;
for(auto pi: qu) v.push_back(pi.second);
sort(v.begin(), v.end());
v.resize(unique(v.begin(), v.end()) - v.begin());
lin = ((int) v.size());
tin = cnt + 1;
for(int i = 0; i < tin; i++) que[i] = -1;
for(int i = 0; i < lin; i++) rx[i] = v[i];
for(auto pi: qu) que[pi.first] = lower_bound(v.begin(), v.end(), pi.second) - v.begin();
litree.init(lin);
for(int i = 0; i < ((int) tim.size()); i++) titree.insert(1, 0, tin - 1, li[i], tim[i].first, tim[i].second);
titree.travel(1, 0, tin - 1);
return 0;
}
```
:::
### Remark
Double is good at problem setting. :)
## Problem 203
### 題解
* 以$s_i$代表DE語長度是$i$的前綴
* 以$a_{ij}$代表長度是$i$的字串中,不包含DE語且其最長的後綴使得其為DE語前綴的長度為$j$的字串個數
* 以$f(i, j, c)$代表$s_i+c$的長度是$j$的後綴是否是最長後綴使得其為DE語前綴,其中若滿足則為$1$否則為$0$
* \begin{align*}&\begin{pmatrix}
a_{i+1, 0}\\
a_{i+1, 1}\\
\vdots\\
a_{i+1, m-1}
\end{pmatrix}\\&=\sum\limits_{c='0'}^{'9'}\begin{pmatrix}
f(0, 0, c)&f(0, 1, c)&\cdots&f(0, m-1, c)\\
f(1, 0, c)&f(1, 1, c)&\cdots&f(1, m-1, c)\\
\vdots&\vdots&\ddots&\vdots\\
f(m-1, 0, c)&f(m-1, 1, c)&\cdots&f(m-1, m-1, c)
\end{pmatrix}\\
&\begin{pmatrix}
a_{i, 0}\\
a_{i, 1}\\
\vdots\\
a_{i, m-1}
\end{pmatrix}\end{align*}
* 我們知道
$$\begin{pmatrix}
a_{0, 0}\\
a_{0, 1}\\
\vdots\\
a_{0, m-1}
\end{pmatrix}=\begin{pmatrix}
1\\0\\\vdots\\0
\end{pmatrix}$$
* 所以只需要計算$A=\sum_{c='0'}^{'9'}$的上一頁的大矩陣,快速冪求$A^n$,就可以求得$\begin{pmatrix}
a_{n, 0}\\
a_{n, 1}\\
\vdots\\
a_{n, m-1}
\end{pmatrix}$,而$a_{n, 0}+a_{n, 1}+\cdots+a_{n, m-1}$即為長度是$n$且不包含DE語的字串的個數,因此$1-\frac{a_{n, 0}+\cdots+a_{n, m-1}}{10^n}$即為答案。
### 關於 $q=40000$
* 原先的計算使用快速冪每個詢問會做$64$次矩陣乘法
* 先預處理所有的$A$的$a2^{8b}$冪次的矩陣的值,其中$a\in\{0, 1, \dots, 2^8-1\},\ b\in\{0, 1, \dots, 7\}$,每次計算快速冪時改用$2^8$進位去計算,即可省掉$8$的常數
## Problem 204
* 選一個點 $v$ 往下走 $k$ 步後能到的所有點,一起加值或求和
先算好每個點的深度,並且把每個深度的點分別收集起來,會變成
* 在深度為 $depth(v)+k$ 那堆點中,把所有屬於 $v$ 子樹的點一起加值或求和
判斷是否在子樹可以用 DFS 序(開始與離開時間),所以先把每個深度的點分別照 DFS 序 sort 好,每次二分搜出那個區間,就變成一堆區間操作了,可以用線段樹等資料結構解決
複雜度 $O(N+Q\log N)$
## Problem 205
### Subtask 1
暴力。
### Subtask 2
講義上有寫答案是 $(n + 1) ^ {n - 1}$。
### Subtask 3
仔細想一想,會發現對一個(全部位置已知的) $p$,有解的條件是對於所有 $k$,$p$ 裡面 $\geq k$ 的元素的個數要 $\leq n - k + 1$。
因此可以想到以下dp: $dp[i][j]$ 目前已經有 $j$ 個位置用 $\geq i$ 的數填好了。這裡 $j$ 個位置包含那些本來就指定 $\geq i$ 的位置,以及一些用 $\geq i$的數填入未指定的位置。
轉移如下(跟標程代號一樣):
定義 $f[i]$ 表示給定包含了幾個 $i$,$g[i]$ 表示給定包含了幾個 $\geq i$,$m$ 表示輸入中位指定的個數。
考慮哪些 state 可以轉移到 $dp[i][j]$。
假設放了 $k$ 個 $i$,那麼轉移來自 $dp[i + 1][j - k]$。在 $dp[i + 1][j - k]$ 還有 $m -(j - k - g[i + 1])$ 個未指定的位置可用,且 $k$ 個 $i$ 中 $k - f[i]$ 個是填在未指定的位置,因此轉移要乘以一個 $\binom {m - (j - k - g[i + 1])}{k - f[i]}$的係數。
### Sample code
:::spoiler Sample Code
```cpp
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int P = 998244353;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
auto solve = [&]() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
a[i]--;
}
int m = count(a.begin(), a.end(), -1);
vector<int> f(n);
for (int i = 0; i < n; i++) {
if (a[i] != -1) {
f[a[i]]++;
}
}
vector<int> g(n + 1);
for (int i = n - 1; i >= 0; i--) {
g[i] = g[i + 1] + f[i];
// if (g[i] > n - i) {
// cout << "0\n";
// return;
// }
}
vector binom(n + 1, vector<int>(n + 1));
for (int i = 0; i <= n; i++) {
binom[i][0] = 1;
for (int j = 1; j <= i; j++) {
binom[i][j] = (binom[i - 1][j - 1] + binom[i - 1][j]) % P;
}
}
vector dp(n + 1, vector<int>(n + 1));
dp[n][0] = 1;
for (int i = n - 1; i >= 0; i--) {
for (int j = g[i]; j <= n - i && j <= m + g[i]; j++) {
for (int k = f[i]; k + g[i + 1] <= j; k++) {
dp[i][j] = (dp[i][j] + 1LL * dp[i + 1][j - k] * binom[m - (j - k - g[i + 1])][k - f[i]] % P) % P;
}
}
}
cout << dp[0][n] << '\n';
};
solve();
return 0;
}
```
:::
## Problem 206
結論:$(p, q) = (2, N - 2)$ 總是一個解。
證明:先使用反證法。如果有一種答案不包含 $2$ 作為其中一個質數,那麼 $p, q$ 都是奇質數 $\Rightarrow p + q = N$ 是 $\geq 6$ 的偶數 $\rightarrow\leftarrow$
因為保證有解,所以 $(p, q) = (2, N - 2)$ 總是一組解。
## Problem 207
首先,有重複的 $a_i$ 時可以先連起來,因為對一個數字來說連到自己的倍數會最好
接下來考慮 Boruvka,每一輪先對每個 $a_i$ 跑過它的所有因數,在因數 $x$ 的地方紀錄下 $a_i$ 所屬的連通塊,代表如果另一個連通塊內也有 $x$ 的倍數則可以連一條至少是 $x$ 的邊到這一塊。每個因數只需要保留前兩個發現的連通塊,因為任一個連通塊至少和那兩個之一可以連邊
再對每個 $a_i$ 跑一遍它的所有因數,找到它最好的一條邊後,和他所屬的連通塊內其他點的邊比較。這時已經找到每個連通塊這一輪要加的邊了,一邊維護並查集確定不會形成環一邊把它們加上去。$\log N$ 輪後就找到最大生成樹
一開始要預處理每個數字有哪些因數,複雜度 $O(C\log C)$,每一輪花的時間是它們的因數個數和,$10^6$ 以內因數最多的 $10^5$ 個數字一共有 $4911307$ 個數字,進行 $O(\log N)$ 輪,實測會 AC
還有使用 Prim 或 Kruskal 思路的各種作法也可以 AC(砸一般 Kruskal 只能拿到部分分)
## Problem 208
### Subtask 1
題目需要支援單點修改,以及區間查詢總和。跟講義的基礎線段樹的例題好像喔!沒有錯,這個 subtask 就是個一般的線段樹。把講義的範例 code 拿出來抄一抄,把取 max 的部份改成加法就可以了!
### Subtask 2
現在題目是要支援區間修改、區間求和。看到區間修改,其實可以馬上聯想到可能會需要懶人標記。沒錯,這題就是個懶人標記的題目,而這種線段樹也是蠻常出現在題目中的!
這題懶標要怎麼設計呢?首先,操作是區間加值,所以 tag 應該就是要存「子樹以下所有數字要加上多少」。
接下來,要怎麼利用懶標快速知道如何更新答案(這題來說是子樹的總和)呢?其實就是把總和加上 tag * 子樹大小(也就是維護的區間長度)!
### 另解 1
可以用兩個 BIT 維護差分陣列解決喔!
https://oi-wiki.org/ds/fenwick/#%E5%8C%BA%E9%97%B4%E5%8A%A0%E5%8C%BA%E9%97%B4%E5%92%8C
### 另解 2
這題也可以用分塊做掉喔!
https://sprout.tw/algo2021/ppt_pdf/week14/root_method.pdf
可以從第 22 頁開始看喔~
### Sample Code
:::spoiler Sample Code
```cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int kN = 200006;
ll a[kN];
struct Node {
Node *lc, *rc;
ll sum, tag;
Node(): lc(NULL), rc(NULL), sum(0), tag(0){}
void pull() {
sum = lc->sum + rc->sum;
}
};
Node* Build(int L, int R) {
Node* node = new Node();
if (L == R) {
node->sum = a[L];
return node;
}
int mid = (L + R) >> 1;
node->lc = Build(L, mid);
node->rc = Build(mid + 1, R);
node->pull();
return node;
}
void push(Node* node, int L, int R) {
if (L == R) node->tag = 0;
if (node->tag == 0) return;
int mid = (L + R) >> 1;
node->lc->tag += node->tag;
node->lc->sum += node->tag * (mid - L + 1);
node->rc->tag += node->tag;
node->rc->sum += node->tag * (R - mid);
node->tag = 0;
return;
}
void modify(Node* node, int L, int R, int l, int r, ll d) {
if (l > R || L > r) return;
else if (l <= L && R <= r) {
node->tag += d;
node->sum += (R - L + 1) * d;
return;
}
push(node, L, R);
int mid = (L + R) >> 1;
modify(node->lc, L, mid, l, r, d);
modify(node->rc, mid + 1, R, l, r, d);
node->pull();
return;
}
ll query(Node* node, int L, int R, int l, int r) {
if (l > R || L > r) return 0;
else if (l <= L && R <= r) return node->sum;
push(node, L, R);
int mid = (L + R) >> 1;
return query(node->lc, L, mid, l, r) + query(node->rc, mid + 1, R, l, r);
}
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n, q; cin >> n >> q;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
Node* root = Build(1, n);
while (q--) {
int t; cin >> t;
if (t == 1) {
int l, r, d; cin >> l >> r >> d;
modify(root, 1, n, l, r, d);
} else {
int l, r; cin >> l >> r;
cout << query(root, 1, n, l, r) << '\n';
}
}
}
```
:::