洛谷 P3258 [JLOI2014]松鼠的新家 树链剖分+差分前缀和优化

题面

题目链接

P3258 [JLOI2014]松鼠的新家

题目描述

松鼠的新家是一棵树,前几天刚刚装修了新家,新家有 $ n $ 个房间,并且有 $ n-1 $ 根树枝连接,每个房间都可以相互到达,且俩个房间之间的路线都是唯一的。天哪,他居然真的住在”树“上。

松鼠想邀请****前来参观,并且还指定一份参观指南,他希望**能够按照他的指南顺序,先去 $ a_1 $,再去 $ a_2 $,......,最后到 $ a_n $,去参观新家。可是这样会导致**重复走很多房间,懒惰的**不停地推辞。可是松鼠告诉他,每走到一个房间,他就可以从房间拿一块糖果吃。

**是个馋家伙,立马就答应了。现在松鼠希望知道为了保证**有糖果吃,他需要在每一个房间各放至少多少个糖果。

因为松鼠参观指南上的最后一个房间 $ a_n $ 是餐厅,餐厅里他准备了丰盛的大餐,所以当**在参观的最后到达餐厅时就不需要再拿糖果吃了。

输入输出格式

输入格式

第一行一个整数 $ n $,表示房间个数第二行n个整数,依次描述 $ a_1-a_n $

接下来 $ n-1 $ 行,每行两个整数 $ x $ , $ y $ ,表示标号x和y的两个房间之间有树枝相连。

输出格式

一共 $ n $ 行,第 $ i $ 行输出标号为i的房间至少需要放多少个糖果,才能让**有糖果吃。

输入输出样例

输入样例:

5
1 4 5 3 2
1 2
2 4
2 3
4 5

输出样例:

1
2
1
2
1

说明

【数据范围】

$ 2 \leq n \leq 300000 $

说明

【时空限制】

1000ms,128M

思路

整理题意,可参考如下理解:

给定一个n棵结点的树,一个人从给定的起点出发按一定的顺序走至终点;他经过每个点时,该点点权加1 (包括起点但不包括终点)。求最终每点的点权。

首先我考虑的是每次改变a[i]和a[i+1]之间所有点的点权。可以拿纸模拟一下,会发现这样的问题。

这样计算,那么样例得到的结果最终为1 3 2 3 2

由于在a[i]到a[i+1]再到a[i+2]的过程中,按照上面的算法,先改变a[i]和a[i+1]之间所有点的点权,再改变a[i+1]和a[i+2]之间所有点的点权,那么a[i+1]的点权就重复算了一次。故除了起点和终点,其他每个点点权都多算了1,应该减去;而终点也多算了一次,原因是最后一次到达终点时点权不用加1;起点并没有重复,不用减去。

所以最终算法归纳为:

1.每次改变a[i]和a[i+1]之间所有点的点权,$ 1 \leq i < n $
2.将除起点外的所有点点权减1

这样基本上是一道树剖的板子题了吧

AC代码

#include<bits/stdc++.h>
const int maxn=300010;
using namespace std;

int n,a[maxn];
int tot,to[maxn<<1],nxt[maxn<<1],head[maxn];
int dep[maxn],fa[maxn],son[maxn],len[maxn];
int cnt,nid[maxn],nw[maxn],top[maxn];
struct SegmentTree
{
    int l,r,tag,sum;
    #define l(a) tree[a].l
    #define r(a) tree[a].r
    #define m(a) ((l(a)+r(a))>>1)
    #define len(a) (r(a)-l(a)+1)
    #define t(a) tree[a].tag
    #define s(a) tree[a].sum
}tree[maxn<<2];

void dfs1(int u,int f,int d)
{
    dep[u]=d;fa[u]=f;len[u]=1;
    for(int i=head[u];i;i=nxt[i])
    {
        int v=to[i];
        if(v==f) continue;
        dfs1(v,u,d+1);
        len[u]+=len[v];
        if(len[v]>len[son[u]]) son[u]=v;
    }
}

void dfs2(int p,int t)
{
    nid[p]=++cnt;
    top[p]=t;
    if(!son[p]) return;
    dfs2(son[p],t);
    for(int i=head[p];i;i=nxt[i])
    {
        int v=to[i];
        if(v==fa[p] || v==son[p]) continue;
        dfs2(v,v);
    }
}

void BuildTree(int p,int l,int r)
{
    l(p)=l;r(p)=r;
    if(l==r) return;
    BuildTree(p<<1,l,m(p));
    BuildTree(p<<1|1,m(p)+1,r);
}

void PushDown(int p)
{
    if(t(p))
    {
        s(p<<1)+=t(p)*len(p<<1);
        s(p<<1|1)+=t(p)*len(p<<1|1);
        t(p<<1)+=t(p);
        t(p<<1|1)+=t(p);
        t(p)=0;
    }
}

void Change1(int p,int l,int r)
{
    if(l<=l(p) && r>=r(p))
    {
        t(p)++;
        s(p)+=len(p);
        return;
    }
    PushDown(p);
    if(l<=m(p)) Change1(p<<1,l,r);
    if(r>m(p)) Change1(p<<1|1,l,r);
    s(p)=s(p<<1)+s(p<<1|1);
}

int Ask(int p,int l,int r)
{
    if(l<=l(p) && r>=r(p)) return s(p);
    PushDown(p);
    int ans=0;
    if(l<=m(p)) ans+=Ask(p<<1,l,r);
    if(r>m(p)) ans+=Ask(p<<1|1,l,r);
    return ans;
}

void Change2(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(dep[top[u]]<dep[top[v]]) swap(u,v);
        Change1(1,nid[top[u]],nid[u]);
        u=fa[top[u]];
    }
    if(dep[u]>dep[v]) swap(u,v);
    Change1(1,nid[u],nid[v]);
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<n;i++)
    {
        int u,v;scanf("%d%d",&u,&v);
        to[++tot]=v;nxt[tot]=head[u];head[u]=tot;
        to[++tot]=u;nxt[tot]=head[v];head[v]=tot;
    }
    dfs1(a[1],a[1],1);
    dfs2(a[1],a[1]);
    BuildTree(1,1,n);
    for(int i=1;i<n;i++) Change2(a[i],a[i+1]);
    for(int i=1;i<=n;i++) printf("%d\n",Ask(1,nid[i],nid[i])-(i==a[1]? 0:1));
    return 0;
}

优化

然而,这个题的修改操作很简单,就是很多个区间上同时加1;而如果用线段树,还会存储一堆区间和,有点浪费。可以考虑差分前缀和的方式

差分

如果b是a的差分数组,那么在a的某个[i,j]内同时加上1,等效于b[i]++,b[j+1]--;统计答案时只需要从左到右扫一遍就可以了

优化后AC代码

#include<bits/stdc++.h>
const int maxn=300010;
using namespace std;

int n,a[maxn];
int tot,to[maxn<<1],nxt[maxn<<1],head[maxn];
int dep[maxn],fa[maxn],son[maxn],len[maxn];
int cnt,nid[maxn],top[maxn];
int ans[maxn];
struct SegmentTree
{
    int l,r,tag,sum;
    #define l(a) tree[a].l
    #define r(a) tree[a].r
    #define m(a) ((l(a)+r(a))>>1)
    #define len(a) (r(a)-l(a)+1)
    #define t(a) tree[a].tag
    #define s(a) tree[a].sum
}tree[maxn<<2];

void dfs1(int u,int f,int d)
{
    dep[u]=d;fa[u]=f;len[u]=1;
    for(int i=head[u];i;i=nxt[i])
    {
        int v=to[i];
        if(v==f) continue;
        dfs1(v,u,d+1);
        len[u]+=len[v];
        if(len[v]>len[son[u]]) son[u]=v;
    }
}

void dfs2(int p,int t)
{
    nid[p]=++cnt;
    top[p]=t;
    if(!son[p]) return;
    dfs2(son[p],t);
    for(int i=head[p];i;i=nxt[i])
    {
        int v=to[i];
        if(v==fa[p] || v==son[p]) continue;
        dfs2(v,v);
    }
}

void Change1(int i,int j)
{
    ans[i]++;ans[j+1]--;
}

void Change2(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(dep[top[u]]<dep[top[v]]) swap(u,v);
        Change1(nid[top[u]],nid[u]);
        u=fa[top[u]];
    }
    if(dep[u]>dep[v]) swap(u,v);
    Change1(nid[u],nid[v]);
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<n;i++)
    {
        int u,v;scanf("%d%d",&u,&v);
        to[++tot]=v;nxt[tot]=head[u];head[u]=tot;
        to[++tot]=u;nxt[tot]=head[v];head[v]=tot;
    }
    dfs1(a[1],a[1],1);
    dfs2(a[1],a[1]);
    for(int i=1;i<n;i++) Change2(a[i],a[i+1]);
    for(int i=2;i<=n;i++) ans[i]+=ans[i-1];
    for(int i=1;i<=n;i++) printf("%d\n",ans[nid[i]]-(i==a[1]? 0:1)); ///判断是否是起点,如果不是起点则-1
    return 0;
}

总结

区间加1可以用差分,这样仅仅只用更改两个端点

posted @ 2018-09-26 15:55  Mercury04  阅读(130)  评论(0编辑  收藏  举报