洛谷 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可以用差分,这样仅仅只用更改两个端点