Venice Technique(easy but useful trick)
Venice Technique(easy but useful trick)
本文翻译一篇cf博客。
问题引入:
N次操作,每次操作在集合加入1个V[i],然后集合中所有数减min(v[i],T[i]),问每次操作总共减了多少。
此题VeniceSet支持这几种操作:
1.插入x
2.删除x
3.集合中所有数减x
4.查询集合最小值
实际上其和普通set的区别仅在于操作3,对于该操作其实只需要使用一个全局delta(博客记为water_level),修改和查询时都加上/减去该值即可。
问题解决:
每次先插入v[i]再将所有数减去T[i],然后只要把集合中所有小于0的数删除即可。每个元素最多被加入删除一次,因此复杂度是\(O(nlog_n)\)。
代码:
#include <bits/stdc++.h>
using namespace std;
struct VeniceSet {
multiset<long long> S;
long long water_level = 0;
void add(long long v) {
S.insert(v + water_level);
}
void remove(long long v) {
S.erase(S.find(v + water_level));
}
void updateAll(long long v) {
water_level += v;
}
long long getMin() {
return *S.begin() - water_level;
}
int size() {
return S.size();
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
vector<long long> V(N), T(N);
for (int i = 0; i < N; i++) cin >> V[i];
for (int i = 0; i < N; i++) cin >> T[i];
VeniceSet mySet;
for (int i = 0; i < N; ++i) {
mySet.add(V[i]);
mySet.updateAll(T[i]); // decrease all by T[i]
long long total = T[i] * mySet.size(); // we subtracted T[i] from all elements
// in fact some elements were already less than T[i]. So we probbaly are counting
// more than what we really subtracted. So we look for all those elements
while (mySet.size() && mySet.getMin() < 0) {
// get the negative number which we really did not subtracted T[i]
long long toLow = mySet.getMin();
// remove from total the amount we over counted
total -= abs(toLow);
// remove it from the set since I will never be able to substract from it again
mySet.remove(toLow);
}
cout << total << ' ';
}
cout << endl;
return 0;
}
例题:
Vitya and Strange Lesson
给一数组,每次操作先让数组里每个数异或x,再求mex。每次操作后原数组会变成异或后的数组。
分析:
根据venice的思想,可以记录一个全局异或delta,每次只需将初始的数组异或该全局delta即可。因为原数组中的数异或上一个数的集合A里的数都不能选,考虑原数组中未出现的数都异或上一个数得到的集合B,集合A和集合B是全集的一个划分,因此每次的答案都是在集合B中找最小值。实现上求mex可以将原数组中未出现的数插入字典树贪心求解。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
long long lim = 1ll << 19;
int tr[N * 20][2], tot;
struct MexTrie {
void insert(int v) {
int idx = 0;
for (int i = 19; i >= 0; i--) {
int c = ((v >> i) & 1);
if (!tr[idx][c]) tr[idx][c] = ++tot;
idx = tr[idx][c];
}
}
int mex(int v) {
int idx = 0, res = 0;
for (int i = 19; i >= 0; i--) {
int c = ((v >> i) & 1);
if (tr[idx][c]) idx = tr[idx][c];
else {
res |= (1ll << i);
idx = tr[idx][c ^ 1];
}
}
return res;
}
};
struct VeniceSet {
int globalX = 0;
MexTrie T;
void insert(int x) {
T.insert(x);
}
void xorAll(int x) {
globalX ^= x;
}
int getMex() {
return T.mex(globalX);
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
VeniceSet mySet;
int n, m;
cin >> n >> m;
vector<bool> vis(lim, false);
for (int i = 1; i <= n; i++) {
int ai;
cin >> ai;
vis[ai] = true;
}
for (int i = 0; i < lim; i++) {
if (!vis[i]) mySet.insert(i);
}
for (int i = 1; i <= m; i++) {
int x;
cin >> x;
mySet.xorAll(x);
cout << mySet.getMex() << '\n';
}
return 0;
}
Village Fair
给定一棵树,有1个精灵在根。树上每个点都有一个小朋友,他们要走向根与精灵玩。每个小朋友刚开始都有一个欢乐值,树上的每个点都会增加欢乐值。当小朋友经过这个点时,欢乐值就会增加。你需要计算出树上每个点,小朋友在去根的路上经过它时,会产生多少个不同的欢乐值。
分析:
一个点的答案为其所有子树的答案合并。容易想到启发式合并,但合并时兄弟之间增加的欢乐值互不干扰,此处可以使用Venice Technique考虑相对值减少编码难度。对于u和v的合并(假设让v合并到u),因为两者互不干扰每个v中的元素应该加上其子树的贡献,并且减去u子树的贡献。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
vector<int> G[N];
set<ll> joys[N];
ll J[N], F[N], jplus[N];
int ans[N];
void dfs(int p, int u) {
for(auto v : G[u]) {
if(v != p) {
dfs(u, v);
}
}
// init
F[u] = 0;
// merge
for(auto v : G[u]) {
if(v != p) {
// Sum joy gain to child set
F[v] += jplus[v];
// Small-to-Large technique
if(joys[u].size() < joys[v].size()) {
swap(joys[u], joys[v]);
swap(F[u], F[v]);
}
// Applying venice technique on merge
for(auto val : joys[v]) {
ll nval = val + F[v];
joys[u].insert(nval - F[u]);
}
}
}
// Insert root
joys[u].insert(J[u] - F[u]);
// ans
ans[u] = joys[u].size();
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> J[i];
int rt;
for (int i = 1; i <= n; i++) {
int fa;
cin >> fa;
if (fa) G[fa].push_back(i);
else rt = i;
}
for (int i = 1; i <= n; i++) cin >> jplus[i];
dfs(0, rt);
for (int i = 1; i <= n; i++) cout << ans[i] << '\n';
return 0;
}
Copy or Prefix Sum
给出数组b,问有多少个a数组满足任意\(1≤i≤n\),\(b_i=a_i或b_i=\sum_{j=1}^{i}a_j\)
分析:
考虑dp,\(dp[i][j]\)表示前i位a[i]前缀和为j的方案数。
转移:\(dp[i][j] = dp[i-1][j-b_i],dp[i][b_i]+=\sum[j≠0]dp[i-1][j]\)
该dp转移的本质就是目前所有j向后平移b[i]位,并且b[i]位置的值要加上一次所有的dp值,然而这样会重复计算j=0即0向后平移b[i]位的情况,需要减去。对于所有值平移b[i]位可以使用Venice技巧,使用全局变量维护偏移量,当更新b[i]的时候下标减去该偏移量即可。
代码:
#include <bits/stdc++.h>
using namespace std;
#define mod 1000000007
struct VeniceMap {
long long global_shift = 0;
map<long long, long long> dp;
VeniceMap() {
dp[0] = 1;
}
void shift(long long s) {
global_shift += s;
}
long long get_begin() {
return dp[-global_shift];
}
void add(long long p, long long v) {
dp[p - global_shift] += v;
if (dp[p - global_shift] < 0) dp[p - global_shift] += mod;
if (dp[p - global_shift] > mod) dp[p - global_shift] -= mod;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int tt;
cin >> tt;
while (tt--) {
VeniceMap myMap;
int n;
cin >> n;
long long ans = 1;
for (int i = 1; i <= n; i++) {
long long b;
cin >> b;
long long double_counted = myMap.get_begin();
// get dp[0], which is double-counted
myMap.shift(b);
// move all dp[j] to dp[j + b]
myMap.add(b, ans - double_counted);
// add last ans to pos b, but because dp[b] has changed to dp[0]
// we need to subtract the double-counted
ans += ans - double_counted;
// ans should add last ans but subtract the double-counted
if (ans < 0) ans += mod;
if (ans > mod) ans -= mod;
}
cout << ans << '\n';
}
return 0;
}
总结:
Venice Technique在处理整体移动的问题上有很大的帮助,其核心思想在于记录一个全局的差值,当需要修改整体的时候只需修改那个差值,当需要得到局部的时候只需要累加全局差值带来的贡献即可得到原本应该得到的值。