题解: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;
}

//剩下的基本同上一篇前缀和的注释

后记

这里讲一些需要注意的点(发现自己代码有错后可以看看,笔者犯的错误有没有你的类型):

总的可能出现的错误:

  1. 精度问题。

前缀和:

  1. 求前缀和时会出现一种特殊的情况:求到某一个点时权值大于了 \(10^9 + 7\) 会导致权值小于了之前的某个值,前缀和算出来是负的,所以要将它变为正的。

  2. 如果左边的权值算的是 \(1\)\(x\) 的,而右边权值算的是 \(x\)\(n\) 的,此时会导致 \(x\) 位置的路灯亮度重复算了两遍(这个在之前算式推导的时候就写清了范围)。

差分:

  1. 树状数组相加的时候,这里取到 \(n+1\) 就并非为保险措施了,在刚好路灯在 \(n\) 位置时,差分会将 \(n+1\) 的位置减去权值会导致溢出。
posted @ 2026-08-03 21:45  fullstop  阅读(3)  评论(0)    收藏  举报