---
tags: 成大高階競技程式設計 2021
image: https://i.imgur.com/IIj9BcL.png
---
# 2021 高階競程 Contest #4 - 題解
## [區間 xor](https://judge.csie.ncku.edu.tw/problems/104)
- Task Provider: leo
- Task Setter: leo
### 首殺 team21_25 (00:02)
### 解法說明
整數的 xor 運算符合以下條件,下方以「$\oplus$」當作 xor 運算符:
1. 交換律:$a\oplus b=b\oplus a$
2. 結合律:$(a\oplus b)\oplus c=a\oplus(b\oplus c)$
3. 單位元為 $0$:$a\oplus0=0\oplus a=a$
4. 反元素為自身:$a\oplus a=0$
#### 解法 1
可以注意到 xor 有結合律的特性,因此可以直接用線段樹維護,
建造 O($N\log N$),單次查詢 $O(\log N)$,
總複雜度 $O((N+Q)\log N)$。
:::spoiler 參考程式
```cpp
#include <iostream>
#include <vector>
#include <functional>
using namespace std;
template<typename value_t, class merge_t>
class SGT {
int n;
vector<value_t> t; // root starts at 1
merge_t merge; // associative function for SGT
public:
explicit SGT(int _n = 0, const merge_t& _merge = merge_t{})
: n{_n}, t(2 * n), merge{_merge} {}
void modify(int p, const value_t& x) {
for (t[p += n] = x; p > 1; p >>= 1) t[p >> 1] = merge(t[p], t[p ^ 1]);
}
value_t query(int l, int r, value_t init) { // [l:r)
for (l += n, r += n; l < r; l >>= 1, r >>= 1) {
if (l & 1) init = merge(init, t[l++]);
if (r & 1) init = merge(init, t[--r]);
}
return init;
}
};
void solve() {
int n, q;
cin >> n >> q;
vector<int> v(n);
for (auto& x : v) cin >> x;
SGT<int, bit_xor<int>> sgt{n};
for (int i{0}; i < n; ++i) sgt.modify(i, v[i]);
while (q--) {
int l, r;
cin >> l >> r, --l;
cout << sgt.query(l, r, 0) << '\n';
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t{1};
//cin >> t;
while (t--) solve();
return 0;
}
```
:::
#### 解法 2
先思考區間合的問題,在解區間和問題時可以預處理前綴和,
在此假設預處理完的前綴和陣列名字叫做 $pre$
每次查詢 $[l,r]$ 的和時,只要輸出 $pre[r]-pre[l-1]$ 即可。
前綴和這個做法利用了上述四種特性。
而 xor 也有這種特性,因此可以預處理前綴 xor 的值,
每次查詢輸出 $pre[r]\oplus pre[l-1]$ 即可。
預處理 $O(N)$,單次查詢 $O(1)$,總複雜度 $O(N+Q)$
### 標準程式
:::spoiler
```cpp
#include <iostream>
using namespace std;
int a[400001];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n, q, l, r;
cin >> n >> q;
for(int i = 1; i <= n; i++){
cin >> a[i];
a[i] ^= a[i - 1];
}
while(q--){
cin >> l >> r;
cout << (a[r] ^ a[l - 1]) << "\n";
}
}
```
:::
## [Range Max](https://judge.csie.ncku.edu.tw/problems/105)
- Task Provider: D4nnyLee
- Task Setter: D4nnyLee
### 首殺 team21_12 (00:28)
### 解法說明
定義一個新的長度為 $n - 1$ 的陣列 $b$,而 $b_i = |a_{i + 1} - a_i|$。
#### 解法 1
考慮以下兩個陣列:
1. $[b_1, -b_2, b_3, \dots, (-1)^{n - 2}b_{n - 1}]$
2. $[-b_1, b_2, -b_3, \dots, (-1)^{n - 1}b_{n - 1}]$
題目要求的答案其實就是上面兩個陣列的最大連續和的最大值。
第 $1$ 個陣列的最大連續和就是所有 $l$ 為奇數的 $[l, r]$ 代入 $f$ 後的最大值,
以此類推,第 $2$ 個陣列的最大連續和就是 $l$ 為偶數的最大值,因此兩個取最大值就是所有可能的 $[l, r]$ 的最大值。
#### 解法 2
定義 $dp_{i, 0}$ 是所有以 $i$ 為開頭的區間代入 $f$ 後可以得到的最小值,而 $dp_{i, 1}$ 就是最大值。
則我們可以得到以下關係式:
* $dp_{i, 0} = \min(b_i, b_i - dp_{i + 1, 1})$
* $dp_{i, 1} = \max(b_i, b_i - dp_{i + 1, 0})$
最後的答案就是 $\displaystyle \max_{1 \leq i \leq n - 1}\{dp_{i, 1}\}$。
### 標準程式
:::spoiler 解法 1
```cpp
#include <bits/stdc++.h>
#define Int long long
using namespace std;
int main() {
int n; cin >> n;
vector<Int> A(n);
for(auto &it: A) cin >> it;
vector<Int> B(n-1);
for(int i = 0; i < n-1; i++) B[i] = abs(A[i] - A[i+1]);
for(int i = 0; i < n-1; i+=2) B[i] = -B[i];
Int ans = 0, now = 0;
for(int i = 0; i < n-1; i++)
now = max(B[i], now+B[i]), ans = max(ans, now);
for(int i = 0; i < n-1; i++) B[i] = -B[i];
now = 0;
for(int i = 0; i < n-1; i++)
now = max(B[i], now+B[i]), ans = max(ans, now);
cout << ans << endl;
return 0;
}
```
:::
:::spoiler 解法 2
```cpp
#include <bits/stdc++.h>
using namespace std;
void test_case()
{
int n;
cin >> n;
vector<int> a(n);
for (auto &ai : a)
cin >> ai;
for (int i{1}; i < n; ++i)
a[i - 1] = abs(a[i] - a[i - 1]);
--n;
a.pop_back();
vector<pair<long long, long long>> dp(n);
dp.back() = {a.back(), a.back()};
long long ans{a.back()};
for (int i{n - 2}; i >= 0; --i) {
auto &[mx, mn]{dp[i]};
auto [prev_mx, prev_mn]{dp[i + 1]};
mx = mn = a[i];
mx -= min(0LL, prev_mn);
mn -= max(0LL, prev_mx);
ans = max(ans, mx);
}
cout << ans << '\n';
}
int main(void)
{
ios::sync_with_stdio(0);
cin.tie(0);
int Case = 1;
// cin >> Case;
for (int i = 1; i <= Case; i++) {
// cout << "Case #" << i << ": ";
test_case();
}
return 0;
}
```
:::
## [該睡覺了 晚安](https://judge.csie.ncku.edu.tw/problems/106)
- Task Provider: ys
- Task Setter: ys
### 首殺 team21_24 (01:21)
### 解法說明
將一段區間從行程表中移除,就只剩下**前綴**及**後綴**的行程表
正好,題目要求的就是前後綴相加期間變化的最高及最低值
對於區間 $[l, r]$ 算出 $l$ 以前的**前綴和極值**,以及 $r$ 之後的**後綴和極值**
接著處理這些極值就能得到題目所要求的答案
定義 $p(i)$ 為以 $i$ 結尾的前綴和,則 $p(i) = p(i-1) + A_i$ 且 $p(0) = 0$
- 定義 $p_{\min}(i)$ 為以 $i$ 結尾的**最小前綴和**
則 $p_{\min}(i) = \min\{p_{\min}(i-1), p(i)\}$ 且 $p_{\min}(0) = 0$
- 定義 $p_{\max}(i)$ 為以 $i$ 結尾的**最大前綴和**
則 $p_{\max}(i) = \max\{p_{\max}(i-1), p(i)\}$ 且 $p_{\max}(0) = 0$
後綴和極值也能以**動態規劃**的方式去思考:
- 定義 $s_{\min}(i)$ 為以 $i$ 開頭的**最小後綴和**
則 $s_{\min}(i) = \min\{A_i + s_{\min}(i+1), 0\}$ 且 $s_{\min}(n+1) = 0$
> 因為後綴和是以 $0$ 開始累加的,所以該**上界**為 $0$
- 定義 $s_{\max}(i)$ 為以 $i$ 開頭的**最大後綴和**
則 $s_{\max}(i) = \max\{A_i + s_{\max}(i+1), 0\}$ 且 $s_{\max}(n+1) = 0$
> 因為後綴和是以 $0$ 開始累加的,所以該**下界**為 $0$
### 標準程式
:::spoiler
```cpp
#include<bits/stdc++.h>
using namespace std;
int constexpr maxn = 5e5 + 10;
int n, m, A[maxn], l, r;
int main()
{
ios_base::sync_with_stdio(false), cin.tie(NULL);
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> A[i];
vector<int> pr(n+1), pr_mi(n+1), pr_mx(n+1); // prefix minimum & maximum
pr[0] = pr_mi[0] = pr_mx[0] = 0;
for(int i = 1; i <= n; i++) {
pr[i] = pr[i-1] + A[i];
pr_mi[i] = min(pr_mi[i-1], pr[i]);
pr_mx[i] = max(pr_mx[i-1], pr[i]);
}
vector<int> su_mi(n+2), su_mx(n+2); // surfix minimum & maximum
su_mi[n+1] = su_mx[n+1] = 0;
for(int i = n; i >= 1; i--) {
su_mi[i] = min(0, A[i] + su_mi[i+1]);
su_mx[i] = max(0, A[i] + su_mx[i+1]);
}
while(m--) {
cin >> l >> r;
cout << min(pr_mi[l-1], pr[l-1] + su_mi[r+1]) << ' ';
cout << max(pr_mx[l-1], pr[l-1] + su_mx[r+1]) << '\n';
}
return 0;
}
```
:::
## [眼神殺 - 風起](https://judge.csie.ncku.edu.tw/problems/107)
- Task Provider: raiso
- Task Setter: raiso
### 首殺 team21_12 (02:00)
### 解法說明
首先考慮討論團彼此之間不會交錯,也就是最終的討論團順序屬於三個字串的共同子序列。
在上課的時候我們有學到如何解決兩個字串的共同子序列,這時候就可以參考他的 DP 轉移式的設計方式,相同時 $+1$,不同時找附近最大值繼承。
這題是 3 個字串的 LCS 題目,不過有卡記憶體用量,並不能開 $300^3$ 個狀態。需要壓縮到 $O(N^2)$ 的狀態數量才會過。
觀察可以發現固定第一行的某元素下去找同字母的 LCS 對於第一行而言,只會用到當前跟上一個的值,所以不用存整條的 DP 式,因此 $i \geq 2$ 的時候就可以把前面的 clear 掉
### 標準程式
:::spoiler
```cpp
#include <bits/stdc++.h>
using namespace std;
vector<vector<vector<int>>> dp;
int n;
int DP(int i, int j, int k) {
if(i >= 0 && j >= 0 && k >= 0) return dp[i][j][k];
else return 0;
}
int main() {
cin >> n;
string str[3];
dp.resize(n);
for(int i = 0; i < 3; i++) cin >> str[i];
for(int i = 0; i < n; i++) {
dp[i].assign(n, vector<int>(n, 0));
if(i-2 >= 0) dp[i-2].clear();
for(int j = 0; j < n; j++) for(int k = 0; k < n; k++) {
if(str[0][i] == str[1][j] && str[0][i] == str[2][k] && str[0][i] != '?')
dp[i][j][k] = DP(i-1, j-1, k-1)+1;
else
dp[i][j][k] = max({DP(i-1,j,k), DP(i,j-1,k), DP(i,j,k-1)});
}
}
cout << dp[n-1][n-1][n-1] * 3 << endl;
return 0;
}
```
:::
## [男廁的默契](https://judge.csie.ncku.edu.tw/problems/108)
- Task Provider: arashi87
- Task Setter: arashi87
### 首殺 team21_15 (01:31)
### 解法說明
我們可以根據題序很簡單的想到這是一題 dp,但是因為有單點修改以及變相區間求和所以需要加上線段樹。
我們可以在線段樹上每個節點都開一個 dp 數組,對於每個 root 節點維護一個 dp 數組,$dp[root][i][j]$,且 $i, j\in \{1, 0\}$,其代表意義為 root 節點的左節點選 (不選),右節點選(不選),給定初值只要對於每個葉節點進行 $dp[root][1][1] = arr[idx]$ 就行。
因為相鄰兩個節點不能被同時選中,所以在進行 pushup 回溯時要注意不能出現像是 $dp[root<<1][0][1] + dp[root<<1|1][1][0]$ 這種,接下來我們就可以推出下面一個很長的轉移式。
$$
lc = root<<1, rc = (root<<1)|1
$$
$$
dp[rt][0][0]=max(dp[lc][0][0]+dp[rc][0][0], dp[lc][0][0]+dp[rc][1][0], dp[lc][0][1]+dp[rc][0][0])
$$
$$
dp[rt][0][1]=max(dp[lc][0][0]+dp[rc][0][1], dp[lc][0][1]+dp[rc][0][1], dp[lc][0][0]+dp[rc][1][1])
$$
$$
dp[rt][1][0]=max(dp[lc][1][0]+dp[rc][0][0], dp[lc][1][0]+dp[rc][1][0], dp[lc][1][1]+dp[rc][0][0])
$$
$$
dp[rt][1][1]=max(dp[lc][1][0]+dp[rc][0][1], dp[lc][1][1]+dp[rc][0][1],dp[lc][1][0]+dp[rc][1][1])
$$
其實這可以直接合併成下列轉移式。
$$
dp[rt][i][j]=max(dp[lc][i][0]+dp[rc][0][j], dp[lc][i][\{0, 1\}]+dp[rc][\{0, 1\}][j],dp[lc][i][\{0, 1\}]+dp[rc][\{0, 1\}][j])
$$
然後因為這長度有點令人崩潰,所以我將後面兩維壓成一維,然後將其視為二進位轉換成十進位。
![喵喵喵](https://i.imgur.com/E58ME62.png)
最後我們只要取個根節點的 `std::max(std::max(seg[1][0], seg[1][1]), std::max(seg[1][2], seg[1][3]))` 就能得到答案了。
### 標準程式
:::spoiler
```cpp
#include <algorithm>
#include <stdio.h>
#define int long long
#define lc (rt << 1)
#define rc (rt << 1 | 1)
const int maxN = 2e5 + 7;
int n, m, arr[maxN];
int seg[maxN << 2][5], ans = 0;
inline int max(int a, int b, int c)
{
return std::max(a, std::max(b, c));
}
void pushup(int rt)
{
seg[rt][0] = max(seg[lc][0] + seg[rc][0], seg[lc][1] + seg[rc][0], seg[lc][0] + seg[rc][2]);
seg[rt][1] = max(seg[lc][0] + seg[rc][1], seg[lc][1] + seg[rc][1], seg[lc][0] + seg[rc][3]);
seg[rt][2] = max(seg[lc][2] + seg[rc][0], seg[lc][2] + seg[rc][2], seg[lc][3] + seg[rc][0]);
seg[rt][3] = max(seg[lc][2] + seg[rc][1], seg[lc][3] + seg[rc][1], seg[lc][2] + seg[rc][3]);
}
void build(int rt, int L, int R)
{
if (L == R)
seg[rt][3] = arr[L];
else {
int mid = (L + R) >> 1;
build(rt << 1, L, mid);
build(rt << 1 | 1, mid + 1, R);
pushup(rt);
}
}
void modify(int rt, int L, int R, int x, int d)
{
if (L == R)
seg[rt][3] = d;
else {
int mid = (L + R) >> 1;
if (x <= mid)
modify(rt << 1, L, mid, x, d);
else
modify(rt << 1 | 1, mid + 1, R, x, d);
pushup(rt);
}
}
signed main()
{
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%lld", arr + i);
build(1, 1, n);
for (int i = 1, x, y; i <= m; i++) {
scanf("%lld%lld", &x, &y);
modify(1, 1, n, x, y);
printf("%lld\n", std::max(std::max(seg[1][0], seg[1][1]), std::max(seg[1][2], seg[1][3])));
}
}
```
:::