前缀和,差分,离散化 总结
前缀和,差分,离散化 总结
前缀和
适合解决一些静态查询问题。
一维
很常用的前缀和。
令 \(s_i\) 表示从 \([1, i]\) 的答案。
则 \(s_{i + 1} = s_i + (i 这位贡献)\) 。
二维
其实也不是很难。
令 \(s_{i, j}\) 表示从原点到点 \((i, j)\) 的答案。
但是求和怎么求?
可以容斥。
但是三维容斥有八个项,太麻烦了。
所以我们可以对每一维都做前缀和。
rep(i, 1, n) rep(j, 1, m) sum[i][j] += sum[i - 1][j];
rep(i, 1, n) rep(j, 1, m) sum[i][j] += sum[i][j - 1];
这样就好了。
差分
适合用来解决先进行所有修改,再查询的问题。
先差分,在对差分数组做一遍前缀和,就能知道每个位置的 \(\Delta\) 了。
一维
假如我们每次要将区间 \([l, r]\) 的值 \(\pm k\) 。
我们发现,由于是在区间 \([l, r]\) 修改, 所以我们再位置 \(l\) 的值 \(\pm \ k\) 再将位置 \(r + 1\) 的值 \(\mp \ k\) 。
这样前缀和后就可以只对区间 \([l, r]\) 产生影响了。
二维
当初主包二维差分公式在忘带纸笔且不能用画图的情况下没记住直接场上现推推出来了。
当时现推的problem :https://www.luogu.com.cn/problem/P13879
其实就是这样:
a[x1][y1]++, a[x1][y2 + 1]--, a[x2 + 1][y1]--, a[x2 + 1][y2 + 1]++;
想要更具一般性,只需要再改改就行了。
离散化
当我们只关心数据之间的关系,而不关心准确数值时,可以使用离散化。
本质上其实是一种hash,将每一个数字都变成它的排名。
是一种缩小值域很好的方法。
可以避免用 map 的常数。
std::sort(all(v));
v.erase(std::unique(all(v)), v.end());
for (auto &[l, r] : seg) {
l = std::lower_bound(all(v), l) - v.begin();
r = std::lower_bound(all(v), r + 1) - v.begin();
}
上面就是一种离散化。
例题
A
前缀和的一种推广。
\(sum_i\) 表示区间 \([1, i]\) 满足条件的数的个数。
则每次区间 \([l, r]\) 的答案就是 \(sum_r - sum_{l - 1}\) 。
vis.set(1, true);
for (int i = 2; i <= 100000; i++) if (!vis[i]) for (ll j = 1ll * i * i; j <= 100000; j += i) vis.set(j, true);
for (int i = 1; i <= 100000; i++) {
if (!vis[i] && !vis[i + 1 >> 1]) sum[i] = 1;
sum[i] += sum[i - 1];
}
std::cin >> q;
while (q--) {
int l, r;
std::cin >> l >> r;
std::cout << sum[r] - sum[l - 1] << '\n';
}
时间复杂度: \(O(100000\log{100000})\) 。
B
第一个就是原数组做前缀和,第二个就是从小到大排序然后前缀和。
rep(i, 1, n) std::cin >> a[i], b[i] = a[i];
std::sort(b + 1, b + n + 1);
rep(i, 1, n) a[i] += a[i - 1], b[i] += b[i - 1];
std::cin >> m;
while (m--) {
int op, l, r;
std::cin >> op >> l >> r;
if (op == 1) std::cout << a[r] - a[l - 1] << '\n';
else std::cout << b[r] - b[l - 1] << '\n';
}
时间复杂度: \(O(n\log n)\) 。
C
先将每次给出的温度区间在 \(cnt\) 数组上做一个差分。
此时 \(cnt_i\) 就表示温度 \(i\) 有多少个配方推荐。
令 \(cnt1_i = [cnt_i >= k]\) 然后再对 \(cnt_1\) 做一个前缀和。
最后输出 \(cnt1_b - cnt1_{a - 1}\) 就行了。
std::cin >> n >> k >> q;
rep(i, 1, n) std::cin >> l[i] >> r[i], cnt[l[i]]++, cnt[r[i] + 1]--;
rep(i, 1, 200000) cnt[i] += cnt[i - 1];
rep(i, 1, 200000) cnt1[i] = (cnt[i] >= k), cnt1[i] += cnt1[i - 1];
while (q--) {
int l, r;
std::cin >> l >> r;
std::cout << cnt1[r] - cnt1[l - 1] << '\n';
}
时间复杂度: \(O(n)\) 。
D
如果你看过 yixinc 的 blog 可以发现,他在这篇总结中提到过一个 trick :统计满足条件的东西可以先移项,再统计。
我们尝试移项。
令 \(sum_i\) 表示数组 \(a\) 中区间 \([1, i]\) 的和。
则题目要求 \(sum[r] - sum[l - 1] = r - l + 1\)
当且仅当 \(sum[r] = r - l + 1 + sum[l - 1]\)
当且仅当 \(sum[r] - r = sum[l - 1] - (l - 1)\)
所以代码就好写了。
void solve() {
std::cin >> n;
ans = 0;
cnt.clear(), cnt[0] = 1;
rep(i, 1, n) {
char ch;
std::cin >> ch;
a[i] = ch - '0', a[i] += a[i - 1];
}
rep(i, 1, n) ans += cnt[a[i] - i], cnt[a[i] - i]++;
std::cout << ans << '\n';
}
这个 trick 真的还挺常见,建议记住。
时间复杂度: \(O(\sum n)\) 。
E
贪心,将每次区间 \([l, r]\) 都标记上新访问了一次,只需将访问次数最多的地方放最大的,访问次数最少的放最小的,以此类推。
std::cin >> n >> q;
rep(i, 1, n) std::cin >> a[i];
while (q--) {
int l, r;
std::cin >> l >> r;
cnt[l]++, cnt[r + 1]--;
}
rep(i, 1, n) cnt[i] += cnt[i - 1];
std::sort(cnt + 1, cnt + n + 1);
std::sort(a + 1, a + n + 1);
rep(i, 1, n) ans += 1ll * cnt[i] * a[i];
std::cout << ans << '\n';
时间复杂度: \(O(n\log n)\) 。
F
我们注意到,可以离线,所以可以将每个询问将执行多少次求出来,然后直接将 \(d_i\) 乘上执行的次数,再用现在的 \(d_i\) 和 \(l_i\) , \(r_i\) 做一个差分,最后输出即可。
std::cin >> n >> m >> k;
rep(i, 1, n) std::cin >> a[i];
frep(i, n, 1) a[i] -= a[i - 1];//因为最后还要做前缀和,所以要变成差分数组。
rep(i, 1, m) std::cin >> l[i] >> r[i] >> d[i];
while (k--) {
int l, r;
std::cin >> l >> r;
cnt[l]++, cnt[r + 1]--;
}
rep(i, 1, m) {
cnt[i] += cnt[i - 1];
d[i] *= cnt[i];
a[l[i]] += d[i];
a[r[i] + 1] -= d[i];
}
rep(i, 1, n) a[i] += a[i - 1];
rep(i, 1, n) std::cout << a[i] << " \n"[i == n];
时间复杂度: \(O(n)\) 。
G
因为这个星星亮度会变,而且是 \(mod\ {c + 1}\) 意义下的,所以最多一个星星的亮度可以有 \(c\) 个版本,我们只需要对于这 \(c\) 个版本各做一遍前缀和即可。
实现有一些小细节。
std::cin >> n >> q >> c;
rep(i, 1, n) {
int x, y, s;
std::cin >> x >> y >> s;
sum[x][y][0] += s;
rep(j, 1, c) sum[x][y][j] += (s + 1) % (c + 1), s = (s + 1) % (c + 1);
}
rep(k, 0, c) rep(i, 1, 100) rep(j, 1, 100) sum[i][j][k] += sum[i - 1][j][k];
rep(k, 0, c) rep(i, 1, 100) rep(j, 1, 100) sum[i][j][k] += sum[i][j - 1][k];
c++;
while (q--) {
int t, x1, y1, x2, y2;
std::cin >> t >> x1 >> y1 >> x2 >> y2, t %= c;
std::cout << sum[x2][y2][t] - sum[x1 - 1][y2][t] - sum[x2][y1 - 1][t] + sum[x1 - 1][y1 - 1][t] << '\n';
}
H
一道离散化加差分的题。
注意到 \(l, r\) 的值域均为 \(\leq 10^{18}\) 所以普通的数组肯定不能存,而 map 常数大,所以要用离散化。
离散化完之后再用差分数组统计一下每个位置被覆盖的次数,然后再存下来。
std::cin >> n;
rep(i, 1, n) {
ll l, r;
std::cin >> l >> r;
seg.pb({l, r});
v.pb(l), v.pb(r + 1);
}
std::sort(all(v));
v.erase(std::unique(all(v)), v.end());
for (auto &[l, r] : seg) {
l = std::lower_bound(all(v), l) - v.begin() + 1;
r = std::lower_bound(all(v), r + 1) - v.begin();
cnt[l]++, cnt[r + 1]--;
}
int m = v.size();
rep(i, 1, m) cnt[i] += cnt[i - 1], ans[cnt[i]] += v[i] - v[i - 1];
rep(i, 1, n) std::cout << ans[i] << " \n"[i == n];
时间复杂度: \(O(n)\) 。
I
想不到除了让你写更多代码外这道题有什么意义。
二分加上二维前缀和。
先二分出长度 \(len\) 。
令 \(cnt_{i, j}\) 表示位置 \(i, j\) 的值是否大于等于 \(l\) 。
然后再前缀和。
然后枚举矩形的左上角,如果当前矩形的 \(cnt\) 等于 \(len^2\) 如果是就返回 true 。
否则没找到就返回 false 。
void solve() {
std::cin >> n >> m;
std::vector<std::vector<int>> a(n + 1, std::vector<int>(m + 1)), sum(n + 1, std::vector<int>(m + 1));
rep(i, 1, n) rep(j, 1, m) std::cin >> a[i][j];
auto check = [&](int len) {
rep(i, 1, n) rep(j, 1, m) sum[i][j] = (0 || a[i][j] >= len);
rep(i, 1, n) rep(j, 1, m) sum[i][j] += sum[i - 1][j];
rep(i, 1, n) rep(j, 1, m) sum[i][j] += sum[i][j - 1];
rep(x1, 1, n - len + 1) {
int x2 = x1 + len - 1;
rep(y1, 1, m - len + 1) {
int y2 = y1 + len - 1;
if (sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1] == len * len) return true;
}
}
return false;
};
int l = 0, r = std::min(n, m) + 1;
while (l + 1 < r) {
int mid = l + r >> 1;
if (check(mid)) l = mid;
else r = mid;
}
std::cout << l << '\n';
}
时间复杂度: \(O(nm\log n)\) 。

浙公网安备 33010602011771号