題目連結
https://codeforces.com/problemset/problem/1415/D
題目大意
給定一個長為 \(n\) 的不降序列,每次操作可以任選相鄰的兩個數,并将這兩個數替換為兩個數按位異或的結果,現在需要破壞序列的不降,求最少操作次數,無解輸出 \(-1\)。
方法一:二分答案
首先,要破壞數列的非嚴格單調遞增特性,肯定會選擇兩個連續的元素 \(a_i\) 和 \(a_{i+1}\),然後:
- 選擇将 \(a_i\) 結尾的連續若幹個元素(\(\ldots, a_{i-2}, a_{i-1}, a_i\))進行異或;
- 選擇将 \(a_{i+1}\) 開頭的連續若幹個元素(\(a_{i+1}, a_{i+2}, \ldots\))進行異或
并令 前者(以 \(a_i\) 結尾的連續若幹個數的異或和)大于 後者(以 \(a_{i+1}\)
為什麼操作連續的若幹個數?
舉個形象一點的例子,這個就跟伐木一樣,比如說我要砍倒一棵樹,我肯定會選擇同一個高度砍,不會說先在距離地面 \(0.2\) 米的位置砍兩刀,然後再到距離地面 \(0.5\)
因為我們的目的是破壞數列“非嚴格單調遞增“的性質,是以我們隻需要存在一對元素滿足條件即可,很明顯:
- 它們是相鄰的
- 左邊的那個數是一段連續的元素的異或和(對應以某個 \(a_i\)
- 右邊的那個數也是一段連續的元素的異或和(對應以 \(a_{i+1}\)
考慮可以操作 \(m\) 次時是否滿足條件(破壞數列的“非嚴格單調遞增“性質),因為要将一段連續的區間内的元素消到隻剩 \(2\) 個數(然後左邊那個數大于右邊那個數),操作一次會消去一個數,是以 \(m\) 次操作對應一個長度為 \(m+2\)
當 \(m\) 确定時,枚舉每個下标 \(i\),對于下标 \(i\),我們要确定的事情是:
是否存在一個以 \(a_i\) 結尾的連續子序列(假設這個連續子序列的異或和為 \(x\))以及一個以 \(a_{i+1}\) 開頭的連續子序列(假設這個連續子序列的異或和為 \(y\)),滿足 \(x \gt y\) 且兩個連續子序列包含的元素個數不超過 \(m+2\)。
如果有的話就說明 \(\Rightarrow\) 消除不超過 \(m\)
注意:是 ”不超過 \(m\) 而不是 ”恰好 \(m\)。
這是為什麼呢?我們多定義兩個狀态 \(b_j\) 和 \(c_j\),這兩個狀态在 \(j \le i\) 和 \(j \gt i\)
當 \(j \le i\)
\(b_j\) 表示的是 \(a_j \oplus a_{j+1} \oplus \ldots a_i\)(即以 \(a_j\) 開頭以 \(a_i\)
\[b_j = \begin{cases}
a_i & j = i \\
a_j \oplus b_{j+1} & j \lt i
\end{cases}
\]
\(c_j\) 表示的是 \(b_j, b_{j+1}, \ldots, b_i\)
\[c_j = \begin{cases}
b_i & j = i \\
\max\{ b_j, c_{j+1} \} & j \lt i
\end{cases}
\]
此時的狀态是從後往前推(\(i \rightarrow i-1 \rightarrow i-2 \rightarrow \ldots\)),可以發現 \(c_j\) 從後往前是非降的,因為 \(c_j\) 表示的并不是從 \(a_i\) 異或到 \(a_j\) 的結果(這是 \(b_j\) 表示的),而是從 \(a_i\) 異或到 \(a_j\)
也就是說,\(c_j\) 表示的含義是 \(a_i\) 往前合并 不超過 \(i - j\)
當 \(j \gt i\)
\(b_j\) 表示的是 \(a_{i+1} \oplus a_{i+2} \oplus \ldots a_j\)(即以 \(a_i\) 開頭以 \(a_j\)
\[b_j = \begin{cases}
a_{i+1} & j = i+1 \\
a_j \oplus b_{j-1} & j \gt i+1
\end{cases}
\]
\(c_j\) 表示的是 \(b_{i+1}, b_{i+2}, \ldots, b_j\)
\[c_j = \begin{cases}
b_{i+1} & j = i+1 \\
\min\{ b_j, c_{j-1} \} & j \gt i+1
\end{cases}
\]
(注意,差別于 \(j \le i\) 的情況,這裡 \(c_j\)
此時的狀态是從前往後推(\(i+1 \rightarrow i+2 \rightarrow i+3 \rightarrow \ldots\)),可以發現 \(c_j\) 從前往後是非增的,因為 \(c_j\) 表示的是從 \(a_{i+1}\) 異或到 \(a_j\)
也就是說,在 \(g \gt i\) 時,\(c_j\) 表示的含義是 \(a_{i+1}\) 往後合并 不超過 \(j - i - 1\)
那麼怎麼能確定不超過 \(m\)
枚舉合并的過程中最前面那個數的下标!
以 \(a_i\) 結尾合并 \(m\) 次能合并到 \(a_{i-m}\),是以我們從 \(\max\{ i-m, 1\}\) 到 \(i\) 枚舉下标 \(j\),對于的下标區間範圍是 \([j, j+m+1]\) —— 注意 \(j+m+1\) 可能大于 \(n\),所可以開一個 \(k = j+m+1\),對于的區間範圍是 \([j, k]\),當 \(k \gt n\) 時就不用繼續操作了(因為 \(k \gt n\) 是左邊一段變小,而右邊一段固定,繼續 \(j++\) 隻會讓左邊一段異或和的最大值變小,而右邊一段異或和的最小值是不會發生任何變換,是以繼續 \(j++\)
- \(a_i\) 能夠合并到的最左邊的數是 \(a_j\)
- \(a_{i+1}\) 能夠合并到的最右邊的數是 \(a_k\)
那麼 \(a_i\) 往左合并的過程中的最大值就是 \(c_j\),\(a_{i+1}\) 往右合并的過程中的最小值就是 \(c_k\)。隻要 \(c_j \gt c_k\),就說明不超過 \(m\)
對應的我可以開一個
check(int m)
函數進行判斷。
但是這種方法隻能判斷在不超過 \(m\) 次合并時能夠滿足條件,不過可以發現,當 \(m\) 大于等于我們的最小操作次數時,check 函數總會傳回 true;當 \(m\)
是以可以對 check 函數二分答案。
示例程式:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int n, a[maxn], b[maxn], c[maxn];
bool check(int m) {
for (int i = 1; i < n; i++) {
b[i] = c[i] = a[i];
for (int j = i-1; j >= max(1, i-m); j--) {
b[j] = a[j] ^ b[j+1];
c[j] = max(b[j], c[j+1]);
}
b[i+1] = c[i+1] = a[i+1];
for (int j = i+2; j <= min(i+m+1, n); j++) {
b[j] = a[j] ^ b[j-1];
c[j] = min(b[j], c[j-1]);
}
for (int j = max(1, i-m); j <= i && j+m+1 <= n; j++)
if (c[j] > c[j+m+1])
return true;
}
return false;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int l = 0, r = n-2, res = -1;
while (l <= r) {
int mid = (l + r) / 2;
if (check(mid)) res = mid, r = mid - 1;
else l = mid + 1;
}
cout << res << endl;
return 0;
}
方法二:貪心思想+枚舉答案
上面二分答案的解法雖然能通過,但是分析一下,由于
check(m)
的時間複雜度是 \(O(n \cdot m)\) 的,\(m\) 接近 \(n\),是以總的時間複雜度是 \(O(n^2 \log n)\)
- 當 \(n\)
- 題目的 \(n\)
- 或者别的原因(歡迎評論區補充)
可以發現,因為 \(1 \le a_i \le 10^9\),是以 \(a_i\) 的二進制表示最多 \(30\) 位,而數列是非嚴格單調遞增的,是以根據鴿籠/雀巢/抽屜原理,當 \(n \gt 60\) 時,必然存在連續的三個數 —— 假設這三個數是 \(a_i, a_{i+1}, a_{i+2}\) —— 的最高位的位數是相同的,此時将後兩個數(即 \(a_{i+1}\) 和 \(a_{i+2}\))異或能夠消去最高位的 \(1\),此時 \(a_i \gt a_{i+1} \oplus a_{i+2}\)(注意:本題中 \(a_i \ge 1\),若 \(a_i\) 可以取 \(0\) 那還需要考慮排除 \(0\) 的影響),即:當 \(n \gt 60\) 時,答案為 \(1\)。
而當 \(n \le 60\)
也就是說,在題目裡面可以加一行
if (n > 60) {
cout << 1 << endl;
return 0;
}