HDU 6955. Xor Sum
題目鏈接:HDU 6955. Xor Sum
題意:
給一個長度為\(n\)的一個整數序列\({a_n}\),尋找最短的,滿足異或和大於等於\(k\)的連續子序列。輸出子序列的左端點和右端點,若有多個最短長度的連續子序列,輸出位置靠前的。不存在滿足條件的連續子序列,輸出\(-1\)。
輸入:
第\(1\)行一個整數\(t(t\leq100)\),代表測試樣例個數。
對於每一個樣例:
第\(1\)行,有兩個整數\(n(1\leq n\leq 10^5),k(0\leq k<2^{30})\),分別代表整數序列的長度和題意中的\(k\)。
第\(2\)行,有\(n\)個整數\(a_1,a_2,...,a_n(0\leq a_i<2^{30})\),代表這個整數序列。
輸出:
對於每一個樣例:
若存在這樣的連續子序列,輸出連續子序列的左端點和右端點。
若有多個最短長度的連續子序列,輸出位置靠前的那一個。
若不存在滿足條件的連續子序列,輸出\(-1\)。
分析:
首先,異或滿足這樣的性質:
這使得我們可以使用前綴異或和。
設\(sum[1...i]=S_i\),特別地,定義\(S_0=0\),於是有
從而可以將原問題轉化為,在\(S_0,S_1,...,S_n\)中尋找一對在序列中距離最短的\(S_i,S_j(i<j)\),滿足\(S_i\oplus S_j\geq k\),最終答案即為\([i+1,j]\)(\(i+1\)是左端點,\(j\)是右端點)
最暴力的想法為:使用二層循環,第一層從\(1\)到\(n\)枚舉距離\(d\),第二層枚舉\(i(0\leq i\leq n+1-d)\),判斷\(S_i\oplus S_{i+d-1}\geq k\)是否成立,成立則說明答案已經找到,跳出循環,不成立則繼續循環。
暴力做法時間復雜度為\(O(n^2)\),會超時,所以需要優化。不妨考慮枚舉右側端點\(i\),試試看能否在區間\([0,i-1]\)上快速找到離\(i\)最近的\(j\),使得\(S_j\oplus S_i\geq k\)。
既然是異或問題,一定和二進制相關,而題目給出的范圍是\(0\leq a_i<2^{30}\),所以\(S_i\)也在這個范圍中,說明\(S_i\)可以用\(30\)位二進制表示,於是\(S_i\)可以看成長度為\(30\)的"01"字符串。
故而可以考慮在枚舉\(i\)的時候,動態地維護一個包含\(S_0,S_1,...,S_{i-1}\)的"01"字典樹,其中深度小的節點存儲高位,深度大的節點存儲低位。字典樹的每個節點附加存儲着這個節點所表示的前綴(從高位開始的"01"串)最后一次在數列\(S_0,S_1,...,S_{i-1}\)中出現的位置,沒出現過位置就記成\(-1\)。
然后讓\(S_i\)和字典樹中的串進行帶剪枝的逐位異或:
為了方便描述,記\(S_i\)中從高位到低位數起第\(j\)位(以下簡稱“第\(j\)位”)為\(S_{ij}\),\(k\)中第\(j\)位是\(k_j\)。
假設目前正在考慮第\(j\)位的情況,即到達了字典樹的第\(j-1\)層(根節點為空前綴,把它當成第\(0\)層),考慮往哪個方向上的子樹深入下去(並不是兩個方向上都需要深入,即剪枝)。
-
當\(k_j=1\),就要求字典樹中所表示串的第\(j\)位和\(S_{ij}\)異或的結果也是\(1\),才有可能使得最終異或結果大於等於\(k\),由於\(S_{ij}\oplus1\)與\(S_{ij}\)異或結果是\(1\),故此時需要往\(S_{ij}\oplus1\)方向的那個子樹上深入。
-
當\(k_j=0\),說明字典樹中所表示串的第\(j\)位和\(S_{ij}\)異或的結果可以是\(0\),也可以是\(1\)
-
若字典樹中所表示串的第\(j\)位和\(S_{ij}\)異或的結果是\(1\),由於\(S_{ij}\oplus1\)與\(S_{ij}\)異或結果是\(1\),即考慮往\(S_{ij}\oplus1\)方向,發現此時無需進行后續位的異或,也可知道最終異或結果大於\(k\),故無需往\(S_{ij}\oplus1\)方向的子樹下深入,直接利用往\(S_{ij}\oplus1\)方向的節點所附加的信息更新答案(別忘了,每個節點附加記錄了這個節點所代表前綴最后一次在數列\(S_0,S_1,...,S_{i-1}\)中出現的位置,這是一種剪枝,也是優化的關鍵)。
-
若字典樹中所表示串的第\(j\)位和\(S_{ij}\)異或的結果是\(0\),由於\(S_{ij}\)與\(S_{ij}\)異或結果是\(0\),即考慮往\(S_{ij}\)方向,此時還需要進行后續位的異或,故需要往\(S_{ij}\)方向的子樹上深入。
-
假如逐位異或能夠進行到最后一位,那說明異或到最后才比較出大於等於\(k\),此時直接利用葉節點附加信息更新答案。
在\(S_i\)與字典樹中的串進行帶剪枝的逐位異或之后,就需要把\(S_i\)這個串插入到字典樹中,注意插入過程需要更新節點的附加信息,以便后續計算。
時間復雜度分析:由於字典樹只會往一個方向遍歷,設整數序列最大的數為\(P\),則樹的最大深度是\(\log P\),整數序列長度為\(n\),故復雜度為\(O(n\log P)\),本題中可以認為是\(O(30n)\)。
代碼:
#include <algorithm>
#include <cassert>
#include <cstdio>
using namespace std;
const int maxn = 1e5 + 10;
const int maxm = 3e6 + 10;
int a[maxn];
int ch[maxm][2], val[maxm];
void solve() {
int n, k, tot = 1;
scanf("%d%d", &n, &k);
a[0] = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
a[i] ^= a[i - 1];
}
ch[1][0] = ch[1][1] = 0;
val[1] = -1;
int ans_l = -1, ans_r = n + 1;
for (int i = 0; i <= n; i++) {
int now = 1, res = -1;
for (int j = 29; j >= 0; j--) {
int dig = (a[i] >> j) & 1;
if ((k >> j) & 1) { // k的當前位為1,只能和dig異或結果為1,才可能大於等於k
now = ch[now][dig ^ 1]; // 與dig異或結果為1的數是dig^1
} else { // k的當前位為0,和dig異或結果可以是1也可以是0
if (ch[now][dig ^ 1]) { // 和dig異或結果為1,后面的位都無須看,結果一定大於k
res = max(res, val[ch[now][dig ^ 1]]);
}
// 和dig異或結果是1的情況就無須遍歷,只需要遍歷和dig異或結果為0的情況
now = ch[now][dig];
}
if (now == 0) { // 節點沒了
break;
}
}
if (now) res = max(res, val[now]);
if (res >= 0 && i - res < ans_r - ans_l) {
ans_l = res;
ans_r = i;
}
now = 1;
for (int j = 29; j >= 0; j--) {
int dig = (a[i] >> j) & 1;
if (!ch[now][dig]) {
ch[now][dig] = ++tot;
ch[tot][0] = ch[tot][1] = 0;
val[tot] = -1;
}
now = ch[now][dig];
val[now] = max(val[now], i);
}
}
if (ans_l == -1 && ans_r == n + 1) {
puts("-1");
} else {
printf("%d %d\n", ans_l + 1, ans_r);
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) solve();
return 0;
}