题解:P13719 [GCPC 2024] Dark Alley
正文
题目分析
注释:以下的文章皆将 \(p\) 当做 \(1 - p\)。
每次输入涉及到:增添路灯、删除之前的某个路灯、查询当前某个点的亮度。
首先,我们来举个例子:令当前查询的点的位置为 \(y\),之前只有位置 \(x\),放过路灯且路灯的亮度为 \(b_x\),迷雾的浓度为 \(p\),所以当前的亮度为 \(b \times p^{|y - x|}\)。
观察可以发现:
\[ans_y = \begin{cases}
b_x \times p^{y-x} & y \ge x \\
b_x \times p^{x-y} & y < x
\end{cases}
\]
因为 \(y\) 是在查询的时候才用到的,所以整理可得:
\[ans_y = \begin{cases}
p^y \times b_x \times p^{-x} & y \ge x \\
p^{-y} \times b_x \times p^x & y < x
\end{cases}
\]
如果此时是对于全局的路灯求亮度,那么可以化为以下式子:
\[ans_y = p^{y} \times \sum_{x = 1}^{y - 1} (b_x \times p^{-x}) + p^{-y} \times \sum_{x = y}^{n} (b_x\times p^{x})
\]
观察这个式子,我们会发现需要查询的值只有两个:
\[\begin{cases}
\sum_{x = 1}^{y - 1}(b_x \times p ^{-x}) \\
\\
\sum_{x = y}^{n}(b_x \times p^{x}) \\
\end{cases}
\]
观察以上的两个式子我们很容易的发现是求区间和以及单点修改(差分),当然也可以是求单点的值以及区间修改(前缀和)。
但是这里的数据范围为:\(1 \le n, q \le 2 \times 10^5\),单纯使用前缀和或差分一定会超时,这个时候我们很容易就想到树状数组、线段树这两个结构,但是笔者比较喜欢写树状数组(毕竟代码少一些)。
分析到这里代码就十分的好写了。
AC code
维护前缀和
#include<bits/stdc++.h>
#define lx (x & (-x))
#define ll long long
using namespace std;
const int mod = 1e9 + 7, N = 2e5 + 5;
int n, q;
char op;
ll p;
//----- 快速幂部分 -----
ll ksm(ll a, ll b){
ll res = 1;
while(b){
if(b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
//----- 树状数组部分 -----
ll tr[2][N];
void add(int x, ll y, int kind){
y = (y % mod + mod) % mod;
for(; x <= n + 1;x += lx) // 这里用 n + 1 作为上界是为了保险,虽然不会溢出就是了
tr[kind][x] = (tr[kind][x] + y) % mod;
}
ll query(int x,int kind){
ll sum = 0;
for(; x > 0;x -= lx)
sum = (sum + tr[kind][x]) % mod;
return sum;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> q;
double s;
cin >> s;
s = 1.0 - s;
p = (ll)(s * 1000000 + 0.5) * ksm(1000000, mod - 2) % mod; // 注意精度可能丢失的问题
while(q--){
cin >> op;
if(op == '+'){ //增加路灯
ll b, x;
cin >> b >> x;
ll inv_p = ksm(p, mod - 2);
ll left_val = b * ksm(inv_p, x) % mod;
ll right_val = b * ksm(p, x) % mod;
//分别求对左边和右边的贡献
add(x, left_val, 0);
add(x, right_val, 1);
// 前缀和的基本相加策略
}
else if(op == '-'){ //删除路灯
ll b, x;
cin >> b >> x;
ll inv_p = ksm(p, mod - 2);
ll left_val = b * ksm(inv_p, x) % mod;
ll right_val = b * ksm(p, x) % mod;
// 这里相加的是 mod - 点贡献,因为这样相加之后在对 1e9 + 7 取模会变成 0。
add(x, mod - left_val, 0);
add(x, mod - right_val, 1);
}
else{ //查询亮度
ll x;
cin >> x;
ll sum_left = query(x - 1, 0);
ll sum_right = ((query(n, 1) - query(x - 1, 1)) % mod + mod) % mod;
//需要注意的是有两点:
//1. 这个时候求前缀和会出现一种特殊的情况:求到某一个点时权值大于了 1e9 + 7 会导致权值小于了之前的某个值前缀和算出来是负的,所以要将它变为正的。
//2. 如果左边的权值算的是 1 到 x 的,而右边权值算的是 x 到 n 的,此时会导致 x 位置的路灯亮度重复算了两遍(这个在之前算式推导的时候就写清了范围)。
ll px = ksm(p, x);
ll inv_px = ksm(px, mod - 2);
ll ans = (sum_left * px % mod + sum_right * inv_px % mod) % mod;
cout << ans << "\n";
}
}
return 0;
}
维护差分
#include<bits/stdc++.h>
#define ll long long
#define lx (x & (-x))
using namespace std;
const int mod = 1e9 + 7, N = 2e5 + 5;
int n, q;
ll p;
ll ksm(ll a, ll b) {
ll res = 1;
while (b) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
ll tr[2][N] = {0};
void add(int x, ll y, int kind) {
y = (y % mod + mod) % mod;
for (; x <= n + 1; x += lx) // 这里取到 n+1 就并非为保险措施了,在刚好路灯在 n 位置时,差分会将 n+1 的位置减去权值会导致溢出。
tr[kind][x] = (tr[kind][x] + y) % mod;
}
ll query(int x, int kind) {
ll sum = 0;
for (; x > 0; x -= lx)
sum = (sum + tr[kind][x]) % mod;
return sum;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> q;
double s;
cin >> s;
s = 1.0 - s;
p = (ll)(s * 1000000 + 0.5) * ksm(1000000, mod - 2) % mod;
while (q--) {
char op;
cin >> op;
if (op == '+') {
ll b, x;
cin >> b >> x;
ll inv_p = ksm(p, mod - 2);
ll left_val = b * ksm(p, x) % mod;
ll right_val = b * ksm(inv_p, x) % mod;
add(1, left_val, 0);
add(x, mod - left_val, 0);
add(x, right_val, 1);
add(n + 1, mod - right_val, 1);
}
else if (op == '-') {
ll b, x;
cin >> b >> x;
ll inv_p = ksm(p, mod - 2);
ll left_val = b * ksm(p, x) % mod;
ll right_val = b * ksm(inv_p, x) % mod;
add(1, mod - left_val, 0);
add(x, left_val, 0);
add(x, mod - right_val, 1);
add(n + 1, right_val, 1);
}
else {
ll x;
cin >> x;
ll sum_left = query(x, 0);
ll sum_right = query(x, 1);
ll px = ksm(p, x);
ll inv_px = ksm(px, mod - 2);
ll ans = (sum_left * inv_px % mod + sum_right * px % mod) % mod;
cout << ans << "\n";
}
}
return 0;
}
//剩下的基本同上一篇前缀和的注释
后记
这里讲一些需要注意的点(发现自己代码有错后可以看看,笔者犯的错误有没有你的类型):
总的可能出现的错误:
- 精度问题。
前缀和:
-
求前缀和时会出现一种特殊的情况:求到某一个点时权值大于了 \(10^9 + 7\) 会导致权值小于了之前的某个值,前缀和算出来是负的,所以要将它变为正的。
-
如果左边的权值算的是 \(1\) 到 \(x\) 的,而右边权值算的是 \(x\) 到 \(n\) 的,此时会导致 \(x\) 位置的路灯亮度重复算了两遍(这个在之前算式推导的时候就写清了范围)。
差分:
- 树状数组相加的时候,这里取到 \(n+1\) 就并非为保险措施了,在刚好路灯在 \(n\) 位置时,差分会将 \(n+1\) 的位置减去权值会导致溢出。

浙公网安备 33010602011771号