洛谷P3157 cdq解法

该博客介绍了如何使用CDQ分治法处理动态逆序对问题,涉及数据结构如 Fenwick 树(二进制索引树)和节点的三维偏序关系。在一系列删除操作中,计算每次删除前序列的逆序对数量。通过维护元素的删除时间和权值,将二维偏序问题转化为三维偏序,并利用CDQ分治进行高效求解。
题意:

给出一个nnn的排列,有mmm次删除操作,每次删除其中一个元素xxx,输出每次删除前序列的逆序对个数

cdq分治法:

与主席树相同的是,我们都是要删除一个位置时,这个位置所构成的逆序对个数,换句话说,就是求这个位置前面有多少大于xxx的,后面有多少小于xxx的。主席树能在线处理,动态修改权值线段树,使得某个区间的权值线段树任意查询,所以主席树可以在线处理这个问题。而换个角度思考,逆序对本身就是一种二维偏序关系,那在这里删除xxx,能产生影响的一定是符合逆序对偏序关系的,即位置在前的比自己大的,位置在后比自己小的。但是仅仅按照这样子计算出删除某个位置的影响不能双向修改,比如xxxyyy是一对逆序对,我们知道xxx的影响是1,删去xxxyyy的影响数也要减1。实际上删去xxx能少去多少逆序对仅跟满足偏序条件和当前仍然存在的元素有关系,所以给元素多加一个维度的属性,删去时间

那么要求的就是满足如下偏序关系的对数了:

t[i]t[i]t[i]iii元素删去的时间,val[i]val[i]val[i]iii元素的权值,pos[i]pos[i]pos[i]iii元素的位置,在t[i]<t[j]t[i]<t[j]t[i]<t[j]的条件下,符合

val[j]>val[i],pos[j]<pos[i] val[j]>val[i],pos[j]<pos[i] val[j]>val[i],pos[j]<pos[i]

val[j]<val[i],pos[j]>pos[i] val[j]<val[i],pos[j]>pos[i] val[j]<val[i],pos[j]>pos[i]

至此转换为一个三维偏序问题了,对两组偏序关系分别cdqcdqcdq分治即可

一些细节:

存在一些元素不被删去,那么一个办法是令不被删去的元素的删去时间是无穷大即可

需要求出一开始的逆序对数,一个方法是在输入的时候二维偏序求解逆序对,另一个方法是令不被删去的元素的删去时间分别是[∞,∞+1,∞+2,.....][\infty,\infty+1,\infty+2,.....][,+1,+2,.....],在设置为无穷大时把他们的顺序设为依次,所有位置的答案加起来就是逆序对个数

#include<bits/stdc++.h>
#define ll long long
using namespace std;

int n;

struct fenwick
{
    int tree1[200005],tree2[200005];
    inline int lowbit(int x){return -x&x;} 
    void add(int x,int v)
    {
        int tmp=x*v;
        while(x<=n)
        {
            tree1[x]+=v;
            tree2[x]+=tmp;
            x+=lowbit(x);
        }
    }
    void add(int l,int r,int k)
    {
        add(l,k);
        add(r+1,-k);
    }
    int getsum(int l,int r)
    {
        int ret=0,x=r;
        while(x)
        {
            ret+=(r+1)*tree1[x]-tree2[x];
            x-=lowbit(x);
        }
        x=l-1;
        while(x)
        {
            ret-=l*tree1[x]-tree2[x];
            x-=lowbit(x);
        }
        return ret;
    }
}tree;

struct node
{
    int val,t,pos;
}a[200005],tmp[200005];

int m,del[200005],pos[200005],ans[200005];

void solve1(int l,int r)
{
    if(l==r) return;
    int mid=l+r>>1;
    solve1(l,mid); solve1(mid+1,r);
    int j=l,i=mid+1,k=l;
    while(j<=mid&&i<=r)
    {
        if(a[j].pos<=a[i].pos)
        {
            tree.add(a[j].val,a[j].val,1);
            tmp[k++]=a[j++];
        }
        else
        {
            ans[a[i].pos]+=tree.getsum(a[i].val,n);
            tmp[k++]=a[i++];
        }
    }
    while(j<=mid)
    {
        tree.add(a[j].val,a[j].val,1);
        tmp[k++]=a[j++];
    }
    while(i<=r)
    {
        ans[a[i].pos]+=tree.getsum(a[i].val,n);
        tmp[k++]=a[i++];
    }
    for(int i=l;i<=mid;i++) tree.add(a[i].val,a[i].val,-1);
    for(int i=l;i<=r;i++) a[i]=tmp[i];
}

void solve2(int l,int r)
{
    if(l==r) return;
    int mid=l+r>>1;
    solve2(l,mid); solve2(mid+1,r);
    int j=l,i=mid+1,k=l;
    while(j<=mid&&i<=r)
    {
        if(a[j].pos>=a[i].pos)
        {
            tree.add(a[j].val,a[j].val,1);
            tmp[k++]=a[j++];
        }
        else
        {
            ans[a[i].pos]+=tree.getsum(1,a[i].val);
            tmp[k++]=a[i++];
        }
    }
    while(j<=mid)
    {
        tree.add(a[j].val,a[j].val,1);
        tmp[k++]=a[j++];
    }
    while(i<=r)
    {
        ans[a[i].pos]+=tree.getsum(1,a[i].val);
        tmp[k++]=a[i++];
    }
    for(int i=l;i<=mid;i++) tree.add(a[i].val,a[i].val,-1);
    for(int i=l;i<=r;i++) a[i]=tmp[i];
}

ll tot;
int q[200005];

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i].val);
        a[i].pos=i;
        pos[a[i].val]=i;
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&q[i]);
        a[pos[q[i]]].t=i;
    };
    int pp=0x3fffffff;
    for(int i=1;i<=n;i++)
        if(!a[i].t) a[i].t=++pp;
    sort(a+1,a+1+n,[&](const node& x,const node& y){
        if(x.t!=y.t) return x.t>y.t;
        if(x.pos!=y.pos) return x.pos<y.pos;
        return x.val>y.val;
    }); 
    solve1(1,n);
    sort(a+1,a+1+n,[&](const node& x,const node& y){
        if(x.t!=y.t) return x.t>y.t;
        if(x.pos!=y.pos) return x.pos>y.pos;
        return x.val<y.val;
    }); 
    solve2(1,n);
    for(int i=1;i<=n;i++) tot+=ans[i];
    for(int i=1;i<=m;i++)
    {
        printf("%lld\n",tot);
        tot-=ans[pos[q[i]]];
    }
    return 0;
}
### 三维偏序问题(P3810) 该问题是CDQ分治的经典应用之一,要求统计每个三元组有多少个“前驱”元素,即满足 $ x_i \leq x_j, y_i \leq y_j, z_i \leq z_j $ 的元素 $ j $ 对 $ i $ 产生贡献。该题可以通过CDQ分治将问题拆解为多个二维偏序问题,并使用树状数组进行统计。 该题的解法步骤如下: 1. 对所有元素按照 $ x $ 排序。 2. 使用CDQ分治递归处理左右两部分。 3. 在合并阶段,将左半部分对右半部分的贡献统计出来,使用树状数组维护 $ y $ 和 $ z $ 的信息。 ```cpp struct Node { int x, y, z, cnt, ans; }; vector<Node> a; int tr[N], ans[N]; void cdq(int l, int r) { if (l == r) return; int mid = (l + r) >> 1; cdq(l, mid); cdq(mid + 1, r); // 合并阶段,统计左半部分对右半部分的贡献 } ``` [^1] ### 动态逆序对问题(P3157) 该题要求在支持单点修改的情况下,多次查询某个区间内的逆序对数量。由于树套树结构在时间与空间上均较为复杂,该题卡常数较紧,CDQ分治成为更优的解法选择。 CDQ分治的处理方式是将所有操作离线处理,并按照时间顺序进行分治。在每一步中,将前一半的操作视为修改,后一半的操作视为查询,统计修改对查询的影响。 ```cpp struct Query { int t, x, y, idx; }; vector<Query> queries; void cdq(int l, int r, vector<Query> &q) { if (l == r) return; int mid = (l + r) >> 1; cdq(l, mid, q); cdq(mid + 1, r, q); // 处理跨越 mid 的查询 } ``` [^1] ### K大数查询(P3155) 该题要求支持插入操作和查询第 $ k $ 大数,是CDQ分治与二分答案结合的经典题目。该题可以通过将问题转化为时间与数值范围的二维问题,并在CDQ分治过程中进行二分查找来解决。 CDQ分治的核心思想是按时间分治,统计每个查询区间内满足条件的数的数量,并结合二分查找确定第 $ k $ 大数的值。 ```cpp bool check(int mid) { // 使用CDQ分治统计满足条件的数的数量 } void cdq(int l, int r, ...) { // 分治处理 } ``` [^1] ### 其他经典例题 #### 1. 陌上花开(P3810) 该题是三维偏序问题的标准模板题,适合初学者练习CDQ分治的基本结构和合并策略。 #### 2. 动态逆序对(P3157) 该题展示了CDQ分治在处理动态数据结构问题中的优势,适合深入理解时间维度的分治策略。 #### 3. K大数查询(P3155) 该题结合了CDQ分治与二分查找,适合理解如何将问题转化为多维分治结构。 #### 4. 逆序对(P1908) 该题为二维偏序问题,是CDQ分治的入门题之一,适合掌握基本的分治与合并逻辑。 #### 5. 小凸玩矩阵(P3354) 该题涉及二维矩阵操作,结合CDQ分治与二分答案,适合掌握复杂问题的分治建模能力。 [^1] ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值