理論基礎
異或雜湊是個很神奇的演算法,利用了異或操作的特殊性和雜湊降低衝突的原理,可以用於快速找到一個組合是否出現、序列中的數是否出現了k次
https://blog.csdn.net/notonlysuccess/article/details/130959107
https://codeforces.com/blog/entry/85900
CF1175F
https://codeforces.com/contest/1175/problem/F
那麼,最經典的求組合出現問題
理論基礎中提到了這個問題,並給出了 \(O(n^2)\) 的暴力解法。
std::mt19937_64 rnd(time(0));
using hash = uint64_t;
void solve()
{
int n; std::cin >> n;
std::vector<int> a(n); for (auto& x : a) {std::cin >> x;}
std::vector<hash> code(n + 1), pre_chk(n + 1), pre_xor(n + 1);
// code:(a[i] -> uint64), pre_chk : 1 ~ n 的 字首異或和, pre_xor : a[i] 的 字首異或和
for (int i = 1; i <= n; i++) {
code[i] = rnd();
pre_chk[i] = pre_chk[i - 1] ^ code[i];
}
for (int i = 1; i <= n; i++) {pre_xor[i] = pre_xor[i - 1] ^ code[a[i]];}
std::map<int, int> cnt; cnt[0] = 1;
int res{}; for (int l = 1; l <= n; l++) {
for (int r = l; r <= n; r++) {
if (pre_xor[r] ^ pre_xor[l - 1] == pre_chk[r - l + 1]) {
res += 1;
}
}
}
}
根據問題進一步提取性質:
- 滿足條件的區間肯定有 \(1\) 和 等於區間長度的最大值 \(mx\)
- 分類 \(mx\) 在 \(1\) 的左邊或者右邊處理即可。
int ans{}, one{};//特殊記錄長度為1的個數,因為會統計兩邊
auto calc = [&](auto a) {//正反跑一遍,處理兩個方向的方案數
std::vector<hash> pre_xor(n + 1);//a[i] 的 字首異或和和
for (int i = 0; i < n; i++) {pre_xor[i + 1] = pre_xor[i] ^ code[a[i]];}
int lst_one{-1}, mx = 0; for (int r = 0; r < n; r++) {
if (a[r] == 1) {//進行新一段的處理
one += 1; lst_one = r; mx = 1;
} else if (lst_one != -1) {
mx = std::max(mx, a[r]);
if (mx >= (r - lst_one + 1)) {//如果當前最大值大於等於當前段長度,則可以操作
int l{lst_one - (mx - (r - lst_one + 1))};//找到符合當前最大值長度的段的左端點
if (l >= 0) {ans += ((pre_xor[r + 1] ^ pre_xor[l]) == pre_chk[mx]);}
}
}
}
};
calc(decltype(a)(a.begin(), a.end())); calc(decltype(a)(a.rbegin(), a.rend()));
std::cout << ans + one / 2 << '\n';
CF1418G
https://www.luogu.com.cn/problem/CF1418G
那麼,出現次數問題
這裡要求出現三次,所以不用二進位制異或而是新定義一個三進位制異或:
constexpr int N{60};
using ternary = std::array<int, N>;
ternary operator ^(const ternary &a, const ternary &b){
ternary c;
for (int i = 0; i < N; i++) {
c[i] = a[i] + b[i];
if (c[i] >= 3) c[i] -= 3;
}
return c;
}
首先我們知道一個思想,證明充要條件就要證明它既充分又必要;同樣,要證明一個數等於某個值,必須讓它既小於等於又大於等於這個值。
這個思想運用到這道題上就十分方便。我們讓所有數的出現個數 \(cnt = 3\),便是要去滿足 \(cnt \geq 3 \land cnt \leq 3\) 這倆約束。 |
第一個約束十分好想,可以規約到 \(cnt \equiv 0 \pmod 3\) 上去(三的倍數必然大於等於三),然後顯然用 XOR-Hash 搞一下就行。
然後考慮第二個約束。我們考慮使用類似於雙指標的演算法,具體來說:考慮對於一個滿足約束二的 \([l, r]\) 區間,右指標每次往右移動一次,都可能會破壞原本“滿足約束二”的性質。那麼為了讓其重新滿足,我們需要讓左指標一直向右移動,即:從左到右刪去數字使得區間再次滿足約束二。(只需讓新加入的右指標的值 \(a_r\) 出現的次數小於等於三即可;因為這樣刪除必然不會導致“因為其他數字出現次數減少而導致不能滿足約束二”這種情況,理由顯然)
令 \(pre_r\) 為 \([1, r]\) 區間的異或和(也就是到 \(r\) 為止的字首異或和)。當刪除完畢之後,我們統計滿足 \(pre_r = pre_{pos}\) 且 \(pos \in [l, r]\) 的 \(pos\) 數量,這一點可以使用 map 或者雜湊表完成。那麼這道題就完成了,複雜度 \(\mathcal{O}(N \log_2 N)\) 或者純線性。
void solve()
{
int n; std::cin >> n; std::vector<int> a(n); for (auto& x : a) {std::cin >> x; --x;}
std::vector<ternary> nums(n); for (int i = 0; i < n; i++) for (int j = 0; j < N; j++) {nums[i][j] = rnd() % 3;}
std::vector pos(n, std::vector<int>());//(數字,出現位置)
std::vector<ternary> pre_xor(1);//(字首異或和)
std::map<ternary, int> cnt;//(統計字首異或和值的出現次數)
cnt[pre_xor[0]] = 1;
int p{}; i64 ans{};
for (int i = 0; i < n; i++) {
pre_xor.push_back(pre_xor.back() ^ nums[a[i]]);//當前目標的字首異或和
pos[a[i]].push_back(i);
if (std::size(pos[a[i]]) > 3) {//如果該數字的出現次數已經大於三次了
while (p <= pos[a[i]][std::ssize(pos[a[i]]) - 4]) {//去掉直到最左邊的該數字的位置所有出現的數字, 並更新每個字首異或和出現的次數
cnt[pre_xor[p]] -= 1; p++;
}
}
ans += cnt[pre_xor.back()];
cnt[pre_xor.back()] += 1;
}
std::cout << ans << '\n';
}
CF1996G
https://www.luogu.com.cn/problem/CF1996G
很神奇的雜湊做法
我們設 \(n=6,m=2\),且 \((1,3),(4,6)\) 是朋友,用紫線的連結表示朋友關係
對於每對朋友,要麼是透過優弧聯通,要麼是透過劣弧聯通,所以我們乾脆直接對優弧劣弧都染色一下
其中綠色/橙色是 \((4,6)\) 的劣弧/優弧,藍色/黃色是 \((1,3)\) 的劣弧/優弧
要維護最少的路,就是透過我們對於每隊朋友都選擇他們的劣弧/優弧後使得沒有被染色的道路最多(我們選擇某隊朋友的劣弧後,就使得優弧不存在圖上了)
一個很經典的思路:保留最少相當於刪除最多
為了方便寫部落格,我們分別對上面顏色的曲線進行編號:綠色是1,黃色是2,橙色是3,藍色是4
那麼我們能選擇的弧的集合其實是 \((1,3),(2,3),(1,4),(2,4)\)
其實就是我們要對每對朋友都選擇一個弧,使得僅被這些弧染色的道路儘可能多,然後刪除這些道路
我們改怎麼實現這個想法呢?
我們定義\(edge_i\)為 \(i\rightarrow i+1\) 的這條邊,例如 \(edge_1\) 就是 \(1\) 連向 \(2\) 的道路
我們對每對朋友的兩個端點都 \(\oplus rand\),其中 \(rand\) 是一個六十四位隨機數,即對於 \((1,3)\) 有 \(edge_1\oplus rand,edge_3⊕rand\),其中 \(rand\) 僅在這裡是相同的,即每對朋友在異或時的 \(rand\) 都互不相同。
然後我們維護一個字首和就可以得到 \(i\rightarrow i+1\) 這條路的染色情況了
而這是非常抽象的,我們是怎麼得到染色情況的呢?並且我們不是隻染了一個弧嗎,另一個難道直接不管了?
首先我們先簡化模型,假設只有 \((1,3)\) 這一對朋友,並且我們恰好得到 \(rand=1\),那麼有
然後又加上了 \((4,6)\) 這對朋友,並且 \(rand\) 恰好是 \(2\)
可以發現神奇的每個數值剛好都對應著一種弧的集和
我們對兩端都異或同一個隨機數是透過差分的思想來 \(O(1)\) 染色,這樣可以透過字首和得知當前的染色情況
可以透過字首和得知染色情況是因為,我們透過六十四位的隨機數異或值實現了雜湊的思想,對於每種弧都有特定的雜湊值,而弧集的雜湊值是可以透過異或得到,這個比較抽象,所以建議可以理解為狀壓差不多的思想
還有一個問題:為什麼只對一個弧染色就相當於對兩個弧都染色了呢
因為是異或的隨機值,我們對優弧染上了 \(x\) ,對劣弧染上了 \(y\) ,然後整個圈都同時異或 \(y\),相當於優弧染上了 \(x⊕y\),劣弧染了 \(0\),因為是隨機的異或值,所以 \(x⊕y\) 可以直接相當於 \(x\)。
然後用統計下字首和出現最多的數值,刪除這個數就是答案
std::mt19937_64 rng {std::chrono::steady_clock::now().time_since_epoch().count()};
void solve()
{
#define tests
int n, m; std::cin >> n >> m;
std::vector<u64> f(n);
for (int i = 0, u, v; i < m; i++) {
std::cin >> u >> v; --u; --v; u64 rnd{rng()};
f[u] ^= rnd; f[v] ^= rnd;//相當於差分對優弧劣弧染色
}
// 要維護最少的路,就是透過我們對於每隊朋友都選擇他們的劣弧/優弧後使得沒有被染色的道路最多
// 我們選擇某隊朋友的劣弧後,就使得優弧不存在圖上了
// 因為是異或的隨機值,我們對優弧染上了 x ,對劣弧染上了 y ,然後整個圈都同時異或 y
// 相當於優弧染上了 x⊕y,劣弧染了 0,因為是隨機的異或值,所以 x⊕y 可以直接相當於 x。
// 所以字首和隨便選一個分界點都能代表一種全染色情況,要麼優弧,沒值的久全是劣弧
std::map<u64, int> cnt;
u64 pre{};
int mx{};
for (int i = 0; i < n; i++) {
pre ^= f[i]; cnt[pre] += 1; mx = std::max(mx, cnt[pre]);
}
std::cout << n - mx << '\n';
}