题解 CF840D 【Destiny】

MY(一名蒟蒻)

2021-05-03 20:25:23

题解

CF840D Destiny

P3567 [POI2014]KUR-Couriers的加强版。

看到这题我第一反应是莫队来着。

主席树

由于这题非常模板所以我来介绍一下这玩意是啥。

以下摘自本人的题解 P6166 【[IOI2012]scrivener】

主席树,又名函数式线段树。由于发明人黄嘉泰的名字缩写和某位中国共产党领导人是一样的,故名主席树。

实现可持久化使用复制节点的方式,由于一次修改最多修改 \log n 个节点,于是需要开 n\log n 的空间。

又叫可持久化线段树,顾名思义可持久。

这个东西可以在一个数列中挖出一段来做 \log n 的操作。

本题依靠权值线段树。

权值线段树

就是节点维护的不是区间的信息,而是值域上的信息。

每一个节点对应的是值域上的一段区间。

所以离散化是值域线段树经典操作。

建树

边插入边建树,每次对树的形态产生更改时复制节点。

这里不能再使用线段树访问左右儿子的方式,要记录下每个节点的左右儿子。

插入时从内存池中取一块内存给复制的节点,每一次插入建立在上一个版本上。

询问

先来看弱化版的询问咋搞。

因为这题权值线段树每个数字的贡献是所在区间的个数加一,所以如果我们用 R 版本的数据减去 L-1 版本的数据就是这个区间的对应数据了。

那么如果左子区间有数字出现次数超过一半就递归查左子,否则右子。

注意这里的左子区间是值域线段树上的左子区间。

递归的时候要把区间的左右端点一起传下去,注意这里的区间指的是数列上的区间。

可能很绕啊,放一下代码吧。

Code 弱化版

#include <cstdio>
#include <iostream>
#include <algorithm>

using namespace std;

const int N=5e5+10;

int n,m,a,cnt,rt[N];
struct node {int l,r,sum;} hjt[N << 5];//32倍空间
inline int clone(int o) {hjt[++cnt]=hjt[o]; return cnt;}//复制节点

int build(int o,int l,int r,int p)//边插入边建树
{
    o=clone(o); hjt[o].sum++;//权值线段树对应区间个数++
    if(l == r) return o;
    int mid=(l+r) >> 1;
    if(p <= mid) hjt[o].l=build(hjt[o].l,l,mid,p);
    else hjt[o].r=build(hjt[o].r,mid+1,r,p);
    return o;
}

int query(int l,int r,int L,int R,int size)
{
    if(l == r) return l;
    int mid=(l+r) >> 1;//有点像差分
    if((hjt[hjt[R].l].sum-hjt[hjt[L].l].sum)<<1 > size) return query(l,mid,hjt[L].l,hjt[R].l,size);
    if((hjt[hjt[R].r].sum-hjt[hjt[L].r].sum)<<1 > size) return query(mid+1,r,hjt[L].r,hjt[R].r,size);
    return 0;
}

int main()
{
//  freopen("work.in","r",stdin); freopen("work.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) {scanf("%d",&a); rt[i]=build(rt[i-1],1,n,a);}
    int l,r;
    while(m--)
    {
        scanf("%d%d",&l,&r);
        printf("%d\n",query(1,n,rt[l-1],rt[r],r-l+1));
    }
//  fclose(stdin); fclose(stdout);
    return 0;
}

Code real

我根据这题的描述把 query 函数改成了这个样子。

int query(int l,int r,int L,int R,int size)
{
    if(l == r) return l;
    int mid=(l+r) >> 1;
    if(hjt[hjt[R].l].sum-hjt[hjt[L].l].sum > size) return query(l,mid,hjt[L].l,hjt[R].l,size);
    if(hjt[hjt[R].r].sum-hjt[hjt[L].r].sum > size) return query(mid+1,r,hjt[L].r,hjt[R].r,size);
    return -1;
}

但是这个玩意WA了#6。

因为如果当前你判断左边的区间满足条件,但是答案不一定在左边。左边只是总次数满足条件,但是答案也可能在右边。

接下来直接放代码。不再注释。

/*
主席树 P3567加强版 
边插入边建树
经典操作边递归边减 
与弱化版的区别在于没有一个性质:具有唯一解 
也就是说,当前你判断左边的区间满足条件,但是答案不一定在左边
所以如果左边无解,要考虑右边是否有解 
*/
#include <cstdio>
#include <iostream>

using namespace std;

const int N=3e5+10;

int n,m,k,cnt,rt[N];
struct node {int l,r,sum;} hjt[N << 5];
inline int clone(int o) {hjt[++cnt]=hjt[o]; return cnt;}

int build(int o,int l,int r,int p)
{
    o=clone(o); hjt[o].sum++;
    if(l == r) return o;
    int mid=(l+r) >> 1;
    if(p <= mid) hjt[o].l=build(hjt[o].l,l,mid,p);
    else hjt[o].r=build(hjt[o].r,mid+1,r,p);
    return o; 
}

int query(int l,int r,int L,int R,int size)
{
    if(l == r) return l;
    int mid=(l+r) >> 1,res;
    if((hjt[hjt[R].l].sum-hjt[hjt[L].l].sum)*k > size)
    {
        res=query(l,mid,hjt[L].l,hjt[R].l,size);
        if(~res) return res;
    } 
    if((hjt[hjt[R].r].sum-hjt[hjt[L].r].sum)*k > size) return query(mid+1,r,hjt[L].r,hjt[R].r,size);
    return -1;
}

int main()
{
//  freopen("work.in","r",stdin); freopen("work.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1,x;i<=n;i++) {scanf("%d",&x); rt[i]=build(rt[i-1],1,n,x);}
    int l,r;
    while(m--)
    {
        scanf("%d%d%d",&l,&r,&k);
        printf("%d\n",query(1,n,rt[l-1],rt[r],r-l+1));
    }
//  fclose(stdin); fclose(stdout);
    return 0;
}

希望对您学OI有帮助!

Thank\ you\ for\ your\ reading!