2021牛客暑期多校训练营7 K - xay loves sequence(主席树 + 三分)

题意:
在这里插入图片描述
每次可以选择一个子区间使其+1或-1后对k取模,问把区间内所有数变为0的最小操作次数。

思路:
感觉碰到过很多次,最近的沈阳 J 题,还有西安邀请赛的B也是类似的题。
如果不考虑取模,那么求出差分数组 d,问题转换为每次操作对d数组的一个数+1,一个数-1。那么肯定是对正数+1,负数-1,那么答案就是所有数的绝对值之和除2.
考虑取模,那么相当于对每个数 + k或者 - k,要使贡献减小,必然是对负数 + k ,整数 - k 。此时每个数的贡献变化量为 k − 2 ∗ ∣ d [ i ] ∣ k - 2 * |d[i]| k2d[i],所以我们把整数分为一组,负数分为一组,从大到小排序,每次分别从其中选取最大值,知道贡献不能再减小。对于排序,我们可以用主席树优化,取前k大的和,三分取极小值即可。
具体分析参考以下博客:添加链接描述

代码:

#include <bits/stdc++.h>
#define GO ios::sync_with_stdio(0);cin.tie(0),cout.tie(0)
using namespace std;
typedef long long ll;
const int maxn = 6e5 + 7;
const ll inf = (1ll << 30);
const int mod = 998244353;
int T;
int n,m,a[maxn],d[maxn];
ll pre[maxn];
ll ls[maxn * 20],rs[maxn * 20] , sum[maxn * 20] , sz[maxn * 20],rt[2][maxn];
int id;
int update(int k, int l , int r, int root){
    int res = ++id;
    ls[res] = ls[root],rs[res] = rs[root],sz[res] = sz[root] + 1,sum[res] = sum[root] + k;
    if(l == r) return res;
    int mid = (l + r) / 2;
    if(k <= mid) ls[res] = update(k , l , mid , ls[root]);
    if(k > mid) rs[res] = update(k , mid + 1 , r , rs[root]);
    return res;
}
ll query(int l,int r,int root1,int root2,int k,int val){   //前k大和
    if(l == r){
        return 1ll * l * k; // 注意这里是l * k ,当前还需要选 k 个数 , 只能选 l .不能写 sum[root1] - sum[root2],可能选的大于k 个。
    }
    int mid = (l + r) / 2;
    int x = sz[rs[root1]] - sz[rs[root2]] + (mid + 1 <= val && val <= r);   //判断右区间的值是否大于k个,还要带上 val ,边界。
    ll res = sum[rs[root1]] - sum[rs[root2]] + 1ll * (mid + 1 <= val && val <= r) * val;
    if(x >= k) return query(mid + 1 , r ,rs[root1],rs[root2],k , val);
    return query(l, mid , ls[root1],ls[root2],k - x , val) + res;
}
ll cal(int l , int r , int cnt , int k){     //选前k大的值
    ll sum = (pre[r] - pre[l] + a[l] + a[r]) / 2;
    ll res1 = query(0 , inf , rt[0][r] , rt[0][l] , k , a[l]);
    ll res2 = query(0 , inf , rt[1][r] , rt[1][l] , k , a[r]);
    return sum + 1ll * cnt * k - res1 - res2;
}
int main (){
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n; i ++){
        scanf("%d",&a[i]);
        d[i] = a[i] - a[i - 1];
        pre[i] = pre[i - 1] + abs(d[i]);
    }
    for(int i = 1; i <= n; i ++){
        if(d[i] > 0){
            rt[0][i] = update(d[i], 0 , inf , rt[0][i - 1]);
            rt[1][i] = rt[1][i-1];
        }
        else{
            rt[1][i] = update(-d[i], 0 , inf , rt[1][i - 1]);
            rt[0][i] = rt[0][i-1];
        }
    }
    while(m--){
        int x,y,k;
        scanf("%d%d%d",&x,&y,&k);
        int l = 0 , r = min(sz[rt[0][y]] - sz[rt[0][x]] , sz[rt[1][y]] - sz[rt[1][x]]) + 2 , mid;
        ll ans = 1e18;
        while(l <= r){
            mid = (l + r) / 2;
            ll res1 = cal(x , y , k , mid) , res2 = cal(x , y , k , mid + 1);
            if(res1 > res2){
                l = mid + 1;
                ans = min(ans , res2);
            }
            else{
                r = mid - 1;
                ans = min(ans , res1);
            }
        }
        printf ("%lld\n",ans);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值